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

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


  • 首页

  • 归档

  • 搜索

Python入门(四)动态网页分析及抓取

发表于 2021-10-27

什么是动态网页?动态网页,就是网页中包含通过异步ajax加载出来的内容!
我们在打开某个网页时,点击右键“查看网页源代码”,会发现有一部分网页上显示的内容,源代码里面没有,而这部分就是通过ajax异步加载出来的,这就是动态网页!

就拿csdn博客来举例:Python入门(一)环境搭建

点开这篇文章,下方有一条评论:

在这里插入图片描述

按F12检查元素:

在这里插入图片描述

然后选中这条评论内容:

在这里插入图片描述

此时,就可以确定评论区域所在位置:<div class="comment-list-box" >...</div>

其实,这也就是所谓的网页分析,通过检查元素,确定你想提取的内容的区域位置,后面就可以通过标签id,name,class或其它属性提取内容!

继续往下看:

在这里插入图片描述

这里面包含了一个列表,而那条评论就在其中,此时我们可以在网页中,右键查看网页源代码,然后Ctrl+F,输入“comment-list-box”找到这部分:

在这里插入图片描述

我们会发现,源代码里什么也没有!到这里,是不是就明白了呢?

而如果我们要提取这部分动态内容,仅通过上一篇的方法是无法办到的,除非能分析出来加载动态网页的url,那如何才能简单高效的抓取动态网页内容呢?这里就需要用到动态网页抓取神器:Selenium

Selenium实则是一个web自动化测试工具,可以模拟用户滑动,点击,打开,验证等等一系列网页操作行为,就像一个真实用户在操作一样!这样就可以使用浏览器渲染方法将爬取动态网页,变成爬取静态网页!

安装Selenium:pip install selenium

安装成功后,简单测试:

1
2
3
4
5
python复制代码from selenium import webdriver

# 用selenium打开网页
driver = webdriver.Chrome()
driver.get("https://www.baidu.com")

报错:

WebDriverException( selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

这其实是缺少了谷歌浏览器驱动:chromedriver,下载后放在某个盘符下并记录位置,修改代码重新执行:

1
2
python复制代码driver = webdriver.Chrome(executable_path=r"C:\chromedriver.exe")
driver.get("https://www.baidu.com")

笔者这里使用的是FireFox浏览器,效果是一样的,当然,你要下载火狐浏览器驱动:geckodriver

1
2
python复制代码driver = webdriver.Firefox(executable_path=r"C:\geckodriver.exe")
driver.get("https://www.baidu.com")

在这里插入图片描述

成功打开后,会显示浏览器已被控制!

我们可以在PyCharm中,查看webdriver所提供的方法:

在这里插入图片描述

当所提取内容嵌套在frame中时,我们可以driver.switch_to.frame定位,简单的,我们就可以直接用
driver.find_element_by_css_selector、find_element_by_tag_name等等提取内容,方法中带复数s的提取的是列表,不带s的提取的则是单个数据,很好理解,详细使用方法,可以查看官方文档!

仍以csdn博客为例:Python入门(一)环境搭建,爬取这篇文章的评论,上面我们已经分析到评论所在区域:<div class="comment-list-box" >...</div>:

在这里插入图片描述

那么我们就可以直接通过find_element_by_css_selector获取该div下的内容:

1
2
3
4
5
6
7
8
9
10
11
python复制代码from selenium import webdriver

driver = webdriver.Firefox(executable_path=r"C:\geckodriver.exe")
driver.get("https://baiyuliang.blog.csdn.net/article/details/120473414")

comment_list_box = driver.find_element_by_css_selector('div.comment-list-box')
comment_list = comment_list_box.find_element_by_class_name('comment-list')
comment_line_box = comment_list.find_elements_by_class_name('comment-line-box')
for comment in comment_line_box:
span_text = comment.find_element_by_class_name('new-comment').text
print(span_text)

结果:

在这里插入图片描述

注意find_element_by_css_selector和find_element_by_class_name的用法区别!

本文转载自: 掘金

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

Python OpenCV 图像处理之傅里叶变换,取经之旅第

发表于 2021-10-27

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

Python OpenCV 365 天学习计划,与橡皮擦一起进入图像领域吧。本篇博客是这个系列的第 52 篇。

学在前面

傅里叶变换(Fourier Transform,FT)今天第一次接触,按照以往的套路,我们依旧是先应用起来,然后逐步的迭代学习。

傅里叶变换是对同一个事物观看角度的变化,不在从时域进行观看,从频域进行观看。这里提及了两个新词,时域和频域,先简单理解一下,时域,在时间范围内事物发生的变化,频域是指的事物的变化频率,是不是很绕,没啥问题,先用,先会调 API。

傅里叶原理略过,先说一下应用它之后对图像产生的影响。

  • 使用高通滤波器之后,会保留高频信息,增强图像细节,例如边界增强;
  • 使用低通滤波器之后,会保留低频信息,边界模糊。

傅里叶变换应用

原理既然先放下了,那先应用起来吧,我们将分别学习 numpy 和 OpenCV 两种方式实现傅里叶变换。

Numpy 实现傅里叶变换

通过 numpy 实现傅里叶变换用到的函数是 np.fft.fft2 ,函数原型如下:

1
python复制代码fft2(a, s=None, axes=(-2, -1), norm=None)

参数说明如下:

  • a:输入图像;
  • s:整数序列,输出数组的大小;
  • axex:整数序列,用于计算 FFT 的可选轴;
  • norm:规范化模式。

有些参数如果不实际使用,比较难看出结果,当然函数的说明,还是 官方手册 最靠谱。应用层面的代码编写流程如下:
通过 np.fft.fft2 函数进行傅里叶变换得到频率分布,再调用 np.fft.fftshift 函数将中心位置转移至中间。

测试代码如下:

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
python复制代码import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

img = cv.imread('test.jpg', 0)

# 快速傅里叶变换算法得到频率分布,将空间域转化为频率域
f = np.fft.fft2(img)

# 默认结果中心点位置是在左上角,通过下述代码将中心点转移到中间位置
# 将低频部分移动到图像中心
fshift = np.fft.fftshift(f)

# fft 结果是复数, 其绝对值结果是振幅
result = 20*np.log(np.abs(fshift))

plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('original')
plt.axis('off')

plt.subplot(122)
plt.imshow(result, cmap='gray')
plt.title('result')
plt.axis('off')

plt.show()

这个是很多地方反复提及的案例,在补充一下相关内容。
np.fft.fft2 是一个频率转换函数,它的第一个参数是输入图像,即灰度图像。第二个参数是可选的,它决定输出数组的大小。
第二个参数的大小如果大于输入图像的大小,则在计算 FFT 之前用零填充输入图像;如果小于输入图像,将裁切输入图像。如果未传递任何参数,则输出数组的大小将与输入的大小相同,测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# 生成一个图片
src = np.ones((5, 5), dtype=np.uint8)*100

print(src)
print(src.shape)

f = np.fft.fft2(src,(7,7))
print(f.shape)
print(f)

中心点进行移动之后,可以参考下图运行结果,而且可以知道 np.fft.fft2 函数应用之后得到的是复数矩阵。

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

后面要进行的操作,有部分数学知识,暂未搞懂,摘录如下:
进行完频率变换之后,就可以构建振幅谱了,最后在通过对数变换来压缩范围。
或者可以理解为将复数转为浮点数进行傅里叶频谱图显示,补充代码如下:

1
python复制代码fimg = np.log(np.abs(fshift))

最终得到的结果如下,当然这个是采用的随机图,如果换成一张灰度图,可以验证如下。

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

修改之后的代码如下:

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
python复制代码import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# 生成一个图片
# src = np.ones((5, 5), dtype=np.uint8)*100
src = cv.imread("./test.jpg", 0)
print("*"*100)
print(src)
print(src.shape)

f = np.fft.fft2(src)
print("*"*100)
print(f)

fshift = np.fft.fftshift(f)
print("*"*100)
print(fshift)
# 将复数转为浮点数进行傅里叶频谱图显示
fimg = 20*np.log(np.abs(fshift))
print(fimg)

# 图像显示
plt.subplot(121), plt.imshow(src, "gray"), plt.title('origin')
plt.axis('off')
plt.subplot(122), plt.imshow(fimg, "gray"), plt.title('Fourier')
plt.axis('off')
plt.show()

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

基本结论:左边为原始灰度图像,右边为频率分布图谱,其越靠近中心位置频率越低,灰度值越高亮度越亮的中心位置代表该频率的信号振幅越大。
接下来将其进行逆变换,也就是反向操作回去,通过 numpy 实现傅里叶逆变换,它是傅里叶变换的逆操作,将频谱图像转换为原始图像。用到核心函数与原型分别如下。
np.fft.ifft2

1
2
python复制代码# 实现图像逆傅里叶变换,返回一个复数数组
np.fft.ifft2(a, s=None, axes=(-2, -1), norm=None)

np.fft.ifftshift

1
2
python复制代码# fftshit()函数的逆函数,它将频谱图像的中心低频部分移动至左上角
np.fft.ifftshift(x, axes=None)

依据上述内容,对傅里叶变换进行逆向的操作,测试代码如下:

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
python复制代码import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# 生成一个图片
# src = np.ones((5, 5), dtype=np.uint8)*100
src = cv.imread("./test.jpg", 0)
print("*"*100)
print(src)
print(src.shape)

f = np.fft.fft2(src)
print("*"*100)
print(f)

fshift = np.fft.fftshift(f)
print("*"*100)
print(fshift)

# 将复数转为浮点数进行傅里叶频谱图显示
fimg = np.log(np.abs(fshift))
print(fimg)

# 逆傅里叶变换
ifshift = np.fft.ifftshift(fshift)
# 将复数转为浮点数进行傅里叶频谱图显示
ifimg = np.log(np.abs(ifshift))
if_img = np.fft.ifft2(ifshift)

origin_img = np.abs(if_img)

# 图像显示
plt.subplot(221), plt.imshow(src, "gray"), plt.title('origin')
plt.axis('off')
plt.subplot(222), plt.imshow(fimg, "gray"), plt.title('fourier_img')
plt.axis('off')
plt.subplot(223), plt.imshow(origin_img, "gray"), plt.title('origin_img')
plt.axis('off')
plt.subplot(224), plt.imshow(ifimg, "gray"), plt.title('ifimg')
plt.axis('off')
plt.show()

最终的结果如下:

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

如果在上述傅里叶变换之后的频谱图像中,增加一个低通滤波,将会得到如下结果。

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
python复制代码import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# 实现傅里叶变换的低通滤波
src = cv.imread("./test.jpg", 0)
f = np.fft.fft2(src)
fshift = np.fft.fftshift(f)

rows, cols = src.shape
crow, ccol = rows//2, cols//2

# 制定掩模,大小和图像一样,np.zeros初始化
mask = np.zeros((rows, cols), np.uint8)
# 使中心位置,上下左右距离30,置为1
mask[crow-30:crow+30, ccol-30:ccol+30] = 1

# 掩模与 DFT 后结果相乘只保留出中间区域
fshift = fshift*mask

# 逆傅里叶变换
ifshift = np.fft.ifftshift(fshift)
# 将复数转为浮点数进行傅里叶频谱图显示
ifimg = np.fft.ifft2(ifshift)
dft_img = np.abs(ifimg)


# 图像显示
plt.subplot(121), plt.imshow(src, "gray"), plt.title('origin')
plt.axis('off')
plt.subplot(122), plt.imshow(dft_img, "gray"), plt.title('dft_img')
plt.axis('off')
plt.show()

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

如果希望实现高通滤波,只需要修改掩模数据即可。

1
2
python复制代码mask = np.ones((rows, cols), np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 0

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

参考了很多文章,但是一篇文章被反复提及,这里也备注一下:

zhuanlan.zhihu.com/p/19763358
当然傅里叶变换在官方手册的内容也需要时长进行阅读一下下,官网地址

OpenCV 实现傅里叶变换

OpenCV 实现傅里叶变换与 numpy 基本一致,核心差异在函数的使用上,具体区别如下:
Opencv 中主要通过 cv2.dif 和 cv2.idif(傅里叶逆变换),在输入图像之前需要先转换成把图像从 np.uint8 转换为 np.float32 格式,其得到的结果中频率为 0 的部分会在左上角,要转换到中心位置,通过 shift 变换来实现,与 numpy 一致,cv2.dif 返回的结果是双通道的(实部,虚部),还需要转换成图像格式才能展示。

函数 cv2.dft() 原型如下

1
python复制代码dst = cv.dft(src[, dst[, flags[, nonzeroRows]]])

参数说明:

  • src:输入图像,要求 np.float32 格式;
  • dst:输出图像,双通道(实部+虚部),大小和类型取决于第三个参数 flags;
  • flags:表示转换标记,默认为 0,存在多种取值,参见后文;
  • nonzeroRows:默认为 0,暂时不考虑。

flags 取值如下:

  • DFT_INVERSE:用一维或二维逆变换取代默认的正向变换;
  • DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有 N 个元素,则输出结果以 1/N 缩放输出,常与 DFT_INVERSE 搭配使用;
  • DFT_ROWS:对输入矩阵的每行进行正向或反向的傅里叶变换;此标识符可在处理多种适量的的时候用于减小资源的开销,这些处理常常是三维或高维变换等复杂操作;
  • DFT_COMPLEX_OUTPUT:对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),通过设置标志位可以使函数生成一个全尺寸的复数输出数组;
  • DFT_REAL_OUTPUT:对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有 DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。

以上内容摘抄网络,原创人已经无法找到,尴尬。

总结下来就是:

  • DFT_COMPLEX_OUTPUT:得到一个复数形式的矩阵;
  • DFT_REAL_OUTPUT:只输出复数的实部;
  • DFT_INVERSE:进行傅里叶逆变换;
  • DFT_SCALE:是否除以 MxN (M 行 N 列的图片,共有有 MxN 个像素点);
  • DFT_ROWS:输入矩阵的每一行进行傅里叶变换或者逆变换。

最后需要注意的是,输出的频谱结果是一个复数,需要调用 cv2.magnitude() 函数将傅里叶变换的双通道结果转换为 0 到 255 的范围。

这个函数比较简单,原型和说明如下。

cv2.magnitude 函数原型:cv2.magnitude(x, y)

  • x 表示浮点型 X 坐标值,即实部
  • y 表示浮点型 Y 坐标值,即虚部

基础知识简单过一遍,直接进行案例的说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']

src = cv.imread("test.jpg", 0)

# OpneCV傅里叶变换函数
# 需要将图像进行一次float转换
result = cv.dft(np.float32(src), flags=cv.DFT_COMPLEX_OUTPUT)
# 将频谱低频从左上角移动至中心位置
dft_shift = np.fft.fftshift(result)
# 频谱图像双通道复数转换为 0-255 区间
result1 = 20 * np.log(cv.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))
# 图像显示
plt.subplot(121), plt.imshow(src, 'gray'), plt.title('原图像')
plt.axis('off')
plt.subplot(122), plt.imshow(result1, 'gray'), plt.title('傅里叶变换')
plt.axis('off')
plt.show()

