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

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


  • 首页

  • 归档

  • 搜索

部署手册 Kubernetes 二进制部署手册 (收藏又

发表于 2021-07-13

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

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

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

Github : 👉 github.com/black-ant

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

一 . 前言

本篇文章梳理了二进制安装 Kubernetes 的主流程以及异常的解决流程.

在这里首先要感谢先驱者扩展的大路 , 节省了大量时间 , 个人参考文档配的时候 , 使用的最新版 , 或多或少出现了一些问题 , 在这里整理了下来 , 用于参考.

原文地址为 :简书 , 由于文章又被审核了 , 这里附上转载的地址知乎 , 可以参照原文配 , 也可以按照我的来.

二 . 公用模块配置

公用模块需要集群中每一台机器都进行配置. 这个环节需要为每个服务器安装 Docker 同时配置 Linux 基本配置

2.1 安装 Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// Step 1 : 安装docker所需的工具
yum install -y yum-utils device-mapper-persistent-data lvm2

// Step 2 : 配置阿里云的docker源 (这里我是腾讯云 ,所以没有阿里源)
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

// Step 3 : 指定安装这个版本的docker-ce
yum install docker-ce docker-ce-cli containerd.io

// Step 4 : 启动docker
systemctl enable docker && systemctl start docker

// 补充命令 :
-> 查看版本 :docker version
-> 查看指南 :docker -help
-> 查看正在运行的docker : docker ps

2.2 Kubernetes 基本配置

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复制代码// Step 1 : 关闭防火墙
systemctl disable firewalld
systemctl stop firewalld

-------------------

// Step 2 : 关闭selinux , 可以选择临时或者永久
// - 临时禁用selinux
setenforce 0
// - 永久关闭 修改/etc/sysconfig/selinux文件设置
sed -i 's/SELINUX=permissive/SELINUX=disabled/' /etc/sysconfig/selinux
sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config

-------------------

// Step 3 : 禁用交换分区
// - 临时禁用
swapoff -a
// - 永久禁用,打开/etc/fstab注释掉swap那一行。
sed -i 's/.*swap.*/#&/' /etc/fstab

-------------------

// Step 4 : 修改内核参数
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system

三 . Kubernetes Master 配置

安装好基础配置后 , 就可以开启 Master 服务器的配置了

3.1 Master 安装主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码// Step 1 : 执行配置k8s阿里云源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF


// Step 2 : 安装kubeadm、kubectl、kubelet
yum install -y kubectl-1.21.2-0 kubeadm-1.21.2-0 kubelet-1.21.2-0

// Step 3 : 启动kubelet服务
systemctl enable kubelet && systemctl start kubelet

// Step 4 : 初始化 , 此环节记下配置操作(Step 5) 及 Token(Node 加入集群使用) 语句 -> PS31014
kubeadm init --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --kubernetes-version v1.21.2 --apiserver-advertise-address 11.22.33.111 --pod-network-cidr=10.244.0.0/16 --token-ttl 0

// Step 5 : 执行 admin 配置操作
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

// Step 6 : 查看 Node 节点情况 , 应该可以看到一个 notReady 的节点
kubectl get nodes

PS31014 初始化操作答疑

下载管理节点中用到的6个docker镜像,你可以使用docker images查看到 , 此时如果源配置正确 , 大概 5 分钟以内就会处理完成 , 但是这个过程中会查询很多问题

这里需要大概两分钟等待,会卡在[preflight] You can also perform this action in beforehand using ‘’kubeadm config images pull

  • image-repository : 该参数为镜像地址 ,如果下载慢或者 timeout , 需要重新选择新的地址
  • kubernetes-version : 当前的版本, 可以去官方查询最新版本
  • apiserver-advertise-address : 该地址为你的 apiServer 地址 , node 会调用该地址 (该地址需要外部可调)
  • pod-network-cidr : 指定pod网络的IP地址范围,它的值取决于你在下一步选择的哪个网络网络插件
    • 10.244.0.0/16 : Flannel
    • 192.168.0.0/16 : Calico

3.2 init 初始化完成的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//安装成功后会得到如下的信息 : 

Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

// 以及一条 Token 语句 , Node 节点通过该语句加入集群 , 如下
kubeadm join 11.22.33.111:6443 --token 2onice.mrw3b6dxcsdm5huv \
--discovery-token-ca-cert-hash sha256:0aafa06c71a936868sde3e1fbf82d9fbsadf233da24c774ca80asdc0ccd36d09

如果你一次性拿到了那个结果 , 恭喜一切顺利 , 如果出现异常 , 请参考如下问题记录 :

3.3 detected “cgroupfs” as the Docker cgroup driver. The recommended driver is “systemd”.

问题原因 : 此处由于你的 Docker 存在问题
解决方案 : 修改 Cgroup , 参考自 Hellxz博客

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复制代码// Step 1 : 问题的判断
- 输出 Group 类型 : docker info|grep "Cgroup Driver"

// Step 2 : 重置 kubeadm配置
kubeadm reset
// 或者使用 echo y|kubead reset

// Step 3 : 修改 Docker
1. 打开 /etc/docker/daemon.json
2. 添加 "exec-opts": ["native.cgroupdriver=systemd"]
// PS : 没有可以直接创建 , 最终效果如下
{
"exec-opts":["native.cgroupdriver=systemd"]
}

// Step 4 : 修改 kubelet
cat > /var/lib/kubelet/config.yaml <<EOF
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
EOF

// Step 4 : 重启服务
systemctl daemon-reload
systemctl restart docker
systemctl restart kubelet

// Step 5 : 校验结果 , 应该输出为 systemd
docker info|grep "Cgroup Driver"

// 补充 :
kubelet 的配置文件 : /var/lib/kubelet/kubeadm-flags.env

3.4 Error response from daemon: Head registry-1.docker.io/v2/coredns/…: connection reset by peer

问题原因 : 主要原因为docker 源的配置问题

1
2
3
4
java复制代码// 修改 /etc/docker/daemon.json 中镜像的配置 , 可以直接去阿里云中申请
{
"registry-mirrors":["https://......mirror.aliyuncs.com"]
}

3.5 failed to pull image …./coredns:v1.8.0: output: Error response from daemon: manifest for …../coredns:v1.8.0 not found: manifest unknown

问题原因 : 这里最核心的关键字是 coredns ,该问题是镜像中下载 coredns 出现问题

关键字 : coredns , coredns:v1.8.0 , manifest unknown , registry.aliyuncs.com/google_containers/coredns

1
2
3
4
5
6
7
java复制代码
// Step 1 : docker 拉取 coredns
docker pull coredns/coredns:1.8.0
docker tag coredns/coredns:1.8.0 registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.0

// --- 同时修改 init 的 image-repository 属性 , 例如 (详见 Master 主流程)
kubeadm init --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers ....

3.6 Failed to watch *v1.Service: failed to list *v1.Service: Get “h……/api/v1/services?limit=500&resourceVersion=0”: dial tcp …..:6443: connect: connection refused

问题原因 : 原因为 api server 没有启动 , 这种情况主要是 init 后 ,但是运行时出现

1
2
3
4
5
6
java复制代码
// Step 1 : 查看docker 服务 , 可以看到对应的 K8S 服务
docker ps -a | grep kube | grep -v pause

// Step 2 : docker 查看 log 并且解决
docker logs 70bc13ce697c

3.7 listen tcp 81.888.888.888:2380: bind: cannot assign requested address

1
java复制代码详情请看 : 6.1

如果看到这里 , 你的问题还没有解决 , 参考第六节 ,问题的排查和解决流程 !!!!!!!!!!!!!!!!!!

四 . Kubernetes Nodes 配置

第一不要忘记第一节通用模块中的处理!!!

4.1 Node 创建主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码// Step 1 : 配置阿里源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

// Step 2 : 安装kubeadm、kubectl、kubelet
yum install -y kubeadm-1.21.2-0 kubelet-1.21.2-0

// Step 3 : 启动kubelet服务
systemctl enable kubelet && systemctl start kubelet

// Step 4 : 加入集群 (注意 , 此处时前文获取到的)
kubeadm join 11.22.33.111:6443 --token 2onice.mrw3b6dxcsdm5huv --discovery-token-ca-cert-hash sha256:0aafa06c71a936868sde3e1fbf82d9fbsadf233da24c774ca80asdc0ccd36d09

// Step 5 : check , 如果一切正常 , 在 Master 中可以获取到如下结果
[root@VM-0-5-centos ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
localhost.localdomain NotReady <none> 5m24s v1.21.2
vm-0-5-centos NotReady control-plane,master 37h v1.21.2

如果安装失败, 会出现如下问题 :

4.2 configmaps “cluster-info” is forbidden: User “system:anonymous” cannot get resource “configmaps” in API group “” in the namespace “kube-public”

问题原因 : 此处是匿名登录的问题 , 在测试环境中 , 不需要太复杂 , 添加匿名即可

1
2
3
java复制代码kubectl create clusterrolebinding test:anonymous --clusterrole=cluster-admin --user=system:anonymous

// 正式环境解决 : TODO

4.3 error execution phase preflight: couldn’t validate the identity of the API Server: configmaps “cluster-info” not found

问题原因 : 安装 Master 的时候 init 不成功 ,导致 API Server 参数获取出现问题 , 重装 , 注意 init 时的替换操作 (4.)

1
java复制代码这种问题需要排查 Master 的问题 , 详见 6.2 Master 排查流程

4.5 Failed to load kubelet config file” err=”failed to load Kubelet config file /var/lib/kubelet/config.yaml

1
2
java复制代码如果是要把节点加到集群中 , 再未运行加入命令之前 , 该配置确实为空 <br>
**当运行了加入集群命令后 , 会主动生成配置**

4.6 failed to pull image k8s.gcr.io/kube-proxy:v1.21.2: output: Error response from daemon

1
java复制代码kubeadm config images pull --image-repository=registry.aliyuncs.com/google_containers

4.7 err=”failed to run Kubelet: misconfiguration: kubelet cgroup driver: “systemd” is different from docker cgroup driver: “cgroupfs””

1
java复制代码**详见 3.2**

五 . Flannel 安装

5.1 什么是 Fiannel ?

1
2
3
java复制代码Flannel是CoreOS团队针对Kubernetes设计的一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。

// TODO : Fiannel 详情

5.2 Flannel 的安装

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
// Step 1 : 准备 kube-flannel.yml
详见附录 , 主要是修改 url 下载路径

// Step 2 : kubectl 安装
kubectl apply -f kube-flannel.yml

// 配置好之后稍等一会就可以看到节点就绪
[root@VM-0-5-centos flannel]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
localhost.localdomain Ready <none> 131m v1.21.2
vm-0-5-centos Ready control-plane,master 39h v1.21.2

5.3 “Unable to update cni config” err=”no networks found in /etc/cni/net.d”

注意 ,此处有2种场景 :

场景一 : Master 出现该问题 , flannel 可能版本和 K8S 不匹配 , 我使用的 K8S 为 1.21 , 重新安装 0.13 的 flannel 后正常

场景二 : node 出现该问题 , 原因为 node 节点缺少 cni @ blog.csdn.net/u010264186/…

  1. 复制 master cni 文件到 node 中 : scp -r master:/etc/cni /etc/
  2. 重启 : systemctl daemon-reload && systemctl restart kubelet

核心关键在 cni 文件的创建 ,成功后会在 /etc/cni/net.d 下出现一个 10-flannel.conflist 文件夹

六 . 问题及排查流程

6.1 Master kubelet 问题异常排查步骤 (以 bind: cannot assign requested address 为例)

如果上述的问题解决方案还是无法解决你的问题 , 你可能需要自行排查和定义相关的问题

通常问题会处在 init 环节中 , 如果在之前 ,大概率是镜像地址的问题 , 自行调整

问题详情: init 初始化出现问题 , 始终执行失败

解决思路:

  • 判断 Docker 运行情况
  • 查看对应 pod log
  • 根据 log 解决问题
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
js复制代码// Step 1 : 查看 Docker 运行情况 
docker ps -a | grep kube | grep -v pause

// 这个环节可以看到异常的实例 , 如下就是 etcd 和 API server 出现了问题
"etcd --advertise-cl…" 40 seconds ago Exited (1) 39 seconds ago
"kube-apiserver --ad…" 39 seconds ago Exited (1) 18 seconds ago

---------------------

// Step 2 : 查看 Pod 对应的 log 及 查看 kubelet log
docker logs ac266e3b8189
journalctl -xeu kubelet

// 这里可以看到最终的问题详情
api-server : connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:2379: connect: connection refused". Reconnecting
// PS : 了解到 127.0.0.1:2379 是 etcd 的端口 (81.888.888.888 是我服务器的 IP )
etc-server : listen tcp 81.888.888.888:2380: bind: cannot assign requested address

---------------------

// Step 3 :这里很明显就是 etcd 的问题了 , 解决问题 (查找资料后判断是 etcd 的问题 )
修改 etcd 的配置文件 /etc/kubernetes/manifests/etcd.yml , 将 IP 修改为 0.0.0.0 , 也就是没做任何限制
- --listen-client-urls=https://0.0.0.0:2379,https://0.0.0.0:2379(修改的位置)
- --listen-peer-urls=https://0.0.0.0:2380(修改的位置)


---------------------

// Step 4 : 备份上一步的 etcd.yml , 重置 K8S
kubeadm reset

// PS : 重置的过程中 , 会将 manifests 中的东西删除 , 此处记得要取出备份

---------------------

// Step 5 : 替换文件
- 重新初始化集群
- 当/etc/kubernetes/manifests/etcd.yaml被创建出来时 , 迅速将etcd.yaml文件删除
- 将重置节点之前保存的etcd.yaml文件移动到/etc/kubernetes/manifests/目录

// PS : 操作完成后 , init 还在下载镜像 , 后续就安装成功


// 补充命令 :
- 重启 kubelet : systemctl restart kubelet.service
- 查看 kubelet 日志 : journalctl -xeu kubelet
- 查看 kubelet 状态 : systemctl status kubelet
- 查看 Docker 运行情况 : docker ps -a | grep kube | grep -v pause
- 查看 log : docker logs ac266e3b8189
- 获取所有的 node 节点 : kubectl get nodes

七 . 操作命令补充

7.1 完全卸载 Kubernetes

1
2
3
4
5
6
7
8
java复制代码# 卸载服务
kubeadm reset

# 删除rpm包
rpm -qa|grep kube*|xargs rpm --nodeps -e

# 删除容器及镜像
docker images -qa|xargs docker rmi -f

7.2 API Server

1
2
3
4
5
6
java复制代码https://youKurbernatesHost:6443/


// 常用 API 接口
- 访问 Node 节点 : /api/v1/nodes
- 访问 Pods 节点 : /api/v1/pods

7.3 Master 常见命令

1
java复制代码显示 Token 列表 : kubeadm token list

总结

虽然Kubernetes 每次都是一样的流程部署 ,但是每次都会出现各种各样的问题...

零零碎碎记录了这么多, 后续继续补充 TODO

附录

kube-flannel.yml 文件

原版的 URL 还是存在问题 , 主要修改 quay-mirror.qiniu.com/coreos/flannel:v0.13.0-ppc64le

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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
yml复制代码---
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: psp.flannel.unprivileged
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
spec:
privileged: false
volumes:
- configMap
- secret
- emptyDir
- hostPath
allowedHostPaths:
- pathPrefix: "/etc/cni/net.d"
- pathPrefix: "/etc/kube-flannel"
- pathPrefix: "/run/flannel"
readOnlyRootFilesystem: false
# Users and groups
runAsUser:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
fsGroup:
rule: RunAsAny
# Privilege Escalation
allowPrivilegeEscalation: false
defaultAllowPrivilegeEscalation: false
# Capabilities
allowedCapabilities: ['NET_ADMIN']
defaultAddCapabilities: []
requiredDropCapabilities: []
# Host namespaces
hostPID: false
hostIPC: false
hostNetwork: true
hostPorts:
- min: 0
max: 65535
# SELinux
seLinux:
# SELinux is unused in CaaSP
rule: 'RunAsAny'
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: flannel
rules:
- apiGroups: ['extensions']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['psp.flannel.unprivileged']
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flannel
subjects:
- kind: ServiceAccount
name: flannel
namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: flannel
namespace: kube-system
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-flannel-cfg
namespace: kube-system
labels:
tier: node
app: flannel
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-amd64
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
hostNetwork: true
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay.io/coreos/flannel:v0.13.0-amd64
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay.io/coreos/flannel:v0.13.0-amd64
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-arm64
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- arm64
hostNetwork: true
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-arm64
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-arm64
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-arm
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- arm
hostNetwork: true
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-arm
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-arm
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-ppc64le
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- ppc64le
hostNetwork: true
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-ppc64le
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-ppc64le
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-s390x
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- s390x
hostNetwork: true
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-s390x
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay-mirror.qiniu.com/coreos/flannel:v0.13.0-s390x
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg

参考和感谢

blog.csdn.net/haishan8899…

www.cnblogs.com/hellxz/p/ku…

my.oschina.net/u/4479011/b…

kubernetes.io/docs/tasks/…

docs.docker.com/engine/inst…

www.jianshu.com/p/25c01cae9…

本文转载自: 掘金

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

SpringBoot-RabbitMQ篇(1)-四大交换器

发表于 2021-07-13

零、文章前言

  1. SpringBoot-RabbitMQ高级篇系列开始更新,本系列主要为SpringBoot整合RabbitMQ,实现高可用、可靠传递等
  2. 核心有四大交换器、死信队列、可靠传递、异常消费处理
  3. 系列共计三篇文章,本系列核心主讲整合和企业级内容,需要先具备SpringBoot和RabbitMQ基础知识
  4. 文章源码放到了网盘,没有放git仓库,需要的自行下载,脚本等信息在common下面
  5. 个人水平有限,有错误的地方欢迎指正

链接: pan.baidu.com/s/1lpZC6fr8… 提取码: qtvi

一、环境搭建

  1. 采用maven多module模式,共计创建三个子module
    • common:通用实体信息
    • rabbitmq-publisher:消息发布者,基于SpringBoot
    • rabbitmq-subscriber:消息订阅者,基于SpringBoot
  2. 在消息发布者和订阅者两个项目中加入rabbitmq maven依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 在两个项目中加入rabbitmq的配置信息
1
2
3
4
5
6
7
8
yaml复制代码spring:
rabbitmq:
host: xxx.xxx.xxx.xxx
port: 5672
username: username
password: password
# 虚拟主机,需要后台先配置
# virtual-host: springboot
  1. 上述三步完成后,rabbitmq的基础环境搭建完成
  2. rabbitmq配置属性类
    • org.springframework.boot.autoconfigure.amqp.RabbitProperties

二、四大交换器

2.1 direct - 直连交换器

2.1.1 消息发送者

  1. 在消息发布者中新建配置类,声明交换器信息
    • 只用声明交换器,队列和交换器绑定是订阅者操作
    • 不同的类型提供不同的交换器
    • 如果只声明交换器并不会创建交换器,而是绑定时或者发送消息时才创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmqpPublisherConfig {
@Bean
public DirectExchange emailDirectExchange() {
// 声明方式一
// return new DirectExchange("exchange.direct.springboot.email");
// 声明方式二
return ExchangeBuilder.directExchange("exchange.direct.springboot.email").build();
}
}
  1. 发送消息时,使用的是RabbitTemplate,为SpringBoot提供的RabbitMQ消息发送器
    • org.springframework.amqp.rabbit.core.RabbitTemplate
    • 发送消息示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class PublishController {
@Resource
private RabbitTemplate rabbitTemplate;

@RequestMapping("/direct")
public Object direct(String message) {
try {
rabbitTemplate.convertAndSend("交换器", "路由键", message);
return message;
} catch (AmqpException e) {
System.out.println(e.getMessage());
return "网络中断,请稍后再试~";
}
}
}

2.2.2 消息接收者

  1. 接收者需要配置以下内容
    • 交换器:直接new对应的交换器类型
    • 队列:只有Queue类型,通过名称区分
    • 交换器和队列的绑定:通过BindingBuilder.bind(队列).to(交换器).with(路由键);
    • 只声明交换器和队列绑定,并不会马上创建,而是在发送消息或者监听队列时才会创建
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复制代码import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmqpSubscriberConfig {
/**
* 直连交换器
*/
@Bean
public DirectExchange emailDirectExchange() {
// 声明方式一
// return new DirectExchange("exchange.direct.springboot.email");
// 声明方式二
return ExchangeBuilder.directExchange("exchange.direct.springboot.email").build();
}

/**
* 声明队列
*/
@Bean
public Queue emailQueue() {
// 声明方式一
// return new Queue("queue.direct.springboot.email");
// 声明方式二
return QueueBuilder.durable("queue.direct.springboot.email").build();
}

/**
* 交换器和队列绑定
*/
@Bean
@Resource
public Binding emailBiding(Queue emailQueue, DirectExchange emailDirectExchange) {
// 将路由使用路由键绑定到交换器上
return BindingBuilder.bind(emailQueue).to(emailDirectExchange).with("springboot.email.routing.key");
}
}
  1. 监听队列
    • 监听的队列必须存在,否则将会报错
    • 监听的队列消费完成会自动确认消息
    • 如果多个队列同时监听一个队列,则消息会轮训地由不同方法处理
    • 可以在参数中指定接收类型,消息将会自动转为对应类型
    • 也可以指定Message参数获取对应消息信息
      • org.springframework.amqp.core.Message
      • 获取消息属性:message.getMessageProperties()
      • 获取消息内容:message.getBody()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
* 消息订阅监听
*/
@Component
public class SubscriberListener {
/**
* direct监听,相同监听队列消息将会轮流处理
*/
@RabbitListener(queues = "queue.direct.springboot.email")
public void receiver01(String msg) {
System.out.println("receiver01 message = " + msg);
}

@RabbitListener(queues = "queue.direct.springboot.email")
public void receiver02(String msg) {
System.out.println("receiver02 message = " + msg);
}
}

2.1.3 消息发布订阅

  1. 先启动订阅者,可以看到队列声明


2. 启动发布者,然后发布消息

  • http://127.0.0.1:8071/direct?message=direct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class PublishController {
@Resource
private RabbitTemplate rabbitTemplate;

@RequestMapping("/direct")
public Object direct(String message) {
try {
// 指定发送的交换器和路由键
rabbitTemplate.convertAndSend("exchange.direct.springboot.email", "springboot.email.routing.key", message);
return message;
} catch (AmqpException e) {
System.out.println(e.getMessage());
return "网络中断,请稍后再试~";
}
}
}
  1. 订阅者会轮流收到信息
1
2
3
4
5
6
java复制代码receiver01 message = direct
receiver02 message = direct
receiver01 message = direct
receiver02 message = direct
receiver01 message = direct
receiver02 message = direct

2.2 topic - 主题交换器

2.2.1 消息发送者

  1. 声明topic交换器
1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BlogPublisherConfig {
@Bean
public Exchange blogTopicExchange() {
return ExchangeBuilder.topicExchange("exchange.topic.springboot.blog").build();
}
}
  1. 声明controller
1
2
3
4
5
java复制代码@RequestMapping("/topic")
public Object topic(String routingKey, String message) {
rabbitTemplate.convertAndSend("exchange.topic.springboot.blog", routingKey, message);
return routingKey + " : " + message;
}

2.2.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class BlogSubscriberConfig {
/**
* 主题交换器
*/
@Bean
public TopicExchange blogTopicExchange() {
return ExchangeBuilder.topicExchange("exchange.topic.springboot.blog").build();
}

@Bean
public Queue blogJavaQueue() {
return QueueBuilder.durable("queue.topic.springboot.blog.java").build();
}

@Bean
public Queue blogMqQueue() {
return QueueBuilder.durable("queue.topic.springboot.blog.mq").build();
}

@Bean
public Queue blogAllQueue() {
return QueueBuilder.durable("queue.topic.springboot.blog.all").build();
}

@Bean
@Resource
public Binding blogJavaBinding(TopicExchange blogTopicExchange, Queue blogJavaQueue) {
return BindingBuilder.bind(blogJavaQueue).to(blogTopicExchange).with("springboot.blog.java.routing.key");
}

@Bean
@Resource
public Binding blogMqBinding(TopicExchange blogTopicExchange, Queue blogMqQueue) {
return BindingBuilder.bind(blogMqQueue).to(blogTopicExchange).with("springboot.blog.mq.routing.key");
}

@Bean
@Resource
public Binding blogAllBinding(TopicExchange blogTopicExchange, Queue blogAllQueue) {
// #: 匹配一个或者多个 *:匹配一个
return BindingBuilder.bind(blogAllQueue).to(blogTopicExchange).with("springboot.blog.#.routing.key");
}
}
  1. 监听队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class BlogService {
/**
* topic监听
*/
@RabbitListener(queues = "queue.topic.springboot.blog.java")
public void blogJavaListener(String message) {
System.out.println("blogJavaListener message = " + message);
}

@RabbitListener(queues = "queue.topic.springboot.blog.mq")
public void blogMqListener(String message) {
System.out.println("blogMqListener message = " + message);
}

@RabbitListener(queues = "queue.topic.springboot.blog.all")
public void blogAllaListener(String message) {
System.out.println("blogAllListener message = " + message);
}
}

2.2.3 消息发布订阅

  1. 发布者发送消息
    • http://localhost:8071/topic?routingKey=springboot.blog.java.routing.key&message=hello
    • http://localhost:8071/topic?routingKey=springboot.blog.mq.routing.key&message=hello
  2. 订阅者收到消息
    • 全匹配和模糊匹配
    • 全匹配无论是哪个都会被匹配上
1
2
3
4
5
java复制代码blogJavaListener message = hello
blogAllListener message = hello

blogAllListener message = hello
blogMqListener message = hello

2.3 fanout - 广播交换器

2.3.1 消息发送者

  1. 声明fanout交换器
1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.amqp.core.FanoutExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NoticePublisherConfig {
@Bean
public Exchange radioFanoutExchange() {
return ExchangeBuilder.fanoutExchange("exchange.fanout.springboot.radio").build();
}
}
  1. 声明controller
1
2
3
4
5
java复制代码@RequestMapping("/fanout")
public Object fanout(String message) {
rabbitTemplate.convertAndSend("exchange.fanout.springboot.radio", null, message);
return message;
}

2.32 消息接收者

  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
java复制代码import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class NoticeSubscriberConfig {
@Bean
public FanoutExchange radioFanoutExchange() {
return ExchangeBuilder.fanoutExchange("exchange.fanout.springboot.radio").build();
}

@Bean
public Queue radioQueue() {
return QueueBuilder.durable("queue.fanout.springboot.radio").build();
}

@Bean
@Resource
public Binding radioBinding(FanoutExchange radioFanoutExchange, Queue radioQueue) {
// 广播交换器绑定没有路由键,只要绑定即可收到
return BindingBuilder.bind(radioQueue).to(radioFanoutExchange);
}
}
  1. 监听队列
1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class NoticeService {

@RabbitListener(queues = "queue.fanout.springboot.radio")
public void radioListener(String message) {
System.out.println("radioListener message = " + message);
}
}

2.3.3 消息发布订阅

  1. 发布者发送消息
    • http://localhost:8071/fanout?message=fanout
  2. 订阅者收到消息
1
java复制代码radioListener message = fanout

2.4 headers - 头交换器

2.4.1 消息发送者

  1. headers模式通过头匹配,会忽略路由键
  2. 发送者需要创建队列
1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.amqp.core.HeadersExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HeadersPublisherConfig {
@Bean
public Exchange radioHeadersExchange() {
return ExchangeBuilder.headersExchange("exchange.headers.springboot.headers").build();
}
}
  1. 创建controller发送消息
    • MessageProperties和Message包是:org.springframework.amqp.core
    • 需要创建MessageProperties对象用于设置头信息
    • Message用于存储消息和消息属性信息
1
2
3
4
5
6
7
8
9
java复制代码@RequestMapping("/headers")
public Object headers(@RequestParam Map<String, String> param) {
MessageProperties properties = new MessageProperties();
properties.setHeader("name", param.get("name"));
properties.setHeader("token", param.get("token"));
Message mqMessage = new Message(param.get("message").getBytes(), properties);
rabbitTemplate.convertAndSend("exchange.headers.springboot.headers", null, mqMessage);
return properties;
}

2.4.2 消息接收者

  1. 接收者和上面三种一样,同样需要声明交换器、队列、绑定
  • 在队列绑定时需要使用不同规则
    • BindingBuilder.bind(headersQueue01).to(headersExchange).whereAll(key).match()
      • 所有字段属性和值全部匹配
    • BindingBuilder.bind(headersQueue02).to(headersExchange).whereAny(key).match()
      • 任意字段属性和值全部匹配
    • BindingBuilder.bind(headersQueue03).to(headersExchange).whereAll(“name”, “token”).exist()
      • 指定所有属性字段存在
    • BindingBuilder.bind(headersQueue03).to(headersExchange).whereAny(“name”, “token”).exist()
      • 指定任意属性存在
  • headerMap中存放的属性就是发送者中封装的属性,属性完全匹配则正确路由到此处
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复制代码import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.HeadersExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class HeadersSubscriberConfig {
@Bean
public HeadersExchange headersExchange() {
return ExchangeBuilder.headersExchange("exchange.headers.springboot.headers").build();
}

@Bean
public Queue headersQueue01() {
return QueueBuilder.durable("queue.headers.springboot.01").build();
}

@Bean
public Queue headersQueue02() {
return QueueBuilder.durable("queue.headers.springboot.02").build();
}

@Bean
public Queue headersQueue03() {
return QueueBuilder.durable("queue.headers.springboot.03").build();
}

@Bean
@Resource
public Binding headers01Binding(HeadersExchange headersExchange,Queue headersQueue01) {
Map<String, Object> key = new HashMap<>(4);
key.put("name", "java");
key.put("token", "001");
return BindingBuilder.bind(headersQueue01).to(headersExchange).whereAll(key).match();
}

@Bean
@Resource
public Binding headers02Binding(HeadersExchange headersExchange,Queue headersQueue02) {
Map<String, Object> key = new HashMap<>(4);
key.put("name", "java");
key.put("token", "002");
return BindingBuilder.bind(headersQueue02).to(headersExchange).whereAny(key).match();
}

@Bean
@Resource
public Binding headers03Binding(HeadersExchange headersExchange,Queue headersQueue03) {
// name和token都需要存在
return BindingBuilder.bind(headersQueue03).to(headersExchange).whereAll("name", "token").exist();
// 任意name或者token存在
// return BindingBuilder.bind(headersQueue03).to(headersExchange).whereAny("name", "token").exist();
}
}
  1. 队列监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class HeadersService {

@RabbitListener(queues = "queue.headers.springboot.01")
public void headers01Listener(String message) {
System.out.println("headers01Listener message = " + message);
}

@RabbitListener(queues = "queue.headers.springboot.02")
public void headers02Listener(String message) {
System.out.println("headers02Listener message = " + message);
}

@RabbitListener(queues = "queue.headers.springboot.03")
public void headers03Listener(String message) {
System.out.println("headers03Listener message = " + message);
}
}

2.4.3 消息发布订阅

  1. 发送消息
    • http://localhost:8071/headers?name=java&token=001&message=headers
    • http://localhost:8071/headers?name=java&token=002&message=headers
    • http://localhost:8071/headers?name=mq&token=003&message=headers
  2. 接收消息
1
2
3
4
5
6
7
8
java复制代码headers01Listener message = headers
headers02Listener message = headers
headers03Listener message = headers

headers02Listener message = headers
headers03Listener message = headers

headers03Listener message = headers

本文转载自: 掘金

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

pandas系列之csv文件的基本操作 1文件导入 2文

发表于 2021-07-13

本文所使用的CSV内容截图如下:

image.png

1.文件导入

1.1 不同分隔符导入

1.1.1 以逗号分隔符导入

使用read_csv()方法导入csv文件,该方法默认文件中的数据都是以逗号作为分隔符的

1
2
3
4
python复制代码import pandas as pd

df = pd.read_csv(r'C:\Users\admin\Desktop\数据分析测试表.csv')
print(df)

result:

1
2
3
4
5
6
复制代码   区域  省份  城市
0 东北 辽宁 大连
1 西北 陕西 西安
2 华南 广东 深圳
3 华北 北京 北京
4 华中 湖北 武汉

1.1.2 其他指定分隔符导入

若csv文件不是以逗号分隔,而是洽谈符号,此时读取需要使用sep参数来指定分隔符,否则会报错

eg:

1
2
3
4
python复制代码import pandas as pd

df = pd.read_csv(r'C:\Users\admin\Desktop\数据分析测试表.csv',sep=" ")
print(df)

result:

1
2
3
4
5
6
复制代码   区域  省份  城市
0 东北 辽宁 大连
1 西北 陕西 西安
2 华南 广东 深圳
3 华北 北京 北京
4 华中 湖北 武汉

1.2 导入部分数据

文件较大时,可以只导入前几行数据

1
2
python复制代码df = pd.read_csv(r'C:\Users\admin\Desktop\中文\数据分析测试表.csv', nrows=1)
print(df)

result:

区域 省份 城市
0 东北 辽宁 大连

2.文件编码

2.1 utf-8编码

image-20210712231358904.png
保存CSV格式文件时如果选择UTF-8(逗号分隔),导入时需要添加encoding参数,

1
2
python复制代码df = pd.read_csv(r'C:\Users\admin\Desktop\数据分析测试表.csv', encoding='utf-8')
print(df)

result:

1
2
3
4
5
6
复制代码   区域  省份  城市
0 东北 辽宁 大连
1 西北 陕西 西安
2 华南 广东 深圳
3 华北 北京 北京
4 华中 湖北 武汉

你也可以不加encoding参数,因为python默认的编码格式就是utf-8编码。此时就和1导入CSV的结果一致

1
2
python复制代码df = pd.read_excel(r'C:\Users\admin\Desktop\数据分析测试表.csv')
print(df)

result:

1
2
3
4
5
6
复制代码   区域  省份  城市
0 东北 辽宁 大连
1 西北 陕西 西安
2 华南 广东 深圳
3 华北 北京 北京
4 华中 湖北 武汉

2.2 gbk编码

QQ拼音截图未命名.png

此时须设置encoding参数的值为gbk,否则会报错

1
2
python复制代码df = pd.read_csv(r'C:\Users\admin\Desktop\数据分析测试表.csv', encoding='gbk')
print(df)

result:

1
2
3
4
5
6
复制代码   区域  省份  城市
0 东北 辽宁 大连
1 西北 陕西 西安
2 华南 广东 深圳
3 华北 北京 北京
4 华中 湖北 武汉

3.中文路径问题

CSV文件路径含有中文时,低版本的pandas读取文件时会报错。

有4个解决办法:

(1)将中文路径改为英文路径

(2)升级pandas版本,我电脑上目前装的1.3.0版本支持中文路径

(3)在文件地址名前加open

1
2
python复制代码df = pd.read_csv(open(r'C:\Users\admin\Desktop\中文\数据分析测试表.csv'))
print(df)

(4)添加engine参数

1
2
python复制代码df = pd.read_csv(r'C:\Users\admin\Desktop\中文\数据分析测试表.csv', engine='python', encoding='utf-8-sig')
print(df)

read_csv()方法默认采用c语言作为解析语言,此时只需要将c换成python就可以了。如果CSV编码方式是utf-8,需要将utf-8替换为utf-8-sig

注:方法3和方法4目前没有使用过,效果未知

4.行列索引以及数据读取问题

和xlsx文件操作方式一致,参见juejin.cn/post/698366…

本文转载自: 掘金

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

一文彻底弄懂cookie、session、token

发表于 2021-07-13

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

前言

作为一个JAVA开发,之前有好几次出去面试,面试官都问我,JAVAWeb掌握的怎么样,我当时就不知道怎么回答,Web,日常开发中用的是什么?今天我们来说说JAVAWeb最应该掌握的三个内容。

发展历程

1、很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应, 尤其是我不用记住是谁刚刚发了HTTP请求, 每个请求对我来说都是全新的。

2、但是随着交互式Web应用的兴起,像在线购物网站,需要登录的网站等等,马上就面临一个问题,那就是要管理会话,必须记住哪些人登录系统, 哪些人往自己的购物车中放商品, 也就是说我必须把每个人区分开,这就是一个不小的挑战,因为HTTP请求是无状态的,所以想出的办法就是给大家发一个会话标识(session id), 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了。

3、这样大家很嗨皮了,可是服务器就不嗨皮了,每个人只需要保存自己的session id,而服务器要保存所有人的session id ! 如果访问服务器多了, 就得由成千上万,甚至几十万个。

这对服务器说是一个巨大的开销 , 严重的限制了服务器扩展能力, 比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统, 那session id会保存在机器A上, 假设小F的下一次请求被转发到机器B怎么办? 机器B可没有小F的 session id啊。

有时候会采用一点小伎俩: session sticky , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。

那只好做session 的复制了, 把session id 在两个机器之间搬来搬去, 快累死了。

image-20210712162145823

后来有个叫Memcached的支了招: 把session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍, 估计得被人骂死。

image-20210712162600049

也尝试把这个单点的机器也搞出集群,增加可靠性, 但不管如何, 这小小的session 对我来说是一个沉重的负担

4 于是有人就一直在思考, 我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好?

可是如果不保存这些session id , 怎么验证客户端发给我的session id 的确是我生成的呢? 如果不去验证,我们都不知道他们是不是合法登录的用户, 那些不怀好意的家伙们就可以伪造session id , 为所欲为了。

嗯,对了,关键点就是验证 !

比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。

不过这和session id没有本质区别啊, 任何人都可以可以伪造, 所以我得想点儿办法, 让别人伪造不了。

那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。

image-20210712163843772

这个token 我不保存, 当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id , 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。

image-20210712163519280

Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。

当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。

这样一来, 我就不保存session id 了, 我只是生成token , 然后验证token , 我用我的CPU计算时间获取了我的session 存储空间 !

解除了session id这个负担, 可以说是无事一身轻, 我的机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!

Cookie

1.什么是Cookie

Cookie翻译成中文的意思是‘小甜饼’,是由W3C组织提出,最早由Netscape社区发展的一种机制。目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。

服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。Cookie存储的数据量有限,且都是保存在客户端浏览器中。不同的浏览器有不同的存储大小,但一般不超过4KB。因此使用Cookie实际上只能存储一小段的文本信息(key-value格式)。

2.Cookie的机制

当用户第一次访问并登陆一个网站的时候,cookie的设置以及发送会经历以下4个步骤:

  1. 客户端发送一个请求到服务器;
  2. 服务器发送一个HttpResponse响应到客户端,其中包含Set-Cookie的头部;
  3. 客户端保存cookie,之后向服务器发送请求时,HttpRequest请求中会包含一个Cookie的头部;
  4. 服务器返回响应数据。

image-20210709083823647

为了探究这个过程,写了代码进行测试,如下:

我在doGet方法中,new了一个Cookie对象并将其加入到了HttpResponse对象中

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

@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);
}
}

