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

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


  • 首页

  • 归档

  • 搜索

LeetCode 1143 最长公共子序列【c++/jav

发表于 2021-11-11

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

1、题目

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

1
2
3
ini复制代码输入:text1 = "abcde", text2 = "ace" 
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

1
2
3
ini复制代码输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

1
2
3
ini复制代码输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成。

2、思路

(动态规划) O(nm)O(nm)O(nm)

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度 (子序列可以不连续)。

样例:


如样例所示,字符串abcde与字符串ace的最长公共子序列为ace,长度为3。最长公共子序列问题是典型的二维动态规划问题,下面来讲解动态规划的做法。

状态表示: 定义 f[i][j]表示字符串text1的[1,i]区间和字符串text2的[1,j]区间的最长公共子序列长度(下标从1开始)。

状态计算:

可以根据text1[i]和text2[j]的情况,分为两种决策:

  • 1、若text1[i] == text2[j] ,也就是说两个字符串的最后一位相等,那么问题就转化成了字符串text1的[1,j-1]区间和字符串text2的[1,j-1]区间的最长公共子序列长度再加上一,即f[i][j] = f[i - 1][j - 1] + 1。(下标从1开始)

  • 2、若text1[i] != text2[j],也就是说两个字符串的最后一位不相等,那么字符串text1的[1,i]区间和字符串text2的[1,j]区间的最长公共子序列长度无法延长,因此f[i][j]就会继承f[i-1][j]与f[i][j-1]中的较大值,即f[i][j] = max(f[i - 1][j],f[i][j - 1]) 。 ( 下标从1开始)

  • 如上图所示:我们比较text1[3]与text2[3],发现'f'不等于'e',这样f[3][3]无法在原先的基础上延长,因此继承"ac"与"cfe" ,"acf"与"cf"的最长公共子序列中的较大值,即 f[3][3] = max(f[2][3] ,f[3][2]) = 2。

因此,状态转移方程为:

f[i][j] = f[i-1][j-1] + 1 ,当text1[i] == text2[j]。

f[i][j] = max(f[i - 1][j],f[i][j - 1]),当text1[i] != text2[j]​ 。

初始化:

f[i][0] = f[0][j] = 0,(0 <=i<=n, 0<=j<=m)

空字符串与有长度的字符串的最长公共子序列长度肯定为0。

实现细节:

我们定义的状态表示f数组和text数组下标均是从1开始的,而题目给出的text数组下标是从0开始的,为了一 一对应,在判断text1和text2数组的最后一位是否相等时,往前错一位,即使用text1[i - 1]和text2[j - 1]来判断。

时间复杂度分析: O(nm)O(nm)O(nm),其中nnn 和 mmm 分别是字符串 text1 和 text2的长度。

3、c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size(), m = text2.size();
vector<vector<int>> f(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (text1[i - 1] == text2[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
return f[n][m];
}
};

4、java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n = text1.length(), m = text2.length();
int[][] f = new int[n + 1][m + 1];
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
}
}
}
return f[n][m];
}
}

原题链接: 1143. 最长公共子序列
在这里插入图片描述

本文转载自: 掘金

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

k8s series 22 calico初级(calico

发表于 2021-11-11

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

本文将介绐calicoctl命令工具的使用

安装

calicoctl工具,以二进制命令方式安装在主机中

1
2
3
js复制代码curl -o calicoctl -O -L  "https://github.com/projectcalico/calicoctl/releases/download/v3.20.0/calicoctl" 
mv calicoctl /usr/local/bin
chmod +x calicoctl

需要注意的是,版本必须一致,否则无法使用

使用

查看节点列表

1
2
3
4
js复制代码#查看节点简单信息
calicoctl get node
#查看节点详细信息,输出会很长,可以输出到文件再查看
calicoctl get node -o yaml

图片.png

查看节点状态

1
js复制代码calicoctl node status

全互联模式(node-to-node mesh)模式,节点状态都是正常的(up),路由连接都已经相互建立好了
图片.png

节点检查

检查当前节点各模块以及内核是否满足calico的安装要求

1
js复制代码calicoctl node checksystem

图片.png

节点诊断

执行诊断命令,会采集系统和calico相关日志,输出到一个文件

1
js复制代码calicoctl node diags

该命令虽然会报错,但是采集的日志实际上好了。直接查看红框目录下的诊断日志既可
图片.png

查看ip池

1
js复制代码calicoctl get ippool

在初始k8s集群没的指定cdir的网段,这里默认就是192.168.0.0/16,另外ipip模式,nat出口都是启用的,vxlan虚拟子网模式是关闭的
图片.png

ipam

ipam是calico的一个ip管理模块

查看ip的地址的总量和已经分配的ip详细信息

1
js复制代码calicoctl ipam show

图片.png

查看ipam当前的详细信息

1
js复制代码calicoctl ipam check

图片.png

ipam网络管理配置

释放ip地址,只会释放已经没有使有的端点ip,已经在使用的不会释放,此命令慎用

1
js复制代码calicoctl ipam release --ip=192.168.1.2

允许借用ip

1
js复制代码calicoctl ipam configure --strictaffinity=true

网络资源管理

创建一个新网络

因为我们安装calico时默认都创建好了,所以不需要动,基本上不需要人工干预

1
js复制代码calicoctl create -f xxx.json

查看网络资源信息

过滤出calico创建的资源

1
js复制代码calicoctl get profile | grep calico

查看资源详细

1
js复制代码calicoctl get profile projectcalico-default-allow -o json

替换网络资源

就是更新已经创建好的资源,不清楚配置前不能随意改动

1
js复制代码calicoctl replace -f xxxx2.json

apply

综合了 create 和 replace 命令。没有资源时就创建,有资源时就替换。和kubectl 的appley 技术相似

删除网络资源

慎用,,执行可能会导致集群崩溃

1
js复制代码calicoctl delete profile xxxxxx

查看get

get命令 除了node,profile等还是其他的有效资源都可以查看,如果资源没有创建,返回是空

图片.png

元数据备份

在执行备份之前,需要有calicoctl对应的配置文件,另外在执行导出导入时,需要先加锁,导出导入成功后再解锁。

calicoctl配置文件默认在: /etc/calico/calicoctl.cfg,但是我们是二进制安装的,其实是没有的。需要自行创建

配置文件例子

根据自已的证书路径自行修改

1
2
3
4
5
6
7
8
js复制代码apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
etcdEndpoints: https://etcd1:2379,https://etcd2:2379,https://etcd3:2379
etcdKeyFile: /etc/calico/key.pem
etcdCertFile: /etc/calico/cert.pem
etcdCACertFile: /etc/calico/ca.pem

数据导出

1
2
3
js复制代码calicoctl datastore migrate lock
calicoctl datastore migrate export
calicoctl datastore migrate unlock

数据导入

1
2
3
js复制代码calicoctl datastore migrate lock
calicoctl datastore migrate import
calicoctl datastore migrate unlock

本文转载自: 掘金

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

netty(四)nio之网络编程 一、阻塞与非阻塞 二、多路

发表于 2021-11-11

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

一、阻塞与非阻塞

1.1 阻塞

1.1.1 阻塞模式会存在哪些问题?

1)在阻塞模式下,以下的方法都会导致线程暂停

  • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
  • SocketChannel.read 会在没有数据可读时让线程暂停
  • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程处于闲置状态

2)单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持

3)多线程下,有新的问题,体现在以下方面

  • 32 位 jvm 一个线程最大堆栈是 320k,64 位 jvm 一个线程 最大堆栈是1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低。
  • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive(不活跃),会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