运行结果与 numpy 一致。

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

套用上面的知识,使用 OpenCV 里面的函数对图片使用低通滤波。

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
python复制代码import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']

src = cv.imread("test.jpg", 0)

# OpneCV傅里叶变换函数
# 需要将图像进行一次float转换
result = cv.dft(np.float32(src), flags=cv.DFT_COMPLEX_OUTPUT)
# 将频谱低频从左上角移动至中心位置
dft_shift = np.fft.fftshift(result)
# 频谱图像双通道复数转换为 0-255 区间
result = 20 * np.log(cv.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))

rows, cols = src.shape
crow, ccol = rows//2, cols//2

mask = np.zeros((rows, cols, 2), np.uint8)
mask[int(crow-30):int(crow+30), int(ccol-30):int(ccol+30)] = 1
# LPF(低通滤波)
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:, :, 0], img_back[:, :, 1])

# 图像显示
plt.subplot(131), plt.imshow(src, 'gray'), plt.title('原图像')
plt.axis('off')
plt.subplot(132), plt.imshow(result, 'gray'), plt.title('傅里叶变换')
plt.axis('off')
plt.subplot(133), plt.imshow(img_back, 'gray'), plt.title('低通滤波之后的图像')
plt.axis('off')
plt.show()

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

高通滤波的代码和运行效果,就交给你自己来实现吧。

橡皮擦的小节

HPF(高通滤波),LPF(低通滤波)
希望今天的 1 个小时(貌似不太够)你有所收获,我们下篇博客见~

本文转载自: 掘金

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

实战!聊聊工作中使用了哪些设计模式

发表于 2021-10-27

前言

大家好,我是捡田螺的小男孩。

平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了。如何在写代码中找到乐趣呢,我觉得,最好的方式就是:使用设计模式优化自己的业务代码。今天跟大家聊聊日常工作中,我都使用过哪些设计模式。

工作中常用到哪些设计模式

  • 干货公众号:捡田螺的小男孩
  • 我的github地址,感谢给个star

1.策略模式

1.1 业务场景

假设有这样的业务场景,大数据系统把文件推送过来,根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码:

1
2
3
4
5
6
7
8
go复制代码if(type=="A"){
//按照A格式解析

}else if(type=="B"){
//按B格式解析
}else{
//按照默认格式解析
}

这个代码可能会存在哪些问题呢?

  • 如果分支变多,这里的代码就会变得臃肿,难以维护,可读性低。
  • 如果你需要接入一种新的解析类型,那只能在原有代码上修改。

说得专业一点的话,就是以上代码,违背了面向对象编程的开闭原则以及单一原则。

  • 开闭原则(对于扩展是开放的,但是对于修改是封闭的):增加或者删除某个逻辑,都需要修改到原来代码
  • 单一原则(规定一个类应该只有一个发生变化的原因):修改任何类型的分支逻辑代码,都需要改动当前类的代码。

如果你的代码就是酱紫:有多个if...else等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。

1.2 策略模式定义

策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。这个策略模式的定义是不是有点抽象呢?那我们来看点通俗易懂的比喻:

假设你跟不同性格类型的小姐姐约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去逛街买买买最合适。当然,目的都是为了得到小姐姐的芳心,请看电影、吃小吃、逛街就是不同的策略。

策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。

1.3 策略模式使用

策略模式怎么使用呢?酱紫实现的:

  • 一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)
  • 不同策略的差异化实现(就是说,不同策略的实现类)
  • 使用策略模式

1.3.1 一个接口,两个方法

1
2
3
4
5
6
7
8
csharp复制代码public interface IFileStrategy {

//属于哪种文件解析类型
FileTypeResolveEnum gainFileType();

//封装的公用算法(具体的解析方法)
void resolve(Object objectparam);
}

1.3.2 不同策略的差异化实现

A 类型策略具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@Component
public class AFileResolve implements IFileStrategy {

@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_A_RESOLVE;
}

@Override
public void resolve(Object objectparam) {
logger.info("A 类型解析文件,参数:{}",objectparam);
//A类型解析具体逻辑
}
}

B 类型策略具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Component
public class BFileResolve implements IFileStrategy {

@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_B_RESOLVE;
}


@Override
public void resolve(Object objectparam) {
logger.info("B 类型解析文件,参数:{}",objectparam);
//B类型解析具体逻辑
}
}

默认类型策略具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@Component
public class DefaultFileResolve implements IFileStrategy {

@Override
public FileTypeResolveEnum gainFileType() {
return FileTypeResolveEnum.File_DEFAULT_RESOLVE;
}

@Override
public void resolve(Object objectparam) {
logger.info("默认类型解析文件,参数:{}",objectparam);
//默认类型解析具体逻辑
}
}

1.3.3 使用策略模式

如何使用呢?我们借助spring的生命周期,使用ApplicationContextAware接口,把对用的策略,初始化到map里面。然后对外提供resolveFile方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码/**
* @author 公众号:捡田螺的小男孩
*/
@Component
public class StrategyUseService implements ApplicationContextAware{


private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();

public void resolveFile(FileTypeResolveEnum fileTypeResolveEnum, Object objectParam) {
IFileStrategy iFileStrategy = iFileStrategyMap.get(fileTypeResolveEnum);
if (iFileStrategy != null) {
iFileStrategy.resolve(objectParam);
}
}

//把不同策略放到map
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService));
}
}
  1. 责任链模式

2.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
30
31
32
typescript复制代码public class Order {

public void checkNullParam(Object param){
//参数非空校验
throw new RuntimeException();
}
public void checkSecurity(){
//安全校验
throw new RuntimeException();
}
public void checkBackList(){
//黑名单校验
throw new RuntimeException();
}
public void checkRule(){
//规则拦截
throw new RuntimeException();
}

public static void main(String[] args) {
Order order= new Order();
try{
order.checkNullParam();
order.checkSecurity ();
order.checkBackList();
order2.checkRule();
System.out.println("order success");
}catch (RuntimeException e){
System.out.println("order fail");
}
}
}

这段代码使用了异常来做逻辑条件判断,如果后续逻辑越来越复杂的话,会出现一些问题:如异常只能返回异常信息,不能返回更多的字段,这时候需要自定义异常类。

并且,阿里开发手册规定:禁止用异常做逻辑判断。

【强制】
异常不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。

如何优化这段代码呢?可以考虑责任链模式

2.2 责任链模式定义

当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。

责任链模式为请求创建了一个接收者对象的链。执行链上有多个对象节点,每个对象节点都有机会(条件匹配)处理请求事务,如果某个对象节点处理完了,就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。

责任链模式实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递:

责任链模式

打个比喻:

假设你晚上去上选修课,为了可以走点走,坐到了最后一排。来到教室,发现前面坐了好几个漂亮的小姐姐,于是你找张纸条,写上:“你好, 可以做我的女朋友吗?如果不愿意请向前传”。纸条就一个接一个的传上去了,后来传到第一排的那个妹子手上,她把纸条交给老师,听说老师40多岁未婚…

2.3 责任链模式使用

责任链模式怎么使用呢?

  • 一个接口或者抽象类
  • 每个对象差异化处理
  • 对象链(数组)初始化(连起来)

2.3.1 一个接口或者抽象类

这个接口或者抽象类,需要:

  • 有一个指向责任下一个对象的属性
  • 一个设置下一个对象的set方法
  • 给子类对象差异化实现的方法(如以下代码的doFilter方法)
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
vbscript复制代码/**
* 关注公众号:捡田螺的小男孩
*/
public abstract class AbstractHandler {

//责任链中的下一个对象
private AbstractHandler nextHandler;

/**
* 责任链的下一个对象
*/
public void setNextHandler(AbstractHandler nextHandler){
this.nextHandler = nextHandler;
}

/**
* 具体参数拦截逻辑,给子类去实现
*/
public void filter(Request request, Response response) {
doFilter(request, response);
if (getNextHandler() != null) {
getNextHandler().filter(request, response);
}
}

public AbstractHandler getNextHandler() {
return nextHandler;
}

abstract void doFilter(Request filterRequest, Response response);

}

2.3.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
scala复制代码/**
* 参数校验对象
**/
@Component
@Order(1) //顺序排第1,最先校验
public class CheckParamFilterObject extends AbstractHandler {

@Override
public void doFilter(Request request, Response response) {
System.out.println("非空参数检查");
}
}

/**
* 安全校验对象
*/
@Component
@Order(2) //校验顺序排第2
public class CheckSecurityFilterObject extends AbstractHandler {

@Override
public void doFilter(Request request, Response response) {
//invoke Security check
System.out.println("安全调用校验");
}
}
/**
* 黑名单校验对象
*/
@Component
@Order(3) //校验顺序排第3
public class CheckBlackFilterObject extends AbstractHandler {

@Override
public void doFilter(Request request, Response response) {
//invoke black list check
System.out.println("校验黑名单");
}
}

/**
* 规则拦截对象
*/
@Component
@Order(4) //校验顺序排第4
public class CheckRuleFilterObject extends AbstractHandler {

@Override
public void doFilter(Request request, Response response) {
//check rule
System.out.println("check rule");
}
}

2.3.3 对象链连起来(初始化)&& 使用

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
typescript复制代码@Component("ChainPatternDemo")
public class ChainPatternDemo {

//自动注入各个责任链的对象
@Autowired
private List<AbstractHandler> abstractHandleList;

private AbstractHandler abstractHandler;

//spring注入后自动执行,责任链的对象连接起来
@PostConstruct
public void initializeChainFilter(){

for(int i = 0;i<abstractHandleList.size();i++){
if(i == 0){
abstractHandler = abstractHandleList.get(0);
}else{
AbstractHandler currentHander = abstractHandleList.get(i - 1);
AbstractHandler nextHander = abstractHandleList.get(i);
currentHander.setNextHandler(nextHander);
}
}
}

//直接调用这个方法使用
public Response exec(Request request, Response response) {
abstractHandler.filter(request, response);
return response;
}

public AbstractHandler getAbstractHandler() {
return abstractHandler;
}

public void setAbstractHandler(AbstractHandler abstractHandler) {
this.abstractHandler = abstractHandler;
}
}

