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

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


  • 首页

  • 归档

  • 搜索

java用于监听文件的状态变化

发表于 2021-11-03

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

昨天的时候我们已经说了,如何遍历文件夹下的所有文件及附属子文件夹,今天我们再来看看一个文件夹下那么多文件,如果我们想知道这个文件夹下面的文件的变化动态该咋办呢,我通过pom来解决来引包 。

百度百度下我们会发现common-io包已经为我们提供了对应的方法我们可以创建一个监听类并继承FileAlterationListenerAdaptor,该类实现了FileAlterationListener 接口,FileAlterationListenerAdaptor里只有对应的方法,却没有内部逻辑,所以需要我们进行重写,首先写上我们监听的目录;

1
ini复制代码File file = new File("D:\\java\\测试文件夹");

在进行项目监听的时候我们可以创建过滤器,过滤器有两种 一个是目录的过滤,一个是文件的过滤,过滤可加可不加,我没有选择加,然后为目录创建一个FileAlterationObserver并注册监听器,你如果想加上监听器的过滤,可以在后面追加参数,百度一下就出来的,,其实就是在下面的构造方法后面追加参数IOFileFilter的对象,这样就可以过滤掉你不想监听的文件或文件夹;

1
ini复制代码FileAlterationObserver fileObserver = new FileAlterationObserver(file);

调用addListener添加文件监听器,

1
arduino复制代码fileObserver.addListener(new FileListenUtil());

构造一个具有指定间隔5秒下的观察者集的监视器。

1
ini复制代码FileAlterationMonitor fileMonitor = new FileAlterationMonitor(TimeUnit.SECONDS.toMillis(5), fileObserver);

然后开始监控,

1
ini复制代码fileMonitor.start();

此处记着,在重写的方法里加上标志,以便可以观察到文件变化监听在控制台的日志,监听器可以监听文件和文件夹的增加、删除、修改的状态我们执行一下看下结果;为了测试结果,我将监听的轮询时间调成了十秒一次;

最后将代码提交上去,需要可以看下,我的代码也有很多百度的

gitee.com/sixiaoluo/c…

谢谢思密达!!!

本文转载自: 掘金

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

centos7部署OpenVpn 一、简介 二、环境规划 三

发表于 2021-11-03

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

一、简介

VPN直译就是虚拟专用通道,是提供给企业之间或者个人与公司之间安全数据传输的隧道,OpenVPN无疑是Linux下开源VPN的先锋,提供了良好的性能和友好的用户GUI。

OpenVPN大量使用了OpenSSL加密库中的SSLv3/TLSv1协议函数库。

OpenVPN 是一个基于 OpenSSL 库的应用层 VPN 实现。和传统 VPN 相比,它的优点是简单易用。

openvpn原理

二、环境规划

openvpn 服务端 centos7

IP 192.168.31.168

双网卡

三、安装部署

1.配置yum源(安装epel)

参考地址:fedoraproject.org/wiki/EPEL

1
2
3
bash复制代码yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum update
yum repolist

2.生成证书

2.1.下载证书生成工具easy-rsa

1
bash复制代码yum -y install easy-rsa

2.2.创建证书环境目录