浏览器输入地址进行访问,结果如图所示:

image-20210708111331506

可见Response Headers中包含Set-Cookie头部,而Request Headers中包含了Cookie头部。name和value正是上述设置的。

3.Cookie的属性

Expires

该属性用来设置Cookie的有效期。Cookie中的maxAge用来表示该属性,单位为秒。Cookie中通过getMaxAge()和setMaxAge(int maxAge)来读写该属性。maxAge有3种值,分别为正数,负数和0。

如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中(每个浏览器存储的位置不一致)。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。下面代码中的Cookie信息将永远有效。

1
2
3
4
java复制代码Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE,永久有效
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);

当maxAge属性为负数,则表示该Cookie只是一个临时Cookie,不会被持久化,仅在本浏览器窗口或者本窗口打开的子窗口中有效,关闭浏览器后该Cookie立即失效。

1
2
3
4
java复制代码Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE,永久有效
cookie.setMaxAge(-1);
resp.addCookie(cookie);

当maxAge为0时,表示立即删除Cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码Cookie[] cookies = req.getCookies();
Cookie cookie = null;

// get Cookie
for (Cookie ck : cookies) {
if ("jiangwang".equals(ck.getName())) {
cookie = ck;
break;
}
}

if (null != cookie) {
// 删除一个cookie
cookie.setMaxAge(0);
resp.addCookie(cookie);
}