运行结果如下:

1
2
3
4
sql复制代码非空参数检查
安全调用校验
校验黑名单
check rule
  1. 模板方法模式

3.1 业务场景

假设我们有这么一个业务场景:内部系统不同商户,调用我们系统接口,去跟外部第三方系统交互(http方式)。走类似这么一个流程,如下:

一个请求都会经历这几个流程:

  • 查询商户信息
  • 对请求报文加签
  • 发送http请求出去
  • 对返回的报文验签

这里,有的商户可能是走代理出去的,有的是走直连。假设当前有A,B商户接入,不少伙伴可能这么实现,伪代码如下:

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复制代码
// 商户A处理句柄
CompanyAHandler implements RequestHandler {
Resp hander(req){
//查询商户信息
queryMerchantInfo();
//加签
signature();
//http请求(A商户假设走的是代理)
httpRequestbyProxy()
//验签
verify();
}
}
// 商户B处理句柄
CompanyBHandler implements RequestHandler {
Resp hander(Rreq){
//查询商户信息
queryMerchantInfo();
//加签
signature();
// http请求(B商户不走代理,直连)
httpRequestbyDirect();
// 验签
verify();
}
}

假设新加一个C商户接入,你需要再实现一套这样的代码。显然,这样代码就重复了,一些通用的方法,却在每一个子类都重新写了这一方法。

如何优化呢?可以使用模板方法模式。

3.2 模板方法模式定义

定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。

打个通俗的比喻:

模式举例:追女朋友要先“牵手”,再“拥抱”,再“接吻”, 再“拍拍..额..手”。至于具体你用左手还是右手牵,无所谓,但是整个过程,定了一个流程模板,按照模板来就行。

3.3 模板方法使用

  • 一个抽象类,定义骨架流程(抽象方法放一起)
  • 确定的共同方法步骤,放到抽象类(去除抽象方法标记)
  • 不确定的步骤,给子类去差异化实现

我们继续那以上的举例的业务流程例子,来一起用 模板方法优化一下哈:

3.3.1 一个抽象类,定义骨架流程

因为一个个请求经过的流程为一下步骤:

  • 查询商户信息
  • 对请求报文加签
  • 发送http请求出去
  • 对返回的报文验签

所以我们就可以定义一个抽象类,包含请求流程的几个方法,方法首先都定义为抽象方法哈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码/**
* 抽象类定义骨架流程(查询商户信息,加签,http请求,验签)
*/
abstract class AbstractMerchantService {

//查询商户信息
abstract queryMerchantInfo();
//加签
abstract signature();
//http 请求
abstract httpRequest();
// 验签
abstract verifySinature();

}

3.3.2 确定的共同方法步骤,放到抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码abstract class AbstractMerchantService  { 

//模板方法流程
Resp handlerTempPlate(req){
//查询商户信息
queryMerchantInfo();
//加签
signature();
//http 请求
httpRequest();
// 验签
verifySinature();
}
// Http是否走代理(提供给子类实现)
abstract boolean isRequestByProxy();
}

3.3.3 不确定的步骤,给子类去差异化实现

因为是否走代理流程是不确定的,所以给子类去实现。

商户A的请求实现:

1
2
3
4
5
6
7
8
typescript复制代码CompanyAServiceImpl extends AbstractMerchantService{
Resp hander(req){
return handlerTempPlate(req);
}
//走http代理的
boolean isRequestByProxy(){
return true;
}

商户B的请求实现:

1
2
3
4
5
6
7
8
9
typescript复制代码
CompanyBServiceImpl extends AbstractMerchantService{
Resp hander(req){
return handlerTempPlate(req);
}
//公司B是不走代理的
boolean isRequestByProxy(){
return false;
}
  1. 观察者模式

4.1 业务场景

登陆注册应该是最常见的业务场景了。就拿注册来说事,我们经常会遇到类似的场景,就是用户注册成功后,我们给用户发一条消息,又或者发个邮件等等,因此经常有如下的代码:

1
2
3
4
5
scss复制代码void register(User user){
insertRegisterUser(user);
sendIMMessage();
sendEmail();
}

这块代码会有什么问题呢? 如果产品又加需求:现在注册成功的用户,再给用户发一条短信通知。于是你又得改register方法的代码了。。。这是不是违反了开闭原则啦。

1
2
3
4
5
6
scss复制代码void register(User user){
insertRegisterUser(user);
sendIMMessage();
sendMobileMessage();
sendEmail();
}

并且,如果调发短信的接口失败了,是不是又影响到用户注册了?!这时候,是不是得加个异步方法给通知消息才好。。。

实际上,我们可以使用观察者模式优化。

4.2 观察者模式定义

观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被完成业务的更新。

观察者模式属于行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。它的主要成员就是观察者和被观察者。

  • 被观察者(Observerable):目标对象,状态发生变化时,将通知所有的观察者。
  • 观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。

使用场景: 完成某件事情后,异步通知场景。如,登陆成功,发个IM消息等等。

4.3 观察者模式使用

观察者模式实现的话,还是比较简单的。

  • 一个被观察者的类Observerable ;
  • 多个观察者Observer ;
  • 观察者的差异化实现
  • 经典观察者模式封装:EventBus实战

4.3.1 一个被观察者的类Observerable 和 多个观察者Observer

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
csharp复制代码public class Observerable {

private List<Observer> observers
= new ArrayList<Observer>();
private int state;

public int getState() {
return state;
}

public void setState(int state) {
notifyAllObservers();
}

//添加观察者
public void addServer(Observer observer){
observers.add(observer);
}

//移除观察者
public void removeServer(Observer observer){
observers.remove(observer);
}
//通知
public void notifyAllObservers(int state){
if(state!=1){
System.out.println(“不是通知的状态”);
return ;
}

for (Observer observer : observers) {
observer.doEvent();
}
}
}

4.3.2 观察者的差异化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
csharp复制代码 //观察者
interface Observer {
void doEvent();
}
//Im消息
IMMessageObserver implements Observer{
void doEvent(){
System.out.println("发送IM消息");
}
}

//手机短信
MobileNoObserver implements Observer{
void doEvent(){
System.out.println("发送短信消息");
}
}
//EmailNo
EmailObserver implements Observer{
void doEvent(){
System.out.println("发送email消息");
}
}

4.3.3 EventBus实战

自己搞一套观察者模式的代码,还是有点小麻烦。实际上,Guava EventBus 就封装好了,它
提供一套基于注解的事件总线,api可以灵活的使用,爽歪歪。

我们来看下EventBus的实战代码哈,首先可以声明一个EventBusCenter类,它类似于以上被观察者那种角色Observerable。

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

private static EventBus eventBus = new EventBus();

private EventBusCenter() {
}

public static EventBus getInstance() {
return eventBus;
}
//添加观察者
public static void register(Object obj) {
eventBus.register(obj);
}
//移除观察者
public static void unregister(Object obj) {
eventBus.unregister(obj);
}
//把消息推给观察者
public static void post(Object obj) {
eventBus.post(obj);
}
}

然后再声明观察者EventListener

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
typescript复制代码public class EventListener {

@Subscribe //加了订阅,这里标记这个方法是事件处理方法
public void handle(NotifyEvent notifyEvent) {
System.out.println("发送IM消息" + notifyEvent.getImNo());
System.out.println("发送短信消息" + notifyEvent.getMobileNo());
System.out.println("发送Email消息" + notifyEvent.getEmailNo());
}
}

//通知事件类
public class NotifyEvent {

private String mobileNo;

private String emailNo;

private String imNo;

public NotifyEvent(String mobileNo, String emailNo, String imNo) {
this.mobileNo = mobileNo;
this.emailNo = emailNo;
this.imNo = imNo;
}
}

使用demo测试:

1
2
3
4
5
6
7
8
9
typescript复制代码public class EventBusDemoTest {

public static void main(String[] args) {

EventListener eventListener = new EventListener();
EventBusCenter.register(eventListener);
EventBusCenter.post(new NotifyEvent("13372817283", "123@qq.com", "666"));
}
}

运行结果:

1
2
3
css复制代码发送IM消息666
发送短信消息13372817283
发送Email消息123@qq.com
  1. 工厂模式

5.1 业务场景

工厂模式一般配合策略模式一起使用。用来去优化大量的if...else...或switch...case...条件语句。

我们就取第一小节中策略模式那个例子吧。根据不同的文件解析类型,创建不同的解析对象

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码 
IFileStrategy getFileStrategy(FileTypeResolveEnum fileType){
IFileStrategy fileStrategy ;
if(fileType=FileTypeResolveEnum.File_A_RESOLVE){
fileStrategy = new AFileResolve();
}else if(fileType=FileTypeResolveEnum.File_A_RESOLV){
fileStrategy = new BFileResolve();
}else{
fileStrategy = new DefaultFileResolve();
}
return fileStrategy;
}

其实这就是工厂模式,定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

策略模式的例子,没有使用上一段代码,而是借助spring的特性,搞了一个工厂模式,哈哈,小伙伴们可以回去那个例子细品一下,我把代码再搬下来,小伙伴们再品一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码/**
* @author 公众号:捡田螺的小男孩
*/
@Component
public class StrategyUseService implements ApplicationContextAware{

private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();

//把所有的文件类型解析的对象,放到map,需要使用时,信手拈来即可。这就是工厂模式的一种体现啦
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IFileStrategy> tmepMap = applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService -> iFileStrategyMap.put(strategyService.gainFileType(), strategyService));
}
}

5.2 使用工厂模式

定义工厂模式也是比较简单的:

  • 一个工厂接口,提供一个创建不同对象的方法。
  • 其子类实现工厂接口,构造不同对象
  • 使用工厂模式

5.3.1 一个工厂接口

1
2
3
csharp复制代码interface IFileResolveFactory{
void resolve();
}

5.3.2 不同子类实现工厂接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码class AFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("文件A类型解析");
}
}

class BFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("文件B类型解析");
}
}

class DefaultFileResolve implements IFileResolveFactory{
void resolve(){
System.out.println("默认文件类型解析");
}
}

5.3.3 使用工厂模式

1
2
3
4
5
6
7
8
9
10
11
ini复制代码//构造不同的工厂对象
IFileResolveFactory fileResolveFactory;
if(fileType=“A”){
fileResolveFactory = new AFileResolve();
}else if(fileType=“B”){
fileResolveFactory = new BFileResolve();
}else{
fileResolveFactory = new DefaultFileResolve();
}

fileResolveFactory.resolve();

一般情况下,对于工厂模式,你不会看到以上的代码。工厂模式会跟配合其他设计模式如策略模式一起出现的。

  1. 单例模式

6.1 业务场景

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。 I/O与数据库的连接,一般就用单例模式实现de的。Windows里面的Task Manager(任务管理器)也是很典型的单例模式。

来看一个单例模式的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码/**
* 公众号:捡田螺的小男孩
*/
public class LanHanSingleton {

private static LanHanSingleton instance;

private LanHanSingleton(){

}

public static LanHanSingleton getInstance(){
if (instance == null) {
instance = new LanHanSingleton();
}
return instance;
}

}

以上的例子,就是懒汉式的单例实现。实例在需要用到的时候,才去创建,就比较懒。如果有则返回,没有则新建,需要加下 synchronized 关键字,要不然可能存在线性安全问题。

6.2 单例模式的经典写法

其实单例模式还有有好几种实现方式,如饿汉模式,双重校验锁,静态内部类,枚举等实现方式。

6.2.1 饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class EHanSingleton {

private static EHanSingleton instance = new EHanSingleton();

private EHanSingleton(){
}

public static EHanSingleton getInstance() {
return instance;
}

}

饿汉模式,它比较饥饿、比较勤奋,实例在初始化的时候就已经建好了,不管你后面有没有用到,都先新建好实例再说。这个就没有线程安全的问题,但是呢,浪费内存空间呀。