1
2
3
bash复制代码mkdir -p /opt/easy-rsa
cp -a /usr/share/easy-rsa/3.0.8/* /opt/easy-rsa/
cp -a /usr/share/doc/easy-rsa-3.0.8/vars.example /opt/easy-rsa/vars

2.3.生成秘钥前,准备vars文件

修改文件/opt/easy-rsa/vars中的如下配置(要取消注释)

1
2
3
4
5
6
7
bash复制代码set_var EASYRSA_DN      "cn_only"
set_var EASYRSA_REQ_COUNTRY "CN"
set_var EASYRSA_REQ_PROVINCE "Shanghai"
set_var EASYRSA_REQ_CITY "Shanghai"
set_var EASYRSA_REQ_ORG "lucifer"
set_var EASYRSA_REQ_EMAIL "pc1107750981@163.com"
set_var EASYRSA_NS_SUPPORT "yes"

2.4.初始化

在当前目录下创建pki目录,用于存储证书

1
2
3
4
5
6
7
bash复制代码[root@openvpn easy-rsa]# cd /opt/easy-rsa/
[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa init-pki

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /opt/easy-rsa/pki

2.5.创建根证书

根证书用于ca对之后生成的server和client证书签名时使用。(输入两次密码,直接回车)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa build-ca

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars
Using SSL: openssl OpenSSL 1.0.2k-fips 26 Jan 2017

Enter New CA Key Passphrase:
Re-Enter New CA Key Passphrase:
Generating RSA private key, 2048 bit long modulus
.....+++
..........................................+++
e is 65537 (0x10001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/opt/easy-rsa/pki/ca.crt

2.6.创建server端证书和私钥文件

nopass表示不加密私钥文件,生成过程中直接回车默认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa gen-req server nopass

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars
Using SSL: openssl OpenSSL 1.0.2k-fips 26 Jan 2017
Generating a 2048 bit RSA private key
...........................+++
........................................................................+++
writing new private key to '/opt/easy-rsa/pki/easy-rsa-1326.TIfM4D/tmp.rxSnIM'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [server]:

Keypair and certificate request completed. Your files are:
req: /opt/easy-rsa/pki/reqs/server.req
key: /opt/easy-rsa/pki/private/server.key

2.7.给server证书签名(输入yes,输入密码)

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
bash复制代码[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa sign server server

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars
Using SSL: openssl OpenSSL 1.0.2k-fips 26 Jan 2017


You are about to sign the following certificate.
Please check over the details shown below for accuracy. Note that this request
has not been cryptographically verified. Please be sure it came from a trusted
source or that you have verified the request checksum with the sender.

Request subject, to be signed as a server certificate for 825 days:

subject=
commonName = server


Type the word 'yes' to continue, or any other input to abort.
Confirm request details: yes
Using configuration from /opt/easy-rsa/pki/easy-rsa-1397.ds5qpo/tmp.lX0IFN
Enter pass phrase for /opt/easy-rsa/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName :ASN.1 12:'server'
Certificate is to be certified until Jun 3 14:02:46 2023 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

Certificate created at: /opt/easy-rsa/pki/issued/server.crt

2.8.创建Diffie-Hellman文件,秘钥交换时的Diffie-Hellman算法

1
bash复制代码/opt/easy-rsa/easyrsa gen-dh

2.9.创建client端证书和私钥文件

nopass表示不加密私钥文件,生成过程中直接回车默认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa gen-req client nopass

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars
Using SSL: openssl OpenSSL 1.0.2k-fips 26 Jan 2017
Generating a 2048 bit RSA private key
..................................................................+++
...................................................................................................................+++
writing new private key to '/opt/easy-rsa/pki/easy-rsa-1761.HYs4Xv/tmp.z02JuI'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [client]:

Keypair and certificate request completed. Your files are:
req: /opt/easy-rsa/pki/reqs/client.req
key: /opt/easy-rsa/pki/private/client.key

2.10. 给client端证书签名(输入yes,输入密码)

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
bash复制代码[root@openvpn easy-rsa]# /opt/easy-rsa/easyrsa sign client client

Note: using Easy-RSA configuration from: /opt/easy-rsa/vars
Using SSL: openssl OpenSSL 1.0.2k-fips 26 Jan 2017


You are about to sign the following certificate.
Please check over the details shown below for accuracy. Note that this request
has not been cryptographically verified. Please be sure it came from a trusted
source or that you have verified the request checksum with the sender.

Request subject, to be signed as a client certificate for 825 days:

subject=
commonName = client


Type the word 'yes' to continue, or any other input to abort.
Confirm request details: yes
Using configuration from /opt/easy-rsa/pki/easy-rsa-1828.VwQHeF/tmp.eYqBSS
Enter pass phrase for /opt/easy-rsa/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName :ASN.1 12:'client'
Certificate is to be certified until Jun 3 14:09:37 2023 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

Certificate created at: /opt/easy-rsa/pki/issued/client.crt

四、OpenVPN服务端部署

1.安装openvpn软件

1
bash复制代码yum -y install openvpn

2.修改配置文件

自行创建配置文件/etc/openvpn/server.conf,并加入如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码port 1194 #端口
proto udp #协议
dev tun #采用路由隧道模式tun
ca ca.crt #ca证书文件位置
cert server.crt #服务端公钥名称
key server.key #服务端私钥名称
dh dh.pem #交换证书
server 10.8.0.0 255.255.255.0 #给客户端分配地址池,注意:不能和VPN服务器内网网段有相同
push "route 192.168.31.1 255.255.255.0" #允许客户端访问内网192.168.31.1网段
ifconfig-pool-persist ipp.txt #地址池记录文件位置
keepalive 10 120 #存活时间,10秒ping一次,120 如未收到响应则视为断线
max-clients 100 #最多允许100个客户端连接
status openvpn-status.log #日志记录位置
verb 3 #openvpn版本
client-to-client #客户端与客户端之间支持通信
log /var/log/openvpn.log #openvpn日志记录位置
persist-key #通过keepalive检测超时后,重新启动VPN,不重新读取keys,保留第一次使用的keys。
persist-tun #检测超时后,重新启动VPN,一直保持tun是linkup的。否则网络会先linkdown然后再linkup
duplicate-cn
  1. 拷贝证书到openvpn主配置文件目录下

1
2
3
4
bash复制代码cp -a /opt/easy-rsa/pki/ca.crt /etc/openvpn/
cp -a /opt/easy-rsa/pki/issued/server.crt /etc/openvpn/
cp -a /opt/easy-rsa/pki/private/server.key /etc/openvpn/
cp -a /opt/easy-rsa/pki/dh.pem /etc/openvpn/

4 启动openvpn

1
2
bash复制代码systemctl -f enable openvpn@server.service
systemctl start openvpn@server.service

五、OpenVPN客户端部署

1.安装OpenVPN客户端软件

这里是在windows环境下部署OpenVPN的客户端的,首先需要下载安装OpenVPN客户端软件

2.配置客户端

拷贝服务端生成的证书到OpenVPN安装目录的config目录下

分别拷贝以下几个文件

1
2
3
bash复制代码/opt/easy-rsa/pki/ca.crt 
/opt/easy-rsa/pki/issued/client.crt
/opt/easy-rsa/pki/private/client.key

3.编写客户端配置文件

在OpenVPN安装目录的config目录下,新建一个client.ovpn文件,在文件中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码client #指定当前VPN是客户端
dev tun #使用tun隧道传输协议
proto udp #使用udp协议传输数据
remote 192.168.31.168 1194 #openvpn服务器IP地址端口号
resolv-retry infinite #断线自动重新连接,在网络不稳定的情况下非常有用
nobind #不绑定本地特定的端口号
ca ca.crt #指定CA证书的文件路径
cert client.crt #指定当前客户端的证书文件路径
key client.key #指定当前客户端的私钥文件路径
verb 3 #指定日志文件的记录详细级别,可选0-9,等级越高日志内容越详细
persist-key #通过keepalive检测超时后,重新启动VPN,不重新读取keys,保留第一次使用的keys
persist-tun #检测超时后,重新启动VPN,一直保持tun是linkup的。否则网络会先linkdown然后再linkup

4.启动OpenVPN客户端软件

双击安装好后的OpenVPN软件,然后右键点击连接。

连接成功后,在托任务栏位置的OpenVPN图标会变绿色,则说明OpenVPN已经连接成功。

OpenVPN会分配一个IP地址给客户端,客户端会使用该虚拟网络IP地址与服务端进行通信。

本文转载自: 掘金

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

算法的本质是什么?程序员如何学好算法?

发表于 2021-11-03

这几年来,小灰一直在用漫画的形式分享算法知识,每一次所分享的,都是一个具体的算法知识点。

今天,让我从宏观的角度来讲一讲,算法到底是什么?如何来学好算法。

1. 算法是什么

所谓算法,指的是解决实际问题的一系列方法。

很多人以为,算法是一个新词汇,只和计算机有关系。

其实,算法的历史非常悠久,跟人类的生产生活有着密切的关系。

比如,在公元前500多年,古希腊数学家泰勒斯,利用一根木棍测量出了埃及胡夫金字塔的高度。

算法的本质是什么?程序员如何学好算法?

南北朝时期,中国数学家祖冲之利用割圆术近似求解圆周率。

算法的本质是什么?程序员如何学好算法?

他们所使用的方法,都可以称为算法。

不太了解这两个故事的小伙伴,可以去温习一下中小学课本。

说完了古代的事情,我们再来说一说计算机当中的算法。

计算机当中的算法,本质就是一系列程序指令,用以解决特定的运算和逻辑问题。

2. 基础算法和专业领域算法

按照专业程度来划分,算法可以分为基础算法和专业领域算法。

所谓专业领域算法,也就是服务于特定领域的算法。

比如在2016年,谷歌的AlphaGO战胜人类围棋冠军,背后是人工智能算法。

算法的本质是什么?程序员如何学好算法?

我们刷抖音、快手的时候,刷到各种你感兴趣的短视频,背后是推荐算法。

算法的本质是什么?程序员如何学好算法?

在一些游戏当中,我们能看到的非常酷炫的3D效果,背后是3D图形渲染算法。

算法的本质是什么?程序员如何学好算法?

上面说的这些都属于专业领域算法,由相应的算法工程师来研究和开发。

如果大家对某个特定领域的算法有兴趣,可以专门去学习,但正所谓术业有专攻,这样的算法并不需要每一个程序员都去掌握。

而所谓的基础算法,则是所有的程序员都应该掌握的关键知识。大学里计算机专业所学习的数据结构与算法这门课,讲授的就是基础算法。

3. 基础算法解决的问题

基础算法,可以解决哪些问题呢?

首先,是解决运算问题。

有人说了,运算不就是算算数吗?这还不简单?

其实还真不简单。例如求出两个数的最大公约数,要做到效率的极致,确实需要好好想一想。

再比如计算两个大整数的和,按照正常方式计算,肯定会导致溢出。这又该如何求解呢?

算法的本质是什么?程序员如何学好算法?

其次,还可以解决查找问题。

在一组有序的整数数列当中,我们想要最快的查找到某个整数是否存在;或者在一篇文章当中,想要快速检索到某个关键词,我们都需要高效的查找算法。

算法的本质是什么?程序员如何学好算法?

同时,算法也可以解决排序问题。

在很多情况下,我们需要把一些无序的元素按照顺序进行排列。

比如浏览电商网站的时候,我们希望按照商品价格从低到高来排序。浏览学生成绩单的时候,我们希望按照学生成绩从高到低排序。

算法的本质是什么?程序员如何学好算法?

这就要用到各种各样的排序算法。

最后,算法也可以帮助寻找最优决策。

当我们面对复杂的问题时候,需要一步一步做出选择,从而达到一个整体最优的结果。

算法的本质是什么?程序员如何学好算法?

这时候,某些算法可以帮助我们做出正确的决策,比如贪心算法、比如动态规划算法等等。

4. 学习算法有什么用

可能有人说了,我只要安心写好项目代码不就完了吗?我干嘛非要学习这些个算法呢?

如果你能够满足于在一家小公司,踏踏实实地做一个增删改查程序员,每个月拿个一两万工资的话,那没问题。

但如果你想要进入大厂,想要成为一个真正优秀程序员,你还是需要学好算法的。

为什么呢?

首先,懂算法的程序员能写出更高效的代码。

虽然我们在实际工作当中,大多数时候不需要去直接实现某个算法,但是我们需要知道自己正在使用的类库、接口的底层算法是什么样的,采用的数据结构是什么样的,从而在合适的场景下使用合适的工具。

其次,大厂面试过程中,往往会考察算法。衡量一个程序员是否优秀,不是看他有没有硕士博士学历,也不是看他熟悉多少种框架,而是看他的“内功”是否精神。而我们程序员的内功,就是算法和数据结构的运用能力。

算法的本质是什么?程序员如何学好算法?

5. 如何更好地学习算法

那么,我们怎么来提升自己的算法能力呢?

我认为,要提升算法能力,我们需要从基础和实战两个方向分别提升。

怎么学习算法基础知识呢?

我们可以通过看书,看网上的视频课程,来了解常用的各种算法和数据结构原理。

入门级别的书,比较推荐程杰老师的**《大话数据结构》,以及我自己出版的《漫画算法》系列。进阶级别的书,推荐看看《算法4》、《算法导论》**。

课程的话,推荐极客时间王争老师的**《数据结构与算法之美》**,讲的非常全面。

怎么提升实战能力呢?

首先,在一些相关图书当中,会讲解常见的算法面试题,这里推荐《剑指offer》这本书,里面包含不少面试常考的题目。

其次,大家可以上LeetCode这个网站,有大量的算法题目可以去刷。LeetCode网站的题目按照难度分成了三个级别,easy、Middle和Hard。大家可以从Easy题目开始尝试,后面再逐渐刷更难的题目。

算法的本质是什么?程序员如何学好算法?

再有一个提升方法,就是找时间多去其他公司面试,在面试中,非常能够锻炼一个人的临场发挥能力。不过,如果大家是在职状态的话,一定不要请假过于频繁,不然你连现在的工作都保不住了。

好了,关于算法的基本概念,以及提升算法的途径,小灰就给大家介绍到这里。如果觉得这篇文章对你有帮助,记得收藏和点赞哦~~

本文转载自: 掘金

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

redisson分布式锁怎么不自动续锁了? 前言 分布式锁

发表于 2021-11-03

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

前言

实际应用中,我们往往一些逻辑在同一时刻只会被调用一次,为了防止多次插入导致重复数据的产生。这种并发问题,我们往往第一时间想到的是java的synchronized、lock来解决,但是这前提条件是在同一个jvm下,那在分布式集群场景下改怎么解决呢?

分布式锁

顾名思义就是在分布式场景下使用锁,简单来说就是由一个第三方来管理锁(redis),分布式服务的各个节点从redis上获得锁,谁拿到锁,谁处理。

RedissonLock

image.png

续锁

当设置的分布式锁有效时间小于业务执行时间(业务执行时间不好预知),锁将提前释放,导致业务逻辑重复执行。如果将有效期设置为永久则服务异常宕机、重启等情况没有执行释放锁逻辑,就会导致锁一直无法释放,从而影响后续业务。RedissonLock实现了可以在业务执行没有完成的时候,自动重置锁的有效期。来解决以上两个问题。

RedissonLock的续锁逻辑是用netty下的Timeout定时器来实现的,默认锁时间是30秒,定时器每锁时间/3也就是10秒执行一次。来判断是否还持有锁,如果还持有锁就重置锁时间为30秒。从而避免业务执行时间大于锁时间而导致的锁被释放了的问题。

源码:
image.png
但当我们这样使用锁的时候,就会发现无法自动续锁

boolean isLock = lock.tryLock(6, 6, TimeUnit.SECONDS);

第一个参数6代表锁的有效期是6,第二个参数6代表阻塞等待获取所的超时时间是6,第三个参数是时间单位。

理论上按照续锁的规则,应该每2秒会去判断是否还持有锁,可是为什么没执行呢?问题出在了这一行代码上

image.png
这里有个判断逻辑,如果自定义了锁的有效期则会只执行tryLockInnerAsync方法,而不会去执行续锁方法scheduleExpirationRenewal。

总结

当我们需要用到续锁功能时,一要记住不要设置锁的过期时间,可以设置成-1.一旦设了时间,RedissonLock就会认为你需要自己控制锁时间,而放弃执行续锁逻辑。

查看源码,不难发现续锁逻辑开销挺大的,需要起定时器。所以要注意这点,并不是所有分布式场景都需要续锁逻辑的。当我们很难判断业务逻辑的执行时间时,不妨开启续锁。

本文转载自: 掘金

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

InnoDB的数据页到底长什么样?

发表于 2021-11-03

我们执行crud的时候,都会从磁盘上加载数据页到Buffer Pool的缓存页上去,更新了缓存页后,又会刷新回磁盘上的数据页里去。

InnoDB进行数据操作的最小单位,就是数据页,那么我们之前已经给大家分析过了一行一行数据在磁盘和缓存中存储,具体格式是什么样的。

​图片​

每个数据页默认是16K,是不是每个数据页里边就是一行一行的数据呢?

没这么简单!

数据页会分为很多个部分,大体上来说分为文件头、数据页头、最小记录最大记录、数据行、空闲空间、数据页目录、文件尾部。​

图片​

各部分含义详细介绍:​

名称

中文名

占用空间大小

简单描述

File Header

文件头部

38字节

页的一些通用信息

Page Header

数据页头部

56字节

数据页专有的一些信息

Infimum + Supremum

最小记录和最大记录

26字节

两个虚拟的行记录

User Records

数据行

不确定

实际存储的行记录内容

Free Space

空闲区域

不确定

页中尚未使用的空间

Page Directory

数据页目录

不确定

页中的某些记录的相对位置

File Trailer

文件尾部

8字节

校验页是否完整

​通过上图这种特殊的存储格式,InnoDB在磁盘文件里存放了一个又一个的数据页,每个数据页在磁盘里实际存储的,就是包含了上述一些特殊的数据。每个数据页里还有专门的区域包含了多行数据,每个数据行,就是用图1的格式来存储的了。

当我们第一次插入数据到数据页的时候,数据页可能是空的,这时候的数据页是没有数据行那个区域的。​

图片​

假如我们现在要插入一行数据,此时数据库里一行数据都没有的。

当向缓存页里插入一条数据时,实际上就是在数据行那个区域里插入一行数据,然后空闲区域的空间会减少一些,此时当缓存页插入了一行数据后,此时缓存页看起来就像这样了。

​​图片​

随着不停的crud,就会不停的插入数据到这个缓存页里去,直到空闲区域耗尽,这个页就写满了,此时数据行去也就会有很多行数据。​​

图片​

在更新缓存页的同时,它在lru链表里的位置也会不停的变动,而且肯定会在flush链表里,所以最终它会通过后台的IO线程把这个缓存页刷到磁盘上去。

对数据页内部结构的介绍就差不多了,后边还会介绍InnoDB对数据页的管理,表空间这些概念。

END

如果你喜欢本文,请关注 南山的架构笔记

​

本文转载自: 掘金

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

给你的SpringBoot做埋点监控--JVM应用度量框架M

发表于 2021-11-03

这世上有三样东西是别人抢不走的:一是吃进胃里的食物,二是藏在心中的梦想,三是读进大脑的书

JVM应用度量框架Micrometer实战

前提

spring-actuator做度量统计收集,使用Prometheus(普罗米修斯)进行数据收集,Grafana(增强ui)进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为”埋点”。SpringBoot中的依赖spring-actuator中集成的度量统计API使用的框架是Micrometer,官网是Micrometer.io。在实践中发现了业务开发者滥用了Micrometer的度量类型Counter,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于Micrometer分析其他的度量类型API的作用和适用场景。

Micrometer提供的度量类库

Meter是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为”米”或者”千分尺”,但是显然听起来都不是很合理,因此下文直接叫Meter,理解它为度量接口即可。Meter是由MeterRegistry创建和保存的,可以理解MeterRegistry是Meter的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个MeterRegistry的具体实现。Micrometer中,Meter的具体类型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。下面分节详细介绍这些类型的使用方法和实战使用场景。而一个Meter具体类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。

MeterRegistry

MeterRegistry在Micrometer是一个抽象类,主要实现包括:

  • 1、SimpleMeterRegistry:每个Meter的最新数据可以收集到SimpleMeterRegistry实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。
  • 2、CompositeMeterRegistry:多个MeterRegistry聚合,内部维护了一个MeterRegistry的列表。
  • 3、全局的MeterRegistry:工厂类io.micrometer.core.instrument.Metrics中持有一个静态final的CompositeMeterRegistry实例globalRegistry。

当然,使用者也可以自行继承MeterRegistry去实现自定义的MeterRegistry。SimpleMeterRegistry适合做调试的时候使用,它的简单使用方式如下:

1
2
3
java复制代码MeterRegistry registry = new SimpleMeterRegistry(); 
Counter counter = registry.counter("counter");
counter.increment();

CompositeMeterRegistry实例初始化的时候,内部持有的MeterRegistry列表是空的,如果此时用它新增一个Meter实例,Meter实例的操作是无效的

1
2
3
4
5
6
7
8
9
java复制代码CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 实际上这一步操作是无效的,但是不会报错

SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple); // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例

compositeCounter.increment(); // <-计数成功

全局的MeterRegistry的使用方式更加简单便捷,因为一切只需要操作工厂类Metrics的静态方法:

1
2
3
java复制代码Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();

Tag与Meter的命名

Micrometer中,Meter的命名约定使用英文逗号(dot,也就是”.”)分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。Micrometer中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口NamingConvention进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖NamingConvention实现自定义的命名转换规则:registry.config().namingConvention(myCustomNamingConvention);。在Micrometer中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式,例如当我们使用下面的命名时候:

1
2
java复制代码MeterRegistry registry = ...
registry.timer("http.server.requests");

对于不同的监控系统或者存储系统,命名会自动转换如下:

  • 1、Prometheus - http_server_requests_duration_seconds。
  • 2、Atlas - httpServerRequests。
  • 3、Graphite - http.server.requests。
  • 4、InfluxDB - http_server_requests。

其实NamingConvention已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。

另外,Tag(标签)是Micrometer的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据Tag的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

1
2
3
java复制代码MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

这样,当我们选择命名为”database.calls”的计数器,我们可以进一步选择分组”db”或者”users”分别统计不同分组对总调用数的贡献或者组成。一个反例如下:

1
2
3
4
5
6
7
8
java复制代码MeterRegistry registry = ...
registry.counter("calls",
"class", "database",
"db", "users");

registry.counter("calls",
"class", "http",
"uri", "/api/users");

通过命名”calls”得到的计数器,由于标签混乱,数据是基本无法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使用到的Meter上(只要是使用同一MeterRegistry),全局的Tag可以这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码MeterRegistry registry = ...
registry.counter("calls",
"class", "database",
"db", "users");

registry.counter("calls",
"class", "http",
"uri", "/api/users");


MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意义是一样的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));

像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。

还有两点点需要注意:

  • 1、Tag的值必须不为null。
  • 2、Micrometer中,Tag必须成对出现,也就是Tag必须设置为偶数个,实际上它们以Key=Value的形式存在,具体可以看io.micrometer.core.instrument.Tag接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface Tag extends Comparable<Tag> {
String getKey();

String getValue();

static Tag of(String key, String value) {
return new ImmutableTag(key, value);
}

default int compareTo(Tag o) {
return this.getKey().compareTo(o.getKey());
}
}

当然,有些时候,我们需要过滤一些必要的标签或者名称进行统计,或者为Meter的名称添加白名单,这个时候可以使用MeterFilter。MeterFilter本身提供一些列的静态方法,多个MeterFilter可以叠加或者组成链实现用户最终的过滤策略。例如:

1
2
3
4
java复制代码MeterRegistry registry = ...
registry.config()
.meterFilter(MeterFilter.ignoreTags("http"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略”http”标签,拒绝名称以”jvm”字符串开头的Meter。更多用法可以参详一下MeterFilter这个类。

Meter的命名和Meter的Tag相互结合,以命名为轴心,以Tag为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。

Meters

前面提到Meter主要包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。下面逐一分析它们的作用和个人理解的实际使用场景(应该说是生产环境)。

Counter

Counter是一种比较简单的Meter,它是一种单值的度量类型,或者说是一个单值计数器。Counter接口允许使用者使用一个固定值(必须为正数)进行计数。准确来说:Counter就是一个增量为正数的单值计数器。这个举个很简单的使用例子:

1
2
3
4
java复制代码MeterRegistry meterRegistry = new SimpleMeterRegistry();
Counter counter = meterRegistry.counter("http.request", "createOrder", "/order/create");
counter.increment();
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=1.0}]

使用场景:

Counter的作用是记录XXX的总量或者计数值,适用于一些增长类型的统计,例如下单、支付次数、Http请求总量记录等等,通过Tag可以区分不同的场景,对于下单,可以使用不同的Tag标记不同的业务来源或者是按日期划分,对于Http请求总量记录,可以使用Tag区分不同的URL。用下单业务举个例子:

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
java复制代码//实体
@Data
public class Order {

private String orderId;
private Integer amount;
private String channel;
private LocalDateTime createTime;
}


public class CounterMain {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

static {
Metrics.addRegistry(new SimpleMeterRegistry());
}

public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.setCreateTime(LocalDateTime.now());
createOrder(order1);
Order order2 = new Order();
order2.setOrderId("ORDER_ID_2");
order2.setAmount(200);
order2.setChannel("CHANNEL_B");
order2.setCreateTime(LocalDateTime.now());
createOrder(order2);
Search.in(Metrics.globalRegistry).meters().forEach(each -> {
StringBuilder builder = new StringBuilder();
builder.append("name:")
.append(each.getId().getName())
.append(",tags:")
.append(each.getId().getTags())
.append(",type:").append(each.getId().getType())
.append(",value:").append(each.measure());
System.out.println(builder.toString());
});
}

private static void createOrder(Order order) {
//忽略订单入库等操作
Metrics.counter("order.create",
"channel", order.getChannel(),
"createTime", FORMATTER.format(order.getCreateTime())).increment();
}
}

控制台输出

1
2
java复制代码name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
name:order.create,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]

上面的例子是使用全局静态方法工厂类Metrics去构造Counter实例,实际上,io.micrometer.core.instrument.Counter接口提供了一个内部建造器类Counter.Builder去实例化Counter,Counter.Builder的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class CounterBuilderMain {

public static void main(String[] args) throws Exception{
Counter counter = Counter.builder("name") //名称
.baseUnit("unit") //基础单位
.description("desc") //描述
.tag("tagKey", "tagValue") //标签
.register(new SimpleMeterRegistry());//绑定的MeterRegistry
counter.increment();
}
}

FunctionCounter

FunctionCounter是Counter的特化类型,它把计数器数值增加的动作抽象成接口类型ToDoubleFunction,这个接口JDK1.8中对于Function的特化类型接口。FunctionCounter的使用场景和Counter是一致的,这里介绍一下它的用法:

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

public static void main(String[] args) throws Exception {
MeterRegistry registry = new SimpleMeterRegistry();
AtomicInteger n = new AtomicInteger(0);
//这里ToDoubleFunction匿名实现其实可以使用Lambda表达式简化为AtomicInteger::get
FunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() {
@Override
public double applyAsDouble(AtomicInteger value) {
return value.get();
}
}).baseUnit("function")
.description("functionCounter")
.tag("createOrder", "CHANNEL-A")
.register(registry);
//下面模拟三次计数
n.incrementAndGet();
n.incrementAndGet();
n.incrementAndGet();
}
}

FunctionCounter使用的一个明显的好处是,我们不需要感知FunctionCounter实例的存在,实际上我们只需要操作作为FunctionCounter实例构建元素之一的AtomicInteger实例即可,这种接口的设计方式在很多框架里面都可以看到。

Timer

Timer(计时器)适用于记录耗时比较短的事件的执行时间,通过时间分布展示事件的序列和发生频率。所有的Timer的实现至少记录了发生的事件的数量和这些事件的总耗时,从而生成一个时间序列。Timer的基本单位基于服务端的指标而定,但是实际上我们不需要过于关注Timer的基本单位,因为Micrometer在存储生成的时间序列的时候会自动选择适当的基本单位。Timer接口提供的常用方法如下:

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 interface Timer extends Meter {
...
void record(long var1, TimeUnit var3);

default void record(Duration duration) {
this.record(duration.toNanos(), TimeUnit.NANOSECONDS);
}

<T> T record(Supplier<T> var1);

<T> T recordCallable(Callable<T> var1) throws Exception;

void record(Runnable var1);

default Runnable wrap(Runnable f) {
return () -> {
this.record(f);
};
}

default <T> Callable<T> wrap(Callable<T> f) {
return () -> {
return this.recordCallable(f);
};
}

long count();

double totalTime(TimeUnit var1);

default double mean(TimeUnit unit) {
return this.count() == 0L ? 0.0D : this.totalTime(unit) / (double)this.count();
}

double max(TimeUnit var1);
...
}

实际上,比较常用和方便的方法是几个函数式接口入参的方法:

1
2
3
4
5
6
java复制代码Timer timer = ...
timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());

Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());

使用场景:

根据个人经验和实践,总结如下:

  • 1、记录指定方法的执行时间用于展示。
  • 2、记录一些任务的执行时间,从而确定某些数据来源的速率,例如消息队列消息的消费速率等。

这里举个实际的例子,要对系统做一个功能,记录指定方法的执行时间,还是用下单方法做例子:

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 TimerMain {

private static final Random R = new Random();

static {
Metrics.addRegistry(new SimpleMeterRegistry());
}

public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.setCreateTime(LocalDateTime.now());
Timer timer = Metrics.timer("timer", "createOrder", "cost");
timer.record(() -> createOrder(order1));
}

private static void createOrder(Order order) {
try {
TimeUnit.SECONDS.sleep(R.nextInt(5)); //模拟方法耗时
} catch (InterruptedException e) {
//no-op
}
}
}

在实际生产环境中,可以通过spring-aop把记录方法耗时的逻辑抽象到一个切面中,这样就能减少不必要的冗余的模板代码。上面的例子是通过Mertics构造Timer实例,实际上也可以使用Builder构造:

1
2
3
4
5
6
java复制代码MeterRegistry registry = ...
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // 可选
.tags("region", "test") // 可选
.register(registry);

另外,Timer的使用还可以基于它的内部类Timer.Sample,通过start和stop两个方法记录两者之间的逻辑的执行耗时。例如:

1
2
3
4
5
6
java复制代码Timer.Sample sample = Timer.start(registry);

// 这里做业务逻辑
Response response = ...

sample.stop(registry.timer("my.timer", "response", response.status()));

FunctionTimer

FunctionTimer是Timer的特化类型,它主要提供两个单调递增的函数(其实并不是单调递增,只是在使用中一般需要随着时间最少保持不变或者说不减少):一个用于计数的函数和一个用于记录总调用耗时的函数,它的建造器的入参如下:

1
2
3
4
5
6
7
8
java复制代码public interface FunctionTimer extends Meter {
static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction,
ToDoubleFunction<T> totalTimeFunction,
TimeUnit totalTimeFunctionUnit) {
return new Builder<>(name, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit);
}
...
}

官方文档中的例子如下:

1
2
3
4
5
6
java复制代码IMap<?, ?> cache = ...; // 假设使用了Hazelcast缓存
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,
c -> c.getLocalMapStats().getGetOperationCount(), //实际上就是cache的一个方法,记录缓存生命周期初始化的增量(个数)
c -> c.getLocalMapStats().getTotalGetLatency(), // Get操作的延迟时间总量,可以理解为耗时
TimeUnit.NANOSECONDS
);

按照个人理解,ToDoubleFunction用于统计事件个数,ToDoubleFunction用于记录执行总时间,实际上两个函数都只是Function函数的变体,还有一个比较重要的是总时间的单位totalTimeFunctionUnit。简单的使用方式如下:

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

public static void main(String[] args) throws Exception {
//这个是为了满足参数,暂时不需要理会
Object holder = new Object();
AtomicLong totalTimeNanos = new AtomicLong(0);
AtomicLong totalCount = new AtomicLong(0);
FunctionTimer.builder("functionTimer", holder, p -> totalCount.get(),
p -> totalTimeNanos.get(), TimeUnit.NANOSECONDS)
.register(new SimpleMeterRegistry());
totalTimeNanos.addAndGet(10000000);
totalCount.incrementAndGet();
}
}

LongTaskTimer

LongTaskTimer也是一种Timer的特化类型,主要用于记录长时间执行的任务的持续时间,在任务完成之前,被监测的事件或者任务仍然处于运行状态,任务完成的时候,任务执行的总耗时才会被记录下来。LongTaskTimer适合用于长时间持续运行的事件耗时的记录,例如相对耗时的定时任务。在Spring应用中,可以简单地使用@Scheduled和@Timed注解,基于spring-aop完成定时调度任务的总耗时记录:

1
2
3
4
5
java复制代码@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {
//这里做相对耗时的业务逻辑
}

当然,在非spring体系中也能方便地使用LongTaskTimer:

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

public static void main(String[] args) throws Exception{
MeterRegistry meterRegistry = new SimpleMeterRegistry();
LongTaskTimer longTaskTimer = meterRegistry.more().longTaskTimer("longTaskTimer");
longTaskTimer.record(() -> {

//这里编写Task的逻辑
});
//或者这样
Metrics.more().longTaskTimer("longTaskTimer").record(()-> {
//这里编写Task的逻辑
});
}
}

Gauge

Gauge(仪表)是获取当前度量记录值的句柄,也就是它表示一个可以任意上下浮动的单数值度量Meter。Gauge通常用于变动的测量值,测量值用ToDoubleFunction参数的返回值设置,如当前的内存使用情况,同时也可以测量上下移动的”计数”,比如队列中的消息数量。官网文档中提到Gauge的典型使用场景是用于测量集合或映射的大小或运行状态中的线程数。Gauge一般用于监测有自然上界的事件或者任务,而Counter一般使用于无自然上界的事件或者任务的监测,所以像Http请求总量计数应该使用Counter而非Gauge。MeterRegistry中提供了一些便于构建用于观察数值、函数、集合和映射的Gauge相关的方法:

1
2
3
java复制代码List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); 
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>());
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

上面的三个方法通过MeterRegistry构建Gauge并且返回了集合或者映射实例,使用这些集合或者映射实例就能在其size变化过程中记录这个变更值。更重要的优点是,我们不需要感知Gauge接口的存在,只需要像平时一样使用集合或者映射实例就可以了。此外,Gauge还支持java.lang.Number的子类,java.util.concurrent.atomic包中的AtomicInteger和AtomicLong,还有Guava提供的AtomicDouble:

1
2
3
java复制代码AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0));
n.set(1);
n.set(2);

除了使用MeterRegistry创建Gauge之外,还可以使用建造器流式创建:

1
2
3
4
5
6
java复制代码//一般我们不需要操作Gauge实例
Gauge gauge = Gauge
.builder("gauge", myObj, myObj::gaugeValue)
.description("a description of what this gauge does") // 可选
.tags("region", "test") // 可选
.register(registry);

使用场景:

根据个人经验和实践,总结如下:

  • 1、有自然(物理)上界的浮动值的监测,例如物理内存、集合、映射、数值等。
  • 2、有逻辑上界的浮动值的监测,例如积压的消息、(线程池中)积压的任务等,其实本质也是集合或者映射的监测。

举个相对实际的例子,假设我们需要对登录后的用户发送一条短信或者推送,做法是消息先投放到一个阻塞队列,再由一个线程消费消息进行其他操作:

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
java复制代码public class GaugeMain {

private static final MeterRegistry MR = new SimpleMeterRegistry();
private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
private static BlockingQueue<Message> REAL_QUEUE;

static {
REAL_QUEUE = MR.gauge("messageGauge", QUEUE, Collection::size);
}

public static void main(String[] args) throws Exception {
consume();
Message message = new Message();
message.setUserId(1L);
message.setContent("content");
REAL_QUEUE.put(message);
}

private static void consume() throws Exception {
new Thread(() -> {
while (true) {
try {
Message message = REAL_QUEUE.take();
//handle message
System.out.println(message);
} catch (InterruptedException e) {
//no-op
}
}
}).start();
}
}

上面的例子代码写得比较糟糕,只为了演示相关使用方式,切勿用于生产环境。

TimeGauge

TimeGauge是Gauge的特化类型,相比Gauge,它的构建器中多了一个TimeUnit类型的参数,用于指定ToDoubleFunction入参的基础时间单位。这里简单举个使用例子:

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
java复制代码public class TimeGaugeMain {

private static final SimpleMeterRegistry R = new SimpleMeterRegistry();

public static void main(String[] args) throws Exception{
AtomicInteger count = new AtomicInteger();
TimeGauge.Builder<AtomicInteger> timeGauge = TimeGauge.builder("timeGauge", count,
TimeUnit.SECONDS, AtomicInteger::get);
timeGauge.register(R);
count.addAndGet(10086);
print();
count.set(1);
print();
}

private static void print()throws Exception{
Search.in(R).meters().forEach(each -> {
StringBuilder builder = new StringBuilder();
builder.append("name:")
.append(each.getId().getName())
.append(",tags:")
.append(each.getId().getTags())
.append(",type:").append(each.getId().getType())
.append(",value:").append(each.measure());
System.out.println(builder.toString());
});
}
}

//输出
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}]
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=1.0}]

DistributionSummary

Summary(摘要)主要用于跟踪事件的分布,在Micrometer中,对应的类是DistributionSummary(分发摘要)。它的使用方式和Timer十分相似,但是它的记录值并不依赖于时间单位。常见的使用场景:使用DistributionSummary测量命中服务器的请求的有效负载大小。使用MeterRegistry创建DistributionSummary实例如下:

1
java复制代码DistributionSummary summary = registry.summary("response.size");

通过建造器流式创建如下:

1
2
3
4
5
6
7
java复制代码DistributionSummary summary = DistributionSummary
.builder("response.size")
.description("a description of what this summary does") // 可选
.baseUnit("bytes") // 可选
.tags("region", "test") // 可选
.scale(100) // 可选
.register(registry);

DistributionSummary中有很多构建参数跟缩放和直方图的表示相关,见下一节。

使用场景:

根据个人经验和实践,总结如下:

  • 1、不依赖于时间单位的记录值的测量,例如服务器有效负载值,缓存的命中率等。

举个相对具体的例子:

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
java复制代码public class DistributionSummaryMain {

private static final DistributionSummary DS = DistributionSummary.builder("cacheHitPercent")
.register(new SimpleMeterRegistry());

private static final LoadingCache<String, String> CACHE = CacheBuilder.newBuilder()
.maximumSize(1000)
.recordStats()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return selectFromDatabase();
}
});

public static void main(String[] args) throws Exception{
String key = "doge";
String value = CACHE.get(key);
record();
}

private static void record()throws Exception{
CacheStats stats = CACHE.stats();
BigDecimal hitCount = new BigDecimal(stats.hitCount());
BigDecimal requestCount = new BigDecimal(stats.requestCount());
DS.record(hitCount.divide(requestCount,2,BigDecimal.ROUND_HALF_DOWN).doubleValue());
}
}

直方图和百分数配置

直方图和百分数配置适用于Summary和Timer,这部分相对复杂,等研究透了再补充。

基于SpirngBoot、Prometheus、Grafana集成

集成了Micrometer框架的JVM应用使用到Micrometer的API收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据,一般大佬只喜欢看炫酷的图表或者动画。常见的存储系统就是时序数据库,主流的有Influx、Datadog等。比较主流的监控系统(主要是用于数据收集和处理)就是Prometheus(一般叫普罗米修斯,下面就这样叫吧)。而展示的UI目前相对用得比较多的就是Grafana。另外,Prometheus已经内置了一个时序数据库的实现,因此,在做一套相对完善的度量数据监控的系统只需要依赖目标JVM应用,Prometheus组件和Grafana组件即可。下面花一点时间从零开始搭建一个这样的系统,之前写的一篇文章基于Windows系统,操作可能跟生产环境不够接近,这次使用CentOS7。

SpirngBoot中使用Micrometer

SpringBoot中的spring-boot-starter-actuator依赖已经集成了对Micrometer的支持,其中的metrics端点的很多功能就是通过Micrometer实现的,prometheus端点默认也是开启支持的,实际上actuator依赖的spring-boot-actuator-autoconfigure中集成了对很多框架的开箱即用的API,其中prometheus包中集成了对Prometheus的支持,使得使用了actuator可以轻易地让项目暴露出prometheus端点,作为Prometheus收集数据的客户端,Prometheus(服务端软件)可以通过此端点收集应用中Micrometer的度量数据。

我们先引入spring-boot-starter-actuator和spring-boot-starter-web,实现一个Counter和Timer作为示例。依赖:

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
xml复制代码<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>

接着编写一个下单接口和一个消息发送模块,模拟用户下单之后向用户发送消息:

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
java复制代码//实体
@Data
public class Message {

private String orderId;
private Long userId;
private String content;
}

@Data
public class Order {

private String orderId;
private Long userId;
private Integer amount;
private LocalDateTime createTime;
}

//控制器和服务类
@RestController
public class OrderController {

@Autowired
private OrderService orderService;

@PostMapping(value = "/order")
public ResponseEntity<Boolean> createOrder(@RequestBody Order order){
return ResponseEntity.ok(orderService.createOrder(order));
}
}

@Slf4j
@Service
public class OrderService {

private static final Random R = new Random();

@Autowired
private MessageService messageService;

public Boolean createOrder(Order order) {
//模拟下单
try {
int ms = R.nextInt(50) + 50;
TimeUnit.MILLISECONDS.sleep(ms);
log.info("保存订单模拟耗时{}毫秒...", ms);
} catch (Exception e) {
//no-op
}
//记录下单总数
Metrics.counter("order.count", "order.channel", order.getChannel()).increment();
//发送消息
Message message = new Message();
message.setContent("模拟短信...");
message.setOrderId(order.getOrderId());
message.setUserId(order.getUserId());
messageService.sendMessage(message);
return true;
}
}

@Slf4j
@Service
public class MessageService implements InitializingBean {

private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
private static BlockingQueue<Message> REAL_QUEUE;
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
private static final Random R = new Random();

static {
REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size);
}

public void sendMessage(Message message) {
try {
REAL_QUEUE.put(message);
} catch (InterruptedException e) {
//no-op
}
}

@Override
public void afterPropertiesSet() throws Exception {
EXECUTOR.execute(() -> {
while (true) {
try {
Message message = REAL_QUEUE.take();
log.info("模拟发送短信,orderId:{},userId:{},内容:{},耗时:{}毫秒", message.getOrderId(), message.getUserId(),
message.getContent(), R.nextInt(50));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
});
}
}

//切面类
@Component
@Aspect
public class TimerAspect {

@Around(value = "execution(* club.throwable.smp.service.*Service.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName());
ThrowableHolder holder = new ThrowableHolder();
Object result = timer.recordCallable(() -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
holder.throwable = e;
}
return null;
});
if (null != holder.throwable) {
throw holder.throwable;
}
return result;
}

private class ThrowableHolder {

Throwable throwable;
}
}

yaml的配置如下:

1
2
3
4
5
6
7
8
9
10
yaml复制代码server:
port: 9091
management:
server:
port: 10091
endpoints:
web:
exposure:
include: '*'
base-path: /management

注意多看spring官方文档关于Actuator的详细描述,在SpringBoot-2.x之后,配置Web端点暴露的权限控制和1.x有很大的不同。总结一下就是:除了shutdown端点之外,其他端点默认都是开启支持的这里仅仅是开启支持,并不是暴露为Web端点,端点必须暴露为Web端点才能被访问,禁用或者开启端点支持的配置方式如下:

1
xml复制代码management.endpoint.${端点ID}.enabled=true/false可以查

可以查看actuator-api文档查看所有支持的端点的特性,这个是2.1.0.RELEASE版本的官方文档,不知道日后链接会不会挂掉。端点只开启支持,但是不暴露为Web端点,是无法通过http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}访问的。暴露监控端点为Web端点的配置是:

1
2
xml复制代码management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus

management.endpoints.web.exposure.exclude用于指定不暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔management.endpoints.web.exposure.include默认指定的只有info和health两个端点,我们可以直接指定暴露所有的端点:management.endpoints.web.exposure.include=*,如果采用YAML配置,记得*要加单引号’*‘。暴露所有Web监控端点是一件比较危险的事情,如果需要在生产环境这样做,请务必先确认http://{host}:{management.port}不能通过公网访问(也就是监控端点访问的端口只能通过内网访问,这样可以方便后面说到的Prometheus服务端通过此端口收集数据)。

Prometheus的安装和配置

Prometheus目前的最新版本是2.5,鉴于笔者没深入玩过Docker,这里还是直接下载它的压缩包解压安装。

1
2
3
shell复制代码wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz
tar xvfz prometheus-*.tar.gz
cd prometheus-*

先编辑解压出来的目录下的prometheus配置文件prometheus.yml,主要修改scrape_configs节点的属性:

1
2
3
4
5
6
7
8
9
10
11
yml复制代码scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
# 这里配置需要拉取度量信息的URL路径,这里选择应用程序的prometheus端点
metrics_path: /management/prometheus
static_configs:
# 这里配置host和port
- targets: ['localhost:10091']

配置拉取度量数据的路径为localhost:10091/management/metrics,此前记得把前一节提到的应用在虚拟机中启动。接着启动Prometheus应用:

1
2
shell复制代码# 参数 --storage.tsdb.path=存储数据的路径,默认路径为./data
./prometheus --config.file=prometheus.yml --log.level=debug

Prometheus引用的默认启动端口是9090,启动成功后,日志如下:

此时,访问ttp://${虚拟机host}:9090/targets就能看到当前Prometheus中执行的Job

访问ttp://${虚拟机host}:9090/graph以查找到我们定义的度量Meter和spring-boot-starter-actuator中已经定义好的一些关于JVM或者Tomcat的度量Meter。我们先对应用的/order接口进行调用,然后查看一下监控前面在应用中定义的rder_count_total``ethod_cost_time_seconds_sum

可以看到,Meter的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。

Grafana的安装和使用

Grafana的安装过程如下:

1
2
bash复制代码wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm 
sudo yum localinstall grafana-5.3.4-1.x86_64.rpm

安装完成后,通过命令service grafana-server start启动即可,默认的启动端口为3000,通过ttp://${host}:3000即可。初始的账号密码都为admin,权限是管理员权限。接着需要在Home面板添加一个数据源,目的是对接Prometheus服务端从而可以拉取它里面的度量数据。数据源添加面板如下:


其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个Graph的面板


配置面板的时候,需要在基础(General)中指定Title:


接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:

最好参考一下Prometheus的官方文档,稍微学习一下它的查询语言PromQL的使用方式,一个面板可以支持多个PromQL查询。前面提到的两项是基本配置,其他配置项一般是图表展示的辅助或者预警等辅助功能,这里先不展开,可以取Grafana的官网挖掘一下使用方式。然后我们再调用一下下单接口,过一段时间,图表的数据就会自动更新和展示:


接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:

自定义系统输出 例如:

1
2
html复制代码app_register 0.0
app_login 0.0
  • 来统计这个数据
  • 那么在java中可以使用 Metrics.counter(“app_login”)来增加数据
  • 在prometheus监听自定义路径 /app/meter 代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码    @RequestMapping(value = "/meter", method = RequestMethod.GET)
@ResponseBody
public void getMeg(HttpServletResponse response) throws IOException {

Map map = Metrics.globalRegistry.getMeters().stream().collect(
Collectors.toMap(meter -> meter.getId().getName(),
meter -> Metrics.counter(meter.getId().getName()).count()));

StringBuffer html = new StringBuffer();
map.forEach((key,value) ->{
html.append(key).append(" ").append(value).append("\n");
});
response.setContentType("text/plain; version=0.0.4;charset=utf-8");
response.setContentLength(html.toString().length());
ServletOutputStream out = response.getOutputStream();
out.write(html.toString().getBytes());
out.flush();
}
  • 写自定义的原因是因为 prometheus需要返回响应头为text/plain; version=0.0.4;charset=utf-8 并且Content-Length需要文本长度。 比较坑。 所以留下日志

来之不易,给个关注吧 github.com/yunlongn

本文转载自: 掘金

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

从零蛋开始集成Spring Security 5 (一)

发表于 2021-11-03

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

1 如何进行权限管理

在web开发中, 权限管理是大部分系统的重中之重, 也是每位开发人员最头疼的内容之一

当下常用的权限解决方案有:

  1. ACL: Access Control List, 访问控制列表
  2. RBAC: Role Base Access Control, 基于角色的权限控制
  3. ABAC: Attribute Base Access Control, 基于属性的权限控制

每种方案都有自己的优缺点, 笔者未深入学习, 这里就不班门弄斧了

在笔者经历的大部分工作项目中, 使用最频繁的是第二个RBAC

所以本文后续所有内容是基于Spring Security 5的RBAC权限管理模型的实现

2 为什么是Spring Security 5

在众多权限管理方案中, 一定有部分设计思想或代码实现是共通的

Spring Security的出现为我们提供了多种解决方案

按照官网的说法, Spring Security是一种安全框架, 它可以让我们更便捷的定制自己应用的权限管理

那为什么选择最新的版本呢?

电子产品买新不买旧, 对于笔者而言, 开源项目也是一样的

笔者选择Spring Security 5的主要原因还包括Authorization Server相关仓库的废弃

3 实现步骤

想要集成Spring Security 5, 我们需要完成以下内容的处理:

  1. RBAC模型数据库设计
  2. 设计数据类
  3. 实现UserDetails
  4. 实现UserDetailsService
  5. Spring Security 5 配置

3.1 RBAC模型数据库设计

基于角色的权限控制, 顾名思义, 即通过为用户配置不同的角色, 每个角色又拥有不同的权限, 从而完成系统的权限管理

所以必然会涉及三个数据表: 用户表(t_user)/角色表(t_role)/权限表(t_permission)

这里的权限表又可以延申为菜单表, 二者在抽象上的意义是有重叠的

简单的系统到这里便可以运转了, 但当下大部分采用的是前后端分离的开发模式, 所以还需要再增加一个概念, 即接口表(t_api)

在用户概念上, 许多系统还会增加一个用户组, 笔者面对的更多是业务系统的开发, 于是将用户组的概念简化为了组织机构和部门, 但这部分对RBAC本身影响大大, 后续内容也不会涉及, 按照实际需求进行调整即可

最后总结一下, 其实权限的结构关系可以理解为: 用户->角色->权限->接口

用一张数据库关系图可以非常直观的进行说明:

3-1-1 RBAC.png

未完待续…

本文转载自: 掘金

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

SpringBoot中的Controller详解 Sprin

发表于 2021-11-03

SpringBoot中的Controller注册

本篇将会以Servlet为切入点,通过源码来看web容器中的Controller是如何注册到HandlerMapping中。请求来了之后,web容器是如何根据请求路径找到对应的Controller方法并执行的。

先讲下本文的大概思路和流程图:

  1. 我们经常使用的RequestMapping这个注解对应的方法最终会被RequestMappingHandlerMapping处理,并封装成一个HandlerMethod注入到自己内部的mappingRegistry 容器中。这一步是Controller的注册,被执行的触发点是因为RequestMappingHandlerMapping这个类实现了InitializingBean接口,由Spring容器触发。
  2. tomcat容器被启动的时候,最后会调用Servlet的init方法,这里会把所有的HandlerMapping注册到自己内部的handlerMappings属性中。这样Servlet和RequestMapping注解的Controller就建立起了间接关系。
  3. 当请求到来的时候,tomcat拿到并封装好请求体后会调用Servlet的service方法。这个方法最终会走到 DispatcherServlet的doDispatch方法,这个方法中会找到最适合的HandlerMapping并取出对应的HadlerMethod,然后给对应的HandlerAdapter执行.
  4. controller注册流程图
    controller注册流程.png
  5. controller发现和使用流程图
    controller发现和使用.png
    正文开始

处理请求的DispatcherServlet

DispatcherServlet_onRefresh.png
Servlet接口的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public interface Servlet {
//初始化
public void init(ServletConfig config) throws ServletException;

//响应请求
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;

//获取servlet信息
public String getServletInfo();

//服务停止时回调
public void destroy();
}

springboot内置了tomcat容器,而tomcat容器是遵循了servlet规范的。servlet规范中定义了初始化、响应、获取配置信息和销毁时回调钩子。从servlet的规范中可以看出,tomcat启动时会调用servlet的init方法,处理请求时会调用service方法,容器销毁时会调用destroy方法。servlet中最核心的实现就是我们所熟知的DispatchServlet,看下DispatchServlet的继承体系

image.png
从DispatchServlet的继承体系中,看下Servlet的初始化做了什么。

Servlet的初始化 init

HttpServletBean中的init方法源码

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复制代码@Override
public final void init() throws ServletException {

// 设置servlet的属性
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// 具体的初始化方法交给子类实现
initServletBean();
}
//空实现,具体的由子类实现
protected void initServletBean() throws ServletException {
}

从HttpServletBean中的init方法可以看到,这里核心的就是设置了Servlet的一些
bean properties,继续到子类
FrameworkServlet中看initServletBean方法

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
java复制代码@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
//这里初始化了web的上下文,这里会初始化Servlet的九大策略
this.webApplicationContext = initWebApplicationContext();
//这个方法也是空实现
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}

//初始化上下文
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}

if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
//springboot只会进入这个方法,这个方法是空实现,具体实现在子类DispatchServlet
onRefresh(wac);
}
}

if (this.publishContext) {
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}

return wac;
}

接着跟进DispatchServlet的onRefresh方法,这个方法中会初始化DispatchServlet的九大策略,这里我们只关心initHandlerMappings方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}

//初始化策略
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
//这个是我们关注的核心,Controller注册就在这里实现
initHandlerMappings(context);
//这个是处理Controller方法调用的,逻辑跟上面的initHandlerMappings差不多
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

核心看下initHandlerMappings方法

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
java复制代码private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
//默认为true
if (this.detectAllHandlerMappings) {
//默认的HandlerMapping有8个,这里我们只关心RequestMappingHandlerMapping这个类
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
//排序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
//这里就让Serlvet和Controller建立起了间接关系了,这个方法主要是为了给handlerMappings属性赋值
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
}
}

//这里如果没有HanlderMapping的话,会给一个默认的处理
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

看下默认的 HandlerMapping 有哪些

image.png
这里我们只关心RequestMappingHandlerMapping这个类,这个类就是处理我们Controller上的RequestMapping注解的类。

注意这里的handlerMappings,后面处理请求的时候,会从handlerMappings中选择一个最合适的HandlerMapping来处理请求

Servlet的请求处理 service

HttpServlet中的service方法源码

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
java复制代码/**
* 这个方法只是将ServletRequest强转为HttpServletRequest
* ServletResponse强转为HttpServletResponse
*/
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {

HttpServletRequest request;
HttpServletResponse response;

try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
//接着看这个
service(request, response);
}

protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//获取方法类型
String method = req.getMethod();
//根据不同的方法类型调用不同的方法,从doGet进去,看子类FrameworkServlet的doGet方法
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}

} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);

} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);

} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);

} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);

} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);

resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

//FrameworkServlet的doGet方法
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//继续跟
processRequest(request, response);
}

rotected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

long startTime = System.currentTimeMillis();
Throwable failureCause = null;

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

initContextHolders(request, localeContext, requestAttributes);

try {
//这里处理请求,继续跟,看子类DispatchServlet的doService方法
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}

finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

DispatchServlet的doService方法

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
java复制代码@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 打印日志
logRequest(request);
//保存快照
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
//设置属性
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

try {
//处理请求核心方法
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

DispatchServlet的doDispatch方法

DispatcherServlet_doService.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
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
java复制代码protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 获取当前请求的handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
//404
noHandlerFound(processedRequest, response);
return;
}

// 获取处理当前请求的handlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//调用前置拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 请求对应的方法在这里被执行
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
//调用后置拦截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
//这里的mapping就是接下来要讲到的RequestMappingHandlerMapping
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

这里可以记录Servlet处理请求的调用链 service -> doGet -> processRequest -> doService -> doDispatch

RequestMappingHandlerMapping 做了啥

AbstractHandlerMethodMapping_afterPropertiesSet.png
从上面的继承图可以看出RequestMappingHandlerMapping实现了InitializingBean接口,所以初始化的时候会调用afterPropertiesSet方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {
//配置
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager());
//这里是核心,会把所有controller注册进去
super.afterPropertiesSet();
}