修改或者删除Cookie

HttpServletResponse提供的Cookie操作只有一个addCookie(Cookie cookie),所以想要修改Cookie只能使用一个同名的Cookie来覆盖原先的Cookie。如果要删除某个Cookie,则只需要新建一个同名的Cookie,并将maxAge设置为0,并覆盖原来的Cookie即可。

新建的Cookie,除了value、maxAge之外的属性,比如name、path、domain都必须与原来的一致才能达到修改或者删除的效果。否则,浏览器将视为两个不同的Cookie不予覆盖。

Cookie的域名

Cookie是不可以跨域名的,隐私安全机制禁止网站非法获取其他网站的Cookie。

正常情况下,同一个一级域名下的两个二级域名也不能交互使用Cookie,比如a1.jiangwang.com和a2.jiangwang.com,因为二者的域名不完全相同。如果想要jiangwnag.com名下的二级域名都可以使用该Cookie,需要设置Cookie的domain参数为.jiangwang.com,这样使用a1.jiangwang.com和a2.jiangwang.com就能访问同一个cookie

一级域名又称为顶级域名,一般由字符串+后缀组成。熟悉的一级域名有baidu.com,qq.com。com,cn,net等均是常见的后缀。
二级域名是在一级域名下衍生的,比如有个一级域名为abc.com,则blog.abc.com和www.abc.com均是其衍生出来的二级域名。

Cookie的路径

path属性决定允许访问Cookie的路径。比如,设置为”/“表示允许所有路径都可以使用Cookie

4.应用

Cookies最典型的应用是判定注册用户是否已经登录网站,用户可能会得到提示,是否在下一次进入此网站时保留用户信息以便简化登录手续,这些都是Cookies的功用。另一个重要应用场合是“购物车”之类处理。用户可能会在一段时间内在同一家网站的不同页面中选择不同的商品,这些信息都会写入Cookies,以便在最后付款时提取信息。

Session

1.什么是Session

在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。

2.Session实现原理

服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。可以用如下的代码证明:

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

@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//将数据存储到session中
session.setAttribute("mayun", "马云");
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在该session了,session的id是:"+sessionId);
}
}

}

第一次访问时,服务器会创建一个新的sesion,并且把session的Id以cookie的形式发送给客户端浏览器,如下图所示:

image-20210708145107823

再次请求服务器,此时就可以看到浏览器再请求服务器时,会把存储到cookie中的session的Id一起传递到服务器端了,如下图所示:

image-20210708145337564

3.session创建和销毁

在程序中第一次调用request.getSession()方法时就会创建一个新的Session,可以用isNew()方法来判断Session是不是新创建的

1
2
3
4
5
6
7
8
9
10
java复制代码//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在session,session的id是:"+sessionId);
}

session对象默认30分钟没有使用,则服务器会自动销毁session,也可以手工配置session的失效时间,例如:

1
java复制代码session.setMaxInactiveInterval(10*60);//10分钟后session失效

当需要在程序中手动设置Session失效时,可以手工调用session.invalidate方法,摧毁session。