1.1.2 测试代码:

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
csharp复制代码public class BioServerTest {

public static void main(String[] args) throws IOException {
// 使用 nio 来理解阻塞模式, 单线程
// 0. ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 2. 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 3. 连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
// 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
System.out.println("connecting...");
// 阻塞方法,线程停止运行
SocketChannel sc = ssc.accept();
System.out.println("connected... " + sc);
channels.add(sc);
for (SocketChannel channel : channels) {
// 5. 接收客户端发送的数据
System.out.println("before read..."+channel);
try {
// 阻塞方法,线程停止运行
channel.read(buffer);
} catch (IOException e) {
}
buffer.flip();
System.out.println(print(buffer));
buffer.clear();
System.out.println("after read..."+channel);
}
}
}

static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}

客户端代码:

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

public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
System.out.println("waiting...");
//模拟长连接一直存在
while (true) {
}
}
}

启动服务端看结果,一直在connecting,此时线程阻塞了:

1
erlang复制代码connecting...

启动客户端看结果,此时连接成功,又阻塞到收到消息之前:

1
2
3
arduino复制代码connecting...
connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64800]
before read...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64800]

1.2 非阻塞

1.2.1 相比阻塞改变了什么?

非阻塞模式下,相关方法都不会让线程暂停

  • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
  • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
  • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去

1.2.2 非阻塞模型存在哪些问题?

1)但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu

2)数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

1.2.3 测试代码

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
arduino复制代码public class NonIoServerTest {

public static void main(String[] args) throws IOException {
// 使用 nio 来理解非阻塞模式, 单线程
// 0. ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建了服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 非阻塞模式
ssc.configureBlocking(false);
// 2. 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 3. 连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
// 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
// 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null
SocketChannel sc = ssc.accept();
if (sc != null) {
System.out.println("connected... " + sc);
// 非阻塞模式
sc.configureBlocking(false);
channels.add(sc);
}
for (SocketChannel channel : channels) {
// 5. 接收客户端发送的数据
// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
int read = channel.read(buffer);
if (read > 0) {
buffer.flip();
System.out.println(print(buffer));
buffer.clear();
System.out.println("after read..."+channel);
}
}
// 用于查非阻塞状态,连接客户端时可关闭,便于观看结果
System.out.println("wait connecting...");
}
}

static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}
启动服务端结果如下,不断刷新:
1
2
3
4
5
6
7
8
9
10
erlang复制代码wait connecting...
wait connecting...
wait connecting...
wait connecting...
wait connecting...
wait connecting...
wait connecting...
wait connecting...
wait connecting...
... ...

服务端与前面测试阻塞时一样,我们将服务端的System.out.println(“wait connecting…”);这行代码注释掉,方便看结果。

启动客户端,看服务端结果:

1
arduino复制代码connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61254]

二、多路复用

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用。

  • 多路复用仅针对网络 IO,普通文件 IO 没法利用多路复用
  • 如果使用非阻塞模式,而不使用selector,则线程大部分时间都在做无用功,使用Selector 能够保证以下三点:
+ 有可连接事件时才去连接
+ 有可读事件才去读取
+ 有可写事件才去写入( 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件)

2.1 Selector

image.png

上述方案的好处:

  • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功。
  • 让这个线程能够被充分利用
  • 节约了线程的数量
  • 减少了线程上下文切换

2.1.1 如何使用Selector?

如下代码及注释描述:

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

public static void main(String[] args) throws IOException {
// 创建Selector
Selector selector = Selector.open();

// 绑定channel事件(SelectionKey当中有四种事件:OP_ACCEPT,OP_CONNECT, OP_READ, OP_WRITE)
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);

// 监听channel事件,返回值是发生事件的channel数
// 监听方法1,阻塞直到绑定事件发生
int count1 = selector.select();
// 监听方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count2 = selector.select(1000);
// 监听方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count3 = selector.selectNow();
}
}

上述代码当中,在selector进行channel时间监听时,会发生阻塞,直到时间发生,那么有哪些情况会使线程变成不阻塞状态呢?如下所示:

1)事件发生时(SelectionKey当中有四种事件:OP_ACCEPT,OP_CONNECT, OP_READ, OP_WRITE)

  • 客户端发起连接请求,会触发 accept 事件
  • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
  • channel 可写,会触发 write 事件
  • 在 linux 下 nio bug 发生时

2)调用 selector.wakeup()

3)调用 selector.close()

4)selector 所在线程 interrupt

2.2 处理accept事件

服务端如下所示:

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
ini复制代码public class AcceptEventServerTest {

public static void main(String[] args) {
try {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(8080));
System.out.println(channel);
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
int count = selector.select();
// int count = selector.selectNow();
System.out.println("select count: " + count);
// if(count <= 0) {
// continue;
// }

// 获取所有事件
Set<SelectionKey> keys = selector.selectedKeys();

// 遍历所有事件,逐一处理
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 判断事件类型
if (key.isAcceptable()) {
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 必须处理
SocketChannel sc = c.accept();
System.out.println(sc);
}
// 处理完毕,必须将事件移除
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

客户端如下:

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

public static void main(String[] args) {
// accept事件
try (Socket socket = new Socket("localhost", 8080)) {
System.out.println(socket);
// read事件
socket.getOutputStream().write("world".getBytes());
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}

服务端打印结果:

1
2
3
ini复制代码sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080]
select count: 1
java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60469]

上述代码中服务端注释掉了使用selector.selectNow()的方法,如果使用该方法,需要自己去判断返回值是否为0。

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发。

2.3 处理read事件

此处仍然使用代码的方式讲解,客户端与前面的客户端相同,只是此处会同时启动两个客户端,其中发送的内容分别是“hello” 和 “world”。

服务端代码如下所示:

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
ini复制代码public class ReadEventServerTest {
public static void main(String[] args) {
try {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(8080));
System.out.println(channel);
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
int count = selector.select();
System.out.println("select count:" + count);

// 获取所有事件
Set<SelectionKey> keys = selector.selectedKeys();

// 遍历所有事件,逐一处理
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 判断事件类型
if (key.isAcceptable()) {
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 必须处理
SocketChannel sc = c.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
System.out.println("连接已建立:" + sc);
} else if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
sc.close();
} else {
buffer.flip();
System.out.println(print(buffer));
}
}
// 处理完毕,必须将事件移除
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}

启动服务端,并先后启动两个客户端,看结果,首先服务端channel自己注册到selector,客户端1发送accept事件,服务端接收到后,继续while循环,监听到read事件,打印内容为“hello”,客户端2步骤相同。

1
2
3
4
5
6
7
8
9
lua复制代码sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080]
select count:1
连接已建立:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61693]
select count:1
hello
select count:1
连接已建立:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:51495]
select count:1
world

注意:最后的iter.remove(),为什么要移除?
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如

  • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
  • 第二次触发了 ssckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常

上述代码中cancel有什么作用?
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件

2.3.1 关注消息边界

首先看如下的代码是否有问题,客户端代码如下:

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

public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(9000);
while (true) {
Socket s = ss.accept();
InputStream in = s.getInputStream();
// 每次读取4个字节
byte[] arr = new byte[4];
while (true) {
int read = in.read(arr);
// 读取长度是-1,则不读取了
if (read == -1) {
break;
}
System.out.println(new String(arr, 0, read));
}
}
}
}

服务端代码如下:

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

public static void main(String[] args) throws IOException {
Socket max = new Socket("localhost", 9000);
OutputStream out = max.getOutputStream();
out.write("hello".getBytes());
out.write("world".getBytes());
out.write("你好".getBytes());
out.write("世界".getBytes());
max.close();
}
}

结果:

1
2
3
4
5
6
复制代码hell
owor
ld�
�好
世�
��

为什么会产生上述的问题?
这里面涉及到消息边界的问题。消息的长短是不同的,当我们指定相同长度的ByteBuffer去接收消息时,必然存在不同时间段存在很多种情况,如下所示:

image.png

由于buffer长度固定,必然存在消息被截断的情况,那么如何解决这些问题呢?

1)一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽

2)另一种思路是按分隔符拆分,缺点是效率低

3)TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量

  • Http 1.1 是 TLV 格式
  • Http 2.0 是 LTV 格式

通过面给出的答案都不是最好的解决方案,重点的问题在于如何分配Bytebuffer的大小?

buffer是给一个channel独立使用的,不能被多个channel共同使用,因为存在粘包、半包的问题。

buffer的大小又不能太大,如果要支持很大的连接数,同时又设置很大的buffer,则必然需要庞大的内存。

所以我们需要设置一个大小可变的ByteBuffer。

目前有两种较为简单实现方案,其都有其优缺点:

1)预先分配一个较小的buffer,例如4k,如果发现不能装下全部内容,则创建一个更大的buffer,比如8k,将已写入的4k拷贝到新分配的8kbuffer,将剩下的内容继续写入。

其优点是消息必然是连续的,但是不断的分配和拷贝,必然会对性能造成较大的影响。

2)使用多个数组的形式组成buffer,当一个数组存不下数据内容,就将剩余数据放入下一个数组当中。在netty中的CompositeByteBuf类,就是这种方式。

其缺点是数据不连续,需要再次解析整合,优点是解决了上一个方案的造成性能损耗的问题。

2.4 处理write事件

什么是两阶段策略?

其出现原因有如下两个:

1)在非阻塞模式下,我们无法保证将buffer中的所有数据全部写入channel当中,所以我们需要追踪写入后的返回值,也就是实际写入字节的数值。

1
arduino复制代码int write = channel.write(buffer);

2)我们可以使所有的selector监听channel的可写事件,每个channel都会有一个key用来跟踪buffer,这样会占用过多的内存。(关于此点不太理解的,下面可以通过代码来理解)

鉴于以上问题,出现的两阶段策略:

1)当第一次写入消息时,我们才将channel注册到selector

2)如果第一次没写完,再次添加写事件, 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册(不取消则每次都会出现写事件)。

下面通过代码的方式演示:
服务端:

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
scss复制代码public class ServerTest {

public static void main(String[] args) throws IOException {
// 开启一个服务channel
ServerSocketChannel ssc = ServerSocketChannel.open();
// 设置非阻塞
ssc.configureBlocking(false);
// 绑定端口
ssc.bind(new InetSocketAddress(8080));

//初始化selector
Selector selector = Selector.open();
//注册服务端到selector
ssc.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
//监听事件,此处会阻塞
selector.select();

//获取所有事件key,遍历
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//不管处理成功与否,移除key
iter.remove();
//如果是建立连接事件
if (key.isAcceptable()) {
//处理accept事件
SocketChannel sc = ssc.accept();
//设置非阻塞
sc.configureBlocking(false);
// 此处是第一阶段
//注册一个read事件到selector
SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
// 1. 向客户端发送内容
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
sb.append("a");
}
//字符串转buffer
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
//2. 写入数据到客户端channel
int write = sc.write(buffer);
// 3. write 表示实际写了多少字节
System.out.println("实际写入字节:" + write);
// 4. 如果有剩余未读字节,才需要关注写事件
// 此处是第二阶段
if (buffer.hasRemaining()) {
// 在原有关注事件的基础上,多关注 写事件
sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
// 把 buffer 作为附件加入 sckey
sckey.attach(buffer);
}
} else if (key.isWritable()) {
// 检索key中的附件buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 获取客户端channel
SocketChannel sc = (SocketChannel) key.channel();
// 根据上次的position继续写
int write = sc.write(buffer);
System.out.println("实际写入字节:" + write);
// 如果写完了,需要将绑定的附件buffer去掉,并且去掉写事件
// 如果没写完将会继续while,执行写事件,知道完成为止
if (!buffer.hasRemaining()) {
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
key.attach(null);
}
}
}
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
scss复制代码public class ClientTest {

public static void main(String[] args) throws IOException {
// 开启selector
Selector selector = Selector.open();
// 开启客户端channel
SocketChannel sc = SocketChannel.open();
//设置非阻塞
sc.configureBlocking(false);
// 注册一个连接事件和读事件
sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
//连接到服务端
sc.connect(new InetSocketAddress("localhost", 8080));
int count = 0;
while (true) {
//此处监测事件,阻塞
selector.select();
//获取时间key集合,并遍历
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//无论成功与否,都要移除
iter.remove();
//连接
if (key.isConnectable()) {
System.out.println(sc.finishConnect());
} else if (key.isReadable()) {
//分配内存buffer
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
//读数据到buffer
count += sc.read(buffer);
//清空buffer
buffer.clear();
//打印总字节数
System.out.println(count);
}
}
}
}
}

分别启动服务端和客户端,结果如下:

1
2
3
4
5
6
7
8
9
10
makefile复制代码实际写入字节:3801059
实际写入字节:3014633
实际写入字节:4063201
实际写入字节:4718556
实际写入字节:2490349
实际写入字节:2621420
实际写入字节:2621420
实际写入字节:2621420
实际写入字节:2621420
实际写入字节:1426522
1
2
3
4
5
6
7
8
9
10
erlang复制代码true
131071
262142
393213
524284
655355
... ...
29753117
29884188
30000000

兄弟们,看到这了就给个赞呗,感谢

本文转载自: 掘金

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

MySQL主备延迟竟17小时了!然而可能还要等2年

发表于 2021-11-11

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

本文讲述一个工作中真实发生的故事(事故:)。

故事发生

事情是这样的,很简单的一个开端:

binlog.png

  • 第1天:
    • 我在正常的编码,完成开发任务排期
    • 下午5:20 收到DB主备延迟600M告警
    • 正在紧急coding中,心想备机不影响线上业务..等等看,说不定就好了,…先吃个饭🐶
    • 晚上继续忙…
  • 第2天:
    • 一早来上班,忘了这事..
    • 开始习惯性的查看系统各种指标监控告警邮件
    • 发现邮件数据很奇怪..这些任务完成情况都不对啊,那个定时数据计算任务报告没有出?…
    • 查看db中的数据表,一切都正常
    • 那为何监控告警邮件不对?想起报表系统读的备db..所以主备不一致了!!(终于想起昨天的告警主备延迟还没恢复)
    • 为啥会延迟这么多?昨天下午有没有做什么更新删除数据的大事务操作?我似乎没有,别的同学们呢?

继续查看db的监控指标发现,主备延迟了17个小时,也就是说从昨天告警开始到现在的binlog一点都没有恢复。

查找原因

大家都说没有做什么大事务操作。

我们使用云上的MYSQL DB服务,我们使用的db拓扑很简单,如下图:

binlog2.png

只是此时此刻,Master和Slave之间还有大量的binlog没有同步完!

我们找云上的helper帮忙定位问题,定位备机重放binlog的情况。
最终定位到是有一张表还在不停的执行update语句,这个表没有主键!

所以延迟17个小时的原因是这个..

这个表是一张统计表,正是我昨天在处理的那张stat_table…

当机立断解决方案:让DBA重建备机

分析原因

主备同步恢复了,我开始分析原因:

  • 昨天在告警之前确实对stat_table新增了一个字段,执行了一两次update操作,

binlog6.png

  • 这么简单的一个操作5s+5s,为何导致主备延迟17小时呢?
    操作时没当一回事,50万数据的操作很常见,认为产生不了什么后果。

再描述一下具体场景:

  • 我们的binlog格式设置的是ROW格式(因为也有其他监听的需求)
  • stat_table确实没有设置主键
  • Master上执行一次update stat_table set c=0会产生50万条binlog,两次100万
  • 主备之间的吞吐量不大
    那么就是备机回放每条binlog很慢!
    为什么回放很慢,下面会具体分析