接着看父类AbstractHandlerMethodMapping的afterPropertiesSet方法

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
java复制代码@Override
public void afterPropertiesSet() {
initHandlerMethods();
}

protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
//这里会注册controller
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
//这里的isHandler会判断是否有Controller或RequestMapping注解
if (beanType != null && isHandler(beanType)) {
//这里会注册controller,接着跟
detectHandlerMethods(beanName);
}
}

protected void detectHandlerMethods(Object handler) {
//获取类型
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
//这里会把RequestMapping转换为RequestMappingInfo
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
//注册到mappingRegistry中,后面会根据request获取对应的HandlerMethod
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

RequestMapping转换为RequestMappingInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//获取方法上的RequestMapping并且转换为RequestMappingInfo
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//获取类上的RequestMapping并且转换为RequestMappingInfo(这里就是Controller上的RequestMapping)
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//将方法上的RequestMapping和类上的RequestMapping合并,这里会合并url
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}

这个方法会把方法上的RequestMapping转换为RequestMappingInfo,把类上的RequestMapping转换为RequestMappingInfo,然后再把两个RequestMappingInfo合并成一个(url的合并)。

HandlerMethod的注册

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
java复制代码protected void registerHandlerMethod(Object handler, Method method, T mapping) {
//这里直接注册到mappingRegistry中,后面也直接从mappingRegistry获取
this.mappingRegistry.register(mapping, handler, method);
}