1
2
3
ini复制代码HttpSession session = request.getSession();
//手工调用session.invalidate方法,摧毁session
session.invalidate();

面试题:浏览器关闭,session就销毁了? 不对.

Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。

Token

1.什么是Token

token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。

当用户第一次登录后,服务器生成一个token并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。

简单token的组成;uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token的前几位以哈希算法压缩成的一定长度的十六进制字符串。为防止token泄露)。

2.Token的原理

  1. 用户通过用户名和密码发送请求
  2. 程序校验
  3. 程序返回一个Token给客户端
  4. 客户端存储Token,并且每次发送请求携带Token
  5. 服务端验证Token,并返回数据

image-20210708162203287

3.Token的使用

Spring Boot和Jwt集成示例

image-20210714062557183

项目依赖 pom.xml

1
2
3
4
5
6
7
8
9
10
11
java复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>

自定义注解

1
2
3
4
5
6
java复制代码//需要登录才能进行操作的注解LoginToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
1
2
3
4
5
6
java复制代码//用来跳过验证的PassToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}

用户实体类、及查询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
java复制代码public class User {
private String userID;
private String userName;
private String passWord;

public String getUserID() {
return userID;
}

public void setUserID(String userID) {
this.userID = userID;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getPassWord() {
return passWord;
}

public void setPassWord(String passWord) {
this.passWord = passWord;
}
}
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复制代码@Service
public class UserService {

public User getUser(String userid, String password){
if ("admin".equals(userid) && "admin".equals(password)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}

public User getUser(String userid){
if ("admin".equals(userid)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}
}

Token生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class TokenService {
/**
* 过期时间10分钟
*/
private static final long EXPIRE_TIME = 10 * 60 * 1000;

public String getToken(User user) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
String token="";
token= JWT.create().withAudience(user.getUserID()) // 将 user id 保存到 token 里面
.withExpiresAt(date) //十分钟后token过期
.sign(Algorithm.HMAC256(user.getPassWord())); // 以 password 作为 token 的密钥
return token;
}
}

拦截器拦截token

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
java复制代码package com.jw.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.jw.annotation.LoginToken;
import com.jw.annotation.PassToken;
import com.jw.entity.User;
import com.jw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;


public class JwtInterceptor implements HandlerInterceptor{

@Autowired
private UserService userService;

@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.getUser(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassWord())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}

@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

}
}

注册拦截器

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
java复制代码package com.jw.config;

import com.jw.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录

//注册TestInterceptor拦截器
// InterceptorRegistration registration = registry.addInterceptor(jwtInterceptor());
// registration.addPathPatterns("/**"); //添加拦截路径
// registration.excludePathPatterns( //添加不拦截路径
// "/**/*.html", //html静态资源
// "/**/*.js", //js静态资源
// "/**/*.css", //css静态资源
// "/**/*.woff",
// "/**/*.ttf",
// "/swagger-ui.html"
// );
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}

登录Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@RestController
public class LoginController {

@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;

@PostMapping("login")
public Object login(String username, String password){
JSONObject jsonObject=new JSONObject();
User user=userService.getUser(username, password);
if(user==null){
jsonObject.put("message","登录失败!");
return jsonObject;
}else {
String token = tokenService.getToken(user);
jsonObject.put("token", token);
jsonObject.put("user", user);
return jsonObject;
}
}

@LoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通过验证";
}
}

配置全局异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
String msg = e.getMessage();
if (msg == null || msg.equals("")) {
msg = "服务器出错";
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 1000);
jsonObject.put("message", msg);
return jsonObject;
}
}

postman测试

获取token

image-20210714063353922

无token登录

image-20210714063447601

有token登录

image-20210714063557098

错误token登录

image-20210714063830525

image-20210714064150202

4.Token的优缺点

优点:

  1. 支持跨域访问: Cookie是不允许垮域访问的,token支持;
  2. 无状态: token无状态,session有状态的;
  3. 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候, 你可以进行Token生成调用即可;
  4. 更适用于移动应用: Cookie不支持手机端访问的;
  5. 性能: 在网络传输的过程中,性能更好;
  6. 基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)。

缺点:

  1. 占带宽,正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多;
  2. 无法在服务端注销,那么久很难解决劫持问题;
  3. 性能问题,JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

三天刷完《深入JVM虚拟机 第三版》是什么感觉

发表于 2021-07-13

好久没有写过原创了,这篇是2021的第一篇原创,又开始自己的原创之路了,今天分享一个最近刷完的一本书《深入JVM虚拟机 第三版》,一共花了三天的时间刷完,我相信应该很多人还没看过,毕竟七百多页,坚持看完真不容易,在这里分享一下自己刷完的一些经验,以及怎么去刷这本书。

其实在刷这本书之前其实对于这本书的很多内容都已经学过了,所以再来刷这本书算是知识点的回顾吧,看的也比较快,更有目的性,在此之前对于JVM的学习,我还专门的做了自己的学习和总结了自己的思维导图:

我个人的自我学习和总结都是围着调优的目的去的,所以再刷这本书的目的就是很简单:让自己对JVM的调优有更深的理解。

这个思维导图也积累了非常久,大概一年多的时间吧,我还记得我第一看 《深入JVM虚拟机》 的时候,都是蒙蒙的,第一次也是没有看完。

后来因为工作需要零零散散的学了很多关于JVM的知识点,并且把它记录下来,这就成了我最后的这个思维导图,我感觉对于调优方面的话算是比较全的了。

下面我们开始我们的刷书之旅,先来看看这本书的总的目录,一共五大部分,首先第一大部门直接可以略过,是不是贼开心,一秒间就少了一大部分:

还有就是第五部分,因为这部分属于并发编程的内容,而且里面的内容基本我以前的原创博文都写过,所以这部分的内容我是之间花了一小时的时间看完。

这部分内容其实可以不用看的,在《并发编程实战》这本书里面都会有,我下一本就是打算刷《并发编程实战》,大家也可以直接略过,这不又少了一部分。

还有就是第四部分,这部分应该是选看,我个人主要关注的是即时编译器,因为即时编译器还是挺重要的,有些面试也会被问到,其他部分我都是草草的看一下大致的内容,也并不是自己想要的,因为你去看,你也记不住,并且实际中也用不到,至少是现在阶段用不到,面试被问几乎为0。


还有就是第三部分,第三部分也是选看,例如第三部分的第六章,他讲的是一个class文件的结构,,从二进制给你讲起,你记得住吗,完全记不住啊,还有各种字节码指令,这不影响你看书的心情和信心嘛。


举个例子,下图是第三部分第六章的内容,他会带你一个一个去看class文件,每一个位置分别表示什么意思,你觉得你看完这个你能记住多少,不出一天全归零,所以这部分直接就可以略过。

但是第三部分的类加载子系统就是非常重要的,包括里面的类加载过程,典型的三层类加载器、自定义类加载器、双亲委派机制,这是我们需要关注的,我之前面试字节的时候,一面就被问道类加载过程以及双亲委派机制,这个肯定也是重点。

但是,如果大家伙的目和我的目的一样的话关注点是在调优,其实你的重点就是在第二部分,这部分是要你摸透的。

接下来,我们来看第二部分就是JVM的内存管理,也就是内存模型、运行时数据区的内容,还有就是用于观察JVM内存的监控工具,其实这部分的内存模型应该是很多技术博主,都写过的博文,所以感觉大家看这部分内容应该算是只是的回顾了:


第二章内容重点是讲解Java的运行时数据区、Java对象、OOM异常,其中运行时数据区大家都很收悉了,就是这几部分:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区
  • 运行时常量池
  • 直接内存

这里要掌握的就是这几个区域的含义,哪些是线程私有的,哪些是线程共有的,哪些会导致OOM异常出现,带着这几个问题去看基本没什么太大问题。

直接内存可能是有些人会比较陌生,直接内存并不是虚拟机的一部分,他是本地内存,按照常理来说他的大小和出现OOM问题之和本地内存有关,你也可以通过 -XX:MaxDirectMemorySize 参数来设置它的大小。

与这部分内存有关系的Java框架之一就是Netty,因为Netty的底层使用的是NIO,是给予通道和缓冲区的IO方式,它通过一个DirectByteBuffer 对象来对这块内存进行操作。


接下来就是对象的创建、对象内存布局、对象的访问三部分,神奇的Java是怎么通过一个new关键字,把对象创建出来的,在对象的创建部分,主要涉及到这几个概念:指针碰撞、本地线程分配缓冲(Thread Local AllocationBuffer,TLAB)。

对象是怎么分配内存的?怎么避免多线程时内存资源的争抢?在哪里分配内存(堆、栈)?这块的内容基本就是围绕这几个问题讲解。

对象创建出来后并且分配内存后,对象在内存中又是怎么布局的,我相信大家可能之前看过一篇文章说:XX大厂XX面,面试官:你知道Java中对象有多大吗?

一个对象分为几部分(对象头(Header)、实例
数据(Instance Data)和对齐填充(Padding)),以及对象头(Mark Word)里面又包含那些内容等等诸如此类的问题,你都可以这部分找到答案。

最后就是对象的访问:句柄、直接指针。这两种方式有什么区别?分别有什么缺点和优点?搞懂这几个问题,就算是掌握了90%以上了。

以上的内容都是偏理论的,理论总是为实战服务的,后面的OOM异常就是偏实战的,前面介绍的几个运行时数据区除了程序计数器不会出现OOM,其他的部分都有可能会出现OOM。

而除了程序计数器不用考虑外(以下调优可以直接忽略程序计数器),在JVM中Java堆是调优的最重要也是最难的一部分,基本上JVM中的调优参数复杂的都是偏向于Java堆,对于除了Java堆其它的运行时数据期出现OOM的解决办法是啥?最有效的办法就是适当的增大内存。

而Java堆的调优就不是偏偏的增大内存那么简单,有时Java堆增大堆存,反而会适得其反,因为虽然内存大了,可能新生代的可分配的对象数量也增多了,但是单次GC的回收时间也变得长了,这样会导致系统卡顿时间增长。

对于那些用户交互时间频繁,要求响应时间短的应用,就不能接受了,可能老板就是找你:兔崽子,上次是不是你调优的JVM,现在卡顿的时间又更长了,用户每天投诉,你今晚必须搞定,不然明天别来了。

所以Java堆也是调优一块的重点和难点,首先在这部分先了解和熟悉分别各个区域出现OOM异常后,打印出来的堆栈信息一般都是怎么样的,比如堆OOM就如下所示:

1
2
3
ini复制代码java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]

在堆栈信息中都能看到“Java heap space”这个名称,还有就是那些场景会导致这几部分的内存溢出呢?这个也是比较重要的,比如递归深度过大、方法过长,程序中出现大对象,内存泄露,线程池数量设置不合理,过度使用代理、常量数量过多过大、Class信息过多等都会分别导致那些区域OOM呢?

其实对于调优之前还有要说清楚的就是,可能百分之七八十的问题,都可以通过代码优化来解决,比如方法过长,方法拆一下不就行了嘛,出现大对象,创建小一点的对象不就行了嘛。

但是,有时候有些问题你就有可能忽略调,比如大对象(有可能是同一时间内多对象),尽管你在开发中都非常的小心翼翼了,对象已经控制的非常小了,在生产中流量一上来的就出现OOM了,只有生产的流量才有可能验证你的某一些被忽略的功能。

举个例子,在PC端都有会有一个导出的功能,有当前页的导出以及全部的导出,特别是这个全部的导出,数据量小的时候就没啥问题,要是数据量一大,以及流量一上来,那同一时间内创建的Excell对象就非常的多,一不小心就出现OOM了。

再比如,面向用户的C端首页,一般C端首页为了提高用户的体验度都会进行缓存,那么当缓存的数量过大,同一时间从缓存里面获取的对象就会过多或者对象过大的问题。

对于这部分的内容,我之前的思维导图,也是做了非常详细的总结,以下只是其中的一部分,大家可以作为参看,对于大家思考也是非常重要的。

接下来第三章就是围绕着GC来讲解,针对调优的区域就是Java堆,包括怎么判断对象是否存活(引用计数法、可达性分析算法),三种基本的垃圾收集算法(标记-清楚、复制算法、标记-整理),分代模型理论(新生代、老年代),以及常用的经典垃圾收集器、搭配,适用场景。搞懂这些问题,你就无敌了,这部分的内容你就吃透了

其中经典垃圾收集器大家是比较陌生的,其他部分像垃圾收集算法、分代模型理论、判断对象是否存活,很多技术博主都有写过,垃圾收集算法我之前也写过一篇原创,大家可以参考一下。

我们重点来关注一下垃圾收集器,随着jdk的不断发展,版本的不断升级,服务器由单核演变多核,内存的变大,垃圾收集器也变得越来越智能,每种垃圾收集器都有自己的适用场景。

这几种垃圾收集器比较常见的搭配如下图所示:

我个人记忆比较深的搭配就是:

  • Serial和Serial Old
  • PS和PO
  • ParNew和CMS
  • G1

每种垃圾收集器都有自己的使用场景,优点和缺点,所以再看这部分的内容就带着这几个问题去看就行了:

  • 年轻代和老年代分别的垃圾收集器是什么?
  • 每种垃圾收集器的优点和缺点?
  • 每种垃圾收集器的使用场景?
  • 每种垃圾收集器的原理?
  • 每种垃圾收集器的JVM的相关设置参数是啥?

把这几个问题搞懂基本也就摸透了,比如在适用于单核时代的Serial收集器它的主要问题一个就是造成了STW的问题,以及单线程的垃圾回收效率问题,但是对于单核的服务器,它无疑是最佳的选择。

以及后面发展到多线程时代PS和PO,由原来的的单线程收集,现在编程了多线程收集,肯定是比原来垃圾收集的时间效率提高了,但是对于多线程又有设置回收垃圾的线程数是多少,书中都有给出建议的。

后面比较经典的就是CMS,CMS有分为四个阶段初始标记、并发标记、重新标记、并发清理。

这四个阶段的原理又是啥,那些阶段的与用户线程并发操作的,CMS又是怎么最大程度减少STW问题的,CMS一般适用于的堆大小范围是多少等等。

对于这些问题的答案相应的部分都说的明明白白的,大家可以仔细研究,后面一个就是G1,前面经典的垃圾收集器都是给予分代模型的。

但是,分代模型又是有自己存在的问题,比如经典的分代垃圾收集器相对应的都是整个堆大小的收集,随着服务器性能的提高,服务器的内存也会越来越大,现在动不动就好几个g或者10几个g的大小,那么这样就会导致单次的垃圾收集时间越来越长,停顿的时间也会相应变长,因为STW现在位置是没办法消除的,只能尽可能的减少。