6.2.2 双重校验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码public class DoubleCheckSingleton {

private static DoubleCheckSingleton instance;

private DoubleCheckSingleton() { }

public static DoubleCheckSingleton getInstance(){
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}

双重校验锁实现的单例模式,综合了懒汉式和饿汉式两者的优缺点。以上代码例子中,在synchronized关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

6.2.3 静态内部类

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

private static class InnerClassSingletonHolder{
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}

private InnerClassSingleton(){}

public static final InnerClassSingleton getInstance(){
return InnerClassSingletonHolder.INSTANCE;
}
}

静态内部类的实现方式,效果有点类似双重校验锁。但这种方式只适用于静态域场景,双重校验锁方式可在实例域需要延迟初始化时使用。

6.2.4 枚举

1
2
3
4
5
6
7
csharp复制代码public enum SingletonEnum {

INSTANCE;
public SingletonEnum getInstance(){
return INSTANCE;
}
}

枚举实现的单例,代码简洁清晰。并且它还自动支持序列化机制,绝对防止多次实例化。

本文转载自: 掘金

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

保姆级教程!将 Vim 打造一个 IDE (Python 篇

发表于 2021-10-27

从上周开始我就开始折腾 ,搞了一下 Vim IDE for Python & Go,我将整个搭建的过程整理成本篇文章分享出来,本篇是 Python 版本的保姆级教程,实际上我还写了 Go 版本的,有想看的可以本篇文章点个赞,我下篇就发

效果图

一说到 IDE,总有人会因 which one is 世界上最好的编辑工具 而吵得不可开交,但本文不会涉及、也不想误导大家,我相信不同的人、不同的使用场景都有着有不同的最优解,世界上没有一招通吃的编辑器。

如果是在桌面端,PyCharm 和 VS Code 已经做得足够优秀,很难再有第三个编辑器可以与之匹敌。

但若要说在服务端?似乎没得选,Vim 几乎是你唯一的选择。

Vim 是极具生产力的工具,甚至在某些人的眼中,它是一个魔鬼般的编辑器,之所以这么说,是因为它的上手门槛极高,学习曲线非常的陡。

一是,它是针对程序员群体的专有编译器,你需要额外学习并理解它的设计理念,并且需要记住非常多复杂的操作指令。

二是,对于工程项目代码,它并不是一个开箱即用的编辑器,需要你安装大量的插件、进行大量的配置才能成为一个称手的 IDE 工具。

之所以,我使用 Vim 作为开发的工具,原因有四:

  1. 直接:正常本地 IDE 编码完后,要上传远程服务端进行编译及测试,比较麻烦,我直接在 SSH 服务端进行编码更为直接。
  2. 省心:多种语言不用再安装多个专有的编辑器,比如 PyCharm、Goland 等,而且不用再为各种付费软件破解劳心费力。
  3. 方便:提高 iPad 的生产力,外出不带电脑也可以在线写代码,省得每次都带个重重的电脑。
  4. 装逼:你不觉得挺酷的吗?(逃…

如果你对 Vim 操作一无所知,那么请先去了解一下 Vim 的日常使用方法,否则以下内容并不适合你。

  1. 准备工作

本文是在 Mac 环境下进行操作演示的,但同样适用于 Linux 环境(少许差异,我会在相应位置点出),如果你只有 Windows 系统,可以使用 GVim。

在开始安装配置之前,先说一下本文的一个整体思路:

  1. 准备运行环境:安装 Python 或者 Go 环境
  2. 准备Vim 版本:使用 Vim 8.2 的最高版本
  3. 插件安装环境:插件都在 Github 及其他外网,需要你配置一些代理
  4. 插件安装:一键批量安装插件
  5. 插件配置:插件安装上后,要进行一些配置才能好用
  6. 插件使用:演示每个插件的使用方法
  1. 准备运行环境

Vim 原生对 Python 提供了支持,当你安装 8.2 版本的 Vim 时,会自动安装 Python ,只不过该安装版本并不是你需要的版本,不过不要紧,Vim 运行使用的 Python 版本是可以配置的。

我这边使用的版本是 Python 3.10.0

1
2
shell复制代码$ python3 --version
Python 3.10.0
  1. 安装/升级 Vim 8.2

正常的 Mac 或者 Linux 机器都会自带 Vim 工具,只不过可能版本比较低,如果使用这些版本的 Vim ,后面有些插件会安装不上或者使用不了,就比如 YouCompleteMe 这个非常重要的插件,如果你不使用 Vim 8.1+ ,你每次用 vim 都会提示你,非常影响体验

1
2
3
shell复制代码$ vim main.go
YouCompleteMe unavailable: requires Vim 8.1.2269+.
Press ENTER or type command to continue

这些插件已经持续更新了很多年,对于老版的 Vim 不再提供支持这也可以理解。

如果你使用的 Linux ,整个过程会顺畅很多,在这里我使用的是 CentOS 7.6 的 Linux。

首先找到系统里安装的 vim 包有哪些,然后使用 yum remove 去卸载它

1
2
3
4
5
6
7
perl复制代码[root@iswbm ~]# yum list installed | grep -i vim
vim-common.x86_64 2:7.4.629-8.el7_9 @updates
vim-enhanced.x86_64 2:7.4.629-8.el7_9 @updates
vim-filesystem.x86_64 2:7.4.629-8.el7_9 @updates
vim-minimal.x86_64 2:7.4.629-8.el7_9 @updates
[root@iswbm ~]#
[root@iswbm ~]# yum remove vim-common vim-enhanced vim-filesystem vim-minimal

后面我会使用源码编译的方法去安装 Vim 8.2,但编译需要安装如下这些基础依赖

1
2
3
4
5
6
7
8
9
10
11
ini复制代码[root@iswbm ~]# yum install -y gcc make ncurses ncurses-devel
[root@iswbm ~]# yum install ctags git tcl-devel \
ruby ruby-devel \
lua lua-devel \
luajit luajit-devel \
python python-devel \
perl perl-devel \
perl-ExtUtils-ParseXS \
perl-ExtUtils-XSpp \
perl-ExtUtils-CBuilder \
perl-ExtUtils-Embed

从 Github 上下载源代码

1
bash复制代码git clone https://github.com/vim/vim.git

进入 vim/src 目录执行如下三个命令编译安装

1
2
3
4
5
6
7
ini复制代码[root@iswbm ~]# ./configure --prefix=/usr/local/vim \
--enable-pythoninterp=yes \
--enable-python3interp=yes \
--with-python-command=python \
--with-python3-command=python3
[root@iswbm ~]# make && make install
[root@iswbm ~]#

不出意外的话,命令执行完成后,你只要再配置个软件链接,就可以正常使用 8.2 版本的 Vim 了。

1
2
3
sql复制代码[root@iswbm ~]# ln -s /usr/local/vim/bin/vim /usr/bin/vim
[root@iswbm src]# vim --version | head -n 1
VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Oct 19 2021 22:05:46)
  1. 插件安装

Vim 本身提高的功能已经非常强大,但无奈上手难度实在太大,安装一些定制化的插件,能让整个 Vim 界面管理与使用更加符合人类的直觉,降低使用门槛。

具体要安装哪些插件,还要是看你想把 Vim 打造成什么样子?

这个倒不必闷着头空想,对照着桌面端的 IDE 软件去抄作业就 OK 了嘛。

对于我个人来说,我日常使用 IDE 最多的功能有:

  • 自动代码补全
  • 代码追踪跳转
  • 静态代码检查
  • 运行调试代码
  • 全局搜索代码
  • 项目代码书签
  • 代码版本管理
  • 代码高亮显示
  • 工程项目的文件树
  • 单文件代码结构树
  • 可同时打开多文件
  • Markdown 实时预览

那我就对照这个功能去找对应的插件即可

  • YouCompleteMe:提供自动代码补全与代码追踪跳转
  • auto-pairs:自动补全括号的插件,包括小括号,中括号,以及花括号
  • NERDTree:提供工程项目的文件树、支持书签功能
  • vim-nerdtree-tabs:可以打开多个代码文件,使 nerdtree 的 tab 更加友好些
  • nerdtree-git-plugin:可以在导航目录中看到 git 版本信息
  • tagbar:可以查看当前代码文件中的变量和函数列表的插件,并切换和跳转到代码中对应的变量和函数的位置
  • vim-airline:Vim状态栏插件,包括显示行号,列号,文件类型,文件名,以及Git状态
  • vim-gitgutter:可以在文档中显示 git 信息
  • vim-one:代码配色方案
  • markdown-preview.vim:Markdown 预览支持
  • mathjax-support-for-mkdp:Markdown 数学公式预览支持
  • vim-godef:go 中的代码追踪,输入 gd 就可以自动跳转
  • fatih/vim-go:静态检查等一系列 go 相关工具
  • ultisnips / vim-snippets:自动生成 代码块

那么如何安装这些插件呢?

很简单,你只要使用 vi 在你的 ~/.vimrc 文件中,贴入下面这段配置到文件末尾

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
go复制代码" 插件开始的位置
call plug#begin('~/.vim/plugged')

" 代码自动完成,安装完插件还需要额外配置才可以使用
Plug 'ycm-core/YouCompleteMe'

" 用来提供一个导航目录的侧边栏
Plug 'scrooloose/nerdtree'

" 可以使 nerdtree 的 tab 更加友好些
Plug 'jistr/vim-nerdtree-tabs'

" 可以在导航目录中看到 git 版本信息
" Plug 'Xuyuanp/nerdtree-git-plugin'

" 查看当前代码文件中的变量和函数列表的插件,
" 可以切换和跳转到代码中对应的变量和函数的位置
" 大纲式导航, Go 需要 https://github.com/jstemmer/gotags 支持
Plug 'preservim/tagbar'

" 自动补全括号的插件,包括小括号,中括号,以及花括号
Plug 'jiangmiao/auto-pairs'

" Vim状态栏插件,包括显示行号,列号,文件类型,文件名,以及Git状态
Plug 'vim-airline/vim-airline'

" Shorthand notation; fetches https://github.com/junegunn/vim-easy-align
" 可以快速对齐的插件
Plug 'junegunn/vim-easy-align'

" 可以在文档中显示 git 信息
Plug 'airblade/vim-gitgutter'

" markdown 插件
Plug 'iamcco/mathjax-support-for-mkdp'
Plug 'iamcco/markdown-preview.vim'

" 下面两个插件要配合使用,可以自动生成代码块
Plug 'SirVer/ultisnips'
Plug 'honza/vim-snippets'

" go 主要插件
Plug 'fatih/vim-go', { 'tag': '*' }

" go 中的代码追踪,输入 gd 就可以自动跳转
Plug 'dgryski/vim-godef'

" 可以在 vim 中使用 tab 补全
"Plug 'vim-scripts/SuperTab'

" 可以在 vim 中自动完成
"Plug 'Shougo/neocomplete.vim'


" 插件结束的位置,插件全部放在此行上面
call plug#end()

然后输入命令 :wq 保存并退出 vi。

安装插件的管理工具有很多,比如 Vundle,vim-plug 等。

Vundle是一款非常出名且历史悠久的Vim插件管理工具。但随着安装的vim插件越来越多,使用Vundle来管理这些插件时效率变得越来越低,vim启动耗时也越来越大。

而vim-plug是一款非常轻量又高效的vim插件管理工具。它支持全异步、多线程并行安装插件,支持git分支、标签等,可以对插件进行回滚更新、还支持按需加载插件(On-demand loading),可以指定对特定文件类型加载对应vim插件,大大加快了vim启动时间。

因此我这里会使用 vim-plug 这个管理工具,使用如下命令就可以安装 vim-plug 插件管理工具

1
2
ruby复制代码curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

接着请重启一下你的终端,保证重新初始化,不然等你后面执行 PlugInstall 的时候, 有可能报该命令不存在。

1
bash复制代码Not an editor command: PlugInstall

重启完终端后,输入再次打开 vim 输入 :PlugInstall 开始安装过程

如果你没有网络问题(不就科学那点事嘛),那么安装会很顺利。。

输入 :PlugStatus 就会看到所有的插件都安装 OK。

  1. YouCompleteMe

上面的插件安装,其实做的事情也比较简单,就是把 Github 上的仓库拉取到本地的 ~/.vim/plugged 目录

一般情况下,这些插件都是开箱即用的,不会有复杂的依赖,但唯独一个插件比较特殊 ,它就是 YouCompleteMe ,它号称是最难安装的 Vim 插件。

我在本地的 Mac 机器上装了两个晚上,才算把所有的依赖都解决完成,但在 Linux 上就比较顺利。

具体的安装步骤是

  1. 进入 ~/.vim/plugged/YouCompleteMe 插件目录,修改 .gitmodules 中的 github.com 为 镜像网站 hub.fastgit.org
  2. 然后安装一级依赖:git submodule update --init
  3. 一级依赖正确安装后,再修改 third_party/ycmd 目录下所有依赖的.gitmodules 中的 github.com 为 镜像网站 hub.fastgit.org
  4. 然后递归安装其依赖包:git submodule update --init --recursive
  5. 最后执行 python3 install.py --all ,–all 会安装该插件支持的所有语言功能。

在 Mac 上安装的过程中,遇到了相当多的问题,还涉及到了改 YouComplete 的代码,最后才得以正常安装下去,可能你在安装的过程中也会遇到类似的问题,如果有问题,欢迎在评论区留言,我会尽力解答。

  1. 设置镜像代理

上面安装插件的过程其实会去 Github 上下载对应的插件,但由于各种不可描述的原因, 在大陆的服务器上访问 github 是非常慢,甚至是不能访问的。

我在没有进行任何网络设置的情况下, 20 个插件,居然没有一个安装成功。

因此在这里,你得先想办法,让你的服务器能访问以正常速度访问 Github,至于怎么做,有些黑科技我这里不方便展开细说,就给大家介绍一种可以公开、又非常有效的方法。

修改 ~/.vim/autoload/plug.vim 将

1
bash复制代码let fmt = get(g:, 'plug_url_format', 'https://git::@github.com/%s.git')

改成

1
bash复制代码let fmt = get(g:, 'plug_url_format', 'https://git::@hub.fastgit.org/%s.git')

将这行

1
bash复制代码\ '^https://git::@github\.com', 'https://github.com', '')