查看主备binlog

查看binlog需要有root的权限。

首先在Master上查看binlog

  • show binary logs;
    • 查看最后一行就是binlog的最新文件和位置,获取这个文件名Log_name和File_size用于下面的events查看
  • show binlog events in 'Log_name' from File_size limit 100
    • 这个能看到刚刚那个时间点后的binlog事件,但没有显示具体的binlog操作信息
  • mysqlbinlog -uroot -p****** -h****** --read-from-remote-server -vv Log_name --start-position=File_size >binlog.sql
    • 这个能产生binlog的每行的具体执行sql

用vim打开binlog.sql,可以看到binlog的数据大概是这样

1
2
3
4
5
6
7
8
9
sql复制代码 ### UPDATE `db`.`stat_table`
### WHERE
### @1=85481 /* INT meta=0 nullable=0 is_null=0 */
### @2=419263 /* INT meta=0 nullable=0 is_null=0 */
### @3=1 /* INT meta=0 nullable=0 is_null=0 */
### SET
### @1=85481 /* INT meta=0 nullable=0 is_null=0 */
### @2=419263 /* INT meta=0 nullable=0 is_null=0 */
### @3=0 /* INT meta=0 nullable=0 is_null=0 */

binlog7.png

  • stat_table 有a、b、c三列,注意,a、b、c都不是主键。
  • 因为是ROW格式,所以master产生binlog的时候将一条update stat_table set c=0语句拆分成50万,所以binlog里有100万的这个语句

在Slave上也可以用同样的方式查看binlog的执行情况, 只不过它的binlog是从Master复制过来的。

备机回放效率分析

假设有主键,在备机上每个update回放执行大概需要0.1ms。

这已经假设是很好的性能了,不过这只是一个假设,具体看机器性能。
则执行100万update操作需要10万ms,100s,也就是大概1.5分钟,但这是我们能接受的(用只读备机的地方仅限于报表和一些数据监控)。这也是我们偶尔的有主键表的update或delete操作,备机需要承受的延迟代价。

但是如果没有主键,有普通的索引(a上有普通索引),则UPDATE db.stat_table WHERE @1=85481 @2=419263 @3=0 SET @1=85481 @2=419263 @3=1 这个update语句的开销如何呢?

如下图所示:

binlog11.png

  • stat_table有a、b、c三列,a上有普通索引
  • 虽然stat_table没有主键,但是在DB内部还是会为它建立一个主键(row_id)和对应的索引
  • 普通索引a通过B+树查找匹配的a值,获得一个集合Collection(r1,r2,r3,…),其实r1等都是DB内部创建的主键row_id
  • 假设a索引的每个a值平均有1000行数据,这个就有1000*100万次回主表查找
  • Collection(dr1,r2,r3,…)的元素都要回主表查看,判断是否满足 b 和c列的where条件,如果满足则update,否则不处理
    执行一条语句的流程大概是这样,让我们看看不同的情况会发生什么
  • 一共100万个update行,每个1000次回表。和有主键表的情况对比,执行时间放大了1000倍,那么需要1.5*1000分钟!!!因为随机IO的原因,执行时间大于25小时。
  • 如果a索引平均每个值有100行数据,则100多分钟,情况已经好了很多。
  • 如果这个表什么索引也没有,那就要100万倍,也就是2年!

后续和总结

我们的普通索引a平均每个值大概有1000行数据,但是当时没分析之前就重建备机的做法相当明智,毕竟不用再等遥遥无期的主备同步了!

我发现在实际工作中,有这些容易犯错的地方会导致今天的故事:

  • 虽然我们都有很好的DB知识和素养,建表的时候都记得加主键,但我们在做数据计算的时候往往会生成很多表,有时候可能会忘了主键。
  • 另外还有一种类型就是log表,一般任务只会新增不更新,所以可能没有主键问题也不大。但是万一新增功能,更新数据,对主备同步也将造成很大的影响!

此事一出,同学们都将db里的其他无主键表加上了主键..

系统看起来又安全了许多呢~


如果这篇文章对您有所帮助,或有所启发的话,可以点赞收藏或关注:),您的支持是我坚持写作最大的动力。

本文转载自: 掘金

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

二叉树刷题记(三-后序遍历)

发表于 2021-11-11

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

前言

  • 今天做了关于二叉树的后序遍历题目,我的计划是先将二叉树的前中后三种遍历算法掌握,然后再做关于二叉树的其他题目。
  • 今天不将递归和迭代代码分开啦!一起讲了。
  • 我最近的感悟:题目看不懂,那就直接去看题解,题解还看不懂,那就直接看代码,然后带一个例子把程序走一遍,最后在翻过头来看题解,我想看不太懂也差不多了。
  • 重点:看懂之后记得多敲几遍,最好形成条件反射,一看到这个题就知道代码是什么了。
  • 目标:不求所有的都会,我们也做不到,只求我们做过的你还能做出来就足够了。
  • 本文目的
+ 掌握二叉树的后序遍历过程。
+ 掌握后序遍历的递归和迭代代码。
+ 希望亲爱的读者,看完本文章,能掌握这两种算法。
**正文**
  • 预备知识1:
+ 默认读者已了解二叉树的基本概念。
  • 预备知识2:
+ **后序遍历**的顺序是什么呢?
+ 左孩子-》右孩子-》父结点
+ 上例子
![image.png](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/44c2f2c85ba4294bc5d956314af77f4939262d19c60cabbc3e6f385b194f590e)
+ 上图的顺序为[4,5,2,6,3,1]
+ 读者有任何问题,欢迎下方留言,我们一起学习喽!
  • 1.题目如下
    image.png
  • 2.代码实现
+ **思想**:后序遍历中各父结点是最后一个访问,根节点是所有元素访问完之后才会访问。
+ **迭代代码**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码var postorderTraversal = function(root) {
let res = [];//最后要返回的数组
let stack = [];//遍历过程中遇到的元素结点
while(root || stack.length){
while(root){
res.unshift(root.val);//这个是数组的一个方法,可以理解成头插法,往首部插入
stack.push(root);
root = root.right;
}
let node = stack.pop();
root = node.left;
}
return res;
};
  • 解释
+ 这个代码是先找父结点,然后一直找的是**父结点的右孩子**,不知道读者看出来了吗?
+ 每遇到一个结点,就用**头插法**,把该结点的val值插入到返回的数组中。这个过程,就是算法的核心,这个地方理解了,那么这个代码也就通了。我先和读者**一起走一遍**,起初,我们先判断root是否为null,若是null,那么不进入循环,直接return;反之,将根节点的val头插法到res数组中,将根节点整体入stack数组中,然后找它的右孩子,如果有的话,继续循环,这个地方,我们可以知道,每循环一次,总是将一个结点的val插入到到res数组的第一个位置中。
+ 总结 :**根元素的val值被放到了res数组的最后一个**,我们**找的顺序**是父结点-》右孩子-》左孩子,**输出顺序**就是左孩子-》右孩子-》父结点喽!(因为我们是用的**头插法**)
+ 这里我们**上边的图**再来说一遍。请读者耐住性子,相信我,看完不懂你捶我。
+ res数组中的**元素变化**过程


    - [1]
    - [3,1]
    - [6,3,1]
    - [2,6,3,1]
    - [5,2,6,3,1]
    - [4,5,2,6,3,1]
    - 这不就是我们想要的的后序遍历顺序吗?
    - 结束啦,有任何问题欢迎下方留言。