这也就是后面出现了G1,G1弱化了分代理论模型的垃圾收集,改为真个堆划分为一个一个Region,每个Region都不是固定的哪一个代(新生代、老年代),在发生垃圾回收的时候,G1会对每一个Region的回收耗时做记录,这样就可以统计出要回收的成本,所以G1做到了在用户可接受的停顿时间(STW,一般100-300毫秒)的时间范围进行垃圾收集,不用针对整个堆。

以上就是G1来带来的优势,解决传统的垃圾收集器出现的问题,但是G1本身自己还存在问题,比如会产生浮动垃圾,这个是G1至今未解决的问题,还有G1在极端的情况下可能会造成系统假死的现象。

这也是为什么现在垃圾默认的垃圾收集器还不是G1的一些原因,这些问题都可以在文中找到,这部分的内容一定要细细的品,仔细的琢磨。

其中还有两款面向低延时的收集器是,我们现在阶段暂时用不到,你去看一下它使用的场景就知道了,以及适用于Java堆大小,所以大家可以忽略:


当然你有兴趣的话,也可以细细品读,毕竟是大佬写出来的东西,吸收了就都是精华。

熟悉和精通上面集中垃圾收集的原理后,下面就是实战了,我这边我在我的思维导图里面总解决常用的JVM调优参数:

当然书籍里面也有,不过比较零散,我这边基本都已经总结好了。

接下来就是实战部分包括一些场景和原则:对象内存的分配原则、大对象、长期存活的对象、对象年龄、空间的分配担保,这部分的内容也一定要细细的品,肯定是精华。


第四章的就是调优的工具了,对于工具,我自己使用的是jdk自带的VisualVM,它可以配置到idea中,并且可以安装GC插件和其他的插件,非常的方便,不过他一般是本地或者测试环境使用,像线上的工具我推荐的是使用阿里的Arthas工具,百度有一堆的教程。

还有就是比较原始的排查方法,使用linux命令,例如jps、jstat、jinfo、jmap、jstack,这部分在书上有详细的讲解:


对于这些原始命令的使用,主要有两个考点,也是两个实战点,分别是:分析cpu飙高的问题、分析OOM的问题。还有另一个就是分析内存不足的问题,这个就比较简单。

假如你都能够使用上面的原始命令和分析工具对以上问题能够自己独立分析,那你就是无敌了,调优高手啊,老板还不得花重金请你。

大家也可以参考一下我自己的思维导图,方式都差不多,大差不差,但还不完整,因为有些还没整理:

第五章部分,我感觉大家可以略过,可能是大佬的经历和我们不一样,实战的场景,很少遇见过,我是直接跳过的。

到这里要是以上的内容都吃透的话,后面的内容基本就是纯理论的东西,对你来说也不难:

第三部分就直接看虚拟机的加载机制,包括类加载过程、经典的三层加载器、自定义类加载器、双亲委派等,这些就是这部分的重点。

这部分搞懂了可以谢谢代码怎么去实现自己的类加载器,加载自己的类,怎么去破坏双亲委派模型,诸如此类的问题,文章里面都有讲解。

好了到这里基本我个人需要的内容已经吸收的差不多了,我就是带着调优的目的去看这本书的,这里澄清一下不是说不看的内容说写的不好,不是这样的,只是我们是带着目的去学习的,书里的内容很详细,但是并不是所有的东西都是我们需要的,至少是现阶段,我只是帮大家把我们需要的内容提取出来,然后分享自己刷这本书的目的以及经验。

限于篇幅,后面会继续分享自己对于JVM虚拟机的深入的学习和了解,有需要 《深入JVM虚拟机第三版》 电子书可以添加我微信:abc730500468,那本书重点的部分我都打上了标记,便于你们刷,以及刷的过程有技术可以相互讨论,好了我是黎杜,我们下一期见。

本文转载自: 掘金

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

Python 基础语法:开始你的 Python 之旅

发表于 2021-07-13

你好,我是悦创。

如果你已经有 Python 基础了,那先恭喜你已经掌握了这门简洁而高效的语言,这几节课你可以跳过,或者也可以当作复习,自己查漏补缺,你还可以在留言区分享自己的 Python 学习和使用心得。

好了,你现在心中是不是有个问题,要学好数据分析,一定要掌握 Python 吗?

我的答案是,想学好数据分析,你最好掌握 Python 语言。为什么这么说呢?

首先,在一份关于开发语言的调查中,使用过 Python 的开发者,80% 都会把 Python 作为自己的主要语言。Python 已经成为发展最快的主流编程语言,从众多开发语言中脱颖而出,深受开发者喜爱。

其次,在数据分析领域中,使用 Python 的开发者是最多的,远超其他语言之和。最后,Python 语言简洁,有大量的第三方库,功能强大,能解决数据分析的大部分问题,这一点我下面具体来说。

Python 语言最大的优点是简洁,它虽然是 C 语言写的,但是摒弃了 C 语言的指针,这就让代码非常简洁明了。同样的一行 Python 代码,甚至相当于 5 行 Java 代码。我们读 Python 代码就像是读英文一样直观,这就能让程序员更好地专注在问题解决上,而不是在语言本身。

image-20200803220432773

image-20200803220510159

当然除了 Python 自身的特点,Python 还有强大的开发者工具。在数据科学领域,Python 有许多非常著名的工具库:比如科学计算工具 NumPy 和 Pandas 库,深度学习工具 Keras 和 TensorFlow,以及机器学习工具 Scikit-learn,使用率都非常高。

image-20200804134356625

总之,如果你想在数据分析、机器学习等数据科学领域有所作为,那么掌握一项语言,尤其是 Python 语言的使用是非常有必要的,尤其是我们刚提到的这些工具,熟练掌握它们会让你事半功倍。

  1. 安装及 IDE 环境

了解了为什么要学 Python,接下来就带你快速开始你的第一个 Python 程序,所以我们先来了解下如何安装和搭建 IDE 环境。

1.1 Python 的版本选择

Python 主要有两个版本: 2.7.x 和 3.x。两个版本之间存在一些差异,但并不大,它们语法不一样的地方不到 10%。

另一个事实就是:大部分 Python 库都同时支持 Python 2.7.x 和 3.x 版本。虽然官方称 Python2.7 只维护到 2020 年,但是我想告诉你的是:千万不要忽视 Python2.7,它的寿命远不止到 2020 年,而且这两年 Python2.7 还是占据着 Python 版本的统治地位。一份调查显示:在 2017 年的商业项目中 2.7 版本依然是主流,占到了 63.7%,即使这两年 Python3.x 版本使用的增速较快,但实际上 Python3.x 在 2008 年就已经有了。

那么你可能会问:这两个版本该如何选择呢?

版本选择的标准就是看你的项目是否会依赖于 Python2.7 的包,如果有依赖的就只能使用 Python2.7,否则你可以用 Python 3.x 开始全新的项目。

1.2 Python IDE 推荐

确定了版本问题后,怎么选择 Python IDE 呢?有众多优秀的选择,这里推荐几款。

  1. PyCharm

这是一个跨平台的 Python 开发工具,可以帮助用户在使用 Python 时提升效率,比如:调试、语法高亮、代码跳转、自动完成、智能提示等。
2. Sublime Text

SublimeText 是个著名的编辑器,Sublime Text3 基本上可以 1 秒即启动,反应速度很快。同时它对 Python 的支持也很到位,具有代码高亮、语法提示、自动完成等功能。
3. Vim

Vim 是一个简洁、高效的工具,速度很快,可以做任何事,从来不崩溃。不过 Vim 相比于 Sublime Text 上手有一定难度,配置起来有些麻烦。
4. Eclipse+PyDev

习惯使用 Java 的人一定对 Eclipse 这个 IDE 不陌生,那么使用 Eclipse+PyDev 插件会是一个很好的选择,这样熟悉 Eclipse 的开发者可以轻易上手。

如果上面这些 IDE 你之前都没有怎么用过,那么推荐你使用 Sublime Text,上手简单,反应速度快。

  1. Python 基础语法

环境配置好后,我们就来快速学习几个 Python 必会的基础语法。我假设你是 Python 零基础,但已经有一些其他编程语言的基础。下面我们一一来看。

2.1 输入与输出

image-20200804134537188

1
2
3
4
python复制代码name = raw_input("What's your name?")
sum = 100+100
print('hello,%s' %name)
print('sum = %d' %sum)

raw_input 是 Python2.7 的输入函数,在 python3.x 里可以直接使用 input,赋值给变量 name,print 是输出函数,%name 代表变量的数值,因为是字符串类型,所以在前面用的 %s 作为代替。

这是运行结果:

1
2
3
python复制代码What's your name?cy
hello,cy
sum = 200

image-20200804134713431

image-20200804134727542

image-20200804134754002

2.2 判断语句:if … else …

1
2
3
4
5
6
7
8
python复制代码score = 10
if score >= 90:
print('Excellent')
else:
if score < 60:
print('Fail')
else:
print('Good Job')

if … else … 是经典的判断语句,需要注意的是在 if expression 后面有个冒号,同样在 else 后面也存在冒号。

image-20200804134846129

image-20200804134858374

另外需要注意的是,Python 不像其他语言一样使用 {} 或者 begin…end 来分隔代码块,而是采用代码缩进和冒号的方式来区分代码之间的层次关系。所以 代码缩进在 Python 中是一种语法 ,如果代码缩进不统一,比如有的是 tab 有的是空格,会怎样呢?会产生错误或者异常。相同层次的代码一定要采用相同层次的缩进。

2.3 循环语句:for … in

1
2
3
4
python复制代码sum = 0
for number in range(11):
sum = sum + number
print(sum)

运行结果:

1
python复制代码55

for 循环是一种迭代循环机制,迭代即重复相同的逻辑操作。如果规定循环的次数,我们可以使用 range 函数,它在 for 循环中比较常用。

  • range(11) 代表从 0 到 10,不包括 11,也相当于 range(0,11)
  • range 里面还可以增加步长,比如 range(1,11,2) 代表的是 [1,3,5,7,9]。

2.4 循环语句: while

1
2
3
4
5
6
python复制代码sum = 0
number = 1
while number < 11:
sum = sum + number
number = number + 1
print(sum)

运行结果:

1
python复制代码55

1 到 10 的求和也可以用 while 循环来写,这里 while 控制了循环的次数。while 循环是条件循环,在 while 循环中对于变量的计算方式更加灵活。因此 while 循环适合循环次数不确定的循环,而 for 循环的条件相对确定,适合固定次数的循环。

2.5 数据类型:列表、元组、字典、集合

2.5.1 列表:[]

1
2
3
4
5
6
7
python复制代码lists = ['a', 'b', 'c']
lists.append('d')
print(lists)
print(len(lists))
lists.insert(0, 'mm')
lists.pop()
print(lists)

运行结果:

1
2
3
python复制代码['a', 'b', 'c', 'd']
4
['mm', 'a', 'b', 'c']

列表是 Python 中常用的数据结构,相当于数组,具有增删改查的功能,我们可以使用 len() 函数获得 lists 中元素的个数;使用 append() 在尾部添加元素,使用 insert() 在列表中插入元素,使用 pop() 删除尾部的元素。

2.5.2 元组 (tuple)

1
2
python复制代码tuples = ('tupleA','tupleB')
print(tuples[0])

运行结果:

1
python复制代码tupleA

元组 tuple 和 list 非常类似,但是 tuple 一旦初始化就不能修改。因为不能修改所以没有 append(), insert() 这样的方法,可以像访问数组一样进行访问,比如 tuples[0],但不能赋值。

2.5.3 字典 {dictionary}

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码# -*- coding: utf-8 -*
# 定义一个dictionary
score = {'guanyu': 95, 'zhangfei': 96}
# 添加一个元素
score['zhaoyun'] = 98
print(score)
# 删除一个元素
score.pop('zhangfei')
# 查看key是否存在
print('guanyu' in score)
# 查看一个key对应的值
print(score.get('guanyu'))
print(score.get('yase', 99))

运行结果:

1
2
3
4
python复制代码{'guanyu': 95, 'zhaoyun': 98, 'zhangfei': 96}
True
95
99

字典其实就是 {key, value},多次对同一个 key 放入 value,后面的值会把前面的值冲掉,同样字典也有增删改查。增加字典的元素相当于赋值,比如 score[‘zhaoyun’] = 98,删除一个元素使用 pop,查询使用 get,如果查询的值不存在,我们也可以给一个默认值,比如 score.get(‘yase’,99)。

2.5.4 集合:set

1
2
3
4
5
python复制代码s = set(['a', 'b', 'c'])
s.add('d')
s.remove('b')
print(s)
print('c' in s)

运行结果:

1
2
python复制代码{'a', 'd', 'c'}
True

集合 set 和字典 dictory 类似,不过它只是 key 的集合,不存储 value。同样可以增删查,增加使用 add,删除使用 remove,查询看某个元素是否在这个集合里,使用 in。

2.6 注释:#

注释在 python 中使用 #,如果注释中有中文,一般会在代码前添加 # – coding: utf-8 -。

如果是多行注释,使用三个单引号,或者三个双引号,比如:

1
2
3
4
5
6
python复制代码# -*- coding: utf-8 -*
'''
这是多行注释,用三个单引号
这是多行注释,用三个单引号
这是多行注释,用三个单引号
'''

2.7 引用模块 / 包:import

1
2
3
4
5
6
7
8
python复制代码# 导入一个模块
import model_name
# 导入多个模块
import module_name1,module_name2
# 导入包中指定模块
from package_name import moudule_name
# 导入包中所有模块
from package_name import *

Python 语言中 import 的使用很简单,直接使用 import module_name 语句导入即可。这里 import 的本质是什么呢?import 的本质是路径搜索。import 引用可以是模块 module,或者包 package。

针对 module,实际上是引用一个.py 文件。而针对 package,可以采用 from … import …的方式,这里实际上是从一个目录中引用模块,这时目录结构中必须带有一个 _init_.py 文件。

2.8 函数:def

1
2
3
4
python复制代码def addone(score):
return score + 1

print(addone(99))

运行结果:

1
python复制代码100

函数代码块以 def 关键词开头,后接函数标识符名称和圆括号,在圆括号里是传进来的参数,然后通过 return 进行函数结果得反馈。

2.9 A+B Problem

上面的讲的这些基础语法,我们可以用 sumlime text 编辑器运行 Python 代码。另外,告诉你一个相当高效的方法,你可以充分利用一个刷题进阶的网址: acm.zju.edu.cn/onlinejudge… ,这是浙江大学 ACM 的 OnlineJudge。