public void register(T mapping, Object handler, Method method) {
if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
Class<?>[] parameterTypes = method.getParameterTypes();
if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) {
throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
}
}
this.readWriteLock.writeLock().lock();
try {
//创建HandlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);

List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}

String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}

CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
//这里会添加到MappingRegistry中去
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}

这里需要注意的是,注册的Controller是直接注册的HandlerMethod,这个HandlerMethod就是对应的Controller类中具体请求对应的方法,这个对象封装了所有信息,后面获取出HandlerMethod后会通过反射调用具体的方法

进入RequestMappingHandlerMapping的getHandler方法看下,这个方法在父类AbstractHandlerMapping中实现,这里用到了设计模式中的模版方法。

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复制代码@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//这里获取真正的处理器,子类AbstractHandlerMethodMapping实现
Object handler = getHandlerInternal(request);
if (handler == null) {
//没有的话,使用默认的
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// 如果是beanName的话 从ioc容器中获取真正的实例
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
//这里获取对应请求的拦截器链
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
logger.debug("Mapped to " + executionChain.getHandler());
}

if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}

return executionChain;
}

这里核心关注两个方法,一个是获取处理器的getHandlerInternal方法,一个是获取对应拦截器链的getHandlerExecutionChain方法

AbstractHandlerMethodMapping的getHandlerInternal方法

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
java复制代码@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
request.setAttribute(LOOKUP_PATH, lookupPath);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}