改成

1
bash复制代码\ '^https://git::@hub.fastgit\.org', 'https://hub.fastgit.org', '')

然后再进入 vim 执行 :PlugInstall 就可以了

  1. 插件使用

大部分插件安装好后,可以立马使用,但有一些插件需要再进行一些配置才能用得更称手。

由于配置非常多,我这里就不直接贴出来了,有感兴趣的加我v:hello-wbm,找我要一下配置表。

YouComplete

使用 IDE 最基本的诉求,不就是能够在你编码的时候,自动给出提示,然后自动补全嘛,vim 有了 YouComplete 的加持后,也可以 100% 还原桌面端的编码体验。

NERDTree

打开文件后,使用 F9 或者输入 NERDTreeToggle 就会打开侧边栏的文件树,这是 NERDTree 给我们提供的便利。

tagbar

打开 Python 文件后,使用 F9 或者输入 :tagbar 就可以打开 tagbar 窗口,在这个窗口里你可以看到该文件的所有结构体、函数、变量等,这些通通可以称做 tag,当你定位到某个 tag 时,直接回车就可以跳转到左边代码窗口的位置。

vimgrep

vimgrep 可用于工程项目的代码查找,对于经常阅读源代码的同学是必不可少的利器,它是 Vim 自带的工具,非常之强大。

用完 vimgrep 查找后,正常情况下,不会有任何的反馈,如果你需要查看搜索的结果,并跳转到对应的位置,可以使用 QuickFix ,只要输入 :cw 或者 :copen

  1. 运行代码

使用 Vim 写完代码后,想像 PyCharm 一样直接快捷键运行代码,需要你在 .vimrc 中写入如下的配置。

这段配置,不仅包括 Python ,还有 Bash 和 Golang

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码" F5 to run sh/python3
map <F5> :call CompileRunGcc()<CR>
func! CompileRunGcc()
exec "w"
if &filetype == 'sh'
:!time bash %
elseif &filetype == 'python'
exec "!time python3 %"
elseif &filetype == 'go'
exec "!time go run %"
endif
endfunc

配置完后,使用 F5 就可以直接运行当前的脚本。

  1. 在 iPad 上写代码

如果你和我一样,有自己的服务器,那么你根据上面的步骤把 Vim 配置好后,就可以在 iPad 上通过 SSH 连接服务器进行代码的编写了。

如果你没有服务器,只要可以加我v: hello-wbm,我就送你一台一年期的阿里云服务器,名额有限,我只能说先到先得。

刚好我手上有一台 2020 款的 iPad Pro,平时也是用来视频居多,实在有点对不起 Pro 这个配置,有了 Vim 这个神器,生产力 up 了一点点。。

  1. 写在最后

有必要说明一下,之所以花了五天这么长的时间,其实我是把我手上的几台电脑,包括服务器全部配置了 Vim IDE,不同的机器,遇到的问题都有点不太一样,其中在我的 Mac 上,遇到的问题最多,折腾的时间最长,其中有些问题,我 Google 不到答案,最后是看了代码,修改了部分代码才跑下去的。

另外,对于 Vim 来说,最重要的就是 .vimrc 文件,上面的讲解可能我会漏了一些配置讲解,如果你发现使用不是那么顺利,可以下载我的 .vimrc 文件:wwe.lanzoui.com/i9gD5vrzufi

本文是 Python 版本的 Vim IDE 搭建指南,代码演示也基本是用的是 Python 代码,根据文中我的思路一步一步操作,你可以搭建属于自己的一套在线 IDE 环境。

我不仅写 Python 代码,还写一些 Go 的代码, Vim 对于 Python 原生提供了比较多的支持,而相比之下,Go 却要安装更多的插件才能达到不错的编码体验,但由于本号大多数是 Python 开发者,这一部分内容,我会再写一篇 Vim for Go 的文章。感兴趣的朋友给我可以给我评论区说一下,我会发给你地址。

好了,以上就是本篇文章的全部内容,如在安装配置上有任何疑问,欢迎评论区指出~

原文首发于个人博客:iswbm.com/591.html

文章的最后,插播一个福利

双十一快到了,阿里云也开始搞活动了,刚好我这边可以带大家白Piao 阿里云的服务器。

说白了就是大家 可以一分钱不花,就可以领到服务器,规格是 2c2m(2vcpu 2G memory) 的机器。

昨天在朋友圈发了下,现在已经有 400 人报名参与了,今天借这篇文章再说一下,有想参加的朋友,可以加我v(hello-wbm),带大家一起薅羊毛。

絮叨一下

我在掘金上写过很多的 Python 相关文章,其中包括 Python 实用工具,Python 高效技巧,PyCharm 使用技巧,很高兴得到了很多知乎朋友的认可和支持。

在他们的鼓励之下,我将过往文章分门别类整理成三本 PDF 电子书

PyCharm 中文指南

《PyCharm 中文指南》使用 300 多张 GIF 动态图的形式,详细讲解了最贴合实际开发的 105个 PyCharm 高效使用技巧,内容通俗易懂,适合所有 Python 开发者。

在线体验地址:pycharm.iswbm.com

Python 黑魔法指南

《Python黑魔法指南》目前迎来了 v3.0 的版本,囊集了 100 多个开发小技巧,非常适合在闲时进行碎片阅读。

在线体验地址:magic.iswbm.com

Python 中文指南

学 Python 最好的学习资料永远是 Python 官方文档,可惜现在的官方文档大都是英文,虽然有中文的翻译版了,但是进度实在堪忧。为了照顾英文不好的同学,我自己写了一份 面向零基础的朋友 的在线 Python 文档 – 《Python中文指南》

在线体验地址:python.iswbm.com

**有帮助的话,记得帮我 点个赞哟~

本文转载自: 掘金

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

深入浅出Java内存模型

发表于 2021-10-27

面试官:我记得上一次已经问过了为什么要有Java内存模型

面试官:我记得你的最终答案是:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果

候选者:嗯,对的

面试官:要不,你今天再来讲讲Java内存模型这里边的内容呗?

候选者:嗯,在讲之前还是得强调下:Java内存模型它是一种「规范」,Java虚拟机会实现这个规范。

候选者:Java内存模型主要的内容,我个人觉得有以下几块吧

候选者:1. Java内存模型的抽象结构

候选者:2. happen-before规则

候选者:3.对volatile内存语义的探讨(这块我后面再好好解释)

面试官:那要不你就从第一点开始呗?先聊下Java内存模型的抽象结构?

候选者:嗯。Java内存模型定义了:Java线程对内存数据进行交互的规范。

候选者:线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。

候选者:本地内存是Java内存模型的抽象概念,并不是真实存在的。

候选者:顺便画个图吧,看完图就懂了。

候选者:Java内存模型规定了:线程对变量的所有操作都必须在「本地内存」进行,「不能直接读写主内存」的变量

候选者:Java内存模型定义了8种操作来完成「变量如何从主内存到本地内存,以及变量如何从本地内存到主内存」

候选者:分别是read/load/use/assign/store/write/lock/unlock操作

候选者:看着8个操作很多,对变量的一次读写就涵盖了这些操作了,我再画个图给你讲讲