什么是 OnlineJudge 呢?

它实际上是一个在线答题系统,做题后你可以在后台提交代码,然后 OnlineJudge 会告诉你运行的结果,如果结果正确就反馈:Accepted,如果错误就反馈:Wrong Answer。

不要小看这样的题目,也会存在编译错误、内存溢出、运行超时等等情况。所以题目对编码的质量要求还是挺高的。下面我就给你讲讲这道 A+B 的题目,你可以自己做练习,然后在后台提交答案。

2.10 题目:A+B

输入格式:有一系列的整数对 A 和 B,以空格分开。

输出格式:对于每个整数对 A 和 B,需要给出 A 和 B 的和。

输入输出样例:

1
2
3
4
python复制代码INPUT
1 5
OUTPUT
6

针对这道题,我给出了下面的答案:

1
2
3
4
5
6
7
python复制代码while True:
try:
line = input()
a = line.split()
print(int(a[0]) + int(a[1]))
except:
break

当然每个人可以有不同的解法,官方也有 Python 的答案,这里给你介绍这个 OnlineJudge 是因为:

  1. 可以在线得到反馈,提交代码后,系统会告诉你对错。而且你能看到每道题的正确率,和大家提交后反馈的状态;
  2. 有社区论坛可以进行交流学习;
  3. 对算法和数据结构的提升大有好处,当然对数据挖掘算法的灵活运用和整个编程基础的提升都会有很大的帮助。
  1. 总结

现在我们知道,Python 毫无疑问是数据分析中最主流的语言。今天我们学习了这么多 Python 的基础语法,你是不是体会到了它的简洁。如果你有其他编程语言基础,相信你会非常容易地转换成 Python 语法的。那到此,Python 我们也就算入门了。有没有什么方法可以在此基础上快速提升 Python 编程水平呢?给你分享下我的想法。

在日常工作中,我们解决的问题都不属于高难度的问题,大部分人做的都是开发工作而非科研项目。所以我们要提升的主要是熟练度,而通往熟练度的唯一路径就是练习、练习、再练习!

如果你是第一次使用 Python,不用担心,最好的方式就是直接做题。把我上面的例子都跑一遍,自己在做题中体会。

如果你想提升自己的编程基础,尤其是算法和数据结构相关的能力,因为这个在后面的开发中都会用到。那么 ACM Online Judge 是非常好的选择,勇敢地打开这扇大门,把它当作你进阶的好工具。

你可以从 Accepted 比率高的题目入手,你做对的题目数越多,你的排名也会越来越往前,这意味着你的编程能力,包括算法和数据结构的能力都有了提升。另外这种在社区中跟大家一起学习,还能排名,就像游戏一样,让学习更有趣味,从此不再孤独。

img

我在文章中多次强调练习的作用,这样可以增加你对数据分析相关内容的熟练度。所以我给你出了两道练习题,你可以思考下如何来做,欢迎把答案放到评论下面,我也会和你一起在评论区进行讨论。

  1. 如果我想在 Python 中引用 scikit-learn 库该如何引用?
  2. 求 1+3+5+7+…+99 的求和,用 Python 该如何写?

欢迎你把今天的内容分享给身边的朋友,和他一起掌握 Python 这门功能强大的语言。

学员分享1:

刷题网站:
1、LeetCode
2、Kaggel
3、老师推荐的Online Judge

Python入门:就看这本足够了——《Python编程:从入门到实践》

IDE:pycharm(写爬虫)、jupyter notebook+spyder3(数据分析主要IDE)、Sublime Text 3(牛逼的编辑器)

数据库:PGsql(挺好用的)、Mysql(开源,主流)

py版本:毫不犹豫选择py3(应为2020年py2停止维护了)

提升:没啥好说的,就是“干”,多写多练自然有感觉了,对,当你写多了代码,你看问题的层次也将不一样。所以,对自己狠心一点,不要一直在入门徘徊。

学员分享2:

  1. pycharm、sublime、jupyter都用过,个人认为Pycharm适合比较大一点的项目,平时自己开发一些小脚本什么的可以用sublime,比较简洁方便,目前一直在用Jupyter,比较适合做数据分析,显示图表之类的,可视化、一行代码一个结果都很方便,今天的课程已经用Jupyter全部写了一遍。
  2. 求和:sum(range(1, 100, 2))
    sum(iterable, start),sum的输入是iterable对象,比如list、tuple、set等
    range()的返回值就是一个iterable对象,可以直接作为sum的输入参数
  3. 前面有位同学一直出现 ‘int’ object is not iterable.的错误,我今天用Jupyter也碰到了,应该是前面老师的例子中用了sum做变量,后面求和这道题再用sum()做函数,所以出错了, 重启下Jupyter就行了,或者用魔法命令%reset清除变量应该也可以。

答案:

1、安装完成后 import sklearn
2、
(1)采用for循环
sum = 0
for i in range(1,100,2):
sum+=i
print(sum)
(2)采用递归方法
def sum(x):
if x>99:
return 0
num = sum(x+2)
return x+num
print(sum(1))
平常编程会用jupyter notebook,也可以推荐一下

Q1:不是python内置库

采用命令行安装库pip install scikit-learn

引用库 import scikit-learn

Q2:

方法一:sum函数

print(sum(range(1,100,2)))

方法二:if迭代

a = 0

for i in range(1,100,2):

a += i

print(a)

方法三:while循环

i = 1

b = 0

while i < 100:

if i % 2 != 0 :

b += i

i +=1

print(b)

本文转载自: 掘金

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

npm缓存原理解析

发表于 2021-07-12

缓存命中

npm install或npm update命令,从 registry 下载压缩包之后,都存放在本地的缓存目录:~/.npm/cacache, 里面有三个文件夹

image.png

npm在安装依赖的时候,根据package-lock中具体包的version,name和integrity信息(没有lock只能去请求registry了),用pacote:range-manifest:{url}:{integrity}生成唯一key,通过SHA256得到的hash,去_cacache/index-v5里找对应的文件,就能拿到基本的meta信息缓存了,前四位hash用来分路径
如果依赖信息改变了则生成的hash没有对应缓存信息可以命中,会重新下载再更新缓存

image.png

这个shasum就是tar包的hash,用这个hash去_cacache/content-v2里找到对应的gzip(tgz)缓存,解压缩后得到对应依赖的tar包

缓存机制

npm install在执行的时候,首先构建依赖树,依次安装依赖树中的每个包。

如果缓存中有依赖包,就会向远程仓库确认是否过期(304检查)检查,如果过期,就使用新的返回数据刷新缓存,否则就直接使用缓存中的数据。

另外根据是否离线或失去对目标远程仓库的访问权限,npm还提供了fallback-to-offline模式。该模式使无法访问远程仓库的情况下,npm将直接使用本地缓存。
无论何时离线,npm都会尽可能地回退到缓存中-而不是坚持重试网络请求或失败

此外还提供了新的参数,是用户可以指定缓存使用的策略

–prefer-offline: 将使npm跳过任何条件请求(304检查)直接使用缓存数据,只有在缓存无法匹配到的时候,才去访问网络。这样我们将依赖包添加到项目的过程就会快很多。

例如,npm install express –prefer-offline将现在缓存中匹配express,只有在本地缓存没有匹配到的情况下,才去联网下载。

–prefer-online: 与它将强制npm重新验证缓存的数据(使用304检查),并使用重新验证的新鲜数据刷新缓存。

–offline 将强制npm使用缓存或退出。如果尝试安装的任何内容尚未在缓存中,则它将出现代码错误。

可以通过.npmrc或者npm config set来设置缓存使用的策略。

一个新的npm cache verify命令,它将对你的缓存进行垃圾回收,减少不需要的东西占据的磁盘使用量,并且会对索引和内容进行全面的完整性验证。

本文转载自: 掘金

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

电商后台管理项目-商品分类 一、创建goods_cate分支

发表于 2021-07-12

一、创建goods_cate分支

    1. git checkout -b goods_cat
    1. git branch
    1. git push -u origin goods_cate(因为此前云端还没有goods_cate分支,所以必须使用git push -u origin 分支名称 的方式提交分支,如果此前已有该分支,则只需要git push即可)
    1. 可以在云端中查看到该分支了

二、通过路由加载商品分类组件

1.在components下创建goods/Cate.vue

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码<template>
<div>
<h1>cate组件</h1>
</div>
</template>
<script>
export default {
data: () => ({})
}
</script>
<style lang="less" scoped>
</style>

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
js复制代码import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../components/Login.vue'
import Home from '../components/Home.vue'
import Welcome from '../components/Welcome.vue'
import Users from '../components/users/Users.vue'
import Rights from '../components/power/Rights.vue'
import Roles from '../components/power/Roles.vue'
import Cate from '../components/goods/Cate.vue'

Vue.use(VueRouter)

const routes = [
// 重定向
{
path: '/',
redirect: '/login'
},
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [
{
path: '/welcome',
component: Welcome
},
{
path: '/users',
component: Users
},
{
path: '/rights',
component: Rights
},
{
path: '/roles',
component: Roles
},
{
path: '/categories',
component: Cate
}
]
}
]

const router = new VueRouter({
routes
})

// to 将要访问的路径
// from 代表从哪个路径跳转而来
// next 是一个函数 表示放行
// next() 放行 next('/login') 强制跳转
router.beforeEach((to, from, next) => {
// 如果将要访问的路径是login,就放行
if (to.path === '/login') return next()
// 获取token
const token = window.sessionStorage.getItem('token')
// 如果token不存在,则强制跳转到登录页面
if (!token) return next('/login')
// 如果token存在,则放行
next()
})

export default router

三、绘制商品分类组件的基本页面布局

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
js复制代码<template>
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>商品管理</el-breadcrumb-item>
<el-breadcrumb-item>商品列表</el-breadcrumb-item>
</el-breadcrumb>
<!-- 卡片视图区域 -->
<el-card>
<el-row>
<el-col>
<el-button type="primary">添加分类</el-button>
</el-col>
</el-row>
<!-- 表格区域 -->
<!-- 分页区域 -->
</el-card>
</div>
</template>
<script>
export default {
data: () => ({})
}
</script>
<style lang="less" scoped>
</style>

四、调用API获取商品分类列表数据

1.在页面加载时就获取数据列表(created中调用getCateList函数)

1
2
3
js复制代码  created() {
this.getCateList()
},

2.在methods中定义getCateList函数,并获取数据列表

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码  methods: {
async getCateList() {
const { data: res } = await this.$http.get('categories', { params: this.queryInfo })
if (res.meta.status !== 200) {
return this.$message.error('获取商品分类列表失败!')
}
// 把数据列表 赋值给catelist
this.catelist = res.data.result
// 为总数据条数赋值
this.total = res.data.total
}
}

3.在data中定义需要用到的数据

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码  data: () => ({
// 获取商品分类列表的查询条件
queryInfo: {
type: 3,
pagenum: 1,
pagesize: 5
},
// 商品分类数据列表
catelist: [],
// 总数据条数
total: 0
}),

五、初步使用vue-table-with-tree-grid树形表格组件

  1. 安装vue-table-with-tree-grid依赖

image.png

  1. 根据官方文档进行配置(在main.js中)

image.png

1.导入组件

1
js复制代码import treeTable from 'vue-table-with-tree-grid'

2.注册为全局可用组件

1
2
js复制代码// 注册为全局可用组件
Vue.component('tree-table', treeTable)

3.在cate.vue中使用该组件

1
2
3
4
5
6
7
8
9
10
11
js复制代码<!-- 表格区域 -->
<tree-table
show-index
index-text="#"
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
border
:show-row-hover="false"
></tree-table>

data中:

1
2
3
4
5
6
7
js复制代码// 为table指定列的定义
columns: [
{
label: '分类名称',
prop: 'cat_name'
}
]

image.png

六、使用自定义模板渲染表格数据(第二列数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码<!-- 表格区域 -->
<tree-table
show-index
index-text="#"
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
border
:show-row-hover="false"
>
<template slot="isok" slot-scope="scope">
<i
class="el-icon-success"
v-if="scope.row.cat_deleted === false"
style="color: lightgreen"
></i>
<i class="el-icon-error" v-else style="color: red"></i> </template
></tree-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 为table指定列的定义
columns: [
{
label: '分类名称',
prop: 'cat_name'
},
{
label: '是否有效',
// 表示 将当前列定义为模板列
type: 'template',
template: 'isok'
}
]

七、渲染排序和操作对应的UI结构

1.排序列

image.png

  • 增加模板
1
2
3
4
5
6
7
js复制代码{
label: '排序',
// 表示 将当前列定义为模板列
type: 'template',
// 表示当前这一列使用模板的名称
template: 'order'
}
  • 2.使用模板
1
2
3
4
5
6
js复制代码<!-- 排序 -->
<template slot="order" slot-scope="scope">
<el-tag size="mini" v-if="scope.row.cat_level === 0">一级</el-tag>
<el-tag size="mini" type="success" v-else-if="scope.row.cat_level === 1">二级</el-tag>
<el-tag size="mini" type="warning" v-else>三级</el-tag>
</template>

2.操作列

1
2
3
4
5
6
7
js复制代码{
label: '操作',
// 表示 将当前列定义为模板列
type: 'template',
// 表示当前这一列使用模板的名称
template: 'opt'
}
1
2
3
4
5
6
js复制代码<!-- 操作 -->
<template slot="opt">
<el-button size="mini" type="primary" icon="el-icon-edit">编辑</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</template>
</tree-table>

八、实现分页功能

1
2
3
4
5
6
7
8
9
10
11
js复制代码<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[3, 5, 10, 15]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
1
2
3
4
5
6
7
8
9
10
js复制代码// 监听pagesize变化
handleSizeChange(newszie) {
this.queryInfo.pagesize = newszie
this.getCateList()
},
// 监听pagenum变化
handleCurrentChange(newnum) {
this.queryInfo.pagenum = newnum
this.getCateList()
}

九、渲染添加分类的对话框和表单

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
js复制代码<!-- 添加分类对话框 -->
<el-dialog
title="添加分类"
:visible.sync="addcatedialogVisible"
width="50%"
>
<!-- 添加分类的表单 -->
<el-form
:model="addCateForm"
:rules="addCateFormRules"
ref="addCateFormRef"
label-width="100px"
>
<el-form-item label="分类名称" prop="cat_name">
<el-input v-model="addCateForm.cat_name"></el-input>
</el-form-item>
<el-form-item label="父级分类">
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addcatedialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addcatedialogVisible = false"
>确 定</el-button
>
</span>
</el-dialog>
  • 父级分类可为空,且为空时表示要将上面的分类名称设置为一级
    data中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码// 添加分类对话框的显示隐藏
addcatedialogVisible: false,
// 添加分类的表单数据对象
addCateForm: {
// 将要添加的分类的名称
cat_name: '',
// 父级分类的id
cat_pid: 0,
// 分类的等级,默认要添加的是1级分类
cat_level: 0
},
// 添加分类的表单的验证规则
addCateFormRules: {
cat_name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' }
]
}