if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}

从上面的方法中可以看出,最后是从mappingRegistry属性中取出的HandlerMethod,mappingRegistry在上面的RequestMappingHandlerMapping中有详细讲解

AbstractHandlerMapping的getHandlerExecutionChain方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
//这里会拿到所有的拦截器,然后通过路径匹配到合适的拦截器
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}

到这就已经拿到了对应的拦截器链和响应请求对应的方法了。接下来就是调用方法了,这里就轮到HandlerAdapter出场了,如何获取RequestMappingHandlerAdapter的方法getHandlerAdapter这里就跳过了

再回到DispatchServlet的doDispatch方法中的

1
2
3
java复制代码//这里的ha就是RequestMappingHandlerAdapter类
//核心是RequestMappingHandlerAdapter类的handleInternal方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

RequestMappingHandlerAdapter类的handleInternal方法

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复制代码@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
//这里就是真正调用HandlerMethod的地方了
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
//这里就是真正调用HandlerMethod的地方了
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
//这里就是真正调用HandlerMethod的地方了
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

到这里,整个调用的过程就已经到此为止了。其中的HandlerAdapter的注册、获取、处理请求反射调用HandlerMethod等以后的章节再分析。

本文转载自: 掘金

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

Springboot + mybatis-plus + dy

发表于 2021-11-03