+ **递归代码**
1
2
3
4
5
6
7
8
9
10
11
scss复制代码var postorderTraversal = function(root) {
let res = [];//要返回的数组
const traversal = (root01)=>{
if(root01 == null) return;//找到尽头,需要另换路
traversal(root01.left);//遍历左子树
traversal(root01.right);//遍历右子树
res.push(root01.val);//左右子树遍历结束就只剩中间喽!
}
traversal(root);
return res;
};
+ 递归有问题的,小嘟建议你做题的时候可以画下图,边看代码边画图,不一会你就懂啦。
**结尾**
  • 下如果你认真看完本文章,我相信你肯定会的。
  • 下次出一篇前序遍历的迭代和递归代码,然后看能不能找出它们之间的共性。
  • 本人笔拙,有的地方写的不对,欢迎指出,谢谢!!!
  • 最后,祝大家看完该文章有所收获,我们下期再见。

本文转载自: 掘金

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

Git提交之后自动打版本并钉钉通知

发表于 2021-11-11

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

在gitlab的服务器上,进入gitlab的数据目录\

1
bash复制代码cd /var/opt/gitlab/git-data/repositories/xxx/hooks/post-receive\

利用git的钩子post-receive

post-receive是在提交代码到服务器之后自动执行
然后进入/www/wwwroot/hook.com/git/test.git/hooks

复制一份post-receive.sample 并改名为post-receive
[root@iZbp1938t1plpi1gikahmmZ hooks]# cp post-receive.sample post-receive
1
然后编辑 post-receive 添加如下代码 保存退出

1
typescript复制代码DIR=/www/wwwroot/hook.com/public
1
2
3
ini复制代码git --work-tree=${DIR} clean -fd

git --work-tree=${DIR} checkout --force

修改post-receive 文件的权限

1
bash复制代码chmod -R 777 post-receive

以下是本地配置
在本地上新建一个文件夹 然后添加为远程仓库
// 初始化一个git仓库
git init
// 添加远程链接 把192.168.1.1 换成你真实服务器的ip
git remote add origin root@192.168.1.1:/www/wwwroor/hook.com/git/test.git

推送时会提示如下错误

然后直接执行如下代码

1
arduino复制代码git push --set-upstream origin master

然后输入你的服务器密码即可

然后去到服务端 服务端直接更新了

原ruby文件里,加上

1
perl复制代码system "/opt/gitlab/embedded/service/gitlab-shell/hooks/post-receive-shell #{refs}"

调用shell脚本。

shell脚本内容:

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
bash复制代码#!/bin/bash
data="$(git show --stat)"
string=$data
if [ "$3" == "refs/heads/master" ]; then
last=$(git rev-list --tags --max-count=1)
if [ $last ]; then
tag=$(git describe --tags `git rev-list --tags --max-count=1`)
tagnum=${tag#*v}
let tagnum+=1
tag="v"$tagnum
$(git tag -a $tag -m 'master')
else
tag="v1000"
$(git tag -a $tag -m 'master')
fi
path=$(basename `pwd`)
commit=$(git log --no-merges -n1 | grep "commit" | awk -F" " '{ print $2}')
author=$(git log --pretty=format:"%an" $commit -1)
message=$(git log --pretty=format:"%s" $commit -1)
/usr/bin/curl -s 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "仓库:'$path'\r\n版本号:'$tag'\r\n提交人:'$author'\r\n备注:'$message'\r\n请去http://jenkins.fu51.cn 部署"
}
}'
fi

结果示意图:

2452132304.png

坑的地方:

1.不能删除原ruby脚本,否则gitlab在merge request时会提示找不到源分支,所以在保留原来的基础上,再调用shell脚本。

2.curl要写绝对路径 /usr/bin/curl。在不写绝对路径的时候,手动运行脚本可以成功,手动push到master分支可以成功,但是通过gitlab页面merge request时无法运行,迷一样的问题。

3.修改之后会出现一个问题

1
2
3
4
vbnet复制代码error: unable to write sha1 filename ./objects/10/773c980a96148af4e9fd12c23ecc1e0924c2ad: Permission denied
To gitlab.fu51.cn:wechat_3d_community/cmit_3dsq_server.git
! [remote rejected] test6 -> test6 (unable to migrate objects to permanent storage)
error: failed to push some refs to 'git@gitlab.fu51.cn:wechat_3d_community/cmit_3dsq_server.git'

将gitlab的data目录设置为git的用户

1
bash复制代码chown -R git:git /var/opt/gitlab/git-data

本文转载自: 掘金

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

Flask 入门系列之Hello Flask!

发表于 2021-11-11

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

Flask 是一个 Python 实现的 Web 微框架,之所以称之为微框架,是因为 Flask 核心简单且易于扩展,有两个主要依赖,WSGI工具集:Werkzeug和模板引擎:Jinja2,Flask 只保留了 Web 开发的核心功能,其他的功能都由外部扩展来实现,比如集成数据库、表单认证、文件上传、各种各样的开放认证技术等功能。正是因为 Flask 支持用户灵活选择扩展功能,使得 Flask 越来越受到开发者的喜爱。

安装及简单Flask

可以使用pip install flask命令进行安装。

创建示例程序

安装完成后,我们来写一个Hello Flask!的示例程序。
新建一个Python项目,在项目根目录新建一个app.py文件,写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
return '<h1>Hello Flask!<h1>'


if __name__ == '__main__':
app.run()

代码分解:

  • 导入并实例化了Flask类:
1
2
python复制代码from flask import Flask
app = Flask(__name__)
  • 注册路由:
1
2
3
python复制代码@app.route('/')
def index():
return '<h1>Hello Flask!<h1>'

为函数index()附加app.route()装饰器,并传人 url:/ 作为参数,让 url 与函数建立关联的过程),当用户访问跟地址/就会触发index()函数,这种和路由绑定的函数就被成为视图函数。

  • 启动Web服务器
1
2
python复制代码if __name__ == '__main__':
app.run()

当用python app.py命令直接执行本文件时,就会通过app.run()启动Web服务器。

在命令行窗口执行flask run命令也可启动Web服务器,如下:
image.png

Flask内置的开发服务器默认监听http:/127.0.0.1:5000 地址,当我们打开浏览器访问这个地址时,会显示以下信息:

image.png

也可以在run()方法传入host=0.0.0.0、port=端口号指定监听主机、端口号,除此之外,还可以设置debug调试模式,例如:

1
2
python复制代码if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)

使用flask run命令启动Web服务器时,可以指定参数的方式指定监听主机和端口号:--host=0.0.0 0、--port=8888。

注意: Flask 内置的 Web 服务器主要是开发调试用的,在生产环境中,最好使用gunicorn+Nginx的方式进行部署。

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

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