十、获取父级分类数据列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// 点击按钮,展示添加分类的对话框
showAddCateDialog() {
// 点击添加分类按钮后触发getParentCateList函数
this.getParentCateList()
this.addcatedialogVisible = true
},
// 获取父级分类的数据列表
async getParentCateList() {
const { data: res } = await this.$http.get('categories', { params: { type: 2 } })
if (res.meta.status !== 200) {
return this.$message.error('获取父级分类数据失败!')
}
this.parentCateList = res.data
console.log(this.parentCateList)
}

十一、渲染级联选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// clearable清空
// change-on-select可以选中一级数据名称
<el-form-item label="父级分类">
<!-- options:用来指定数据源 -->
<!-- props:用来指定配置对象 -->
<el-cascader
expand-trigger="hover"
v-model="selectedkeys"
:options="parentCateList"
:props="cascaderProp"
@change="parentCateChange"
clearable
change-on-select
></el-cascader>
</el-form-item>

data:

1
2
3
4
5
6
7
8
js复制代码// 指定级联选择器的配置对象
cascaderProp: {
value: 'cat_id',
label: 'cat_name', // 显示到输入框里的数据
children: 'children' // 与下一级数据相关联
},
// 选中的父级分类的id数组
selectedkeys: []

methods:

1
2
3
4
js复制代码    // 选择项发生变化触发这个函数
parentCateChange() {
console.log(this.selectedkeys)
}

十二、根据父分类的变化处理表单中的数据

1.选择项发生变化触发parentCateChange函数,点击确定按钮触发addCate函数

1
js复制代码<el-button type="primary" @click="addCate">确 定</el-button>

2.实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码// 选择项发生变化触发这个函数
parentCateChange() {
// console.log(this.selectedkeys)
// 如果selectedkeys数组中的length大于0,证明选中了父级分类
// 反之,就说明没有选中任何父级分类
if (this.selectedkeys.length > 0) {
// 父级分类的id
this.addCateForm.cat_pid = this.selectedkeys[this.selectedkeys.length - 1]
// 为当前分类的等级赋值
this.addCateForm.cat_level = this.selectedkeys.length
} else {
// 父级分类的id
this.addCateForm.cat_pid = 0
// 为当前分类的等级赋值
this.addCateForm.cat_level = 0
}
},
// 点击确定,添加新的分类
addCate() {
console.log(this.addCateForm)
}

image.png

十三、在对话框的close事件中重置表单数据

1.给对话框添加关闭事件

1
2
3
4
5
6
7
js复制代码<!-- 添加分类对话框 -->
<el-dialog
title="添加分类"
:visible.sync="addcatedialogVisible"
width="50%"
@close="addCateDialogClose"
>

2.监听关闭事件

1
2
3
4
5
6
7
js复制代码// 监听对话框的关闭事件,重置表单数据
addCateDialogClose() {
this.$refs.addCateFormRef.resetFields()
this.addCateForm.cat_level = 0
this.addCateForm.cat_pid = 0
this.selectedkeys = []
}

十四、完成添加分类的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码// 点击确定,添加新的分类
addCate() {
// 预校验
this.$refs.addCateFormRef.validate(async valid => {
if (!valid) return this.$message.error('预校验失败')
const { data: res } = await this.$http.post('categories', this.addCateForm)
if (res.meta.status !== 201) {
return this.$message.error('添加分类失败')
}
this.$message.success('添加分类成功')
this.getCateList()
this.addcatedialogVisible = false
})
},

十五、提交goods_cate分支、切换到主分支master合并goods_cate分支、再提交主分支

image.png

image.png

本文转载自: 掘金

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

数据结构与算法

发表于 2021-07-12

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 11 篇文章,完整文章目录请移步到文章末尾~


前言

  • 上一篇文章 我们讨论了前缀和技巧,前缀和是一种非常适合处理 区间查询 问题的算法思维。文章最后我提出了一个问题:对于动态数据的区间查询问题,还可以使用前缀和技巧吗,有没有更好的方法?
  • 在这篇文章里,我将一种介绍更加高效的区间查询数据结构 —— 线段树(Segment Tree)。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


  1. 前缀和数组的缺点

上一篇文章,我们使用了「前缀和 + 差分」技巧解决了 303. 区域和检索 - 数组不可变 【题解】 。简单来说,我们开辟了一个前缀和数组,存储「元素所有前驱节点的和」。利用这个前缀和数组,可以很快计算出区间 [i, j] 的和:
nums[i,j]=preSum[j+1]−preSum[i]nums[i, j] = preSum[j + 1] - preSum[i]nums[i,j]=preSum[j+1]−preSum[i]

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class NumArray(nums: IntArray) {
private val sum = IntArray(nums.size + 1) { 0 }

init {
for (index in nums.indices) {
sum[index + 1] = sum[index] + nums[index]
}
}

fun sumRange(i: Int, j: Int): Int {
return sum[j + 1] - sum[i] // 注意加一
}
}

此时,区间查询的时间复杂度是O(1)O(1)O(1),空间复杂度是O(n)O(n)O(n),总体不错。但是,正如前言提到的,目前我们只考虑了静态数据的场景,如果数据是可修改的会怎么样?

我们需要修正前缀和数组了,例如:将num[2]num[2]num[2]更新为 10,那么preSum[3,…,7]preSum[3,…,7]preSum[3,…,7] 都需要更新,这个更新操作的时间复杂度为O(n)O(n)O(n)。要是一次更新操作影响倒是不大,但如果更新操作很频繁,算法的均摊时间复杂度就劣化了。为了解决动态数据的场景,就出现了 “线段树” 这种数据结构,它和其他数据结构的复杂度对比如下表:

数据结构 构建结构 区间更新 区间查询 空间复杂度
遍历,不使用数据结构 O(1) O(1) O(n) O(1)
前缀和数组 O(n) O(n) O(1) O(n)
线段树 O(n) O(lgn) O(lgn) O(4*n)或O(2*n)
树状数组 O(n) O(lgn) O(lgn) O(n)

可以看到「前缀和数组」的优势是O(1)O(1)O(1)查询,但不适合动态数据的场景,而线段树似乎学会了中庸之道,线段树平衡了「区间查询」和「单点更新」两种操作的时间复杂度。它是怎么做到的呢?


  1. 什么是线段树?

这是因为前缀和数组是线性逻辑结构,修改操作一定需要花费线性时间。为了使得修改操作优于线性时间,那么一定需要构建非线性逻辑结构。

2.1 线段树的逻辑定义

一般的二叉树节点上存储的是一个值,而线段树上的节点存储的是一个区间[L,R][L, R][L,R] 上的聚合信息(例如最大值 / 最小值 / 和),并且子节点的区间合并后正好等同于父节点的区间。例如,对于父节点的区间是 [L,R][L, R][L,R],那么左子节点的区间是 [L,(L+R)/2][L, (L+R)/2][L,(L+R)/2],右子节点的区间是 [(L+R)/2,R][(L+R)/2, R][(L+R)/2,R]。叶子节点也是一个区间,不过区间端点 L==RL == RL==R,是一个单点区间。

—— 图片引用自 www.jianshu.com/p/4d9da6745… —— yo1ooo 著

从线段树的逻辑定义可以看出:线段树(Segment Tree)本质上是一棵平衡二叉搜索树,也就是说它同时具备二叉搜索树和平衡二叉树的性质:

  • 二叉搜索树:任意节点的左子树上的节点值都小于根节点的值,右子树上的节点值都大于根节点的值;
  • 平衡二叉树(Balance Tree):任意节点的左右子树高度差不大于 1。

2.2 线段树的物理实现

通常,一个二叉树的物理实现可以基于数组,也可以基于链表。不过,因为线段树本身也是平衡二叉树,除了二叉树最后一层节点外,线段树的其它层是满的,所以采用数组的实现空间利用率更高。

那么,怎么实现一个基于数组的线段树呢?其实都是固定套路了:采用数组存储方式时,树的根节点可以分配在数组第 [0] 位,也可以分配在第 [1] 位,两种方式没有明显的区别,主要是计算子节点 / 父节点下标的公式有所不同:

根节点存储在第[0][0][0]位:

  • 对于第[i][i][i]位上的节点,第[2∗i+1][2 * i +1][2∗i+1]位是左节点,第[2∗i+2][2 * i + 2][2∗i+2]位是右节点
  • 对于第[i][i][i]位上的节点,第[(i−1)/2][(i-1) / 2][(i−1)/2]位是父节点