环境

com.baomidou
dynamic-datasource-spring-boot-starter
3.4.1

com.baomidou
mybatis-plus
3.1.2

配置信息

其中库为 postgresql 和 mysql ,默认postgresql为主库

配置信息如下

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
spring.datasource.dynamic.primary=postgresql
spring.datasource.dynamic.strict=true
spring.datasource.dynamic.datasource.mysql.url=jdbc:mysql://localhost:3306/spring_boot_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
spring.datasource.dynamic.datasource.mysql.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.mysql.username=root
spring.datasource.dynamic.datasource.mysql.password=123456

spring.datasource.dynamic.datasource.postgresql.url=jdbc:postgresql://localhost:7092/postgres
spring.datasource.dynamic.datasource.postgresql.driver-class-name=org.postgresql.Driver
spring.datasource.dynamic.datasource.postgresql.username=postgres
spring.datasource.dynamic.datasource.postgresql.password=123456

mapper文件

利用DS来指定需要操作的数据库

1
2
3
4
5
less复制代码@DS("postgresql")
@Mapper
public interface CtmAskForLeaveMapper extends BaseMapper<CtmAskForLeave> {

}
1
2
3
4
5
less复制代码@DS("mysql")
@Mapper
public interface CtmThirdAskForLeaveMapper extends BaseMapper<CtmThirdAskForLeave> {

}