springboot +logback+阿里数据源(drui

发表于 2021-11-11

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

1、基本介绍

默认情况下,Spring Boot项目就会用Logback来记录日志,并用INFO级别输出到控制台。如下图:
在这里插入图片描述
实际开发中我们不需要直接添加logback日志依赖。
你会发现 spring-boot-starter 其中包含了 spring-boot-starter-logging,该依赖内容就是 Spring Boot 默认的日志框架 logback。

日志级别从低到高分为:

1
xml复制代码TRACE < DEBUG < INFO < WARN < ERROR < FATAL

只能展示 大于或等于 设置的日志级别的日志;也就是说springboot默认级别为INFO,那么在控制台展示的日志级别只有INFO 、WARN、ERROR、FATAL

2、logback.xml日志文件配置

根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:

Logback:logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
Log4j:log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xml
Log4j2:log4j2-spring.xml, log4j2.xml
JDK (Java Util Logging):logging.properties

Spring Boot官方推荐优先使用带有 -spring 的文件名作为你的日志配置(如使用 logback-spring.xml ,而不是logback.xml),命名为logback-spring.xml的日志配置文件,spring boot可以为它添加一些spring boot特有的配置项(下面会提到)。
默认的命名规则,并且放在 src/main/resources 下如果你即想完全掌控日志配置,但又不想用logback.xml作为Logback配置的名字,application.yml可以通过logging.config属性指定自定义的名字:

1
2
xml复制代码logging:
config: classpath:logback-spring.xml

这里写代码片虽然一般并不需要改变配置文件的名字,但是如果你想针对不同运行时Profile使用不同的日志配置,这个功能会很有用。
在这里插入图片描述
项目日志内容;

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 此xml在spring-boot-1.5.3.RELEASE.jar里 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!-- 开启后可以通过jmx动态控制日志级别(springboot Admin的功能) -->
<!--<jmxConfigurator/>-->

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--<File>/home/hfw-client/hfw_log/stdout.log</File>-->
<!--日志的存放位置-->
<File>D:/log/hfw-client/hfw_log/stdout.log</File>
<encoder>
<pattern>%date [%level] [%thread] %logger{60} [%file : %line] %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 添加.gz 历史日志会启用压缩 大大缩小日志文件所占空间 -->
<!--<fileNamePattern>/home/hfw-client/hfw_log/stdout.log.%d{yyyy-MM-dd}.log</fileNamePattern>-->
<fileNamePattern>D:/log/hfw-client/hfw_log/stdout.log.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory><!-- 保留30天日志 -->
</rollingPolicy>
</appender>
<!--指定只打印这个路径下的为debug级别,否则日志打印太多不好看-->
<logger name="com.ratel.link.dao" level="DEBUG" />

<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

3,打印sql日志

比较蠢的方法是直接把logback-spring.xml文件中的root标签的level属性改为DEBUG:

1
2
3
4
xml复制代码<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>

这个改为DEBUG则会打印最详细的日志,包括mybatis的sql语句(量太大建议开发测试时才用)
在这里插入图片描述
我们一般针对DAO的包进行DEBUG日志设置:
logback-spring.xml

1
2
xml复制代码 <!--指定只打印这个路径下的为debug级别,否则日志打印太多不好看-->
<logger name="com.ratel.link.dao" level="DEBUG" />

这样的话,只打印SQL语句:
在这里插入图片描述

4、代码里打印日志

之前我们大多数时候自己在每个类创建日志对象去打印信息,比较麻烦:

1
2
java复制代码private static final Logger logger = LoggerFactory.getLogger(YjServiceImpl.class);
logger.error("xxx");

现在可以直接在类上通过 @Slf4j 注解去声明式注解日志对象
先在pom.xml中添加依赖:

1
2
3
4
5
6
pom复制代码<!--@Slf4j自动化日志对象-log-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>

然后就直接可以使用了:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
@Slf4j
@RequestMapping("slf4j")
public class Slf4jController {
@GetMapping("test/{name}")
public String testslf4j(@PathVariable("name") String name){
log.info(name+",你好,这里是测试@Slf4j");
return name+",你好,这里是测试@Slf4j";

}

}

使用浏览器访问:(注:使用postman测试的话,可能会乱码,大致原因已经定位到 @PathVariable注解的问题目前没找到借据方案)
在这里插入图片描述
在这里插入图片描述

5、项目完整地址

github.com/Dr-Water/Le…

参考链接 Spring Boot 日志配置(超详细)

本文转载自: 掘金

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

剑指 Offer II 031 最近最少使用缓存

发表于 2021-11-11

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

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value)
+ 如果关键字已经存在,则变更其数据值;
+ 如果关键字不存在,则插入该组「关键字-值」。
+ 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/Or…

思路

  • 首先梳理思路最先确定的是get函数
    • 如果去的节点为空,那么返回-1,否则就将这个节点移到最尾部
    • 如果命中缓存,那么久将对应的这个双向链表中的节点移到尾部
  • 其次确定好双向链表的三个函数,移动到尾部,删除元素,插入元素
    • 主要是维护两个哨兵节点,就是下面代码中的head/tail
    • 最后是构造一个双向链表,确定好这几步骤,剩下的就是对链表操作的基本功
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
java复制代码class ListNode{
public int key;
public int value;
public ListNode next;
public ListNode prev;

public ListNode(int k, int v) {
key = k;
value = v;
}
}

class LRUCache {
private ListNode head;
private ListNode tail;
private Map<Integer, ListNode> map;
int capacity;

public LRUCache(int cap) {
map = new HashMap<>();

head = new ListNode(-1, -1);
tail = new ListNode(-1, -1);
head.next = tail;
tail.prev = head;

capacity = cap;
}

public int get(int key) {
ListNode node = map.get(key);
if (node == null) {
return -1;
}

moveToTail(node, node.value);

return node.value;
}

public void put(int key, int value) {
if (map.containsKey(key)) {
moveToTail(map.get(key), value);
} else {
if (map.size() == capacity) {
ListNode toBeDeleted = head.next;
deleteNode(toBeDeleted);

map.remove(toBeDeleted.key);
}

ListNode node = new ListNode(key, value);
insertToTail(node);

map.put(key, node);
}
}

private void moveToTail(ListNode node, int newValue) {
deleteNode(node);

node.value = newValue;
insertToTail(node);
}

private void deleteNode(ListNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

private void insertToTail(ListNode node) {
tail.prev.next = node;
node.prev = tail.prev;
node.next = tail;
tail.prev = node;
}
}

本文转载自: 掘金

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

SpringBoot+Mybatis+Swagger2+dr

发表于 2021-11-11

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

  1. 为什么使用SpringBoot

SpringBoot相对于传统的SSM框架的优点是提供了默认的样板化配置,简化了Spring应用的初始搭建过程,如果你不想被众多的xml配置文件困扰,可以考虑使用SpringBoot替代

  1. 搭建怎样一个环境

本文将基于Spring官方提供的快速启动项目模板集成Mybatis、Swagger2框架,并讲解mybatis generator一键生成代码插件、logback、一键生成文档以及多环境的配置方法,最后再介绍一下自定义配置的注解获取、全局异常处理等经常用到的东西。

3.开发环境

本人使用IDEA作为开发工具,IDEA下载时默认集成了SpringBoot的快速启动项目可以直接创建,如果使用Eclipse的同学可以考虑安装SpringBoot插件或者直接从这里配置并下载SpringBoot快速启动项目,需要注意的是本次环境搭建选择的是SpringBoot2.0的快速启动框架,SpringBoot2.0要求jdk版本必须要在1.8及以上。

4.导入快速启动项目

不管是由IDEA导入还是现实下载模板工程都需要初始化快速启动工程的配置,如果使用IDEA,在新建项目时选择Spring Initializr,主要配置如下图
在这里插入图片描述
在这里插入图片描述

点击next之后finish之后IDEA显示正在下载模板工程,下载完成后会根据pom.xml下载包依赖,依赖下载完毕后模板项目就算创建成功了,如果是直接从官方网站配置下载快速启动项目可参考下图配置

直接下载SpringBoot快速启动项目-项目配置

从Search for dependencies 框中输入并选择Web、Mysql、Mybatis加入依赖,点击Generate Project下载快速启动项目,然后在IDE中选择导入Maven项目,项目导入完成后可见其目录结构如下图
快速启动项目-项目结构

需要关注红色方框圈起来的部分,由上往下第一个java类是用来启动项目的入口函数,第二个properties后缀的文件是项目的配置文件,第三个是项目的依赖包以及执行插件的配置

5.集成前准备

修改.properties为.yml
yml相对于properties更加精简而且很多官方给出的Demo都是yml的配置形式,在这里我们采用yml的形式代替properties,相对于properties形式主要有以下两点不同