候选者:懂了吧?无非就是读写用到了各个操作(:

面试官:懂了,很简单,接下来说什么是happen-before吧?

候选者:嗯,好的(:

候选者:按我的理解下,happen-before实际上也是一套「规则」。Java内存模型定义了这套规则,目的是为了阐述「操作之间」的内存「可见性」

候选者:从上次讲述「指令重排」就提到了,在CPU和编译器层面上都有指令重排的问题。

候选者:指令重排虽然是能提高运行的效率,但在并发编程中,我们在兼顾「效率」的前提下,还希望「程序结果」能由我们掌控的。

候选者:说白了就是:在某些重要的场景下,这一组操作都不能进行重排序,「前面一个操作的结果对后续操作必须是可见的」。

面试官:嗯…

候选者:于是,Java内存模型就提出了happen-before这套规则,规则总共有8条

候选者:比如传递性、volatile变量规则、程序顺序规则、监视器锁的规则…(具体看规则的含义就好了,这块不难)

候选者:只要记住,有了happen-before这些规则。我们写的代码只要在这些规则下,前一个操作的结果对后续操作是可见的,是不会发生重排序的。

面试官:我明白你的意思了

面试官:那最后说下volatile?

候选者:嗯,volatile是Java的一个关键字

候选者:为什么讲Java内存模型往往就会讲到volatile这个关键字呢,我觉得主要是它的特性:可见性和有序性(禁止重排序)

候选者:Java内存模型这个规范,很大程度下就是为了解决可见性和有序性的问题。

面试官:那你来讲讲它的原理吧,volatile这个关键字是怎么做到可见性和有序性的

候选者:Java内存模型为了实现volatile有序性和可见性,定义了4种内存屏障的「规范」,分别是LoadLoad/LoadStore/StoreLoad/StoreStore

候选者:回到volatile上,说白了,就是在volatile「前后」加上「内存屏障」,使得编译器和CPU无法进行重排序,致使有序,并且写volatile变量对其他线程可见。

候选者:Java内存模型定义了规范,那Java虚拟机就得实现啊,是不是?

面试官:嗯…

候选者:之前看过Hotspot虚拟机的实现,在「汇编」层面上实际是通过Lock前缀指令来实现的,而不是各种fence指令(主要原因就是简便。因为大部分平台都支持lock指令,而fence指令是x86平台的)。

候选者:lock指令能保证:禁止CPU和编译器的重排序(保证了有序性)、保证CPU写核心的指令可以立即生效且其他核心的缓存数据失效(保证了可见性)。

面试官:那你提到这了,我想问问volatile和MESI协议是啥关系?

候选者:它们没有直接的关联。

候选者:Java内存模型关注的是编程语言层面上,它是高维度的抽象。MESI是CPU缓存一致性协议,不同的CPU架构都不一样,可能有的CPU压根就没用MESI协议…

候选者:只不过MESI名声大,大家就都拿他来举例子了。而MESI可能只是在「特定的场景下」为实现volatile的可见性/有序性而使用到的一部分罢了(:

面试官:嗯…

候选者:为了让Java程序员屏蔽上面这些底层知识,快速地入门使用volatile变量

候选者:Java内存模型的happen-before规则中就有对volatile变量规则的定义

候选者:这条规则的内容其实就是:对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见

候选者:它通过happen-before规则来规定:只要变量声明了volatile 关键字,写后再读,读必须可见写的值。(可见性、有序性)

面试官:嗯…了解了

本文总结:

  • 为什么存在Java内存模型:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果
  • Java内存模型抽象结构:线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。线程对变量的所有操作都必须在「本地内存」进行,而「不能直接读写主内存」的变量
  • happen-before规则:Java内存模型规定在某些场景下(一共8条),前面一个操作的结果对后续操作必须是可见的。这8条规则成为happen-before规则
  • volatile:volatile是Java的关键字,修饰的变量是可见性且有序的(不会被重排序)。可见性由happen-before规则完成,有序性由Java内存模型定义的「内存屏障」完成,实际HotSpot虚拟机实现Java内存模型规范,汇编底层通过Lock指令来实现。

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

Java版人脸检测详解下篇:编码

发表于 2021-10-27

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概览

  • 如果您看过《三分钟极速体验:Java版人脸检测》一文,甚至动手实际操作过,您应该会对背后的技术细节感兴趣,开发这样一个应用,咱们总共要做以下三件事:
  1. 准备好docker基础镜像
  2. 开发java应用
  3. 将java应用打包成package文件,集成到基础镜像中,得到最终的java应用镜像
  • 对于准备好docker基础镜像这项工作,咱们在前文《Java版人脸检测详解上篇:运行环境的Docker镜像(CentOS+JDK+OpenCV)》已经完成了,接下来要做的就是开发java应用并将其做成docker镜像

版本信息

  • 这个java应用的涉及的版本信息如下:
  1. springboot:2.4.8
  2. javacpp:1.4.3
  3. javacv:1.4.3

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

编码

  • 为了统一管理源码和jar依赖,项目采用了maven父子结构,父工程名为javacv-tutorials,其pom.xml如下,可见主要是定义了一些jar的版本:
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.bolingcavalry</groupId>
<artifactId>javacv-tutorials</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>face-detect-demo</module>
</modules>

<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version>
<springboot.version>2.4.8</springboot.version>

<!-- javacpp当前版本 -->
<javacpp.version>1.4.3</javacpp.version>
<!-- opencv版本 -->
<opencv.version>3.4.3</opencv.version>
<!-- ffmpeg版本 -->
<ffmpeg.version>4.0.2</ffmpeg.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>

<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacpp.version}</version>
</dependency>
<!-- javacpp -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${javacpp.version}</version>
</dependency>
<!-- ffmpeg -->
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>${ffmpeg.version}-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}-${javacpp.version}</version>
</dependency>
</dependencies>

</dependencyManagement>
</project>
  • 在javacv-tutorials下面新建名为face-detect-demo的子工程,这里面是咱们今天要开发的应用,其pom.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
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>javacv-tutorials</artifactId>
<groupId>com.bolingcavalry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>face-detect-demo</artifactId>
<packaging>jar</packaging>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!--FreeMarker模板视图依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
</dependency>
<!-- javacpp -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
</dependency>
<!-- ffmpeg -->
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg-platform</artifactId>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<!-- 如果父工程不是springboot,就要用以下方式使用插件,才能生成正常的jar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.bolingcavalry.facedetect.FaceDetectApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
  • 配置文件如下,要重点关注前段模板、文件上传大小、模型文件目录等配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
properties复制代码### FreeMarker 配置
spring.freemarker.allow-request-override=false
#Enable template caching.启用模板缓存。
spring.freemarker.cache=false
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
#设置面板后缀
spring.freemarker.suffix=.ftl

# 设置单个文件最大内存
spring.servlet.multipart.max-file-size=100MB
# 设置所有文件最大内存
spring.servlet.multipart.max-request-size=1000MB
# 自定义文件上传路径
web.upload-path=/app/images
# 模型路径
opencv.model-path=/app/model/haarcascade_frontalface_default.xml
  • 前端页面文件只有一个index.ftl,请原谅欣宸不入流的前端水平,前端只有一个页面,可以提交页面,同时也是展示处理结果的页面:
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
javascript复制代码<!DOCTYPE html>
<head>
<meta charset="UTF-8" />
<title>图片上传Demo</title>
</head>
<body>
<h1 >图片上传Demo</h1>
<form action="fileUpload" method="post" enctype="multipart/form-data">
<p>选择检测文件: <input type="file" name="fileName"/></p>
<p>周围检测数量: <input type="number" value="32" name="minneighbors"/></p>
<p><input type="submit" value="提交"/></p>
</form>
<#--判断是否上传文件-->
<#if msg??>
<span>${msg}</span><br><br>
<#else >
<span>${msg!("文件未上传")}</span><br>
</#if>
<#--显示图片,一定要在img中的src发请求给controller,否则直接跳转是乱码-->
<#if fileName??>
<#--<img src="/show?fileName=${fileName}" style="width: 100px"/>-->
<img src="/show?fileName=${fileName}"/>
<#else>
<#--<img src="/show" style="width: 200px"/>-->
</#if>
</body>
</html>
  • 再来看后台代码,先是最常见的应用启动类:
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.bolingcavalry.facedetect;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FaceDetectApplication {

public static void main(String[] args) {
SpringApplication.run(FaceDetectApplication.class, args);
}
}
  • 前端上传图片后,后端要做哪些处理呢?先不贴代码,咱们把后端要做的事情捋一遍,如下图:

在这里插入图片描述

  • 接下来是最核心的业务类UploadController.java,web接口和业务逻辑处理都在这里面,是按照上图的流程顺序执行的,有几处要注意的地方稍后会提到:
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
177
178
179
java复制代码package com.bolingcavalry.facedetect.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;

import java.util.UUID;

import static org.bytedeco.javacpp.opencv_objdetect.CV_HAAR_DO_CANNY_PRUNING;

@Controller
@Slf4j
public class UploadController {

static {
// 加载 动态链接库
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

private final ResourceLoader resourceLoader;

@Autowired
public UploadController(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

@Value("${web.upload-path}")
private String uploadPath;

@Value("${opencv.model-path}")
private String modelPath;

/**
* 跳转到文件上传页面
* @return
*/
@RequestMapping("index")
public String toUpload(){
return "index";
}

/**
* 上次文件到指定目录
* @param file 文件
* @param path 文件存放路径
* @param fileName 源文件名
* @return
*/
private static boolean upload(MultipartFile file, String path, String fileName){
//使用原文件名
String realPath = path + "/" + fileName;

File dest = new File(realPath);

//判断文件父目录是否存在
if(!dest.getParentFile().exists()){
dest.getParentFile().mkdir();
}

try {
//保存文件
file.transferTo(dest);
return true;
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
}

/**
*
* @param file 要上传的文件
* @return
*/
@RequestMapping("fileUpload")
public String upload(@RequestParam("fileName") MultipartFile file, @RequestParam("minneighbors") int minneighbors, Map<String, Object> map){
log.info("file [{}], size [{}], minneighbors [{}]", file.getOriginalFilename(), file.getSize(), minneighbors);

String originalFileName = file.getOriginalFilename();
if (!upload(file, uploadPath, originalFileName)){
map.put("msg", "上传失败!");
return "forward:/index";
}

String realPath = uploadPath + "/" + originalFileName;

Mat srcImg = Imgcodecs.imread(realPath);

// 目标灰色图像
Mat dstGrayImg = new Mat();
// 转换灰色
Imgproc.cvtColor(srcImg, dstGrayImg, Imgproc.COLOR_BGR2GRAY);
// OpenCv人脸识别分类器
CascadeClassifier classifier = new CascadeClassifier(modelPath);
// 用来存放人脸矩形
MatOfRect faceRect = new MatOfRect();

// 特征检测点的最小尺寸
Size minSize = new Size(32, 32);
// 图像缩放比例,可以理解为相机的X倍镜
double scaleFactor = 1.2;
// 执行人脸检测
classifier.detectMultiScale(dstGrayImg, faceRect, scaleFactor, minneighbors, CV_HAAR_DO_CANNY_PRUNING, minSize);
//遍历矩形,画到原图上面
// 定义绘制颜色
Scalar color = new Scalar(0, 0, 255);

Rect[] rects = faceRect.toArray();

// 没检测到
if (null==rects || rects.length<1) {
// 显示图片
map.put("msg", "未检测到人脸");
// 文件名
map.put("fileName", originalFileName);

return "forward:/index";
}

// 逐个处理
for(Rect rect: rects) {
int x = rect.x;
int y = rect.y;
int w = rect.width;
int h = rect.height;
// 单独框出每一张人脸
Imgproc.rectangle(srcImg, new Point(x, y), new Point(x + w, y + w), color, 2);
}

// 添加人脸框之后的图片的名字
String newFileName = UUID.randomUUID().toString() + ".png";

// 保存
Imgcodecs.imwrite(uploadPath + "/" + newFileName, srcImg);

// 显示图片
map.put("msg", "一共检测到" + rects.length + "个人脸");
// 文件名
map.put("fileName", newFileName);

return "forward:/index";
}
/**
* 显示单张图片
* @return
*/
@RequestMapping("show")
public ResponseEntity showPhotos(String fileName){
if (null==fileName) {
return ResponseEntity.notFound().build();
}

try {
// 由于是读取本机的文件,file是一定要加上的, path是在application配置文件中的路径
return ResponseEntity.ok(resourceLoader.getResource("file:" + uploadPath + "/" + fileName));
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
}
  • UploadController.java的代码,有以下几处要关注:
  1. 在静态方法中通过System.loadLibrary加载本地库函,实际开发过程中,这里是最容易报错的地方,一定要确保-Djava.library.path参数配置的路径中的本地库是正常可用的,前文制作的基础镜像中已经准比好了这些本地库,因此只要确保-Djava.library.path参数配置正确即可,这个配置在稍后的Dockerfile中会提到
  2. public String upload方法是处理人脸检测的代码入口,内部按照前面分析的流程顺序执行
  3. new CascadeClassifier(modelPath)是根据指定的模型来实例化分类器,模型文件是从GitHub下载的,opencv官方提前训练好的模型,地址是:github.com/opencv/open…
  4. 看似神奇的人脸检测功能,实际上只需一行代码classifier.detectMultiScale,就能得到每个人脸在原图中的矩形位置,接下来,咱们只要按照位置在原图上添加矩形框即可
  • 现在代码已经写完了,接下来将其做成docker镜像

docker镜像制作

  • 首先是编写Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码# 基础镜像集成了openjdk8和opencv3.4.3
FROM bolingcavalry/opencv3.4.3:0.0.3

# 创建目录
RUN mkdir -p /app/images && mkdir -p /app/model

# 指定镜像的内容的来源位置
ARG DEPENDENCY=target/dependency

# 复制内容到镜像
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app

# 指定启动命令
ENTRYPOINT ["java","-Djava.library.path=/opencv-3.4.3/build/lib","-cp","app:app/lib/*","com.bolingcavalry.facedetect.FaceDetectApplication"]
  • 上述Dockerfile内容很简单,就是一些复制文件的处理,只有一处要格外注意:启动命令中有个参数-Djava.library.path=/opencv-3.4.3/build/lib,指定了本地so库的位置,前面的java代码中,System.loadLibrary加载的本地库就是从这个位置加载的,咱们用的基础镜像是bolingcavalry/opencv3.4.3:0.0.3,已经在该位置准备好了opencv的所有本地库
  • 在父工程目录下执行mvn clean package -U,这是个纯粹的maven操作,和docker没有任何关系
  • 进入face-detect-demo目录,执行以下命令,作用是从jar文件中提取class、配置文件、依赖库等内容到target/dependency目录:
1
shell复制代码mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
  • 最后,在Dockerfile文件所在目录执行命令docker build -t bolingcavalry/facedetect:0.0.1 .(命令的最后有个点,不要漏了),即可完成镜像制作
  • 如果您有hub.docker.com的账号,还可以通过docker push命令把镜像推送到中央仓库,让更多的人用到:
  • 最后,再来回顾一下《三分钟极速体验:Java版人脸检测》一文中启动docker容器的命令,如下可见,通过两个-v参数,将宿主机的目录映射到容器中,因此,容器中的/app/images和/app/model可以保持不变,只要能保证宿主机的目录映射正确即可:
1
2
3
4
5
6
shell复制代码docker run \
--rm \
-p 18080:8080 \
-v /root/temp/202107/17/images:/app/images \
-v /root/temp/202107/17/model:/app/model \
bolingcavalry/facedetect:0.0.1
  • 有关SpringBoot官方推荐的docker镜像制作的更多信息,请参考《SpringBoot(2.4)应用制作Docker镜像(Gradle版官方方案)》

需要重点注意的地方

  • 请大家关注pom.xml中和javacv相关的几个库的版本,这些版本是不能随便搭配的,建议按照文中的来,就算要改,也请在maven中央仓库检查您所需的版本是否存在;
  • 至此,《Java版人脸检测》从体验到开发详解都完成了,小小的功能涉及到不少知识点,也让我们体验到了javacv的便捷和强大,借助docker将环境配置和应用开发分离开来,降低了应用开发和部署的难度(不再花时间到jdk和opencv的部署上),如果您正在寻找简单易用的javacv开发和部署方案,希望本文能给您提供参考;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

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

Spring Boot 中使用 Hikari,给我整不会了

发表于 2021-10-26

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

最近自己使用 Spring boot 搭建了一个非常简单的项目,可是不知道为啥控制台总是出现

1
ini复制代码Thread starvation or clock leap detected (housekeeper delta=3h24s779ms457µs999ns).

气的我直接找到源码,GitHub 一顿查询。最终解决了问题,开心。

我是使用 Spring Boot 2.5.4 我们都知道 Spring boot 默认就依赖了 Hikari ,而我的 JDK 版本是 11 ,这里就有问题了 Spring boot 的默认版本和官方推荐 JDK11 使用的版本不一致,对应于 JDK 11, 建议使用 5.0.0 的 Hikari 。

Hikari 介绍

不知道怎么搞的,我一直隐约感觉 Hikari 是阿里的框架,直到我打开 GitHub 啊,这不对啊,这好像是个日本的程序员写的呢?刚好说说这个名字,Hikari 怎么读的呢?可以读成 ”黑卡瑞“ ,大致看了一下 GitHub 的介绍,大呼一声,真秀!一个中国人在看日本人用英语写的文档,总感觉哪里怪怪的,但又说不上来。

说回到 Hikari ,它是一个连接池,官方给了这么几个形容词,fast,simple,reliable,zero-overhead,very light. 嗯听起来很好对吧,据说是史上最快的连接池。

我这里引用一句官方的话

The HikariCP design aesthetic is Minimalism. In keeping with the simple is better or less is more design philosophy, some configuration axis are intentionally left out.

Hikari 的设计美学是极简主义,少即是多的哲学,为此它还刻意减少一些参数,这点真的是直击我心。less is more and keep it simple and stupid.

在 GitHub 上作者甚至还介绍了他的优化点,都是一些比较小的点,但正是这些小的点汇集起来了,才使得 Hikari 的性能这么给力。优化的点是什么我就不具体说了,像什么缓存,静态方法,重写 ArrayList …

Hikari 的使用

作为开发者的我们,使用 Hikari 还是非常简单的,以我使用的 MySQL 为例,JDK 11 配置了 5.0.0 版本的 Hikari.

1 引入依赖

1
2
3
4
5
xml复制代码        <dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.0</version>
</dependency>

2 初始化

如果不是看官网,我不曾知道原来有这么多的初始化方式,我要一一的列举出来,扩展大家的思路。

1
2
3
4
5
6
7
8
9
arduino复制代码HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/simpsons");
config.setUsername("bart");
config.setPassword("51mp50n");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
​
HikariDataSource ds = new HikariDataSource(config);

或者直接这样

1
2
3
4
5
ini复制代码HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/simpsons");
ds.setUsername("bart");
ds.setPassword("51mp50n");
...

或者基于配置文件的初始化

1
2
3
4
5
6
7
8
9
10
11
ini复制代码HikariConfig config = new HikariConfig("/some/path/hikari.properties");
HikariDataSource ds = new HikariDataSource(config);
​
​
// properties file
dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
dataSource.user=test
dataSource.password=test
dataSource.databaseName=mydb
dataSource.portNumber=5432
dataSource.serverName=localhost

或者直接使用 Properties 类

1
2
3
4
5
6
7
8
9
java复制代码Properties props = new Properties();
props.setProperty("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource");
props.setProperty("dataSource.user", "test");
props.setProperty("dataSource.password", "test");
props.setProperty("dataSource.databaseName", "mydb");
props.put("dataSource.logWriter", new PrintWriter(System.out));
​
HikariConfig config = new HikariConfig(props);
HikariDataSource ds = new HikariDataSource(config);

甚至还可以配置环境变量

1
python复制代码There is also a System property available, hikaricp.configurationFile,

看到以上这些初始化的方法,直呼过瘾。

项目配置中的常用参数讲解

以为搭建的 Spring boot 项目为例,看看我的配置吧。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码spring:
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost/database?useUnicode=true&characterEncoding=utf8
   username: root
   password: root
   hikari:
       minimum-idle: 10
       idle-timeout: 30000
       maximum-pool-size: 20
       max-lifetime: 120000
       connection-timeout: 30000

根据 less is more 的设计哲学,以 Hikari 开头的配置都是可选的配置,都有默认的值,不配也行哈。

autoCommit: 默认是 true,自动提交从池中返回的连接。

connectionTimeout:等待来自池的连接的最大毫秒数,默认为 30000 ms = 30 s,允许最小时间是 250 毫秒,如果小于 250 毫秒,则被重置回 30 秒。

idleTimeout: 连接允许在池中闲置的最长时间,默认为 600000,即 10 分钟。如果 idleTimeout + 1 秒 > maxLifetime 且 maxLifetime > 0,则会被重置为 0(代表永远不会退出);如果 idleTimeout != 0 且小于 10 秒,则会被重置为 10 秒。只有当 minimumIdle 小于 maximumPoolSize 时,这个参数才生效,当空闲连接数超过 minimumIdle,而且空闲时间超过 idleTimeout,则会被移除。

keepaliveTime:连接存活时间,这个值必须小于 maxLifetime 值。Keepalive “只会发生在空闲的连接上。当对一个给定的连接进行 “keepalive “的时间到了,该连接将从池中移除。允许的最小值是 30000 ms(30秒),但最理想的值是在分钟范围内。默认值:0

maxLifetime:池中连接最长生命周期。默认为 1800000,如果不等于 0 且小于 30 秒则会被重置回 30 分钟。强烈建议设置这个参数。

minimumIdle:控制连接池空闲连接的最小数量,当连接池空闲连接少于 minimumIdle,而且总共连接数不大于 maximumPoolSize 时,HikariCP 会尽力补充新的连接。为了性能考虑,不建议设置此值,而是让 HikariCP 把连接池当做固定大小的处理,默认 minimumIdle 与 maximumPoolSize 一样。当 minIdle < 0 或者 minIdle > maxPoolSize,则被重置为 maxPoolSize,该值默认为 10。

maximumPoolSize:池中最大连接数,包括闲置和使用中的连接。默认为 10。如果 maxPoolSize 小于1,则会被重置。当 minIdle <=0 被重置为DEFAULT_POOL_SIZE 则为 10;如果 minIdle > 0 则重置为 minIdle 的值。

poolName:连接池的用户定义名称,主要出现在日志记录和 JMX 管理控制台中以识别池和池配置。默认为 HikariPool-1。

readOnly:从池中获取的连接是否默认处于只读模式。默认为 false。这个属性工作与否取决于数据库的实现。

connectionTestQuery:如果你的驱动程序支持 JDBC4,我们强烈建议不要设置这个属性。这是针对不支持 JDBC4 Connection.isValid() API的 “传统 “驱动程序。这是一个查询,在一个连接从池子里给你之前会被执行,以验证与数据库的连接是否仍然有效。同样,尝试在没有这个属性的情况下运行数据库池,如果你的驱动不符合JDBC4标准,HikariCP 会记录一个错误,让你知道。默认值:无。

最后

说一下我的那个警告是怎么解决的吧,更新了 jar 包,更改了配置,idle-timeout 要小于 max-lifetime 的呀,开始不知道在哪里 copy 的,他们的值不对,就很气…… 好吧也怪自己没有好好研究,就直到 copy paste。 官方才是最香的,看看官方文档,这波不亏。

本文转载自: 掘金

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

程序员必备小知识系列--个人网站功能开发与性能优化经历(6)

发表于 2021-10-26

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

自己搭建了一个基于SpringBoot+Spring Security+MyBatis+MySQL+Redis+Thymeleaf的博客网站。上线个人云服务器后,发现服务器访问慢。个人服务器是1核2G的,1M宽带,虽然服务器是低配的,但是可以通过优化代码,中间件等手段,来提升性能。我会讲解个人网站功能的开发与一些性能优化的经历。

这篇主要讲网站安全方面优化

一、修改数据库端口,redis端口等等

刚发布网站的时候,用不了一个月,就有一些人会攻击你的小网站,拿你的网站当成攻击的试验品。

当时博主安全意识不够,像mysql默认端口3306,redis默认端口6379统统没有改,统统都是这些端口,虽然mysql和redis的密码不是123456或者root这种基础级别的,已经改成很复杂的那种,但是还是被人入侵了,网站被黑了。

在这里插入图片描述

数据库的数据被删除,redis被宕机,密码他们很难破解的,我想他们绕过密码了,知道了端口,网站的ip也很容易拿到,ping域名就能拿到ip,有可能采用了CC的肉鸡,或者DDOS,对这些方面不太了解,但是进入服务器后台监控查看,CPU全部爆满,Redis被宕机也就不奇怪了。

虽然我备份了数据,但是还是蛮生气的,也无法抓到真凶,只能加强安全防护。为了安全起见,mysql和redis都重装,并修改mysql和redis的端口等等,并进行定时备份。之前写过定时备份mysql的文章Linux定时备份MYSQL数据

二、开启防火墙

防火墙还是要开启的,安装软件的时候为了方便会把防火墙给关了

1
shell复制代码systemctl start firewalld

三、添加入站规则

在云服务器的管理界面,点击安全组,选中入站规则,这里填入要访问的端口号,开通对应的端口号,不要把所有端口号都开通,不然肯定会遭到一堆CG或DOS攻击
在这里插入图片描述

开启的端口只要数据库,redis,服务等端口就行,其他端口按需来开,没必要的端口不要开。

本文转载自: 掘金

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

电商库存系统的防超卖和高并发扣减方案

发表于 2021-10-26

摘要:如果你要开发一个电商库存系统,最担心的是什么?闭上眼睛想下,当然是高并发和防超卖了!本文给出一个统筹考虑如何高并发和防超卖数据准确性的方案。读者可以直接借鉴本设计,或在此基础上做出更切合使用场景的设计。

下面用电商库存为示例,来说明如何高并发扣减库存,原理同样适用于其他需要并发写和数据一致性的场景。

库存数量模型示例

为了描述方便,我们使用简化的库存数量模型,真实场景中库存数据项会比我的示例多很多,但已经够说明原理。如下表,库存数量表(stockNum)包含商品标识和库存数量两个字段,库存数量代表有多少货可以卖。

字段名 英文名 字段类型
商品标识 skuId 长整型
库存数量 num 整数

传统通过数据库保证不超卖

库存管理的传统方案为了保证不超卖,都是使用数据库的事务来保证的:通过Sql判断剩余的库存数够用,多个并发执行update语句只有一个能执行成功;为了保证扣减不重复,会配合一个防重表来防止重复的提交,做到幂等性,防重表示例(antiRe)设计如下:

字段名 英文名 字段类型
标识 id 长整型
防重码 code 字符串(唯一索引)

比如一个下单过程的扣减过程示例如下:

1
2
3
4
sql复制代码事务开始
Insert into antiRe(code) value (‘订单号+Sku’)
Update stockNum set num=num-下单数量 where skuId=商品ID and num-下单数量>0
事务结束

面临系统流量越来越大,数据库的性能瓶颈就会暴露出来:就算分库分表也是没用的,促销的时候高并发都是针对少量商品的,最终并发流量会打向少数表,只能去提升单分片的抗量能力。我们接下来设计一种使用Redis缓存做库存扣减的方案。

综合使用数据库和Redis满足高并发扣减的原理

扣减库存其实包含两个过程:第一步是超卖校验,第二步是扣减数据的持久化;在传统数据库扣减中,两步是一起完成的。抗写的实现原理其实是巧妙的利用了分离的思想,分离开防超卖和数据持久化;首先防超卖是由Redis来完成的;通过Redis防超卖后,只要落库就可以;落库通过任务引擎,业务数据库使用商品分库分表,任务引擎任务通过单据号分库分表,热点商品的落库会被状态机分散开,消除热点。

整体架构如下:

库存抗写.png

第一关解决超卖检验:我们可以把数据放入Redis中,每次扣减库存,都对Redis中的数据进行incryby 扣减,如果返回的数量大于0,说明库存够,因为Redis是单线程,可以信任返回结果。第一关是Redis,可以抗高并发,性能Ok。超卖校验通过后,进入第二关。

第二关解决库存扣减:经过第一关后,第二关不需要再判断数量是否足够,只需要傻瓜扣减库存就行,对数据库执行如下语句,当然还是需要处理防重幂等的,不需要判断数量是否大于0了,扣减SQL只要如下写就可以。

1
2
3
4
sql复制代码事务开始
Insert into antiRe(code) value (‘订单号+Sku’)
Update stockNum set num=num-下单数量 where skuId=商品ID
事务结束

要点:最终还是要使用数据库,热点怎么解决的呢?任务库使用订单号进行分库分表,这样针对同一个商品的不同订单会散列在任务库的不同库存,虽然还是数据库抗量,但已经消除了数据库热点。

整体交互序列图如下:

库存扣减序列.png

热点防刷

但Redis也是有瓶颈的,如果出现过热SKU就会打向Redis单片,会造成单片性能抖动。库存防刷有个前提是不能卡单的。可以定制设计JVM内毫秒级时间窗的限流,限流的目的是保护Redis,尽可能的不限流。限流的极端情况就是商品本来应该在一秒内卖完,但实际花了两秒,正常并不会发生延迟销售,之所以选择JVM是因为如果采用远端集中缓存限流,还未来得及收集数据就已经把Redis打死。

实现方案可以通过guava之类的框架,每10ms一个时间窗,每个时间窗进行计数,单台服务器超过计数进行限流。比如10ms超过2个就限流,那么一秒一台服务器就是200个,50台服务器一秒就可以卖出1万个货,自己根据实际情况调整阈值就可以。

库存热点防刷.png

Redis扣减原理

Redis的incrby 命令可以用做库存扣减,扣减项可能多个,我们使用Hash结构的hincrby命令,先用Reids原生命令模拟整个过程,为了简化模型我们演示一个数据项的操作,多个数据项原理完全等同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock #查看苹果手机可售库存为1
"1"
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
127.0.0.1:6379> hget iphone inStock #验证剩余0
"0"
127.0.0.1:6379> hincrby iphone inStock -1 #应用并发超卖但Redis单线程返回剩余-1,下单失败
(integer) -1
127.0.0.1:6379> hincrby iphone inStock 1 #识别-1,回滚库存加一,剩余0
(integer) 0
127.0.0.1:6379> hget iphone inStock #库存恢复正常
"0"

扣减的幂等性保证

如果应用调用Redis扣减后,不知道是否成功,可以针对批量扣减命令增加一个防重码,对防重码执行setnx命令,当发生异常的时候,可以根据防重码是否存在来决定是否扣减成功,针对批量命名可以使用pipeline提高成功率。

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码// 初始化库存
127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock #查看苹果手机可售库存为1
"1"

// 应用线程一扣减库存,订单号a100,jedis开启pipeline
127.0.0.1:6379> set a100_iphone "1" NX EX 10 #通过订单号和商品防重码
OK
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
//结束pipeline,执行结果OK和0会一起返回

防止并发扣减后校验:为了防止并发扣减,需要对Redis的hincrby命令返回值是否为负数,来判断是否发生高并发超卖,如果扣减后的结果为负数,需要反向执行hincrby,把数据进行加回。

如果调用中发生网络抖动,调用Redis超时,应用不知道操作结果,可以通过get命令来查看防重码是否存在来判断是否扣减成功。

1
2
3
4
ruby复制代码127.0.0.1:6379> get a100_iphone   #扣减成功
"1"
127.0.0.1:6379> get a100_iphone #扣减失败
(nil)

单向保证

在很多场景中,因为没有使用事务,你很那做到不超卖,并且不少卖,所以在极端情况下,我的抉择是选择不超卖,但有可能少卖。当然还是应该尽量保证数据准确,不超卖,也不少卖;不能完全保证的前提下,选择不超卖单向保证,也要通过手段来尽可能减少少卖的概率。

比如如果扣减Redis过程中,命令编排是先设置防重码,再执行扣减命令失败;如果执行过程网络抖动可能放重码成功,而扣减失败,重试的时候就会认为已经成功,造成超卖,所以上面的命令顺序是错误的,正确写法应该是:

如果是扣减库存,顺序为:1.扣减库存 2.写入放重码。

如果是回滚库存,顺序为 1.写入放重码 2.扣减库存。

为什么使用PiPeline

在上面命令中,我们使用了Redis的Pipeline,来看下Pipeline的原理。

非pipeline模式
request–>执行
–>response
request–>执行
–>response
pipeline模式
request–>执行 server将响应结果队列化
request–>执行 server将响应结果队列化
–>response
–>response

使用Pipeline,能尽量保证多条命令返回结果的完整性,读者可以考虑使用Redis事务来代替Pipeline,实际项目中,个人有过Pipeline的成功抗量经验,并没有使用Redis事务,正常情况下事务比pipeline慢一些,所以没有采用。

Redis事务
1)mutil:开启事务,此后的所有操作将被添加到当前链接事务的“操作队列”中
2)exec:提交事务
3)discard:取消队列执行
4)watch:如果watch的key被修改,触发dicard。

通过任务引擎实现数据库的最终一致性

前面通过任务引擎来保证数据一定持久化数据库,「任务引擎」的设计如下,我们把任务调度抽象为业务无关的框架。「任务引擎」可以支持简单的流程编排,并保证至少成功一次。「任务引擎」也可以作为状态机的引擎出现,支持状态机的调度,所以「任务引擎」也可以称为「状态机引擎」,在此文是同一个概念。

**任务引擎设计核心原理:**先把任务落库,通过数据库事务保证子任务拆分和父任务完成的事务一致性。

**任务库分库分表:**任务库使用分库分表,可以支撑水平扩展,通过设计分库字段和业务库字段不同,无数据热点。

任务引擎的核心处理流程:

状态机引擎-实时任务编排调度.png

**第一步:**同步调用提交任务,先把任务持久化到数据库,状态为「锁定处理」,保证这件事一定得到处理。

注:原来的最初版本,任务落库是待处理,然后由扫描Worker进行扫描,为了防止并发重复处理,扫描后进行单个任务锁定,锁定成功再进行处理。后来优化为落库任务直接标识状态为「锁定处理」,是为了性能考虑,省去重新扫描再抢占任务,在进程内直接通过线程异步处理。

锁定Sql参考:

1
ini复制代码UPDATE 任务表_分表号 SET 状态 = 100,modifyTime = now() WHERE id = #{id} AND 状态 = 0

**第二步:**异步线程调用外部处理过程,调用外部处理完成后,接收返回子任务列表。通过数据库事务把父任务状态设置为已经完成,子任务落库。并把子任务加入线程池。

要点:保证子任务生成和父任务完成的事务性

**第三步:**子任务调度执行,并重新把新子任务落库,如果没有子任务返回,则整个流程结束。

异常处理Worker

异常解锁Worker来把长时间未处理完成的任务解锁,防止因为服务器重启,或线程池满造成的任务一直锁定无服务器执行。

补漏Worker防止服务器重启造成的线程池任务未执行完成,补漏程序重新锁定,触发执行。

任务状态转换过程

状态机引擎-状态转换.png

任务引擎数据库设计

任务表数据库结构设计示例(仅做示例使用,真实使用需要完善)

字段 类型 说明
任务ID标识 Long 主键
状态 Int 0待处理,100锁定处理,1完成
数据 String Json格式的业务数据
执行时间 Date 执行时间

任务引擎数据库容灾:

任务库使用分库分表,当一个库宕机,可以把路由到宕机库的流量重新散列到其他存活库中,可以手工配置,或通过系统监控来自动化容灾。如下图,当任务库2宕机后,可以通过修改配置,把任务库2流量路由到任务库1和3。补漏引擎继续扫描任务库2是因为当任务库2通过主从容灾恢复后,任务库2宕机时未来的及处理的任务可以得到补充处理。

任务引擎调度举例

比如用户购买了两个手机和一个电脑,手机和电脑分散在两个数据库,通过任务引擎先持久化任务,然后驱动拆分为两个子任务,并最终保证两个子任务一定成功,实现数据的最终一致性。整个执行过程的任务编排如下:


任务引擎交互流程:

状态机序列.png

差异对比-异构数据的终极解决方案

只要有异构,一定会有差异的,为了保证差异的影响可控,终极方案还是要靠差异对比来解决。本文篇幅所限,不再展开,后续再单独成文。DB和Redis差异对比的大概过程为:接收库存变化消息,不断跟进对比Redis和DB的数据是否一致,如果连续稳定不一致,则进行数据修复,用DB数据来修改Redis的数据。

常见问题答疑

问:第一步超卖校验Redis内存扣减,第二步扣减数据的持久化,中间断了怎么办?(例:服务重启)

答:如果是服务重启,会在服务器重启之前停止这台服务器的服务;但此方案并不能保证数据的绝对一致,比如扣减redis后,应用服务器故障直接死机,这种情况下的处理就需要更复杂的方案才能保证实时一致(目前我们没有采取更复杂方案),我们通过另一个方案使用库存数据和用户的订单数据进行数据比对修复,达到最终一致性。

本文转载自: 掘金

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

HTTP 方法中 POST 和 PUT 的区别和适用场景是什

发表于 2021-10-26

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

翻译自:

  • stackoverflow.com/questions/6…
  • stackoverflow.com/questions/4…
  • stackoverflow.com/questions/4…

问题:HTTP 方法中 POST 和 PUT 的区别是什么呢?他们分别的适用场景是什么呢?

你可以在网上找到这样的断言:

  • POST用于创建资源,PUT用于修改资源
  • PUT应该用于创建资源,POST应该用于修改资源

其实两者都不完全正确。

更好的方法是根据动作的幂等性在 PUT 和 POST 之间进行选择。

下面我们首先会在这篇文章介绍 幂等性(Idempotence) 的概念,说明哪些 HTTP 方法是幂等的,然后在下一篇文章分别对PUT和POST进行分析,最后进行总结。

幂等性(Idempotence)

幂等性是 HTTP 方法的一种属性。

如果多个完全相同的请求和单个请求,在服务器上的预期效果相同的话,这个请求方法则被认为是幂等的。值得一提的是,幂等是对服务器的资源状态产生影响(即请求完成后服务端的状态),而不是客户端接收到的响应状态码。

为了说明这一点,考虑被定义为幂等的 DELETE 方法,比如客户端进行一个 DELETE 方法的请求,为了删除一个服务器上的资源,服务器处理这个请求,这个资源被删除,并且服务器返回了 204 的状态码。接着客户端重复发送同样的 DELETE 请求,因为这个资源已经被删除了,服务器返回 404。

尽管客户端接收到的状态码不同,但是对于同样一个 URI,单个 DELETE 请求和多个 DELETE 请求产生的效果相同。

最后,如果客户端在能够读取服务器的响应之前发生故障,幂等的请求可以被自动重试。客户端知道重复请求可以产生相同的预期结果,虽然它们的响应可能不同。

所以总结一下,幂等性的核心概念是 - 你可以多次发送请求,而无需对服务器状态做额外的更改。

那么哪些 HTTP 方法是幂等的呢?

安全方法(Safe Methods)

所有的 safe(安全)方法都是幂等的。

如果一个方法对于服务器来说是只读的,也就是说它不会对服务端的数据进行修改,那么它就是安全的。这些方法是安全的:GET,HEAD 和 OPTIONS

参考:www.rfc-editor.org/rfc/rfc7231…

幂等方法(Idempotent Methods)

在正确实现的条件下, GET , HEAD , PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的 safe 方法也都是幂等的。

参考:www.rfc-editor.org/rfc/rfc7231…

HTTP 方法总结

按照上面说的,HTTP 方法可以分为以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码+---------+------+------------+
| Method | Safe | Idempotent |
+---------+------+------------+
| CONNECT | no | no |
| DELETE | no | yes |
| GET | yes | yes |
| HEAD | yes | yes |
| OPTIONS | yes | yes |
| POST | no | no |
| PUT | no | yes |
| TRACE | yes | yes |
+---------+------+------------+

本文转载自: 掘金

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

1…464465466…956

开发者博客

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