根节点存储在第[1][1][1]位(建议采用,在计算父节点时比较简洁):

  • 第[0][0][0]位不存储,根节点存储在第[1][1][1]位
  • 对于第[i][i][i]位上的节点,第[2∗i][2 * i][2∗i]位是左节点,第[2∗i+1][2 * i + 1][2∗i+1]位是右节点
  • 对于第[i][i][i]位上的节点,第[i/2][i / 2][i/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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
kotlin复制代码class SegmentTree<E>(
private val data: Array<E>,
private val merge: (e1: E?, e2: E?) -> E
) {

private val tree: Array<E?>

init {
// 开辟 4 * n 空间
tree = Array<Any?>(4 * data.size) { null } as Array<E?>
buildSegmentTree(0, 0, data.size - 1)
}

/**
* 左子节点的索引
*/
fun leftChildIndex(treeIndex: Int) = 2 * treeIndex + 1

/**
* 右子节点的索引
*/
fun rightChildIndex(treeIndex: Int) = 2 * treeIndex + 2

/**
* 建树
* @param treeIndex 当前线段树索引
* @param left 区间左端点
* @param right 区间右端点
*/
private fun buildSegmentTree(treeIndex: Int, left: Int, right: Int) {
// 见第 3 节
}

/**
* 取原始数据第 index 位元素
*/
fun get(index: Int): E {
if (index < 0 || index > data.size) {
throw IllegalArgumentException("Index is illegal.")
}
return data[index]
}

/**
* 区间查询
* @param left 区间左端点
* @param right 区间右端点
*/
fun query(left: Int, right: Int): E {
if (left < 0 || left >= data.size || right < 0 || right >= data.size || left > right) {
throw IllegalArgumentException("Index is illegal.");
}
// 见第 3 节
}

/**
* 单点更新
* @param index 数据索引
* @param value 新值
*/
fun set(index: Int, value: E) {
if (index < 0 || index >= data.size) {
throw IllegalArgumentException("Index is illegal.");
}
// 见第 3 节
}
}

其中 buildSegmentTree()、query()、update() 三个方法的实现我们在下一节讲。这里我们着重分析下 为什么线段树需要分配 4n4n4n 的空间?

todo


  1. 线段树的基本操作

理解了线段树的逻辑定义和实现,这一节,我带你一步步实现线段树的三个基本操作 —— 建树 & 区间查询 & 更新。

3.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
kotlin复制代码init {
tree = Array<Any?>(4 * data.size) { null } as Array<E?>
buildSegmentTree(0, 0, data.size - 1)
}

/**
* 建树
* @param treeIndex 当前线段树索引
* @param treeLeft 节点区间左端点
* @param right treeRight 节点区间右端点
*/
private fun buildSegmentTree(treeIndex: Int, treeLeft: Int, treeRight: Int) {
if (treeLeft == treeRight) {
// 叶子节点
tree[treeIndex] = merge(data[treeLeft], null)
return
}
val mid = (treeLeft + treeRight) ushr 1
val leftChild = leftChildIndex(treeIndex)
val rightChild = rightChildIndex(treeIndex)
// 构建左子树
buildSegmentTree(leftChild, treeLeft, mid)
// 构建右子树
buildSegmentTree(rightChild, mid + 1, treeRight)
tree[treeIndex] = merge(tree[leftChild], tree[rightChild])
}

建树复杂度分析:

  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(4∗n)O(4 * n)O(4∗n) = O(n)O(n)O(n)

3.2 区间查询

区间查询是查询一段期望区间的结果,基本思路是递归查询子区间的结果,再通过合并子区间的结果来得到期望区间的结果。逻辑如下:

  • 0、从根节点开始查找(根节点是整个区间),递归执行以下步骤:
  • 1、如果查找范围正好等于节点区间范围,直接返回节点聚合数据;
  • 2、如果查找范围正好落在左子树区间范围,那么递归地在左子树查找;
  • 3、如果查找范围正好落在右子树区间范围,那么递归地在右子树查找;
  • 4、如果查找范围横跨两棵子树,那么拆分为两次递归查找,查找完成后 合并 结果。
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
kotlin复制代码/**
* 区间查询
*
* @param left 区间左端点
* @param right 区间右端点
*/
fun query(left: Int, right: Int): E {
if (left < 0 || left >= data.size || right < 0 || right >= data.size || left > right) {
throw IllegalArgumentException("Index is illegal.");
}
return query(0, 0, data.size - 1, left, right) // 注意:取数据长度
}

/**
* 区间查询
*
* @param treeIndex 当前节点索引
* @param dataLeft 当前节点左区间
* @param dataRight 当前节点右区间
* @param left 区间左端点
* @param right 区间右端点
*/
private fun query(treeIndex: Int, dataLeft: Int, dataRight: Int, left: Int, right: Int): E {
if (dataLeft == left && dataRight == right) {
// 查询范围正好是线段树节点区间范围
return tree[treeIndex]!!
}
val mid = (dataLeft + dataRight) ushr 1
val leftChild = leftChildIndex(treeIndex)
val rightChild = rightChildIndex(treeIndex)
// 查询区间都在左子树
if (right <= mid) {
return query(leftChild, dataLeft, mid, left, right)
}
// 查询区间都在右子树
if (left >= mid + 1) {
return query(rightChild, mid + 1, dataRight, left, right)
}
// 查询区间横跨两棵子树
val leftResult = query(leftChild, dataLeft, mid, left, mid)
val rightResult = query(rightChild, mid + 1, dataRight, mid + 1, right)
return merge(leftResult, rightResult)
}

查询复杂度分析:

  • 时间复杂度:取决于树的高度,为 O(lgn)O(lgn)O(lgn)
  • 空间复杂度:O(1)O(1)O(1)

3.3 单点更新

单点更新就是在数据变化之后适当调整线段树的结构,基本思路是递归地修改子区间的结果,再通过合并子区间的结果来更新期望当前节点的结果。逻辑如下:

  • 0、更新原数据(data 数组),然后从根节点开始更新值(根节点是整个区间),递归执行以下步骤:
  • 1、如果是叶子节点(left = right),直接更新;
  • 2、如果更新节点正好落在左子树区间范围,那么递归地在左子树更新;
  • 3、如果更新节点正好落在右子树区间范围,那么递归地在右子树更新;
  • 4、更新左右子树之后,再通过合并子树信息来更新当前节点。
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
kotlin复制代码/**
* 单点更新
*
* @param index 数据索引
* @param value 新值
*/
fun set(index: Int, value: E) {
if (index < 0 || index >= data.size) {
throw IllegalArgumentException("Index is illegal.");
}
data[index] = value
set(0, 0, data.size - 1, index, value) // 注意:取数据长度
}

private fun set(treeIndex: Int, dataLeft: Int, dataRight: Int, index: Int, value: E) {
if (dataLeft == dataRight) {
// 叶子节点
tree[treeIndex] = value
return
}
// 先更新左右子树,再更新当前节点
val mid = (dataLeft + dataRight) ushr 1
val leftChild = leftChildIndex(treeIndex)
val rightChild = rightChildIndex(treeIndex)
if (index <= mid) {
set(leftChild, dataLeft, mid, index, value)
} else if (index >= mid + 1) {
set(rightChild, mid + 1, dataRight, index, value)
}
tree[treeIndex] = merge(tree[leftChild], tree[rightChild])
}

更新复杂度分析:

  • 时间复杂度:取决于树的高度,为 O(lgn)O(lgn)O(lgn)
  • 空间复杂度:O(1)O(1)O(1)

到这里,我们的线段树数据结构就实现完成了,完整代码如下:SegmentTree


  1. 典型例题 · 区域和检索 - 数组可变

307. 区域和检索 - 数组可变 【题解】

给你一个数组 nums ,请你完成两类查询,其中一类查询要求更新数组下标对应的值,另一类查询要求返回数组中某个范围内元素的总和。

1
2
3
4
5
6
7
8
9
kotlin复制代码class NumArray(nums: IntArray) {
fun update(index: Int, `val`: Int) {

}

fun sumRange(left: Int, right: Int): Int {

}
}

这道题与 【题 303】 是差不多的,区别在于数组是否可变,属于 动态数据 的场景。上一节,我们已经实现了一个通用的线段树数据结构,我们直接使用就好啦。

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class NumArray(nums: IntArray) {
private val segmentTree = SegmentTree<Int>(nums.toTypedArray()) { e1: Int?, e2: Int? ->
if (null == e1)
e2!!
else if (null == e2)
e1
else
e1 + e2
}

fun update(index: Int, `val`: Int) {
segmentTree.set(index, `val`)
}

fun sumRange(left: Int, right: Int): Int {
return segmentTree.query(left, right)
}
}

有点东西~~没几行代码就搞定了,运行结果也比采用前缀树的方法优秀更多。但是单纯从做题的角度,如果每做一道题都要编写这么一大串 SegmentTree 代码,似乎就太蛋疼了。有没有别的变通方法呢?


  1. 线段树的解题框架

定义 SegmentTree 数据结构太花力气,这一节,我们来讨论一种不需要定义 SegmentTree 的通用解题框架。这个解法还是很巧妙的,它虽然不严格满足线段树的定义(不是二叉搜索树,但依然是平衡二叉树),但是实现更简单。

参考代码:

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
kotlin复制代码class NumArray(nums: IntArray) {

private val n = nums.size
private val tree = IntArray(2 * n) { 0 } // 注意:线段树大小为 2 * n

init {
// 构建叶子节点
for (index in n until 2 * n) {
tree[index] = nums[index - n]
}
// 依次构建父节点
for (index in n - 1 downTo 0) {
tree[index] = tree[index * 2] + tree[index * 2 + 1]
}
}

fun update(index: Int, `val`: Int) {
// 1、先直接更新对应的叶子节点
var treeIndex = index + n
tree[treeIndex] = `val`
while (treeIndex > 0) {
// 2、循环更新父节点,根据当前节点是偶数还是奇数,判断选择哪两个节点来合并为父节点
val left = if (0 == treeIndex % 2) treeIndex else treeIndex - 1
val right = if (0 == treeIndex % 2) treeIndex + 1 else treeIndex
tree[treeIndex / 2] = tree[left] + tree[right]
treeIndex /= 2
}
}

fun sumRange(i: Int, j: Int): Int {
var sum = 0
var left = i + n
var right = j + n
while (left <= right) {
if (1 == left % 2) {
sum += tree[left]
left++
}
if (0 == right % 2) {
sum += tree[right]
right--
}
left /= 2
right /= 2
}
return sum
}
}

这种实现的优点是只需要 2 * n 空间,而不需要 4 * n 空间下面解释下代码。代码主要由三个部分组成:

5.1 建树

构建线段树需要初始化一个 2∗n2*n2∗n 空间的数组,采用 自底向上 的方式来构建整棵线段树。首先,构建叶子节点,叶子节点的位于数组区间 [n,2n−1][n,2n -1][n,2n−1],随后再根据子节点的结果来构建父节点(下标为indexindexindex的节点,左子节点下标:2∗index2*index2∗index,右子节点下标:2∗index+12*index+12∗index+1)。参考以下示意图:

5.2 区间查询

区间查询是查询一段期望区间的结果,相对于常规方法构造的线段树,这种线段树的区间查询过程相对较难理解。基本思路是递归地寻找能够代表该区间的节点。逻辑如下:

  • 1、一开始的区间查询等同于线段树数组 [n,2n−1][n,2n-1][n,2n−1] 之间的若干个叶子节点 [left,right][left,right][left,right] 的合并,我们需要向上一层寻找能够代表这些节点的父节点;
  • 2、对于节点 indexindexindex,它的左子节点下标:2∗index2*index2∗index,右子节点下标:2∗index+12*index+12∗index+1,这意味着所有左子节点下标是偶数,所有右子节点下标是奇数;
  • 3、left/=2left /= 2left/=2和right/=2right /= 2right/=2则是寻找父节点,如果 leftleftleft 指针是奇数,那么 leftleftleft 指针节点一定是一个右节点,此时 left/2left/2left/2 节点就无法直接代表 leftleftleft 指针节点,于是只能单独加上这个 “落单” 的节点。同理,如果 rightrightright 指针是偶数,那么 righttrighttrightt 指针节点一定是一个左节点,,此时 right/2right /2right/2 节点就无法直接代表 rightright right 指针节点,于是只能单独加上这个 “落单” 的节点;
  • 4、最后循环退出前left==rightleft == rightleft==right,说明当前节点的区间(去除 “落单” 的节点)正好是所求的区间,直接加上。并且下一躺循环leftleftleft一定大于rightrightright,跳出循环。

5.3 单点更新

单点更新就是在数据变化之后适当调整线段树的结构,基本思路是:先更新目标位置对应的节点,递归地更新父节点。需要注意根据当前节点的索引是偶数或奇数,来确定选择哪两个节点来合并为父节点。

例如,更新的节点是 “a” 节点,它在线段树数组索引 index 是偶数(下标为 6),那么它的父节点是 “ab” 节点需要通过合并 tree[index] + tree[index+1] 来获得的。


  1. 总结

  • 前缀和数组与线段树都适用与区间查询问题,前者在数据更新频繁时整体性能会下降,后者平衡了更新与查询两者的时间复杂度,复杂度都是O(lgn)O(lgn)O(lgn);
  • 从解题的角度,常规的构建线段树的方法太复杂,可以采用反常规的线段树构建方式,代码会更加简洁,空间复杂度也更优秀;
  • 除了线段树,你还知道什么类似的数据结构擅长于区间查询和单点更新吗?

参考资料

  • 线段树 · 题集 —— LeetCode
  • 线段树 · 词条 —— 维基百科
  • 线段树 —— yo1ooo 著
  • 第 24 章 · 线段树 —— liweiwei1419 著

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

  • #1 链表问题总结
  • #2 链表相交 & 成环问题总结
  • #3 计算器与逆波兰表达式总结
  • #4 高楼丢鸡蛋问题总结
  • #5 为什么你学不会递归?谈谈我的经验
  • #6 回溯算法解题框架总结
  • #7 下次面试遇到二分查找,别再写错了
  • #8 什么是二叉树?
  • #9 什么是二叉堆 & Top K 问题
  • #10 使用前缀和数组解决 “区间和查询” 问题
  • #11 面试遇到线段树,已经这么卷了吗?
  • #12 使用单调队列解决 “滑动窗口最大值” 问题
  • #13 使用单调栈解决 “下一个更大元素” 问题
  • #14 使用并查集解决 “朋友圈” 问题
  • #15 如何实现一个优秀的 HashTable 散列表
  • #16 简答一波 HashMap 常见面试题
  • #17 二叉树高频题型汇总
  • #18 下跳棋,极富想象力的同向双指针模拟

Java & Android 集合框架系列文章: 跳转阅读

LeetCode 上分之旅系列文章:跳转阅读

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

使用低代码工具,解决pc端表格-表单类CURD业务,生成前后

发表于 2021-07-12

开篇介绍

pc端web开发中,“表格-表单类”通常是需求最多的,为了避免重复开发,特此推出这系列文章,来分享我关于快速开发“表格-表单类”项目的经验。希望大家能一起讨论,分享你遇到的各种奇葩需求,让我们一起干掉“表格-表单”!

复杂表格表单弹窗从此so easy系列–利用代码生成器生成简洁可读的代码(第1章)

注:为了方便演示,本章主要使用代码生成器直接生成前后端双端代码,需要自己搭建后端,会需要spring boot等简单的后端知识。如果是前端儿推荐直接使用NPM包filter-form-table-modal(点我查看),后续几篇文章将会详细介绍这个NPM包的具体使用细节。

介绍代码生成器

准备阶段

下载源码

1
cmd复制代码git clone https://github.com/ailuhaosi/code-gen

代码生成器的源码分成两部分:

  • code-gen-fontend:代码生成器的前端界面;技术栈:vue-element
  • code-gen-backend:代码生成器的后端服务;技术栈:spring boot

修改code-gen-backend的配置文件

修改application.yml

修改MySQL的用户名、密码、连接的数据库名(默认code-gen)

修改generator.properties

这里主要说一下tablePrefix,业务主表的前缀,视你个人情况而修改,默认tb_开头。

创建业务主表

在你需要连接的数据库中创建业务主表

注意点:每张表必须有某一列是主键;表注释会自动成为生成代码的Label。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码CREATE TABLE tb_region( 
regionId INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '小区id',
regionTitle VARCHAR(40) NOT NULL COMMENT '小区title',
regionContact VARCHAR(10) DEFAULT '' COMMENT '联系人',
regionMobile CHAR(11) COMMENT '联系电话',
regionLogo VARCHAR(300) COMMENT '区域图标',
regionRemark VARCHAR(100) DEFAULT '' COMMENT '备注',
regionProvince CHAR(10) DEFAULT '',
regionCity CHAR(10) DEFAULT '',
regionDistict CHAR(10) DEFAULT '',
regionAddress CHAR(50) DEFAULT '',
regionLongitude TINYINT UNSIGNED,
regionLatitude TINYINT UNSIGNED,
regionCreator CHAR(50),
regionCreatetime TIMESTAMP default CURRENT_TIMESTAMP,
regionUpdatetime TIMESTAMP default CURRENT_TIMESTAMP,
regionStatus TINYINT UNSIGNED default 0,
regionMiniprogram VARCHAR(100),
creatorName VARCHAR(50)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

启动项目

  1. 运行code-gen-backend
  2. 打开 http://localhost/web/index.html

代码生成器具体使用步骤

图示步骤

选择数据表

选择字段、下载源码

具体组件配置

视频演示

代码生成器使用视频-文章1(点我查看)

注意:视频14分钟~15分钟前后,后端代码合并的时候有错误,因为我之前建表的表字段没规范,具体细节规范要求请看勘误视频,小伙伴在自己操作过程中只要合理建表就不会有问题了。

视频勘误(点我查看)

将生成的代码合并到已有项目中

目前为了保证灵活性,方便整合到不同前后端架构的原有项目中,必须手动合并;后续会加入新功能:提供架构模板,代码生成器会扫描原工程目录,如果原工程和提供架构模板目录结构相同,代码可以自动合并。

前端代码合并

原工程代码:https://github.com/ailuhaosi/my-blog-demo/tree/master/code-gen-demo-org/code-gen-fontend-demo-org

合并后代码:https://github.com/ailuhaosi/my-blog-demo/tree/master/code-gen-demo-1/code-gen-fontend-demo-1

后端代码合并

原工程代码:https://github.com/ailuhaosi/my-blog-demo/tree/master/code-gen-demo-org/code-gen-backend-demo-org

合并后代码:https://github.com/ailuhaosi/my-blog-demo/tree/master/code-gen-demo-1/code-gen-backend-demo-1

总结

低代码最近几年被不断炒火,但往往被沦落到,“专业程序员看不上,非专业人员不会用”的尴尬境地。但随着前后端生态日益成熟,我们使用的框架(vue、Spring boot等)其实也是一种复杂的低代码而已,“这类低代码”也是屏蔽了底层细节,大部分时间我们要做的只是业务上的CURD即可。因此,我们程序员没必要抵制低代码,反而“那些好的低代码”可以让我们更快的交付任务。

低代码的发展方向,什么才是好的低代码?我个人经验概括以下几点:

  1. 源码开放 且 技术栈主流:意味着具备可扩展性,生成的低代码能被专业的程序员定制修改,能够方便的集成到原本的项目中,这样专业人士就不会排斥。赢生态者赢天下。
  2. 业务场景明确:每一类低代码产品应该针对特定业务场景。业务流程比较清晰,比如:“搜索表单-表格-弹窗表单”。而且通常低代码类产品,不追求个性化的“好看(复杂联动-动效)”,因为“好看(复杂联动-动效)”会需要大量定制,此类场景低代码的复用性优势就体现不出了。但“静态的好看”,可以通过定制皮肤来实现。
  3. 面向前后端的低代码:目前主推前后端分离,因此生成的源代码应该是包括前后端的,否则单纯一端,则工作量依然没减轻。

好啦!今天就分享到这里吧,看到这里的小伙伴,如果觉得文章对你有帮助,也别忘了点赞留言呦!

本文转载自: 掘金

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

1…610611612…956

开发者博客

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