  1. 对于键的描述由原有的 “.” 分割变成了树的形状
  2. 对于所有的键的后面一个要跟一个空格,不然启动项目会报配置解析错误
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码# properties式语法描述
spring.datasource.name = mysql
spring.datasource.url = jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 123
# yml式语法描述
spring:
datasource:
name: mysql
url: jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
username: root
password: 123

6.配置所需依赖

快速启动项目创建成功后我们观察其pom.xml文件中的依赖如下图,包含了我们选择的Web、Mybatis以及Mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码        <!-- spring web mvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

但是我们使用ORM框架一般还会配合数据库连接池以及分页插件来使用,在这里我选择了阿里的druid以及pagehelper这个分页插件,再加上我们还需要整合swagger2文档自动化构建框架,所以增加了以下四个依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码        <!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- alibaba的json格式化对象 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
<!-- 自动生成API文档 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.5.0</version>
</dependency>

7.集成Mybatis

Mybatis的配置主要包括了druid数据库连接池、pagehelper分页插件、mybatis-generator代码逆向生成插件以及mapper、pojo扫描配置

配置druid数据库连接池
添加以下配置至application.yml文件中

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
java复制代码spring:
datasource:
# 如果存在多个数据源,监控的时候可以通过名字来区分开来
name: mysql
# 连接数据库的url
url: jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
# 连接数据库的账号
username: root
# 连接数据库的密码
password: 123
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# 扩展插件
# 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall
filters: stat
# 最大连接池数量
maxActive: 20
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initialSize: 1
# 获取连接时最大等待时间,单位毫秒
maxWait: 60000
# 最小连接池数量
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最长时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql,要求是一个查询语句
# 如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用
validationQuery: select count(1) from 'table'
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnReturn: false
# 是否缓存preparedStatement,即PSCache
poolPreparedStatements: false
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true
maxOpenPreparedStatements: -1

8.配置pagehelper分页插件

1
2
3
4
5
6
xml复制代码# pagehelper分页插件
pagehelper:
# 数据库的方言
helperDialect: mysql
# 启用合理化,如果pageNum < 1会查询第一页,如果pageNum > pages会查询最后一页
reasonable: true

9.代码逆向生成插件mybatis-generator的配置及运行

mybatis-generator插件的使用主要分为以下三步

  1. pom.xml中添加mybatis-generator插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码    <build>
<plugins>
<!-- 将Spring Boot应用打包为可执行的jar或war文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<!-- 扫描resources/generator目录下的generatorConfig.xml配置 -->
<configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>
</build>
  1. 创建逆向代码生成配置文件generatorConfig.xml

参照pom.xml插件配置中的扫描位置,在resources目录下创建generator文件夹,在新建的文件夹中创建generatorConfig.xml配置文件,文件的详细配置信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 运行方式:mvaen运行命令 mybatis-generator:generate -e -->
<!-- 数据库驱动:选择你的本地硬盘上面的数据库驱动包-->
<properties resource="generator/generator.properties"/>
<classPathEntry location="${classPathEntry}"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--数据库链接URL,用户名、密码 -->
<jdbcConnection
driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/${db}?characterEncoding=utf-8"
userId="${userId}"
password="${password}">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<javaModelGenerator targetPackage="${pojoTargetPackage}" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成映射文件的包名和位置-->
<sqlMapGenerator targetPackage="${mapperTargetPackage}" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成DAO的包名和位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="${daoTargetPackage}" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 要生成的表 tableName是数据库中的表名或视图名 schema是数据库名称-->
<table tableName="%" schema="${db}"/>
</context>
</generatorConfiguration>

为了将generatorConfig.xml配置模板化,在这里将变动性较大的配置项单独提取出来作为一个generatorConfig.xml的配置文件,然后通过properties标签读取此文件的配置,这样做的好处是当需要多处复用此xml时只需要关注少量的配置项。
在generatorConfig.xml同级创建generator.properties文件,现只需要配置generator.properties文件即可,配置内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
yml复制代码# 请手动配置以下选项
# 数据库驱动:选择你的本地硬盘上面的数据库驱动包
classPathEntry = D:/CJH/maven-repository/mysql/mysql-connector-java/5.1.30/mysql-connector-java-5.1.30.jar
# 数据库名称、用户名、密码
db = db
userId = root
password = 123
# 生成pojo的包名位置 在src/main/java目录下
pojoTargetPackage = com.spring.demo.springbootexample.mybatis.po
# 生成DAO的包名位置 在src/main/java目录下
daoTargetPackage = com.spring.demo.springbootexample.mybatis.mapper
# 生成Mapper的包名位置 位于src/main/resources目录下
mapperTargetPackage = mapper
  1. 运行mybatis-generator插件生成Dao、Model、Mapping

1
2
3
java复制代码
# 打开命令行cd到项目pom.xml同级目录运行以下命令
mvn mybatis-generator:generate -e

mybatis扫描包配置
至此已经生成了指定数据库对应的实体、映射类,但是还不能直接使用,需要配置mybatis扫描地址后才能正常调用