重写SqlSessionFactory

这步非常关键,如果不重写,可能导致分页不会返回total,以及无法使用原生BaseMapper
导致报错 Invalid bound statement (not found)

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复制代码@Configuration
@MapperScan("mappper所在包")
public class MybatisPlusConfig {


/**
* 分页插件*/
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
sqlSessionFactoryBean.setPlugins(new Interceptor[]{paginationInterceptor()});
return sqlSessionFactoryBean.getObject();
}


}

测试代码

`public interface CtmAskForLeaveService extends IService {

1
2
3
4
5
csharp复制代码/**
* 处理数据
* @return
*/
Boolean messageAskForLeave();

}@Service
@Slf4j
public class CtmAskForLeaveServiceImpl extends ServiceImpl<CtmAskForLeaveMapper, CtmAskForLeave> implements CtmAskForLeaveService {

1
2
3
4
typescript复制代码@Override
public Boolean messageAskForLeave() {
List<CtmThirdAskForLeave> ctmThirdAskForLeaves = ctmThirdAskForLeaveMapper.selectList(null);
}

}`

本文转载自: 掘金

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

【设计模式】策略模式 模式介绍 示例 优缺点

发表于 2021-11-03

模式介绍

一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

示例

1
2
3
4
5
6
7
go复制代码if (type == "A") {
// A逻辑
} else if (type == "B") {
// B逻辑
} else {
// 其他逻辑
}

解决有多个if...else等条件分支的情况

定义策略

1
2
3
4
5
6
arduino复制代码public interface BaseStrategy {

public final static String KEY_CONCAT = "_";

String getStrategyKey();
}
1
2
3
4
5
6
7
8
csharp复制代码public interface IStrategy  extends BaseStrategy {

public final static String ESTATE_STRATEGY_KEY_PREFIX = IStrategy.class.getSimpleName() + KEY_CONCAT;

void deal1();

void deal2();
}

添加策略

A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码import com.design.pattern.strategy.IStrategy;
import com.design.pattern.strategy.TypeEnum;
import org.springframework.stereotype.Service;

@Service
public class AStrategyImpl implements IStrategy {

@Override
public String getStrategyKey() {
return ESTATE_STRATEGY_KEY_PREFIX + TypeEnum.A.getCode();
}

@Override
public void deal1() {
System.out.println("处理A逻辑的1事件");
}

@Override
public void deal2() {
System.out.println("处理A逻辑的1事件");
}
}

B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码import com.design.pattern.strategy.IStrategy;
import com.design.pattern.strategy.TypeEnum;
import org.springframework.stereotype.Service;

@Service
public class BStrategyImpl implements IStrategy {

@Override
public String getStrategyKey() {
return ESTATE_STRATEGY_KEY_PREFIX + TypeEnum.B.getCode();
}

@Override
public void deal1() {
System.out.println("处理B逻辑的1事件");
}

@Override
public void deal2() {
System.out.println("处理B逻辑的2事件");
}
}

其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码import com.design.pattern.strategy.IStrategy;
import com.design.pattern.strategy.TypeEnum;
import org.springframework.stereotype.Service;

@Service
public class OtherStrategyImpl implements IStrategy {

@Override
public String getStrategyKey() {
return ESTATE_STRATEGY_KEY_PREFIX + TypeEnum.Other.getCode();
}

@Override
public void deal1() {
System.out.println("处理其他逻辑的1事件");
}

@Override
public void deal2() {
System.out.println("处理其他逻辑的2事件");
}
}

将策略加载至工厂

使用ApplicationContextAware接口,把所有的策略,初始化到iStrategyMap里面。然后对外提供getStrategy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
public class TypeStrategyFactory implements ApplicationContextAware {

private Map<String, IStrategy> iStrategyMap = new ConcurrentHashMap<>();

public <T extends IStrategy> T getStrategy(String strategyKey) throws Exception {
IStrategy strategy = iStrategyMap.get(IStrategy.ESTATE_STRATEGY_KEY_PREFIX + strategyKey);
if (strategy == null) {
throw new Exception("TypeStrategyFactory.getStrategy is error, strategyKey is " + strategyKey);
}
return (T) strategy;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IStrategy> map = applicationContext.getBeansOfType(IStrategy.class);
map.forEach((key, value) -> iStrategyMap.put(value.getStrategyKey(), value));
}
}

优缺点

优点

  1. 避免使用多重条件判断
  2. 扩展性良好。
  3. 算法可以自由切换

缺点

  1. 策略类会增多。
  2. 所有策略类都需要对外暴露。

本文转载自: 掘金

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

1…427428429…956

开发者博客

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