  1. 在application.yml配置mapper.xml以及pojo的包地址
1
2
3
4
5
xml复制代码mybatis:
# mapper.xml包地址
mapper-locations: classpath:mapper/*.xml
# pojo生成包地址
type-aliases-package: com.spring.demo.springbootexample.mybatis.po
  1. 在SpringBootExampleApplication.java中开启Mapper扫描注解
1
2
3
4
5
6
7
8
java复制代码@SpringBootApplication
@MapperScan("com.spring.demo.springbootexample.mybatis.mapper")
public class SpringBootExampleApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootExampleApplication.class, args);
}
}

测试mapper的有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Controller
public class TestController {
//替换成自己生成的mapper
@Autowired
UserMapper userMapper;

@RequestMapping("/test")
@ResponseBody
public Object test(){
//查询该表的所有数据
return userMapper.selectByExample(null);
}
}

启动SpringBootExampleApplication.java的main函数,如果没有在application.yml特意配置server.port那么springboot会采用默认的8080端口运行,运行成功将打印如下日志

1
java复制代码Tomcat started on port(s): 8080 (http) with context path ''

在浏览器输入地址如果返回表格的中的所有数据代表mybatis集成成功

1
java复制代码http://localhost:8080/test
  1. 集成Swagger2

Swagger2是一个文档快速构建工具,能够通过注解自动生成一个Restful风格json形式的接口文档,并可以通过如swagger-ui等工具生成html网页形式的接口文档,swagger2的集成比较简单,使用需要稍微熟悉一下,集成、注解与使用分如下四步

  1. 建立SwaggerConfig文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码@Configuration
public class SwaggerConfig {
// 接口版本号
private final String version = "1.0";
// 接口大标题
private final String title = "SpringBoot示例工程";
// 具体的描述
private final String description = "API文档自动生成示例";
// 服务说明url
private final String termsOfServiceUrl = "http://www.kingeid.com";
// licence
private final String license = "MIT";
// licnce url
private final String licenseUrl = "https://mit-license.org/";
// 接口作者联系方式
private final Contact contact = new Contact("calebman", "https://github.com/calebman", "chenjianhui0428@gmail.com");

@Bean
public Docket buildDocket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(buildApiInf())
.select().build();
}

private ApiInfo buildApiInf() {
return new ApiInfoBuilder().title(title).termsOfServiceUrl(termsOfServiceUrl).description(description)
.version(version).license(license).licenseUrl(licenseUrl).contact(contact).build();

}

}
2. 在SpringBootExampleApplication.java中启用Swagger2注解
在@SpringBootApplication注解下面加上@EnableSwagger2注解

常用注解示例
~~~xml
//Contorller中的注解示例
@Controller
@RequestMapping("/v1/product")
// 表示标识这个类是swagger的资源
@Api(value = "DocController", tags = {"restful api示例"})
public class DocController extends BaseController {

@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
@ResponseBody
//表示一个http请求的操作
@ApiOperation(value = "修改指定产品", httpMethod = "PUT", produces = "application/json")
//@ApiImplicitParams用于方法,包含多个@ApiImplicitParam表示单独的请求参数
@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "产品ID", required = true, paramType = "path")})
public WebResult update(@PathVariable("id") Integer id, @ModelAttribute Product product) {
logger.debug("修改指定产品接收产品id与产品信息=>%d,{}", id, product);
if (id == null || "".equals(id)) {
logger.debug("产品id不能为空");
return WebResult.error(ERRORDetail.RC_0101001);
}
return WebResult.success();
}
}
//Model中的注解示例
//表示对类进行说明,用于参数用实体类接收
@ApiModel(value = "产品信息")
public class Product {
//表示对model属性的说明或者数据操作更改
@ApiModelProperty(required = true, name = "name", value = "产品名称", dataType = "query")
private String name;
@ApiModelProperty(name = "type", value = "产品类型", dataType = "query")
private String type;
}

生成json形式的文档
集成成功后启动项目控制台会打印级别为INFO的日志,截取部分如下,表明可通过访问应用的v2/api-docs接口得到文档api的json格式数据,可在浏览器输入指定地址验证集成是否成功

1
2
xml复制代码 Mapped "{[/v2/api-docs],methods=[GET],produces=[application/json || application/hal+json]}" 
http://localhost:8080/v2/api-docs
  1. 多环境配置

应用研发过程中多环境是不可避免的,假设我们现在有开发、演示、生产三个不同的环境其配置也不同,如果每次都在打包环节来进行配置难免出错,SpringBoot支持通过命令启动不同的环境,但是配置文件需要满足application-{profile}.properties的格式,profile代表对应环境的标识,加载时可通过不同命令加载不同环境。

1
2
3
4
5
6
xml复制代码application-dev.properties:开发环境
application-test.properties:演示环境
application-prod.properties:生产环境

# 运行演示环境命令
java -jar spring-boot-example-0.0.1-SNAPSHOT --spring.profiles.active=test

基于现在的项目实现多环境我们需要在application.yml同级目录新建application-dev.yml、application-test.yml、application-prod.yml三个不同环境的配置文件,将不变的公有配置如druid的大部分、pagehelper分页插件以及mybatis包扫描配置放置于application.yml中,并在application.yml中配置默认采用开发环境,那么如果不带–spring.profiles.active启动应用就默认为开发环境启动,变动较大的配置如数据库的账号密码分别写入不同环境的配置文件中

1
2
3
4
java复制代码spring:
profiles:
# 默认使用开发环境
active: dev

配置到这里我们的项目目录结构如下图所示
在这里插入图片描述

src/main/resources目录结构
至此我们分别完成了Mybatis、Swagger2以及多环境的集成,接下来我们配置多环境下的logger。对于logger我们总是希望在项目研发过程中越多越好,能够给予足够的信息定位bug,项目处于演示或者上线状态时为了不让日志打印影响程序性能我们只需要警告或者错误的日志,并且需要写入文件,那么接下来就基于logback实现多环境下的日志配置

  1. 多环境下的日志配置

创建logback-spring.xml在application.yml的同级目录,springboot推荐使用logback-spring.xml而不是logback.xml文件,logback-spring.xml的配置内容如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--
简要描述
日志格式 => %d{HH:mm:ss.SSS}(时间) [%-5level](日志级别) %logger{36}(logger名字最长36个字符,否则按照句点分割) - %msg%n(具体日志信息并且换行)

开发环境 => ${basepackage}包下控制台打印DEBUG级别及以上、其他包控制台打印INFO级别及以上
演示(测试)环境 => ${basepackage}包下控制台打印INFO级别及以上、其他包控制台以及文件打印WARN级别及以上
生产环境 => 控制台以及文件打印ERROR级别及以上

日志文件生成规则如下:
文件生成目录 => ${logdir}
当日的log文件名称 => ${appname}.log
其他时候的log文件名称 => ${appname}.%d{yyyy-MM-dd}.log
日志文件最大 => ${maxsize}
最多保留 => ${maxdays}天
-->
<!--自定义参数 -->
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<property name="maxsize" value="30MB" />
<!--只保留最近90天的日志-->
<property name="maxdays" value="90" />
<!--application.yml 传递参数 -->
<!--log文件生成目录-->
<springProperty scope="context" name="logdir" source="resources.logdir"/>
<!--应用名称-->
<springProperty scope="context" name="appname" source="resources.appname"/>
<!--项目基础包-->
<springProperty scope="context" name="basepackage" source="resources.basepackage"/>

<!--输出到控制台 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<!--展示格式 layout-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</pattern>
</layout>
</appender>
<!--输出到文件 FileAppender-->
<appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--
日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<File>${logdir}/${appname}.log</File>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logdir}/${appname}.%d{yyyy-MM-dd}.log</FileNamePattern>
<maxHistory>${maxdays}</maxHistory>
<totalSizeCap>${maxsize}</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- 开发环境-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="consoleLog"/>
</root>
<!--
additivity是子Logger 是否继承 父Logger 的 输出源(appender) 的标志位
在这里additivity配置为false代表如果${basepackage}中有INFO级别日志则子looger打印 root不打印
-->
<logger name="${basepackage}" level="DEBUG" additivity="false">
<appender-ref ref="consoleLog"/>
</logger>
</springProfile>

<!-- 演示(测试)环境-->
<springProfile name="test">
<root level="WARN">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
<logger name="${basepackage}" level="INFO" additivity="false">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</logger>
</springProfile>

<!-- 生产环境 -->
<springProfile name="prod">
<root level="ERROR">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
</springProfile>
</configuration>

日志配置中引用了application.yml的配置信息,主要有logdir、appname、basepackage三项,logdir是日志文件的写入地址,可以传入相对路径,appname是应用名称,引入这项是为了通过日志文件名称区分是哪个应该输出的,basepackage是包过滤配置,比如开发环境中需要打印debug级别以上的日志,但是又想使除我写的logger之外的DEBUG不打印,可过滤到本项目的包名才用DEBUG打印,此外包名使用INFO级别打印,在application.yml中新建这三项配置,也可在不同环境配置不同属性

1
2
3
4
5
6
7
8
xml复制代码#应用配置
resources:
# log文件写入地址
logdir: logs/
# 应用名称
appname: spring-boot-example
# 日志打印的基础扫描包
basepackage: com.spring.demo.springbootexample

使用不同环境启动测试logger配置是否生效,在开发环境下将打印DEBUG级别以上的四条logger记录,在演示环境下降打印INFO级别以上的三条记录并写入文件,在生产环境下只打印ERROR级别以上的一条记录并写入文件

1
2
3
4
5
6
7
8
9
10
11
xml复制代码
@RequestMapping("/logger")
@ResponseBody
public WebResult logger() {
logger.trace("日志输出 {}", "trace");
logger.debug("日志输出 {}", "debug");
logger.info("日志输出 {}", "info");
logger.warn("日志输出 {}", "warn");
logger.error("日志输出 {}", "error");
return "00";
}
  1. 常用配置

加载自定义配置

1
2
3
4
5
6
7
8
9
10
11
xml复制代码@Component
@PropertySource(value = {"classpath:application.yml"}, encoding = "utf-8")
public class Config {

@Value("${resources.midpHost}")
private String midpHost;

public String getMidpHost() {
return midpHost;
}
}

全局异常处理器

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

Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class);

@ExceptionHandler(value = Exception.class)
@ResponseBody
public WebResult exceptionHandle(HttpServletRequest req, Exception ex) {
ex.printStackTrace();
logger.error("未知异常", ex);
return WebResult.error(ERRORDetail.RC_0401001);
}
}

本文转载自: 掘金

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

1…374375376…956

开发者博客

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