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

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


  • 首页

  • 归档

  • 搜索

mysql 按天统计访问量,没有的补0

发表于 2021-08-11

原文我的博客:mysql 按天统计访问量,没有的补0支持原文吧!😋

思路:
先把时间格式一下,然后按时间分组查询

1
2
3
4
5
6
7
sql复制代码SELECT
DATE_FORMAT( created_at,'%Y-%m-%d') access_day,
count( id ) num
FROM
access_logs
GROUP BY
access_day;

执行验证一下

image.png

gorm 中使用

image.png

上面的查询,只能查询出有记录的数据,如果某天没有数据,就会出现下面的现象:

access_day num
2021-8-5 1
2021-8-7 2
2021-8-8 3

而我们想要的是连续的:

access_day num
2021-8-5 1
2021-8-6 0
2021-8-7 2
2021-8-8 3

网上查询了些资料,大概的思路是需要利用一张零时表,生成你需要的日期,然后再连表查询,这里我给出我的实际操作。

比如:我要搜素近7天的访问记录
首先利用interval 、date_sub、curdate函数创建近7天的零时表:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码    SELECT curdate() as createdAt
union all
SELECT date_sub(curdate(), interval 1 day) as createdAt
union all
SELECT date_sub(curdate(), interval 2 day) as createdAt
union all
SELECT date_sub(curdate(), interval 3 day) as createdAt
union all
SELECT date_sub(curdate(), interval 4 day) as createdAt
union all
SELECT date_sub(curdate(), interval 5 day) as createdAt
union all
SELECT date_sub(curdate(), interval 6 day) as createdAt

先看看效果
image.png

然后在另一张表,查出你需要的数据:

1
sql复制代码SELECT DATE_FORMAT( created_at,'%Y-%m-%d') access_day,count(*) access_num FROM access_logs GROUP BY access_day

看下结果:
image.png

最后,连接两张表查出你需要的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码select a.created_at as label,b.access_num as value
from(
SELECT curdate() as created_at
union all
SELECT date_sub(curdate(), interval 1 day) as created_at
union all
SELECT date_sub(curdate(), interval 2 day) as created_at
union all
SELECT date_sub(curdate(), interval 3 day) as created_at
union all
SELECT date_sub(curdate(), interval 4 day) as created_at
union all
SELECT date_sub(curdate(), interval 5 day) as created_at
union all
SELECT date_sub(curdate(), interval 6 day) as created_at
) a left join (
SELECT DATE_FORMAT( created_at,'%Y-%m-%d') access_day,count(*) access_num FROM access_logs GROUP BY access_day
) b on a.created_at = b.access_day order by a.created_at asc;

image.png

这时会发现,没有记录的数据value返回的null,此时需要使用ifnull函数,将null设置为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码select a.created_at as label,IFNULL(b.access_num, 0) as value
from(
SELECT curdate() as created_at
union all
SELECT date_sub(curdate(), interval 1 day) as created_at
union all
SELECT date_sub(curdate(), interval 2 day) as created_at
union all
SELECT date_sub(curdate(), interval 3 day) as created_at
union all
SELECT date_sub(curdate(), interval 4 day) as created_at
union all
SELECT date_sub(curdate(), interval 5 day) as created_at
union all
SELECT date_sub(curdate(), interval 6 day) as created_at
) a left join (
SELECT DATE_FORMAT( created_at,'%Y-%m-%d') access_day,count(*) access_num FROM access_logs GROUP BY access_day
) b on a.created_at = b.access_day order by a.created_at asc;

image.png

此时 mysql查询就完成了。

下面是需要在gorm中使用,为了更好的使用体验,我们肯定是要能指定任意天数的,就需要循环拼接天数的sql语句,下面给出我的方法。
image.png

本文转载自: 掘金

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

Java使用JavaMail收发Email电子邮件 1 邮件

发表于 2021-08-11

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

使用JavaMail收发送电子邮件,包括带有附件和内嵌图片的邮件!

Email就是电子邮件。发邮件是从客户端把邮件发送到邮件服务器,收邮件是把邮件服务器的邮件下载到客户端,Java同样提供了收发邮件的API。

在这里插入图片描述

163、126、QQ、sohu、sina等网站都提供了邮件服务,这些网站都有自己的邮件服务器,我们自己的Email用户页面实际上就相当于客户端。

1 邮件协议

1.1 邮件协议

与HTTP协议相同,收发邮件也是需要有传输协议的:

  1. SMTP:(Simple Mail Transfer Protocol,简单邮件传输协议)发邮件协议;
  2. POP3:(Post Office Protocol Version 3,邮局协议第3版)收邮件协议;
  3. IMAP:(Internet Message Access Protocol,因特网消息访问协议)收发邮件协议。

这些协议都属于应用层协议。

1.2 理解电子邮件收发过程

其实你可以把邮件服务器理解为邮局!如果你需要给朋友寄一封信,那么你需要把信放到邮筒中,这样你的信会“自动”到达邮局,邮局会把信邮到另一个省市的邮局中。然后这封信会被送到收信人的邮箱中。最终收信人需要自己经常查看邮箱是否有新的信件。

其实每个邮件服务器都由SMTP服务器和POP3服务器构成,其中SMTP服务器负责发邮件的请求,而POP3负责收邮件的请求。

在这里插入图片描述

当然,有时我们也会使用163的账号,向126的账号发送邮件。这时邮件是发送到126的邮件服务器,而对于163的邮件服务器是不会存储这封邮件的。

在这里插入图片描述

1.3 邮件服务器名称

smtp服务器的端口号为25,服务器名称为smtp.xxx.xxx。

pop3服务器的端口号为110,服务器名称为pop3.xxx.xxx。

例如:

  1. 163: smtp.163.com和pop3.163.com;
  2. 126: smtp.126.com和pop3.126.com;
  3. qq: smtp.qq.com和pop3.qq.com;
  4. sohu: smtp.sohu.com和pop3.sohu.com;
  5. sina: smtp.sina.com和pop3.sina.com。

2 JavaMail

2.1 JavaMail概述

JavaMail是由SUN公司提供的专门用于Java收发邮件的API,使用Java程序发送邮件时,我们无需关心SMTP协议的底层原理,只需要使用JavaMail这个标准API就可以直接发送邮件。

JavaMail中主要类有javax.mail.Session、javax.mail.internet.MimeMessage、javax.mail.Transport。

  1. Session: 表示会话,即客户端与邮件服务器之间的会话!想获得会话需要给出账户和密码,当然还要给出服务器名称。在邮件服务中的Session对象,就相当于连接数据库时的Connection对象。
  2. MimeMessage: 表示邮件类,它是Message的子类。它包含邮件的主题(标题)、内容,收件人地址、发件人地址,还可以设置抄送和暗送,甚至还可以设置附件。
  3. Transport: 用来发送邮件。它是发送器!

2.2 maven依赖

使用JavaMail的API我们需要引入对应的jar包或者maven依赖:

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/com.sun.mail/javax.mail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>

jar包下载地址为:mvnrepository.com/artifact/co…

在这里插入图片描述

2.3 JavaMail发送邮件

我们使用smtp协议发送邮件!

2.3.1 简单邮件

发送一个简单的邮件需要三步:

  1. 根据服务器参数和认证器信息来获取Session实例;
  2. 根据Session创建MimeMessage对象,MimeMessage中包含了各种可以设置的邮件属性
  3. 通过Transport发送邮件

简单的案例如下:

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复制代码/**
* @author lx
*/
public class SimpleSendEmail {

public static void main(String[] args) throws MessagingException {

/*
* 1 获取Session
*/

/*设置服务器参数*/
Properties props = new Properties();
//设置服务器主机名
props.put("mail.smtp.host", "smtp.163.com");
//设置需要认证
props.put("mail.smtp.auth", "true");
// 启用TLS加密
props.put("mail.smtp.starttls.enable", "true");
/*
* 根据服务器参数集合和认证器来获取Session实例
*
* Authenticator是一个接口表示认证器,即校验客户端的身份。
* 我们需要自己来实现这个接口,实现这个接口需要使用账户和密码。
*/
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
//使用自己的账户和密码,有些邮件服务器可能需要的是申请的授权码
return new PasswordAuthentication("xxxx@163.com", "xxxx");
}
});
// 设置debug模式,将会输出日志信息
session.setDebug(true);

/*
* 2 根据Session创建MimeMessage对象
*
* MimeMessage中包含了各种可以设置的邮件属性
*/
MimeMessage msg = new MimeMessage(session);
//设置发信人
msg.setFrom(new InternetAddress("xxx@163.com"));
//设置收信人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(TO, "xxx@qq.com");
//设置抄送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(CC, "xxx@yeah.net");
//设置暗送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(BCC, "xxx@xx.com");
//设置邮件主题(标题)
msg.setSubject("第一封邮件");
//设置邮件内容(正文)和类型
msg.setContent("说点啥呢?", "text/plain;charset=utf-8");

/*
* 3 发送邮件
*/
Transport.send(msg);
}
}

2.3.2 发送HTML文本

有时候我们的邮件正文可能是一个HTML页面,发送方式和上面的案例完全一致,只需要注意编码格式为“text/html;charset=utf-8”,后面的字符集要与html文本的字符集一致。

当然,也可以发送图片资源等等。

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复制代码/**
* @author lx
*/
public class SendHtmlEmail {

public static void main(String[] args) throws MessagingException {

/*
* 1 获取Session
*/

/*设置服务器参数*/
Properties props = new Properties();
//设置服务器主机名
props.put("mail.smtp.host", "smtp.163.com");
//设置需要认证
props.put("mail.smtp.auth", "true");
// 启用TLS加密
props.put("mail.smtp.starttls.enable", "true");
/*
* 根据服务器参数集合和认证器来获取Session实例
*
* Authenticator是一个接口表示认证器,即校验客户端的身份。
* 我们需要自己来实现这个接口,实现这个接口需要使用账户和密码。
*/
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
//使用自己的账户和密码,有些邮件服务器可能需要的是授权码
return new PasswordAuthentication("xx@163.com", "xxx");
}
});
// 设置debug模式,将会输出日志信息
session.setDebug(true);

/*
* 2 根据Session创建MimeMessage对象
*
* MimeMessage中包含了各种可以设置的邮件属性
*/
MimeMessage msg = new MimeMessage(session);
//设置发信人
msg.setFrom(new InternetAddress("x@163.com"));
//设置收信人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(TO, "xx@qq.com");
//设置抄送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(CC, "xxx@yeah.net");
//设置暗送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(BCC, "xxx@ikang.com");
//设置邮件主题(标题)
msg.setSubject("HTML邮件");
//设置邮件内容(正文)和类型
msg.setContent("<h2><font color=red>2021加油哦!</font></h2>", "text/html;charset =utf-8");

/*
* 3 发送邮件
*/
Transport.send(msg);
}
}

结果:

在这里插入图片描述

2.3.3 发送附件

如果想发送带有附件邮件,那么需要设置邮件的内容为MimeMultiPart,而不仅仅是一段文本。

多部件对象MimeMultiPart,可以理解为是部件的集合。一个Multipart对象可以添加若干个BodyPart,其中第一个BodyPart是文本,即邮件正文,后面的BodyPart是附件,最后将MimeMultiPart设置到msg的content中。

在这里插入图片描述

案例如下:

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
java复制代码/**
* @author lx
*/
public class SendMultipartEmail {

public static void main(String[] args) throws MessagingException, IOException {

/*
* 1 获取Session
*/

/*设置服务器参数*/
Properties props = new Properties();
//设置服务器主机名
props.put("mail.smtp.host", "smtp.163.com");
//设置需要认证
props.put("mail.smtp.auth", "true");
// 启用TLS加密
props.put("mail.smtp.starttls.enable", "true");
/*
* 根据服务器参数集合和认证器来获取Session实例
*
* Authenticator是一个接口表示认证器,即校验客户端的身份。
* 我们需要自己来实现这个接口,实现这个接口需要使用账户和密码。
*/
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
//使用自己的账户和密码,有些邮件服务器可能需要的是申请的授权码
return new PasswordAuthentication("ccc@163.com", "ccc");
}
});
// 设置debug模式,将会输出日志信息
session.setDebug(true);

/*
* 2 根据Session创建MimeMessage对象
*
* MimeMessage中包含了各种可以设置的邮件属性
*/
MimeMessage msg = new MimeMessage(session);
//设置发信人
msg.setFrom(new InternetAddress("ccc@163.com"));
//设置收信人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(TO, "ccc@qq.com");
//设置抄送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(CC, "cc@yeah.net");
//设置暗送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(BCC, "c@ikang.com");
//设置邮件主题(标题)
msg.setSubject("附件");
//设置邮件内容(正文)和类型
//msg.setContent("<h2><font color=red>2021加油哦!</font></h2>", "text/html;charset =utf-8");

/*
* 创建一个多部件对象Multipart,可以理解为是部件的集合。
* 一个Multipart对象可以添加若干个BodyPart,其中第一个BodyPart是文本,即邮件正文,后面的BodyPart是附件
*/

Multipart parts = new MimeMultipart();

/*
* 设置邮件的内容为多部件内容。
*/
msg.setContent(parts);

/*
* 创建第一个部件,为邮件正文
*/
BodyPart contentPart = new MimeBodyPart();
//给部件设置正文内容
contentPart.setContent("<h2><font color=red>附件来了!</font></h2>", "text/html;charset=utf-8");
//把部件添加到部件集中
parts.addBodyPart(contentPart);

/*
* 创建第二个附件部件,一张图片
*/

MimeBodyPart imagepart = new MimeBodyPart();
//设置附件名称
imagepart.setFileName("people.jpg");
//注意,如果在设置文件名称时,文件名称中包含了中文的话,那么需要使用MimeUitlity类来给中文编码:
//imagepart.setFileName(MimeUtility.encodeText("中文.jpg"));

//设置附件,二进制文件可以用application/octet-stream,Word文档则是application/msword。

FileInputStream fileInputStream = new FileInputStream("email\\src\\main\\resources\\people.jpg");
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(fileInputStream, "application/octet-stream")));

//把附件添加到部件集中
parts.addBodyPart(imagepart);

/*
* 3 发送邮件
*/
Transport.send(msg);
}
}

结果:

在这里插入图片描述

2.3.4 发送内嵌图片HTML

发送的HTML文本的内嵌图片可以采用网络链接,也可以使用本地图片。本地内嵌图片实际上也是一个附件,即邮件本身也是Multipart,但需要做一点额外的处理。

在HTML邮件中引用图片时,需要设定一个ID,用类似引用,然后,在添加图片作为BodyPart时,除了要正确设置MIME类型(根据图片类型使用image/jpeg或image/png),还需要设置ContentID为img1与HTML的中的img 标签的img1关联。

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
java复制代码/**
* @author lx
*/
public class SendPicEmail {

public static void main(String[] args) throws MessagingException, IOException {

/*
* 1 获取Session
*/

/*设置服务器参数*/
Properties props = new Properties();
//设置服务器主机名
props.put("mail.smtp.host", "smtp.163.com");
//设置需要认证
props.put("mail.smtp.auth", "true");
// 启用TLS加密
props.put("mail.smtp.starttls.enable", "true");
/*
* 根据服务器参数集合和认证器来获取Session实例
*
* Authenticator是一个接口表示认证器,即校验客户端的身份。
* 我们需要自己来实现这个接口,实现这个接口需要使用账户和密码。
*/
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
//使用自己的账户和密码,有些邮件服务器可能需要的是申请的授权码
return new PasswordAuthentication("xxx@163.com", "xx");
}
});
// 设置debug模式,将会输出日志信息
session.setDebug(true);

/*
* 2 根据Session创建MimeMessage对象
*
* MimeMessage中包含了各种可以设置的邮件属性
*/
MimeMessage msg = new MimeMessage(session);
//设置发信人
msg.setFrom(new InternetAddress("cc@163.com"));
//设置收信人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(TO, "dd@ikang.com");
//设置抄送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(CC, "cc@yeah.net");
//设置暗送人,可以设置多个,采用参数数组或者","分隔
msg.addRecipients(BCC, "xx@qq.com");
//设置邮件主题(标题)
msg.setSubject("这是一封图片HTML邮件");
//设置邮件内容(正文)和类型
//msg.setContent("<h2><font color=red>2021加油哦!</font></h2>", "text/html;charset =utf-8");

/*
* 创建一个多部件对象Multipart,可以理解为是部件的集合。
* 一个Multipart对象可以添加若干个BodyPart,其中第一个BodyPart是文本,即邮件正文,后面的BodyPart是附件
*/

Multipart parts = new MimeMultipart();

/*
* 设置邮件的内容为多部件内容。
*/
msg.setContent(parts);

/*
* 创建第一个部件,为邮件正文
*/
BodyPart contentPart = new MimeBodyPart();
//给部件设置正文内容
contentPart.setContent("<h1>Hello图片来了!</h1><p><img src='cid:img1'></p>", "text/html;charset=utf-8");
//把部件添加到部件集中
parts.addBodyPart(contentPart);

/*
* 创建第二个部件,一张图片,html中的图片也算作附件
*/

MimeBodyPart imagepart = new MimeBodyPart();
//设置附件名称
imagepart.setFileName("people.jpg");
//注意,如果在设置文件名称时,文件名称中包含了中文的话,那么需要使用MimeUitlity类来给中文编码:
//imagepart.setFileName(MimeUtility.encodeText("中文.jpg"));

//设置附件,二进制文件可以用application/octet-stream,Word文档则是application/msword。
FileInputStream fileInputStream = new FileInputStream("email\\src\\main\\resources\\people.jpg");
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(fileInputStream, "image/jpeg")));

//设置ContentID与HTML的中的<img src="cid:img01">关联
imagepart.setContentID("img1");
//把附件添加到部件集中
parts.addBodyPart(imagepart);

/*
* 3 发送邮件
*/
Transport.send(msg);
}
}

2.4 JavaMail收取邮件

我们使用pop3协议收取邮件!

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
java复制代码public class ReceiveEmail {
public static void main(String[] args) throws MessagingException {
//POP3主机名
String host = "pop3.163.com";
//设置传输协议
String protocol = "pop3";
//用户账号
String username = "15732631416@163.com";
//密码或者授权码
String password = "NPYRDKGYNSZIRZYO";


/*
* 获取Session
*/
Properties props = new Properties();
//协议
props.setProperty("mail.store.protocol", protocol);
//POP3主机名
props.setProperty("mail.pop3.host", host);
props.setProperty("mail.smtp.auth", "true");
Session session = Session.getInstance(props);
session.setDebug(true);

/*
* 获取Store,一个Store对象表示整个邮箱的存储
*
*/
URLName urlName = new URLName(protocol, host, 110, null, username, password);
Store store = session.getStore(urlName);
//连接邮件服务器
store.connect();
//要收取邮件,我们需要通过Store访问指定的Folder(文件夹),通常是INBOX表示收件箱
//获取邮箱的邮件夹,通过pop3协议获取某个邮件夹的名称只能为inbox,不区分大小写
Folder folder = store.getFolder("INBOX");
//打开邮箱方式(邮件访问权限),这里的只读权限
folder.open(Folder.READ_ONLY);
//打印邮件总数/新邮件数量/未读数量/已删除数量:
System.out.println("Total messages: " + folder.getMessageCount());
System.out.println("New messages: " + folder.getNewMessageCount());
System.out.println("Unread messages: " + folder.getUnreadMessageCount());
System.out.println("Deleted messages: " + folder.getDeletedMessageCount());

// 获得邮件夹Folder内的所有邮件Message对象,一个Message代表一个邮件
Message[] messages = folder.getMessages();
for (Message message : messages) {
/*解析邮件*/

//获取主题
String subject = message.getSubject();
System.out.println(subject);
//解析其他内容
//………………
}
//传入true表示删除操作会同步到服务器上(即删除服务器收件箱的邮件),无参方法默认传递true
folder.close();
store.close();
}
}

2.5 相关异常

  1. Exception in thread “main” com.sun.mail.smtp.SMTPSendFailedException: 554 DT:SPM
    1. 一般是被邮件服务器识别为垃圾邮件,可以换个复杂点的主题和内容,或者换台主机发送。这很麻烦,比如在运行上面的案例时就有可能抛出这样的异常!
  2. Exception in thread “main” javax.mail.AuthenticationFailedException: 535 Error: authentication failed
    1. PasswordAuthentication中的用户登陆信息错误,可能是密码或者授权码错误。
  3. Exception in thread “main” com.sun.mail.smtp.SMTPSendFailedException: 553 Mail from must equal authorized user
    1. 如果用户信息和发件人信息不一致

3 总结

本次我们学习了使用JavaMail的API简单的收发邮件的过程,电子邮件的收发需要使用道SMTP和POP3协议。在实际开发中,这样的底层API我们用的比较少,因为代码编写很麻烦,因此本文了解就行了。

在实际项目开发中,常常使用Spring框架为我们提供的更高级的发送邮件的API。

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

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

京东:Flink SQL 优化实战 一、背景 二、 Flin

发表于 2021-08-11

简介: 本文着重从 shuffle、join 方式的选择、对象重用、UDF 重用等方面介绍了京东在 Flink SQL 任务方面做的优化措施。

本文作者为京东算法服务部的张颖和段学浩,并由 Apache Hive PMC,阿里巴巴技术专家李锐帮忙校对。主要内容为:

背景Flink SQL 的优化总结

一、背景

目前,京东搜索推荐的数据处理流程如上图所示。可以看到实时和离线是分开的,离线数据处理大部分用的是 Hive / Spark,实时数据处理则大部分用 Flink / Storm。

这就造成了以下现象:在一个业务引擎里,用户需要维护两套环境、两套代码,许多共性不能复用,数据的质量和一致性很难得到保障。且因为流批底层数据模型不一致,导致需要做大量的拼凑逻辑;甚至为了数据一致性,需要做大量的同比、环比、二次加工等数据对比,效率极差,并且非常容易出错。

而支持批流一体的 Flink SQL 可以很大程度上解决这个痛点,因此我们决定引入 Flink 来解决这种问题。

在大多数作业,特别是 Flink 作业中,执行效率的优化一直是 Flink 任务优化的关键,在京东每天数据增量 PB 级情况下,作业的优化显得尤为重要。

写过一些 SQL 作业的同学肯定都知道,对于 Flink SQL 作业,在一些情况下会造成同一个 UDF 被反复调用的情况,这对一些消耗资源的任务非常不友好;此外,影响执行效率大致可以从 shuffle、join、failover 策略等方面考虑;另外,Flink 任务调试的过程也非常复杂,对于一些线上机器隔离的公司来说尤甚。

为此,我们实现了内嵌式的 Derby 来作为 Hive 的元数据存储数据库 (allowEmbedded);在任务恢复方面,批式作业没有 checkpoint 机制来实现failover,但是 Flink 特有的 region 策略可以使批式作业快速恢复;此外,本文还介绍了对象重用等相关优化措施。

二、 Flink SQL 的优化

1. UDF 重用

在 Flink SQL 任务里会出现以下这种情况:如果相同的 UDF 既出现在 LogicalProject 中,又出现在 Where 条件中,那么 UDF 会进行多次调用 (见issues.apache.org/jira/browse…%E3%80%82%E4%BD%86%E6%98%AF%E5%A6%82%E6%9E%9C%E8%AF%A5) UDF 非常耗 CPU 或者内存,这种多余的计算会非常影响性能,为此我们希望能把 UDF 的结果缓存起来下次直接使用。在设计的时候需要考虑:(非常重要:请一定保证 LogicalProject 和 where 条件的 subtask chain 到一起)

  • 一个 taskmanager 里面可能会有多个 subtask,所以这个 cache 要么是 thread (THREAD LOCAL) 级别要么是 tm 级别;
  • 为了防止出现一些情况导致清理 cache 的逻辑走不到,一定要在 close 方法里将 cache 清掉;
  • 为了防止内存无限增大,选取的 cache 最好可以主动控制 size;至于 “超时时间”,建议可以配置一下,但是最好不要小于 UDF 先后调用的时间;
  • 上文有提到过,一个 tm 里面可能会有多个 subtask,相当于 tm 里面是个多线程的环境。首先我们的 cache 需要是线程安全的,然后可根据业务判断需不需要锁。

根据以上考虑,我们用 guava cache 将 UDF 的结果缓存起来,之后调用的时候直接去cache 里面拿数据,最大可能降低任务的消耗。下面是一个简单的使用(同时设置了最大使用 size、超时时间,但是没有写锁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class RandomFunction extends ScalarFunction {
private static Cache<String, Integer> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();

public int eval(String pvid) {
profileLog.error("RandomFunction invoked:" + atomicInteger.incrementAndGet());
Integer result = cache.getIfPresent(pvid);
if (null == result) {
int tmp = (int)(Math.random() * 1000);
cache.put("pvid", tmp);
return tmp;
}
return result;
}
@Override
public void close() throws Exception {
super.close();
cache.cleanUp();
}
}

2. 单元测试

大家可能会好奇为什么会把单元测试也放到优化里面,大家都知道 Flink 任务调试过程非常复杂,对于一些线上机器隔离的公司来说尤甚。京东的本地环境是没有办法访问任务服务器的,因此在初始阶段调试任务,我们耗费了很多时间用来上传 jar 包、查看日志等行为。

为了降低任务的调试时间、增加代码开发人员的开发效率,实现了内嵌式的 Derby 来作为 Hive 的元数据存储数据库 (allowEmbedded),这算是一种优化开发时间的方法。具体思路如下:

首先创建 Hive Conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vbnet复制代码public static HiveConf createHiveConf() {
ClassLoader classLoader = new HiveOperatorTest().getClass().getClassLoader();
HiveConf.setHiveSiteLocation(classLoader.getResource(HIVE_SITE_XML));

try {
TEMPORARY_FOLDER.create();
String warehouseDir = TEMPORARY_FOLDER.newFolder().getAbsolutePath() + "/metastore_db";
String warehouseUri = String.format(HIVE_WAREHOUSE_URI_FORMAT, warehouseDir);

HiveConf hiveConf = new HiveConf();
hiveConf.setVar(
HiveConf.ConfVars.METASTOREWAREHOUSE,
TEMPORARY_FOLDER.newFolder("hive_warehouse").getAbsolutePath());
hiveConf.setVar(HiveConf.ConfVars.METASTORECONNECTURLKEY, warehouseUri);

hiveConf.set("datanucleus.connectionPoolingType", "None");
hiveConf.set("hive.metastore.schema.verification", "false");
hiveConf.set("datanucleus.schema.autoCreateTables", "true");
return hiveConf;
} catch (IOException e) {
throw new CatalogException("Failed to create test HiveConf to HiveCatalog.", e);
}
}

接下来创建 Hive Catalog:(利用反射的方式调用 embedded 的接口)

1
2
3
4
5
6
7
arduino复制代码public static void createCatalog() throws Exception{
Class clazz = HiveCatalog.class;
Constructor c1 = clazz.getDeclaredConstructor(new Class[]{String.class, String.class, HiveConf.class, String.class, boolean.class});
c1.setAccessible(true);
hiveCatalog = (HiveCatalog)c1.newInstance(new Object[]{"test-catalog", null, createHiveConf(), "2.3.4", true});
hiveCatalog.open();
}

创建 tableEnvironment:(同官网)

1
2
3
4
5
6
7
ini复制代码EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
TableConfig tableConfig = tableEnv.getConfig();
Configuration configuration = new Configuration();
configuration.setInteger("table.exec.resource.default-parallelism", 1);
tableEnv.registerCatalog(hiveCatalog.getName(), hiveCatalog);
tableEnv.useCatalog(hiveCatalog.getName());

最后关闭 Hive Catalog:

1
2
3
4
5
csharp复制代码public static void closeCatalog() {
if (hiveCatalog != null) {
hiveCatalog.close();
}
}

此外,对于单元测试,构建合适的数据集也是一个非常大的功能,我们实现了 CollectionTableFactory,允许自己构建合适的数据集,使用方法如下:

1
2
3
4
5
less复制代码CollectionTableFactory.reset();
CollectionTableFactory.initData(Arrays.asList(Row.of("this is a test"), Row.of("zhangying480"), Row.of("just for test"), Row.of("a test case")));
StringBuilder sbFilesSource = new StringBuilder();
sbFilesSource.append("CREATE temporary TABLE db1.`search_realtime_table_dump_p13`(" + " `pvid` string) with ('connector.type'='COLLECTION','is-bounded' = 'true')");
tableEnv.executeSql(sbFilesSource.toString());

3. join 方式的选择

传统的离线 Batch SQL (面向有界数据集的 SQL) 有三种基础的实现方式,分别是 Nested-loop Join、Sort-Merge Join 和 Hash Join。

  • Nested-loop Join 最为简单直接,将两个数据集加载到内存,并用内嵌遍历的方式来逐个比较两个数据集内的元素是否符合 Join 条件。Nested-loop Join 的时间效率以及空间效率都是最低的,可以使用:table.exec.disabled-operators:NestedLoopJoin 来禁用。以下两张图片是禁用前和禁用后的效果 (如果你的禁用没有生效,先看一下是不是 Equi-Join):
  • Sort-Merge Join 分为 Sort 和 Merge 两个阶段:首先将两个数据集进行分别排序,然后再对两个有序数据集分别进行遍历和匹配,类似于归并排序的合并。(Sort-Merge Join 要求对两个数据集进行排序,但是如果两个输入是有序的数据集,则可以作为一种优化方案)。
  • Hash Join 同样分为两个阶段:首先将一个数据集转换为 Hash Table,然后遍历另外一个数据集元素并与 Hash Table 内的元素进行匹配。第一阶段和第一个数据集分别称为 build 阶段和 build table;第二个阶段和第二个数据集分别称为 probe 阶段和 probe table。Hash Join 效率较高但是对空间要求较大,通常是作为 Join 其中一个表为适合放入内存的小表的情况下的优化方案 (并不是不允许溢写磁盘)。

注意:Sort-Merge Join 和 Hash Join 只适用于 Equi-Join ( Join 条件均使用等于作为比较算子)。

Flink 在 join 之上又做了一些细分,具体包括:

  • Repartition-Repartition strategy:Join 的两个数据集分别对它们的 key 使用相同的分区函数进行分区,并经过网络发送数据;
  • Broadcast-Forward strategy:大的数据集不做处理,另一个比较小的数据集全部复制到集群中一部分数据的机器上。

众所周知,batch 的 shuffle 非常耗时间。

  • 如果两个数据集有较大差距,建议采用 Broadcast-Forward strategy;
  • 如果两个数据集差不多,建议采用 Repartition-Repartition strategy。

可以通过:table.optimizer.join.broadcast-threshold 来设置采用 broadcast 的 table 大小,如果设置为 “-1”,表示禁用 broadcast。

下图为禁用前后的效果:

​

4. multiple input

在 Flink SQL 任务里,降低 shuffle 可以有效的提高 SQL 任务的吞吐量,在实际的业务场景中经常遇到这样的情况:上游产出的数据已经满足了数据分布要求 (如连续多个 join 算子,其中 key 是相同的),此时 Flink 的 forward shuffle 是冗余的 shuffle,我们希望将这些算子 chain 到一起。Flink 1.12 引入了 mutiple input 的特性,可以消除大部分没必要的 forward shuffle,把 source 的算子 chain 到一起。

table.optimizer.multiple-input-enabled:true

下图为开了 multiple input 和没有开的拓扑图 ( operator chain 功能已经打开):

​

5. 对象重用

上下游 operator 之间会经过序列化 / 反序列化 / 复制阶段来进行数据传输,这种行为非常影响 Flink SQL 程序的性能,可以通过启用对象重用来提高性能。但是这在 DataStream 里面非常危险,因为可能会发生以下情况:在下一个算子中修改对象意外影响了上面算子的对象。

但是 Flink 的 Table / SQL API 中是非常安全的,可以通过如下方式来启用:

1
2
ini复制代码StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableObjectReuse();

或者是通过设置:pipeline-object-reuse:true

为什么启用了对象重用会有这么大的性能提升?在 Blink planner 中,同一任务的两个算子之间的数据交换最终将调用 BinaryString#copy,查看实现代码,可以发现 BinaryString#copy 需要复制底层 MemorySegment 的字节,通过启用对象重用来避免复制,可以有效提升效率。

下图为没有开启对象重用时相应的火焰图:

6. SQL 任务的 failover 策略

batch 任务模式下 checkpoint 以及其相关的特性全部都不可用,因此针对实时任务的基于 checkpoint 的 failover 策略是不能应用在批任务上面的,但是 batch 任务允许 Task 之间通过 Blocking Shuffle 进行通信,当一个 Task 因为任务未知的原因失败之后,由于 Blocking Shuffle 中存储了这个 Task 所需要的全部数据,所以只需要重启这个 Task 以及通过 Pipeline Shuffle 与其相连的全部下游任务即可:

jobmanager.execution.failover-strategy:region (已经 finish 的 operator 可直接恢复)

table.exec.shuffle-mode:ALL_EDGES_BLOCKING (shuffle 策略)。

7. shuffle

Flink 里的 shuffle 分为 pipeline shuffle 和 blocking shuffle。

  • pipeline shuffle 性能好,但是对资源的要求高,而且容错比较差 (会将该 operator 分到前面的一个 region 里面,对于 batch 任务来说,如果这个算子出问题,将从上一个 region 恢复);
  • blocking shuffle 就是传统的 batch shuffle,会将数据落盘,这种 shuffle 的容错好,但是会产生大量的磁盘、网络 io (如果为了省心的话,建议用 blocking suffle)。blocking shuffle 又分为 hash shuffle 和 sort shuffle,如果你的磁盘是 ssd 并且并发不太大的话,可以选择使用 hash shuffle,这种 shuffle 方式产生的文件多、随机读多,对磁盘 io 影响较大;如果你是 sata 并且并发比较大,可以选择用 sort-merge shuffle,这种 shuffle 产生的数据少,顺序读,不会产生大量的磁盘 io,不过开销会更大一些 (sort merge)。

相应的控制参数:

table.exec.shuffle-mode,该参数有多个参数,默认是 ALL_EDGES_BLOCKING,表示所有的边都会用 blocking shuffle,不过大家可以试一下 POINTWISE_EDGES_PIPELINED,表示 forward 和 rescale edges 会自动开始 pipeline 模式。

taskmanager.network.sort-shuffle.min-parallelism ,将这个参数设置为小于你的并行度,就可以开启 sort-merge shuffle;这个参数的设置需要考虑一些其他的情况,具体的可以按照官网设置。

三、总结

本文着重从 shuffle、join 方式的选择、对象重用、UDF 重用等方面介绍了京东在 Flink SQL 任务方面做的优化措施。另外,感谢京东实时计算研发部付海涛等全部同事的支持与帮助。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

抖音弹幕采集

发表于 2021-08-11

本案例是基于RPC的抖音web直播数据采集。

文章内容仅供参考学习,如有侵权请联系作者进行删除

可采集内容和页面呈现内容相同,包括用户评论、关注、谁来了、送礼物等数据。

RPC(Remote Procedure Call)是远程调用的意思。

在Js逆向时,我们本地可以和浏览器以服务端和客户端的形式通过websocket协议进行RPC通信,这样可以直接调用浏览器中的一些函数方法,不必去在意函数具体的执行逻辑,可以省去大量的逆向调试时间。

像抖音直播间的数据传输采用的是protobuf,如果完全解析的话实在是浪费时间,不适合做案例教程。

还有重要的一点是,通过RPC的方法可以不用搞加密参数signature,开一个页面就可以了。

接口分析

首先通过控制台进行抓包,普通的get请求。有加密参数signature,不过我们不需要搞。

image.png

但是返回的是经过 protobuf 序列化数据。

image.png


更多内容请订阅专栏,查看原文。

专栏链接:blog.csdn.net/weixin_4358…

原文链接:blog.csdn.net/weixin_4358…

本文转载自: 掘金

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

Github标星28K+!这款可视化的对象存储服务真香!

发表于 2021-08-11

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

摘要

在我们平时做项目的时候,文件存储是个很常见的需求。这时候我们就会用到对象存储服务,平时我们可能会选择OSS、AWS S3这类第三方服务。今天带大家搭建一款自己的对象存储服务,带可视化管理,用起来也挺简单!

MinIO简介

MinIO 是一款基于Go语言的高性能对象存储服务,在Github上已有28K+Star。它采用了Apache License v2.0开源协议,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。

安装

使用Docker安装MinIO服务非常简单,几个命令就可以搞定!

  • 首先下载MinIO的Docker镜像;
1
bash复制代码docker pull minio/minio
  • 下载完成后使用如下命令运行MinIO服务,注意使用--console-address指定MinIO Console的运行端口(否则会随机端口运行):
1
2
3
4
5
bash复制代码docker run -p 9090:9000 -p 9001:9001 --name minio \
-v /mydata/minio/data:/data \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
-d minio/minio server /data --console-address ":9001"
  • 运行成功后就可访问MinIO Console的管理界面了,输入账号密码minioadmin:minioadmin即可登录,访问地址:http://192.168.7.142:9090

MinIO Console使用

MinIO Console是MinIO自带的可视化管理工具,比起上一代的可视化工具功能还是强大了不少的,下面我们来体验下这个工具。

  • 先来看下上一代的MinIO Browser,基本只支持存储桶及文件的管理功能;

  • 再来看下MinIO Console,不仅支持了存储桶、文件的管理,还增加了用户、权限、日志等管理功能,强了不少;

  • 在存储文件之前,我们首先得创建一个存储桶;

  • 创建成功后,再上传一个文件;

  • 上传成功后如果你想从外部访问文件的话,需要把访问策略设置为公开,这里的策略只有公开和私有两种,感觉不太灵活;

  • 之后把地址改为外网访问地址即可访问图片,默认只能下载不能直接查看(这个问题我们下面再解决),外网访问地址:http://192.168.7.142:9090/blog/avatar.png

客户端使用

其实对于对象存储来说,MinIO Console的功能还是不够用的,所以官方还提供了基于命令行的客户端MinIO Client(简称mc),下面我们来讲讲它的使用方法。

常用命令

我们先来熟悉下mc的命令,这些命令和Linux中的命令有很多相似之处。

命令 作用
ls 列出文件和文件夹
mb 创建一个存储桶或一个文件夹
rb 删除一个存储桶或一个文件夹
cat 显示文件和对象内容
pipe 将一个STDIN重定向到一个对象或者文件或者STDOUT
share 生成用于共享的URL
cp 拷贝文件和对象
mirror 给存储桶和文件夹做镜像
find 基于参数查找文件
diff 对两个文件夹或者存储桶比较差异
rm 删除文件和对象
events 管理对象通知
watch 监听文件和对象的事件
policy 管理访问策略
session 为cp命令管理保存的会话
config 管理mc配置文件
update 检查软件更新
version 输出版本信息

安装及配置

由于MinIO服务端中并没有自带客户端,所以我们需要安装并配置完客户端后才能使用,这里以Docker环境下的安装为例。

  • 下载MinIO Client 的Docker镜像;
1
bash复制代码docker pull minio/mc
  • 在Docker容器中运行mc;
1
bash复制代码docker run -it --entrypoint=/bin/sh minio/mc
  • 运行完成后我们需要进行配置,将我们自己的MinIO服务配置到客户端上去,配置的格式如下;
1
bash复制代码mc config host add <ALIAS> <YOUR-S3-ENDPOINT> <YOUR-ACCESS-KEY> <YOUR-SECRET-KEY>
  • 对于我们的MinIO服务可以这样配置。
1
bash复制代码mc config host add minio http://192.168.7.142:9090 minioadmin minioadmin

常用操作

  • 查看存储桶和查看存储桶中存在的文件;
1
2
3
4
bash复制代码# 查看存储桶
mc ls minio
# 查看存储桶中存在的文件
mc ls minio/blog

  • 创建一个名为test的存储桶;
1
bash复制代码mc mb minio/test

  • 共享avatar.png文件的下载路径;
1
bash复制代码mc share download minio/blog/avatar.png

  • 查找blog存储桶中的png文件;
1
bash复制代码mc find minio/blog --name "*.png"

  • 设置test存储桶的访问权限为只读。
1
2
3
4
bash复制代码# 目前可以设置这四种权限:none, download, upload, public
mc policy set download minio/test/
# 查看存储桶当前权限
mc policy list minio/test/

兼容AWS S3

当我们对接第三方服务要用到对象存储时,这些服务往往都是支持AWS S3的。比如说一个直播的回放功能,需要对象存储来存储回放的视频,由于MinIO兼容AWS S3的大多数API,我们可以直接拿它当AWS S3来使用。

  • 我们可以下载个AWS S3的客户端来试试,MinIO到底能不能支持S3的API,这里使用的是S3 Browser,下载地址:s3browser.com/

  • 安装好S3 Browser之后,添加一个Account,输入相关登录信息,注意选择Account类型为S3 Compatible Storage;

  • 连接成功后,我们可以看见之前我们创建的存储桶和上传的文件;

  • S3 Browser这个工具功能还是很强大的,MinIO Console和它比起来实在太弱了;

  • 上面有提到一个问题,图片文件无法直接查看,其实是因为访问图片文件时,MinIO返回的Content-Type为application/octet-stream导致的;

  • 接下来我们可以通过S3 Browser来修改默认返回的响应头;

  • 然后将.png开头的文件的响应头改为image/png就可以了;

  • 需要注意的是之前上传的文件需要重新上传下才可以生效,此时访问链接就可以直接查看图片了;

  • 如果你想修改存储桶的访问权限的话直接通过Permissions标签修改即可,是不是比MinIO Console灵活多了。

总结

如果你想自建对象存储服务的话,MinIO确实是首选。它能兼容AWS S3的API,使用MinIO相当于是在使用AWS S3,能兼容一些主流的第三方服务。不过它自带的客户端MinIO Console确实有点鸡肋,还好支持了AWS S3,可以使用一些功能强大的S3客户端工具。

参考资料

官方文档:docs.min.io/

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

发表于 2021-08-11
  1. 前言

大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

之前写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex420余篇源码文章。

写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。

所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。比如工具函数。本文通过学习Vue3源码中的工具函数模块的源码,学习源码为自己所用。歌德曾说:读一本好书,就是在和高尚的人谈话。
同理可得:读源码,也算是和作者的一种学习交流的方式。

阅读本文,你将学到:

1
2
3
4
5
js复制代码1. 如何学习 JavaScript 基础知识,会推荐很多学习资料
2. 如何学习调试 vue 3 源码
3. 如何学习源码中优秀代码和思想,投入到自己的项目中
4. Vue 3 源码 shared 模块中的几十个实用工具函数
5. 我的一些经验分享

shared模块中57个工具函数,本次阅读其中的30余个。

  1. 环境准备

2.1 读开源项目 贡献指南

打开 vue-next,
开源项目一般都能在 README.md 或者 .github/contributing.md 找到贡献指南。

而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来,项目目录结构是怎样的。怎么投入开发,需要哪些知识储备等。

我们可以在 项目目录结构 描述中,找到shared模块。

shared: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).

README.md 和 contributing.md 一般都是英文的。可能会难倒一部分人。其实看不懂,完全可以可以借助划词翻译,整页翻译和百度翻译等翻译工具。再把英文加入后续学习计划。

本文就是讲shared模块,对应的文件路径是:vue-next/packages/shared/src/index.ts

也可以用github1s访问,速度更快。github1s packages/shared/src/index.ts

2.2 按照项目指南 打包构建代码

为了降低文章难度,我按照贡献指南中方法打包把ts转成了js。如果你需要打包,也可以参考下文打包构建。

你需要确保 Node.js 版本是 10+, 而且 yarn 的版本是 1.x Yarn 1.x。

你安装的 Node.js 版本很可能是低于 10。最简单的办法就是去官网重新安装。也可以使用 nvm等管理Node.js版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码node -v
# v14.16.0
# 全局安装 yarn

# 推荐克隆我的项目
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis/vue-next

# 或者克隆官方项目
git clone https://github.com/vuejs/vue-next.git
cd vue-next

npm install --global yarn
yarn # install the dependencies of the project
yarn build

可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js,文件也就是纯js文件。接下来就是解释其中的一些方法。

当然,前面可能比较啰嗦。我可以直接讲 3. 工具函数。但通过我上文的介绍,即使是初学者,都能看懂一些开源项目源码,也许就会有一定的成就感。
另外,面试问到被类似的问题或者笔试题时,你说看Vue3源码学到的,面试官绝对对你刮目相看。

2.3 如何生成 sourcemap 调试 vue-next 源码

熟悉我的读者知道,我是经常强调生成sourcemap调试看源码,所以顺便提一下如何配置生成sourcemap,如何调试。这部分可以简单略过,动手操作时再仔细看。

其实贡献指南里描述了。

Build with Source Maps
Use the --sourcemap or -s flag to build with source maps. Note this will make the build much slower.

所以在 vue-next/package.json 追加 "dev:sourcemap": "node scripts/dev.js --sourcemap",yarn dev:sourcemap执行,即可生成sourcemap,或者直接 build。

1
2
3
4
5
6
7
json复制代码// package.json
{
"version": "3.2.1",
"scripts": {
"dev:sourcemap": "node scripts/dev.js --sourcemap"
}
}

会在控制台输出类似vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js的信息。

其中packages/vue/dist/vue.global.js.map 就是sourcemap文件了。

我们在 Vue3官网找个例子,在 vue-next/examples/index.html。其内容引入packages/vue/dist/vue.global.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// vue-next/examples/index.html
<script src="../../packages/vue/dist/vue.global.js"></script>
<script>
const Counter = {
data() {
return {
counter: 0
}
}
}

Vue.createApp(Counter).mount('#counter')
</script>

然后我们新建一个终端窗口,yarn serve,在浏览器中打开http://localhost:5000/examples/,如下图所示,按F11等进入函数,就可以愉快的调试源码了。

vue-next-debugger

  1. 工具函数

本文主要按照源码 vue-next/packages/shared/src/index.ts 的顺序来写。也省去了一些从外部导入的方法。

我们也可以通过ts文件,查看使用函数的位置。同时在VSCode运行调试JS代码,我们比较推荐韩老师写的code runner插件。

3.1 babelParserDefaultPlugins babel 解析默认插件

1
2
3
4
5
6
7
8
9
10
11
js复制代码/**
* List of @babel/parser plugins that are used for template expression
* transforms and SFC script transforms. By default we enable proposals slated
* for ES2020. This will need to be updated as the spec moves forward.
* Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
*/
const babelParserDefaultPlugins = [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
];

这里就是几个默认插件。感兴趣看英文注释查看。

3.2 EMPTY_OBJ 空对象

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复制代码const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {};

// 例子:
// Object.freeze 是 冻结对象
// 冻结的对象最外层无法修改。
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = '若川';
console.log(EMPTY_OBJ_1.name); // undefined

const EMPTY_OBJ_2 = Object.freeze({ props: { mp: '若川视野' } });
EMPTY_OBJ_2.props.name = '若川';
EMPTY_OBJ_2.props2 = 'props2';
console.log(EMPTY_OBJ_2.props.name); // '若川'
console.log(EMPTY_OBJ_2.props2); // undefined
console.log(EMPTY_OBJ_2);
/**
*
* {
* props: {
mp: "若川视野",
name: "若川"
}
* }
* */

process.env.NODE_ENV 是 node 项目中的一个环境变量,一般定义为:development 和production。根据环境写代码。比如开发环境,有报错等信息,生产环境则不需要这些报错警告。

3.3 EMPTY_ARR 空数组

1
2
3
4
5
6
js复制代码const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];

// 例子:
EMPTY_ARR.push(1) // 报错,也就是为啥生产环境还是用 []
EMPTY_ARR.length = 3;
console.log(EMPTY_ARR.length); // 0

3.4 NOOP 空函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码const NOOP = () => { };

// 很多库的源码中都有这样的定义函数,比如 jQuery、underscore、lodash 等
// 使用场景:1. 方便判断, 2. 方便压缩
// 1. 比如:
const instance = {
render: NOOP
};

// 条件
const dev = true;
if(dev){
instance.render = function(){
console.log('render');
}
}

// 可以用作判断。
if(instance.render === NOOP){
console.log('i');
}
// 2. 再比如:
// 方便压缩代码
// 如果是 function(){} ,不方便压缩代码

3.5 NO 永远返回 false 的函数

1
2
3
4
5
6
7
js复制代码/**
* Always return false.
*/
const NO = () => false;

// 除了压缩代码的好处外。
// 一直返回 false

3.6 isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母

1
2
3
4
5
6
7
js复制代码const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);

// 例子:
isOn('onChange'); // true
isOn('onchange'); // false
isOn('on3change'); // true

onRE 是正则。^符号在开头,则表示是什么开头。而在其他地方是指非。

与之相反的是:$符合在结尾,则表示是以什么结尾。

[^a-z]是指不是a到z的小写字母。

同时推荐一个正则在线工具。

regex101

另外正则看老姚的迷你书就够用了。

老姚:《JavaScript 正则表达式迷你书》问世了!

3.7 isModelListener 监听器

判断字符串是不是以onUpdate:开头

1
2
3
4
5
6
js复制代码const isModelListener = (key) => key.startsWith('onUpdate:');

// 例子:
isModelListener('onUpdate:change'); // true
isModelListener('1onUpdate:change'); // false
// startsWith 是 ES6 提供的方法

ES6入门教程:字符串的新增方法

很多方法都在《ES6入门教程》中有讲到,就不赘述了。

3.8 extend 继承 合并

说合并可能更准确些。

1
2
3
4
5
6
7
8
js复制代码const extend = Object.assign;

// 例子:
const data = { name: '若川' };
const data2 = extend(data, { mp: '若川视野', name: '是若川啊' });
console.log(data); // { name: "是若川啊", mp: "若川视野" }
console.log(data2); // { name: "是若川啊", mp: "若川视野" }
console.log(data === data2); // true

3.9 remove 移除数组的一项

1
2
3
4
5
6
7
8
9
10
11
js复制代码const remove = (arr, el) => {
const i = arr.indexOf(el);
if (i > -1) {
arr.splice(i, 1);
}
};

// 例子:
const arr = [1, 2, 3];
remove(arr, 3);
console.log(arr); // [1, 2]

splice 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。

引申:axios InterceptorManager 拦截器源码 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行,同样效果。axios 拦截器这个场景下,不得不说为性能做到了很好的考虑。

看如下 axios 拦截器代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
this.handlers[id] = null;
}

// 执行
if (h !== null) {
fn(h);
}

3.10 hasOwn 是不是自己本身所拥有的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);

// 例子:

// 特别提醒:__proto__ 是浏览器实现的原型写法,后面还会用到
// 现在已经有提供好几个原型相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数,并执行函数。

hasOwn({__proto__: { a: 1 }}, 'a') // false
hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

对象API可以看我之前写的一篇文章JavaScript 对象所有API解析,写的还算全面。

3.11 isArray 判断数组

1
2
3
4
5
6
7
js复制代码const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以 instanceof 这种情况 不准确

3.12 isMap 判断是不是 Map 对象

1
2
3
4
5
6
7
8
9
js复制代码const isMap = (val) => toTypeString(val) === '[object Map]';

// 例子:
const map = new Map();
const o = { p: 'Hello World' };

map.set(o, 'content');
map.get(o); // 'content'
isMap(map); // true

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

3.13 isSet 判断是不是 Set 对象

1
2
3
4
5
js复制代码const isSet = (val) => toTypeString(val) === '[object Set]';

// 例子:
const set = new Set();
isSet(set); // true

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

3.14 isDate 判断是不是 Date 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码const isDate = (val) => val instanceof Date;

// 例子:
isDate(new Date()); // true

// `instanceof` 操作符左边是右边的实例。但不是很准,但一般够用了。原理是根据原型链向上查找的。

isDate({__proto__: new Date()}); // true
// 实际上是应该是 Object 才对。
// 所以用 instanceof 判断数组也不准确。
// 再比如
({__proto__: [] }) instanceof Array; // true
// 实际上是对象。
// 所以用 数组本身提供的方法 Array.isArray 是比较准确的。

3.15 isFunction 判断是不是函数

1
2
js复制代码const isFunction = (val) => typeof val === 'function';
// 判断函数有多种方法,但这个是比较常用也相对兼容性好的。

3.16 isString 判断是不是字符串

1
2
3
4
js复制代码const isString = (val) => typeof val === 'string';

// 例子:
isString('') // true

3.17 isSymbol 判断是不是 Symbol

1
2
3
4
5
6
7
8
js复制代码const isSymbol = (val) => typeof val === 'symbol';

// 例子:
let s = Symbol();

typeof s;
// "symbol"
// Symbol 是函数,不需要用 new 调用。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。

3.18 isObject 判断是不是对象

1
2
3
4
5
6
js复制代码const isObject = (val) => val !== null && typeof val === 'object';

// 例子:
isObject(null); // false
isObject({name: '若川'}); // true
// 判断不为 null 的原因是 typeof null 其实 是 object

3.19 isPromise 判断是不是 Promise

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};

// 判断是不是Promise对象
const p1 = new Promise(function(resolve, reject){
resolve('若川');
});
isPromise(p1); // true

// promise 对于初学者来说可能比较难理解。但是重点内容,JS异步编程,要着重掌握。
// 现在 web 开发 Promise 和 async await 等非常常用。

可以根据文末推荐的书籍看Promise相关章节掌握。同时也推荐这本迷你书JavaScript Promise迷你书(中文版)

3.20 objectToString 对象转字符串

1
2
3
js复制代码const objectToString = Object.prototype.toString;

// 对象转字符串

3.21 toTypeString 对象转字符串

1
2
3
4
js复制代码const toTypeString = (value) => objectToString.call(value);

// call 是一个函数,第一个参数是 执行函数里面 this 指向。
// 通过这个能获得 类似 "[object String]" 其中 String 是根据类型变化的

3.22 toRawType 对象转字符串 截取后几位

1
2
3
4
5
6
7
js复制代码const toRawType = (value) => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1);
};

// 截取到
toRawType(''); 'String'

可以 截取到 String Array 等这些类型

是 JS 判断数据类型非常重要的知识点。

JS 判断类型也有 typeof ,但不是很准确,而且能够识别出的不多。

这些算是基础知识

mdn typeof 文档,文档比较详细,也实现了一个很完善的type函数,本文就不赘述了。

1
2
3
4
5
6
7
8
9
js复制代码// typeof 返回值目前有以下8种 
'undefined'
'object'
'boolean'
'number'
'bigint'
'string'
'symobl'
'function'

3.23 isPlainObject 判断是不是纯粹的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
//
const isPlainObject = (val) => toTypeString(val) === '[object Object]';

// 前文中 有 isObject 判断是不是对象了。
// isPlainObject 这个函数在很多源码里都有,比如 jQuery 源码和 lodash 源码等,具体实现不一样
// 上文的 isObject([]) 也是 true ,因为 type [] 为 'object'
// 而 isPlainObject([]) 则是false
const Ctor = function(){
this.name = '我是构造函数';
}
isPlainObject({}); // true
isPlainObject(new Ctor()); // true

3.24 isIntegerKey 判断是不是数字型的字符串key值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码const isIntegerKey = (key) => isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key;

// 例子:
isIntegerKey('a'); // false
isIntegerKey('0'); // true
isIntegerKey('011'); // false
isIntegerKey('11'); // true
// 其中 parseInt 第二个参数是进制。
// 字符串能用数组取值的形式取值。
// 还有一个 charAt 函数,但不常用
'abc'.charAt(0) // 'a'
// charAt 与数组形式不同的是 取不到值会返回空字符串'',数组形式取值取不到则是 undefined

3.25 makeMap && isReservedProp

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项。

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
js复制代码/**
* Make a map and return a function for checking if a key
* is in that map.
* IMPORTANT: all calls of this function must be prefixed with
* \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary.
*/
function makeMap(str, expectsLowerCase) {
const map = Object.create(null);
const list = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val];
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted');

// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp('onVnodeMounted'); // true
isReservedProp('onVnodeBeforeUpdate'); // true
isReservedProp('onVnodeUpdated'); // true
isReservedProp('onVnodeBeforeUnmount'); // true
isReservedProp('onVnodeUnmounted'); // true

3.26 cacheStringFunction 缓存

1
2
3
4
5
6
7
js复制代码const cacheStringFunction = (fn) => {
const cache = Object.create(null);
return ((str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
});
};

这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。
《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。

1
2
3
4
5
6
js复制代码var getSingle = function(fn){ // 获取单例
var result;
return function(){
return result || (result = fn.apply(this, arguments));
}
};

以下是一些正则,系统学习正则推荐老姚:《JavaScript 正则表达式迷你书》问世了!,看过的都说好。所以本文不会过多描述正则相关知识点。

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
js复制代码// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
// () 小括号是 分组捕获
const camelizeRE = /-(\w)/g;
/**
* @private
*/
// 连字符 - 转驼峰 on-click => onClick
const camelize = cacheStringFunction((str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
// \B 是指 非 \b 单词边界。
const hyphenateRE = /\B([A-Z])/g;
/**
* @private
*/

const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());

// 举例:onClick => on-click
const hyphenateResult = hyphenate('onClick');
console.log('hyphenateResult', hyphenateResult); // 'on-click'

/**
* @private
*/
// 首字母转大写
const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
/**
* @private
*/
// click => onClick
const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``));

const result = toHandlerKey('click');
console.log(result, 'result'); // 'onClick'

3.27 hasChanged 判断是不是有变化

hasChanged 这个方法,值得一提的是:我刚写这篇文章时,还没有用Object.is,后来看 git 记录发现有人 提PR 修改为Object.is了,尤大合并了。

1
js复制代码const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

以下是原先的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
// 例子:
// 认为 NaN 是不变的
hasChanged(NaN, NaN); // false
hasChanged(1, 1); // false
hasChanged(1, 2); // true
hasChanged(+0, -0); // false
// Obect.is 认为 +0 和 -0 不是同一个值
Object.is(+0, -0); // false
// Object.is 认为 NaN 和 本身 相比 是同一个值
Object.is(NaN, NaN); // true
// 场景
// watch 监测值是不是变化了

// (value === value || oldValue === oldValue)
// 为什么会有这句 因为要判断 NaN 。认为 NaN 是不变的。因为 NaN === NaN 为 false

根据 hasChanged 这个我们继续来看看:Object.is API。

Object.is(value1, value2) (ES6)

该方法用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致。 不同之处只有两个:一是+0不等于-0,而是 NaN 等于自身。

1
2
3
4
5
6
js复制代码Object.is('若川', '若川'); // true
Object.is({},{}); // false
Object.is(+0, -0); // false
+0 === -0; // true
Object.is(NaN, NaN); // true
NaN === NaN; // false

ES5可以通过以下代码部署Object.is。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码Object.defineProperty(Object, 'is', {
value: function() {x, y} {
if (x === y) {
// 针对+0不等于-0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对 NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});

根据举例可以说明

3.28 invokeArrayFns 执行数组里的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg);
}
};

// 例子:
const arr = [
function(val){
console.log(val + '的博客地址是:https://lxchuan12.gitee.io');
},
function(val){
console.log('百度搜索 若川 可以找到' + val);
},
function(val){
console.log('微信搜索 若川视野 可以找到关注' + val);
},
]
invokeArrayFns(arr, '我');

为什么这样写,我们一般都是一个函数执行就行。

数组中存放函数,函数其实也算是数据。这种写法方便统一执行多个函数。

3.29 def 定义对象属性

1
2
3
4
5
6
7
js复制代码const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
});
};

Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)

Object.defineProperty 涉及到比较重要的知识点。

在ES3中,除了一些内置属性(如:Math.PI),对象的所有的属性在任何时候都可以被修改、插入、删除。在ES5中,我们可以设置属性是否可以被改变或是被删除——在这之前,它是内置属性的特权。ES5中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权。这些属性描述符(特性)包括:

value——当试图获取属性时所返回的值。

writable——该属性是否可写。

enumerable——该属性在for in循环中是否会被枚举。

configurable——该属性是否可被删除。

set()——该属性的更新操作所调用的函数。

get()——获取属性值时所调用的函数。

另外,数据描述符(其中属性为:enumerable,configurable,value,writable)与存取描述符(其中属性为enumerable,configurable,set(),get())之间是有互斥关系的。在定义了set()和get()之后,描述符会认为存取操作已被 定义了,其中再定义value和writable会引起错误。

以下是ES3风格的属性定义方式:

1
2
js复制代码var person = {};
person.legs = 2;

以下是等价的ES5通过数据描述符定义属性的方式:

1
2
3
4
5
6
7
js复制代码var person = {};
Object.defineProperty(person, 'legs', {
value: 2,
writable: true,
configurable: true,
enumerable: true
});

其中, 除了value的默认值为undefined以外,其他的默认值都为false。这就意味着,如果想要通过这一方式定义一个可写的属性,必须显示将它们设为true。
或者,我们也可以通过ES5的存储描述符来定义:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码var person = {};
Object.defineProperty(person, 'legs', {
set:function(v) {
return this.value = v;
},
get: function(v) {
return this.value;
},
configurable: true,
enumerable: true
});
person.legs = 2;

这样一来,多了许多可以用来描述属性的代码,如果想要防止别人篡改我们的属性,就必须要用到它们。此外,也不要忘了浏览器向后兼容ES3方面所做的考虑。例如,跟添加Array.prototype属性不一样,我们不能再旧版的浏览器中使用shim这一特性。
另外,我们还可以(通过定义nonmalleable属性),在具体行为中运用这些描述符:

1
2
3
4
5
6
js复制代码var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1 (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)

其他本文就不过多赘述了。更多对象 API 可以查看这篇文章JavaScript 对象所有API解析。

3.30 toNumber 转数字

1
2
3
4
5
6
7
8
9
js复制代码const toNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};

toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true

其实 isNaN 本意是判断是不是 NaN 值,但是不准确的。
比如:isNaN('a') 为 true。
所以 ES6 有了 Number.isNaN 这个判断方法,为了弥补这一个API。

1
2
js复制代码Number.isNaN('a')  // false
Number.isNaN(NaN); // true

3.31 getGlobalThis 全局对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码let _globalThis;
const getGlobalThis = () => {
return (_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}));
};

获取全局 this 指向。

初次执行肯定是 _globalThis 是 undefined。所以会执行后面的赋值语句。

如果存在 globalThis 就用 globalThis。MDN globalThis

如果存在self,就用self。在 Web Worker 中不能访问到 window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。

如果存在window,就用window。

如果存在global,就用global。Node环境下,使用global。

如果都不存在,使用空对象。可能是微信小程序环境下。

下次执行就直接返回 _globalThis,不需要第二次继续判断了。这种写法值得我们学习。

  1. 最后推荐一些文章和书籍

先推荐我认为不错的JavaScript API的几篇文章和几本值得读的书。

JavaScript字符串所有API全解密

【深度长文】JavaScript数组所有API全解密

正则表达式前端使用手册

老姚:《JavaScript 正则表达式迷你书》问世了!

老姚浅谈:怎么学JavaScript?

JavaScript 对象所有API解析 lxchuan12.gitee.io/js-object-a…

MDN JavaScript

《JavaScript高级程序设计》第4版

《JavaScript 权威指南》第7版

《JavaScript面向对象编程2》 面向对象讲的很详细。

阮一峰老师:《ES6 入门教程》

《现代 JavaScript 教程》

《你不知道的JavaScript》上中卷

《JavaScript 设计模式与开发实践》

我也是从小白看不懂书经历过来的。到现在写文章分享。

我看书的方法:多本书同时看,看相同类似的章节,比如函数。看完这本可能没懂,看下一本,几本书看下来基本就懂了,一遍没看懂,再看几遍,可以避免遗忘,巩固相关章节。当然,刚开始看书很难受,看不进。这些书大部分在微信读书都有,如果习惯看纸质书,那可以买来看。

这时可以看些视频和动手练习一些简单的项目。

比如:可以自己注册一个github账号,分章节小节,抄写书中的代码,提交到github,练习了才会更有感觉。

再比如 freeCodeCamp 中文在线学习网站 网站。看书是系统学习非常好的方法。后来我就是看源码较多,写文章分享出来给大家。

  1. 总结

文中主要通过学习 shared 模块下的几十个工具函数,比如有:isPromise、makeMap、cacheStringFunction、invokeArrayFns、def、getGlobalThis等等。

同时还分享了vue源码的调试技巧,推荐了一些书籍和看书籍的方法。

源码也不是那么可怕。平常我们工作中也是经常能使用到这些工具函数。通过学习一些简单源码,拓展视野的同时,还能落实到自己工作开发中,收益相对比较高。

关于

最后可以持续关注我@若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.2k+人)第一的专栏,写有20余篇源码文章。包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite、create-vite 等20余篇源码文章。


作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan02。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,成为高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

动态日志级别:小功能,大用处

发表于 2021-08-11

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

Log4j,Log4j2,Logback是当下主流的日志框架,Slf4j则是新一代的日志框架接口,其中Logback直接实现了Slf4j的接口,同时它也是SpringBoot的默认日志框架。

但从性能以及工程的角度上来看,Log4j2是事实标准,本文也是基于Log4j2来撰写的。

为什么需要动态日志级别

关于Log4j2的名词小知识:

  • 配置方式:工业级项目一般都采用XML的配置方式
  • 同步/异步:Log4j2支持同步/异步日志两种方式,如无特殊的场景需要,建议采用高性能的异步方式
  • Logger:Logger节点是用来单独指定日志的表现形式配置项,包括但不限于:日志级别、关联Appender、Name属性等
  • Appender:Appender通常只负责将事件数据写入目标目标目标,由Logger触发指定的Appender执行。

作为一个合格的程序员,大家对于日志的重要性以及日志框架的基本使用方法都了然于心。

在绝大多数时候,日志都是帮助我们定位问题的利器,但所有事物都有两面性,有时它也会成为问题的导火索。

接口莫名其妙变慢?

一般来说,接口的响应时间基本都花在网络、DB层、IO层或者部分计算上,但我某一次排查B端线上问题时,竟然发现是由于打印日志拖垮了整个接口。

由于老系统的某个业务异常处理不合理,导致有大量的错误日志输出,进而造成当前线程阻塞,机器负载升高,整个接口的吞吐量降低。

定义这种莫名其妙的问题倒也简单:

  1. 查看机器磁盘是否异常(磁盘占用、文件大小)
  2. 通过Jstack命令检查占用较高的线程
  3. 观察日志输出情况(异常情况下非常明显)

PS:当时的处理方案是优化了日志输出,同时将同步日志调整为异步日志

线上CPU飙赠

我们在看日志时都希望准确,干净,可复制至其他工具进行分析,所以日志一般都会这么写:

1
java复制代码 log.info("Method :: Kerwin.Demo, Params: {}, Result:{}", JSONUtil.toJsonStr(demo), JSONUtil.toJsonStr(demo));

这就涉及到了序列化操作,当此类日志数量较多,接口调用次数较高时(每分钟几十万的C端调用),CPU占用就会非常明显的升高,即使整个程序没有任何问题。

所以我们需要使用日志框架的动态化调整日志级别的能力,这样一来我们在编码阶段所留下的日志也不需要在上线前删除了,同时可以更灵活的应对线上问题的排查和日常使用。

如何动态配置日志级别

以Log4j2为例,我们可能会这样配置根节点(ROOT Logger)

1
2
3
4
5
6
xml复制代码<Loggers>
<AsyncRoot level="ERROR" includeLocation="true">
<AppenderRef ref="INFO_LOG"/>
<AppenderRef ref="ERROR_LOG"/>
</AsyncRoot>
</Loggers>

使用以下代码,并查看日志文件

1
2
3
4
5
6
java复制代码@Slf4j
public class LogTest {
public static void main(String[] args) {
log.info("This is a Demo...");
}
}

image-20210809000506403.png

因为我调整ROOT的日志级别为ERROR,因此无任何日志输出

利用Log4j2提供的能力,修改日志级别,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Slf4j
public class LogTest {
public static void main(String[] args) {
log.info("This is a Demo... 111");
Level level = Level.toLevel("info");
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration configuration = context.getConfiguration();
configuration.getLoggerConfig("ROOT").setLevel(level);
context.updateLoggers(configuration);
log.info("This is a Demo... 222");
}
}

结果如下所示:

image-20210809001159978.png

非常容易的实现了修改日志级别的能力,因此我们在构建自己的应用或者系统时,可以使用ZK进行动态化配置,也可以使用HTTP或RPC接口留一个后门,以此来实现动态调整的能力。

如何配置指定类的日志级别

上面的代码演示了如何动态配置ROOT Logger节点,如果您在阅读本文时能使用IDE,自然能看到

org.apache.logging.log4j.core.config.Configuration#getLoggers,该方法包括了每一个自定义logger的配置参数,同样可以使用上述方式进行配置和修改。

代码范例:

1
2
3
4
5
6
7
8
9
10
11
java复制代码String packetLoggerName = "log.demo";
Level level = Level.toLevel("DEBUG");
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration configuration = context.getConfiguration();
for (Map.Entry<String, LoggerConfig> entry : configuration.getLoggers().entrySet()) {
if (packetLoggerName.equals(entry.getKey())) {
entry.getValue().setLevel(level);
}
}

context.updateLoggers(configuration);

下图则是我们的日志级别配置:

image-20210809002325350.png

疑问:框架如何处理没有Logger的类

先说结论:

  1. 每一个Logger不一定都有其对应的配置
  2. 实际工作的Logger并不一定是其本身,如上文中的:log.demo.test.LogTestApp
  3. Logger之间具有继承性,即log.demo.test.LogTestApp在不作其他额外配置的情况下,会使用父级配置:log.demo,以此类推直到ROOT

使用以下配置及代码,即可进行验证:

1
2
3
4
5
6
7
8
9
xml复制代码 <Loggers>
<AsyncRoot level="INFO" includeLocation="true">
<AppenderRef ref="Console"/>
</AsyncRoot>

<logger name="log.demo" level="ERROR">
<AppenderRef ref="ERROR_LOG"/>
</logger>
</Loggers>
1
2
3
4
5
6
7
8
9
java复制代码@Slf4j
public class LogTestApp {
public static void main(String[] args) {
System.out.println(log.getName());
}
}

// 输出结果
// log.demo.test.LogTestApp

通过观察配置文件,我们可以很明显的发现,整个日志框架实际上是不存在:log.demo.test.LogTestApp 这个Logger的,那它到底是如何工作的呢?

核心代码分析如下:

1
java复制代码private static final Logger log = LoggerFactory.getLogger(LogTestApp.class);

通过工厂的方式创建一路跟源码,可以发现类:AbstractConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Override
public LoggerConfig getLoggerConfig(final String loggerName) {
LoggerConfig loggerConfig = loggerConfigs.get(loggerName);
if (loggerConfig != null) {
return loggerConfig;
}
String substr = loggerName;
while ((substr = NameUtil.getSubName(substr)) != null) {
loggerConfig = loggerConfigs.get(substr);
if (loggerConfig != null) {
return loggerConfig;
}
}
return root;
}

该方法的作用就是给每一个类的Logger绑定实际的执行者,其中NameUtil.getSubName()方法即获取当前类的全路径的上一层,通过循环遍历找到最近(所谓最近即以包名为界限,由子向父级递推)的Logger。

了解完以上的特性,我们也就有了问题的答案。

按需配置父级Logger

随着工程化的推进,系统代码的层次性非常明显,以我司为例,主要分为:Dao、Service、Business、Api这四层,其中核心业务一般都放在Business层,所以在一般情况下出问题也都发生Business层,我们可以配置以下Logger,动态调整所有的Business的日志级别,以达到更加精准的控制:

1
2
3
4
5
xml复制代码<Loggers>
<logger name="com.jd.o2o.business" level="INFO">
<!-- INFO_LOG -->
</logger>
</Loggers>

同理,如果需要关注DB层的话,也可以配置父级Logger来监控DB层的日志。

动态生成Logger

其实从上文中动态调整日志级别就可以发现一些端倪,既然日志框架支持动态刷新配置,那么它一定支持动态新增配置(即使当前版本不支持,也只是尚未开发)。

通过阅读源码,可以看出以下方法可以满足我们的要求:

org.apache.logging.log4j.core.config.Configuration#addLogger

PS:个人不建议使用该种方式,因为编码相对繁琐,且过于灵活反而导致问题不好排查

疑问:莫名其妙的输出

有时候我们也会遇到这种问题:某一个Logger我想让它打ERROR日志,只输出到ERROR文件,结果它输出到了INFO、ERROR,这是为什么?

这其实是日志框架的设计问题,核心代码如下所示:

1
2
3
4
5
6
7
java复制代码private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}

日志事件在打印时,会传递给所有的Appenders,最后它还会向父级Logger传递日志事件,这也就导致我们明明只配置了ERROR,结果却输出到了INFO和ERROR。

观察如下代码即可找到解决办法:

1
2
3
4
5
6
7
8
9
java复制代码// 核心代码
private void logParent(final LogEvent event, final LoggerConfigPredicate predicate) {
if (additive && parent != null) {
parent.log(event, predicate);
}
}

// 解决办法 additivity置为false
<logger name="log.demo" level="INFO" additivity="false"></logger>

打印日志的小技巧

在日志使用中除了正确的打印之外也存在一些小Tips,比如:

  • 方案一:
1
java复制代码log.info("This is a Demo, Request:{}", JSONUtil.toJsonStr(new LogTest()));
  • 方案二:
1
2
3
java复制代码if (log.isInfoEnabled()) {
log.info("This is a Demo, Request:{}", JSONUtil.toJsonStr(new LogTest()));
}

以上两种方式你会选择哪一种呢?其实对比就能看出来,一定会选择方案二,因为它可以避免不必要的序列化。

总结

大部分时间日志都是程序员的好朋友,但一些微妙的情况它反而会成为致命的风险,所以我们需要熟悉它的配置,了解它的原理。

那么如何用好日志框架呢,下面是几点建议:

  1. 使用Slf4j的进行桥接,避免直接使用某一个特定日志框架
  2. 合理设置RootLogger及其子Logger,可以将系统依赖的框架级日志分别输出至指定的文件中,便于问题的排查
  3. 合理利用动态日志级别的能力,随时调整线上日志级别
  4. 减少日志中的序列化行为,在使用低级别日志时需要判断当前日志级别是否开启,避免不必要的序列化
  5. 如无特殊场景要求,尽量使用高吞吐的异步日志

如果觉得这篇内容对你有帮助的话:

  1. 当然要点赞支持一下啦~
  2. 另外,可以搜索并关注公众号「是Kerwin啊」,一起在技术的路上走下去吧~ 😋

本文转载自: 掘金

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

java100套毕业设计和课程设计项目案例和配套视频 1关

发表于 2021-08-11

本套100个完整项目源码一共分为5季,每季约20套项目,希望大家持续关注。

很多大四同学苦于没有参考的毕设资料,或者下载的资料不全、代码有问题、数据有问题等等,造成毕设出现问题影响大学毕业。现在,我们提供了经过审核的100个项目源码和对应的辅导视频,让大家在短时间内可以完成自己的毕业设计。

同时,我们也录制了更多的项目视频,2021年计划100套,后续将会有更多。大家可以到 space.bilibili.com/392179313 在线观看和学习,也可以免费下载。

未来,我们将发布H5前端毕设项目、Python毕设项目、大数据毕设项目、人工智能毕设项目等。让我们的大学生朋友再也不用为毕设发愁。请大家随时关注。

想学习java的朋友们可以到上面网址中搜索java300集教程:

3.jpg

该套课程是由高淇老师开讲的Java300集!为初学者而著!适合准备入行开发的零基础员学习Java。 基于最新JDK13、IDEA平台讲解的,视频中穿插多个实战项目。每一个知识点都讲解的通俗易懂,由浅入深。不仅适用于零基础的初学者,有经验的程序员也可做巩固学习。

1.关于各种开发软件的使用说明和配套视频

由于很多大学生对于开发软件不是很熟悉,我们将常见的开发软件使用方式集中进行了录制。大家项目中用到哪些软件,自行对比学习即可。

为了方便大家的学习,我们提供了常用开发软件的安装包,大家可以根据需要直接从网盘下载:

软件的使用方式都特别简单,大家不要有畏惧心理,这里讲解了软件在开发中最常用的使用方式。包含了常见数据库软件的使用(oracle、mysql、sqlserver)、数据库客户端操作软件、eclipse、Myeclipse、Tomcat服务器等的使用。包含如下视频:

1.Eclipse的使用1_开发环境使用原因

2.Eclipse的使用2_下载楼基本选择和使用

3.Eclipse的使用3_建立JAVA项目_项目的结构说明

4.Eclipse的使用4_开发和运行JAVA程序

5.Eclipse(JEE)的使用_Tomcat整合_项目部署

6.JDK安装1_下载和安装_JDK目录介绍

7.JDK安装2_环境变量PATH设置_classpath问题

8.JDK安装3_控制台测试JDK安装和配置成功

9.Myeclipse2014的安装和使用_web项目创建和发布

10.Myeclipse和Tomcat整合_web项目部署

11.Mysql数据库1_安装和配置_命令行简单使用

12.Mysql数据库2_navicat客户端软件的使用_加载SQL文件到库

13.Oracle数据库1_安装

14.Oracle数据库2_客户端plsql安装并连接

15.SqlServer数据库1_安装

16.SqlServer数据库2_连接并回复数据库

17.SqlServer数据库3_客户端操作

18.SqlServer数据库4_卸载

19.Tomcat服务器安装_使用介绍

2.第一季20套项目源代码和配套视频

第一季20套源代码覆盖范围较广,有比较基础的JAVA初级项目,也有比较大的WEB项目。每个项目我们都提供了完整的内容,涵盖:论文、PPT、源代码、数据库文件、配套讲解视频。我们以“土地局档案管理系统”为例:

打开“论文等资料”文件夹,就发现有完整的论文和答辩内容,供大家参考:

打开“项目辅导视频”,就发现有详细的项目讲解视频,帮助大家解决项目部署、项目模块讲解的问题:

为了快速查看这个项目是否符合你的需求,可以打开“项目截图”文件夹:

报表图.png

捕获.png

档案修改.png

登录.png

功能.png

注册.png

第一季视频涵盖如下图所示项目,范围比较广泛。有电子政务项目、也有医疗项目、也有供应链管理项目、互联网项目也有若干。同时,也有几个java基础项目,大家可以用于做JAVA的课程设计。

以上相关的所有资料(电子书+视频教程+项目源码+课程笔记)已经为各位打包好了,希望对各位有所帮助!

文章整理不易,若是对你有帮助,求各位朋友们点赞 + 喜欢 + 收藏支持下啦!❤️

本文转载自: 掘金

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

十 软件工程基础知识 一 🌻软件工程基础 二 🌿需求工程 三

发表于 2021-08-11

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

一 🌻软件工程基础

1.1 能力成熟度模型

1.1.1 软件工程基本原理

用分阶段的生命周期计划严格管理、坚持进行阶段评审、实现严格的产品控制、采用现代程序设计技术、结果应能清楚的审查、开发小组的人员应少而精、承认不断改进软件工程实践的必要性。

1.1.2 软件工程的基本要素

方法、工具、过程。信息系统生命周期

1.1.3 信息系统生命周期

image.png

  1. 系统规划阶段:任务是对组织的环境、目标及现行系统的状况进行初步调查,根据组织目标和发展战略确定信息系统的发展战略,对建设新系统的需求做出分析和预测,同时考虑建设新系统所受的各种约束,研究建设新系统的必要性和可能性。根据需要与可能,给出制建系统的备选方案。
    `输出:可行性研究报告、系统设计任务书。`
  2. 系统分析阶段:任务是根据系统设计任务书所确定的范围,对现行系统进行详细调查,描述现行系统的业务流程,指出现行系统的局限性和不足之处,确定新系统的基本目标和逻辑功能要求,即提出新系统的逻辑模型。系统分析阶段又称为逻辑设计阶段。这个阶段是整个系统建设的关键阶段,也是信息系统建设与一般工程项目的重要区别所在。

输出:系统说明书。
3. 系统设计阶段:系统分析阶段的任务是回答系统“做什么”的问题,而系统设计阶段要回答的问题是“怎么做”。该阶段的任务是根据系统说明书中规定的功能要求,具体设计实现逻辑模型的技术方案,也就是设计新系统的物理模型。这个阶段又称为物理设计阶段,可分为总体设计(概要设计)和详细设计两个子阶段。
输出:系统设计说明书(概要设计、详细设计说明书)。
4. 系统实施阶段:是将设计的系统付诸实施的阶段。这一阶段的任务包括计算机等设备的购置、安装和调试、程序的编写和调试、人员培训、数据文件转换、系统调试与转换等。这个阶段的特点是几个互相联系、互相制约的任务同时展开,必须精心安排、合理组织。系统实施是按实施计划分阶段完成的,每个阶段应写出实施进展报告。系统测试之后写出系统测试分析报告。
输出:实施进展报告、系统测试分析报告。
5. 系统运行和维护阶段:系统投入运行后,需要经常进行维护和评价,记录系统运行的情况,根据一定的规则对系统进行必要的修改,评价系统的工作质量和经济效益。

1.1.4 软件生存周期

可行性分析与项目开发计划、需求分析、概要设计(选择系统解决方案,规划子系统)、详细设计(设计子系统内部具体实现)、编码、测试、维护。

1.1.5 能力成熟度模型CMM

对软件组织化阶段的描述,随着软件组织定义、实施、测量、控制和改进其软件过程,软件组织的能力经过这些阶段逐步提高。针对软件研制和测试阶段。分为如下五个级别:

image.png

1.1.6 能力成熟度模型集成CMMI

是若干过程模型的综合和改进,不仅仅软件,而是支持多个工程学科和领域的、系统的、一致的过程改进框架,能适应现代工程的特点和需要,能提高过程的质量和工作效率。

CMMI两种表示方法:

  1. 阶段式模型:类似于CMM,它关注组织的成熟度,五个成熟度模型如下:
  2. 连续式模型:关注每个过程域的能力,一个组织对不同的过程域可以达到不同的过程域能力等级。

image.png

1.2 软件过程模型

1.2.1 瀑布模型

结构化方法中的模型,是结构化的开发,开发流程如同瀑布一般,一步一步的走下去,直到最后完成项目开发,`只适用于需求明确或者二次开发`(需求稳定),当需求不明确时,最终开发的项目会错误,有很大的缺陷。

1.2.2 原型

与瀑布模型相反,原型针对的就是`需求不明确`的情况,首先快速构造一个功能模型,演示给用户看,并按用户要求及时修改,中间再通过不断的演示与用户沟通,最终设计出项目,就不会出现与用户要求不符合的情况,采用的是迭代的思想。不适合超大项目开发。

1.2.3 增量模型

首先开发核心模块功能,而后与用户`确认`,之后`再开发`次核心模块的功能,即每次开发一部分功能,并与用户需求确认,最终完成项目开发,优先级最高的服务最先交付,但由于并不是从系统整体角度规划各个模块,因此不利于模块划分。难点在于如何将客户需求划分为多个增量。与原型不用的是增量模型的每一次增量版本都可作为独立可操作的作品,而原型的构造一般是为了演示。

1.2.4 螺旋模型

是多种模型的混合,针对`需求不明确`的项目,与原型类似,但是增加了`风险`分析,这也是其最大的特点。适合大型项目开发。

1.2.5 v模型

特点是增加了`很多轮测试`,并且这些测试贯穿于软件开发的各个阶段,不像其他模型都是软件开发完再测试,很大程度上保证了项目的准确性。v模型开发和测试级别对应如下图:

1.2.6 喷泉模型

特点是`面向对象`的模型,而上述其他的模型都是结构化的模型,使用了迭代思想和无间隙开发。

基于构件的开发模型CBSD:特点是增强了复用性,在系统开发过程中,会构建一个构件库,供其他系统复用,因此可以提高可靠性,节省时间和成本。

1.2.7 形式化方法模型

建立在严格`数学基础`上的一种软件开发方法,主要活动是生成计算机软件形式化的数学规格说明。

1.3 软件开发方法

上述的软件过程模型基本都可归属于下面开发方法中

1.3.1 结构化方法主要特点

结构是指系统内各个组成要素之间的相互联系、相互作用的框架。结构化方法也称为生命周期法,是一种传统的信息系统开发方法,由结构化分析(Structured Analysis,SA)、结构化设计(StructuredDesign, SD)和结构化程序设计(Structured Programming,SP)三部分有机组合而成,其精髓是自顶向下、逐步求精和模块化设计。

  1. 开发目标清晰化。结构化方法的系统开发遵循“用户第一”的原则。
  2. 开发工作阶段化。每个阶段工作完成后,要根据阶段工作目标和要求进行审查,这使各阶段工作有条不紊地进行,便于项目管理与控制。
  3. 开发文档规范化。结构化方法每个阶段工作完成后,要按照要求完成相应的文档,以保证各个工作阶段的衔接与系统维护工作的遍历。
  4. 设计方法结构化。在系统分析与设计时,从整体和全局考虑,自顶向下地分解;在系统实现时,根据设计的要求,先编写各个具体的功能模块,然后自底向上逐步实现整个系统。

1.3.2 结构化方法的不足和局限

  1. 开发周期长:按顺序经历各个阶段,直到实施阶段结束后,用户才能使用系统。
  2. 难以适应需求变化:不适用于需求不明确或经常变更的项目。
  3. 很少考虑数据结构:结构化方法是一种面向数据流的开发方法,很少考虑数据结构。

1.3.3 结构化方法常用工具

结构化方法一般利用图形表达用户需求,常用工具有数据流图、数据字典、结构化语言、判定表以及判定树等。

1.3.4 面向对象方法特点

面向对象(Object-Oriented,oO)方法认为,客观世界是由各种对象组成的,任何事物都是对象,每一个对象都有自己的运动规律和内部状态,都属于某个对象类,是该对象类的一个元素。复杂的对象可由相对简单的各种对象以某种方式而构成,不同对象的组合及相互作用就构成了系统。

  1. 使用OO方法构造的系统具有更好的复用性,其关键在于建立一个全面、合理、统一的模型(用例模型和分析模型)。
  2. OO方法也划分阶段,但其中的系统分析、系统设计和系统实现三个阶段之间已经没有“缝隙”。也就是说,这三个阶段的界限变得不明确,某项工作既可以在前一个阶段完成,也可以在后一个阶段完成;前一个阶段工作做得不够细,在后一个阶段可以补充。
  3. 面向对象方法可以普遍适用于各类信息系统的开发。

1.3.5 面向对象方法的不足之处

必须依靠一定的面向对象技术支持,在大型项目的开发上具有一定的局限性,不能涉足系统分析以前的开发环节。


当前,一些大型信息系统的开发,通常是将结构化方法和oO方法结合起来。首先,使用结构化方法进行自顶向下的整体划分;然后,自底向上地采用o0方法进行开发。因此,结构化方法和 oo方法仍是两种在系统开发领域中相互依存的、不可替代的方法。

1.3.6 原型化方法特点

称为快速原型法,或者简称为原型法。它是一种根据用户初步需求,利用系统开发工具,快速地建立一个系统模型展示给用户,在此基础上与用户交流,最终实现用户需求的信息系统快速开发的方法。 \n 按是否实现功能分类:分为水平原型(行为原型,功能的导航)、垂直原型(结构化原型,实现了部分功能)。

. 按最终结果分类:分为抛弃式原型、演化式原型。

  • 原型法可以使系统开发的周期缩短、成本和风险降低、速度加快,获得较高的综合开发效益。
  • 原型法是以用户为中心来开发系统的,用户参与的程度大大提高,开发的系统符合用户的需求,因而增加了用户的满意度,提高了系统开发的成功率。
  • 由于用户参与了系统开发的全过程,对系统的功能和结构容易理解和接受,有利于系统的移交,有利于系统的运行与维护。

1.3.7 原型法的不足之处

  • 开发的环境要求高。
  • 管理水平要求高。

由以上的分析可以看出,原型法的优点主要在于能更有效地确认用户需求。从直观上来看,原型法适用于那些需求不明确的系统开发。事实上,对于分析层面难度大、技术层面难度不大的系统,适合于原型法开发。

从严格意义上来说,目前的原型法不是一种独立的系统开发方法,而只是一种开发思想,它只支持在系统开发早期阶段快速生成系统的原型,没有规定在原型构建过程中必须使用哪种方法。因此,它不是完整意义上的方法论体系。这就注定了原型法必须与其他信息系统开发方法结合使用。

1.3.8 面向服务的方法

面向服务(Service-Oriented,SO)的方法:进一步将接口的定义与实现进行解耦,则催生了服务和面向服务(Service-Oriented,sO)的开发方法。


从应用的角度来看,组织内部、组织之间各种应用系统的互相通信和互操作性直接影响着组织对信息的掌握程度和处理速度。如何使信息系统快速响应需求与环境变化,提高系统可复用性、信息资源共享和系统之间的互操作性,成为影响信息化建设效率的关键问题,而so的思维方式恰好满足了这种需求。

1.3.9 Jackson(面向数据结构)方法

面向数据结构的开发方法,适合于小规模的项目。

1.3.10 敏捷开发

针对中小型项目,主要是为了给程序员减负,去掉一些不必要的会议和文档。指代一组模型(极限编程、自适应开发、水晶方法……),这些模型都具有相同的原则和价值观,具体如下图(要求对该图眼熟,并掌握重要概念):


`开发宣言`:个体和交互胜过过程和工具、可以工作的软件胜过面面俱到的文档、客户合作胜过合同谈判、响应变化胜过遵循计划。

image.png

a 重要概念介绍

  1. 结对编程:一个程序员开发, 另一个程序在一-旁观察审查代码,能够有效的提高代码质量,在开发同时对代码进行初步审查,共同对代码负责。
  2. 自适应开发:强调开发方法的适应性(Adaptive)。不象其他方法那样有很多具体的实践做法,它更侧重为软件的重要性提供最根本的基础,并从更高的组织和管理层次来阐述开发方法为什么要具备适应性。
  3. 水晶方法:每一个不同的项目都需要一套不同的策略、约定和方法论。
  4. 特性驱动开发:是一套针对中小型软件开发项目的开发模式。是一个模型驱动的快速迭代开发过程,它强调的是简化、实用、易 于被开发团队接受,适用于需求经常变动的项目。
  5. 极限编程XP:核心是沟通、简明、反馈和勇气。因为知道计划永远赶不上变化,XP 无需开发人员在软件开始初期做出很多的文档。XP 提倡测试先行,为了将以后出现bug的几率降到最低。
  6. 并列争球法SCRUM:是一种迭代的增量化过程,把每段时间(30 天) - -次的迭代称为一个“冲刺”,并按需求的优先级别来实现产品,多个自组织和自治的小组并行地递增实现产品。

1.3.11 统一过程(RUP)

提供了在开发组织中分派任务和责任的纪律化方法。它的目标是在可预见的日程和预算前提下,确保满足最终用户需求的高质量产品。

a 3个显著特点

用例驱动、以架构为中心、迭代和增量。

b 4个流程:

  1. 初始阶段
  2. 细化阶段
  3. 构建阶段和交付阶段
  4. 每个阶段结束时都要安排一次技术评审以确定这个阶段的目标是否已经达到。

一个通用过程框架,可以适用于种类广泛的软件系统、不同的应用领域、不同的组织类型、不同性能水平和不同的项目规模。

1.4 软件产品线

软件产品线是一个产品集合,这些产品共享一个公共的、可管理的特征集,这个特征集能满足特定领域的特定需求。软件产品线是一个十分适合专业的开发组织的软件开发方法,能有效地提高软件生产率和质量,缩短开发时间,降低总开发成本。


核心资源:包括所有产品所共用的软件架构,通用的构件、文档等。


产品集合:产品线中的各种产品。

产品线的建立方式
image.png

1.5逆向工程

`软件复用`是将已有软件的各种有关知识用于建立新的软件,以缩减软件开发和维护的花费。软件复用是提高软件生产力和质量的一种重要技术。早期的软件复用主要是代码级复用,被复用的知识专指程序,后来扩大到包括领域知识、开发经验、设计决定、体系结构、需求、设计、代码和文档等一切有关方面。

逆向工程:软件的逆向工程是分析程序,力图在比源代码更高抽象层次上建立程序的表示过程,逆向工程是设计的恢复过程。

1.5.1 逆向工程的四个级别:

  1. 实现级: 包括程序的抽象语法树、符号表、过程的设计表示。
  2. 结构级:包括反映程序分量之间相互依赖关系的信息,例如调用图、结构图、程序和数据结构。
  3. 功能级:包括反映程序段功能及程序段之间关系的信息,例如数据和控制流模型。
  4. 领域级:包括反映程序分量或程序诸实体与应用领域概念之间对应关系的信息,例如E-R模型。其中,领域级抽象级别最高,完备性最低,实现级抽象级别最低,完备性最高。

1.5.2 相关的概念

与逆向工程相关的概念有重构、设计恢复、再工程和正向工程。

  • 重构是指在同一抽象级别上转换系统描述形式。
  • 设计恢复是指借助工具从已有程序中抽象出有关数据设计、总体结构设计和过程设计等方面的信息。
  • 再工程是指在逆向工程所获得信息的基础上,修改或重构已有的系统,产生系统的一个新版本。再工程是对现有系统的重新开发过程,包括逆向工程、新需求的考虑过程和正向工程三个步骤。它不仅能从已存在的程序中重新获得设计信息,而且还能使用这些信息来重构现有系统,以改进它的综合质量。在利用再工程重构现有系统的同时,一般会增加新的需求,包括增加新的功能和改善系统的性能。
  • 正向工程是指不仅从现有系统中恢复设计信息,而且使用该信息去改变或重构现有系统,以改善其整体质量。

1.6 软件系统工具

按软件过程活动将软件工具分为

  • 软件开发工具:需求分析工具、设计工具、编码与排错工具。
  • 软件维护工具:版本控制工具、文档分析工具、开发信息库工具、逆向工程工具、再工程工具。
  • 软件管理和软件支持工具:项目管理工具、配置管理工具、软件评价工具、软件开发工具的评价和选择。

二 🌿需求工程

2.1 软件需求

指用户对系统在功能、行为、性能、设计约束等方面的期望。是指用户解决问题或达到目标所需的条件或能力,是系统或系统部件要满足合同、标准、规范或其他正式规定文档所需具有的条件或能力,以及反映这些条件或能力的文档说明。


分为需求开发和需求管理两大过程,如下所示:

image.png

2.1.1 需求的层次

  • 业务需求:反映企业或客户对系统高层次的目标要求,通常来自项目投资人、客户、市场营销部门或产品策划部门。通过业务需求可以确定项目视图和范围。
  • 用户需求:描述的是用户的具体目标,或用户要求系统必须能完成的任务。即描述了用户能使用系统来做什么。通常采取用户访谈和问卷调查等方式,对用户使用的场景进行整理,从而建立用户需求。
  • 系统需求:从系统的角度来说明软件的需求,包括功能需求、非功能需求和设计约束等。
  1. 功能需求:也称为行为需求,规定了开发人员必须在系统中实现的软件功能,用户利用这些功能来完成任务,满足业务需要。
  2. 非功能需求:指系统必须具备的属性或品质,又可以细分为软件质量属性(如可维护性、可靠性、效率等)和其他非功能需求。
  3. 设计约束:也称为限制条件或补充规约,通常是对系统的一些约束说明,例如必须采用国有自主知识产权的数据库系统,必须运行在 UNIX操作系统之下等。

2.1.2 质量功能部署

质量功能部署(QFD)是一种将用户要求转化成软件需求的技术,其目的是最大限度地提升软件工程过程中用户的满意度。为了达到这个目标,QFD将软件需求分为三类。

  1. 常规需求。用户认为系统应该做到的功能或性能,实现越多用户会越满意。
  2. 期望需求。用户想当然认为系统应具备的功能或性能,但并不能正确描述自己想要得到的这些功能或性能需求。如果期望需求没有得到实现,会让用户感到不满意。
  3. 意外需求。意外需求也称为兴奋需求,是用户要求范围外的功能或性能(但通常是软件开发人员很乐意赋予系统的技术特性),实现这些需求用户会更高兴,但不实现也不影响其购买的决策。

2.2 需求获取

是一个确定和理解不同的项目干系人的需求和约束的过程。常见的需求获取法包括:

  1. 用户访谈:1对1-3,有代表性的用户。其形式包括结构化和非结构化两种。
  2. 问卷调查:用户多,无法一一访谈。
  3. 采样:从种群中系统地选出有代表性的样本集的过程。样本数量=0.25*(可信度因子/错误率)2
  4. 情节串联板:一系列图片,通过这些图片来讲故事。
  5. 联合需求计划(JRP):通过联合各个关键用户代表、系统分析师、开发团队代表一起,通过有组织的会议来讨论需求。
  6. 需求记录技术:任务卡片、场景说明、用户故事、Volere白卡。

2.3 需求分析

一个好的需求应该具有无二义性、完整性、一致性、可测试性、确定性、可跟踪性、正确性、必要性等特性,因此,需要分析人员把杂乱无章的用户要求和期望转化为用户需求,这就是需求分析的工作。

2.3.1 需求分析的任务

  1. 绘制系统上下文范围关系图
  2. 创建用户界面原型战
  3. 分析需求的可行性
  4. 确定需求的优先级
  5. 为需求建立模型
  6. 创建数据字典
  7. 使用QFD(质量功能部署)

2.3.2 结构化的需求分析

结构化特点::自顶向下,逐步分解,面向数据。
三大模型:功能模型(数据流图)、行为模型(状态转换图)、数据模型(E-R图)以及数据字典。
image.png

2.4 需求定义

(`软件需求规格说明书`SRS):是需求开发活动的产物,编制该文档的目的是使项目干系人与开发团队对系统的初始规定有一个共同的理解,使之成为整个开发工作的基础。SRS是软件开发过程中最重要的文档之一,对于任何规模和性质的软件项目都不应该缺少。

2.4.1 需求定义方法

  1. 严格定义也称为预先定义,需求的严格定义建立在以下的基本假设之上:所有需求都能够被预先定义。开发人员与用户之间能够准确而清晰地交流。采用图形(或文字)可以充分体现最终系统。
  2. 原型方法,迭代的循环型开发方式,需要注意的问题:并非所有的需求都能在系统开发前被准确地说明。项目干系人之间通常都存在交流上的困难,原型提供了克该服困难的一个手段。特点:需要实际的、可供用户参与的系统模型。有合适的系统开发环境。反复是完全需要和值得提倡的,需求一旦确定,就应遵从严格的方法。

2.5 需求验证

也称为需求确认,目的是与用户一起确认需求无误,对需求规格说明书SAS进行评审和测试,包括两个步骤:
  • 需求评审:正式评审和非正式评审。
  • 需求测试:设计概念测试用例。
需求验证通过后,要请用户签字确认,作为验收标准之一,此时,这个需求规格说明书就是需求基线,不可以再随意更新,如果需要更改必须走需求变更流程。

2.6 需求管理

2.6.1 定义需求基线

通过了评审的需求说明书就是需求基线,下次如果需要变更需求,就需要按照流程来一步步进行。需求的流程及状态如下图所示:

image.png

2.6.2 需求变更和风险

主要关心需求变更过程中的需求风险管理,带有风险的做法有:无足够用户参与、忽略了用户分类、用户需求的不断增加、模棱两可的需求、不必要的特性、过于精简的SRS、不准确的估算。

变更产生的原因:外部环境的变化、需求和设计做的不够完整、新技术的出现、公司机构重组造成业务流程的变化。

变更控制委员会 CCB:也称为配置控制委员会,其任务时对建议的配置项变更做出评价、审批,以及监督已经批准变更的实施。

image.png

2.6.3 需求跟踪:双向跟踪,两个层次,如下图所示:

image.png

正向跟踪表示用户原始需求是否都实现了,反向跟踪表示软件实现的是否都是用户要求的,不多不少,可以用原始需求和用例表格(需求跟踪矩阵)来表示。


若原始需求和用例有对应,则在对应栏打对号,若某行都没有对号,表明原始需求未实现,正向跟踪发现问题;若某列都没有对号,表明有多余功能用例,软件实现了多余功能,反向跟踪发现问题。

三 🌵系统设计

3.1 处理流程设计

3.1.1 业务流程建模

  • 标杆瞄准:以行业领先的标杆企业为目标,结合本企业情况分析建模。
  • IDEF(一系列建模、分析和仿真方法的统称)。
  • DEMO(组织动态本质建模法)
  • Petri 网
  • 业务流程建模语言:BPEL、BPML、BPMN、XPDL。
  • 基于服务的BPM:基于web 服务的思想对业务流程进行建模。

3.1.2 流程表示工具

  • 程序流程图(Program Flow Diagram,PFD)用一些图框表示各种操作,它独立于任何一种程序设计语言,比较直观、清晰,易于学习掌握。任何复杂的程序流程图都应该由顺序、选择和循环结构组合或嵌套而成。
  • IPO图也是流程描述工具,用来描述构成软件系统的每个模块的输入、输出和数据加工。
  • N-S图容易表示嵌套和层次关系,并具有强烈的结构化特征。但是当问题很复杂时,N-S图可能很大,因此不适合于复杂程序的设计。
  • 问题分析图(PAD)是一种支持结构化程序设计的图形工具。PAD具有清晰的逻辑结构、标准化的图形等优点,更重要的是,它引导设计人员使用结构化程序设计方法,从而提高程序的质量。

3.1.3 业务流程重组BPR

BPR是对企业的业务流程进行根本性的再思考和彻底性的再设计,从而获得可以用诸如成本、质量、服务和速度等方面的业绩来衡量的显著性的成就。BPR设计原则、系统规划和步骤如下图所示:

image.png

3.1.4 业务流程管理BPM

BPM是一种以规范化的构造端到端的卓越业务流程为中心,以持续的提高组织业务绩效为目的的系统化方法。


BPM与BPR管理思想最根本的不同就在于流程管理并不要求对所有的流程进行再造。构造卓越的业务流程并不是流程再造,而是根据现有流程的具体情况,对流程进行规范化的设计流程管理包含三个层面:规范流程、优化流程和再造流程

3.2 系统设计

3.2.1 主要目的

  • 为系统制定蓝图
  • 在各种技术和实施方法中权衡利弊
  • 精心设计,合理地使用各种资源,最终勾画出新系统的详细设计方法。

3.2.2 方法

  • 结构化设计方法,
  • 面向对象设计方法。

3.2.3 主要内容

  • 概要设计:又称为系统总体结构设计,是将系统的功能需求分配给软件模块,确定每个模块的功能和调用关系,形成软件的模块结构图,即系统结构图。
  • 详细设计:模块内详细算法设计、模块内数据结构设计、数据库的物理设计、其他设计(代码、输入/输出格式、用户界面)、编写详细设计说明书、评审。

3.2.4 系统设计基本原理

  • 抽象化;
  • 自顶而下,逐步求精:微衣信息隐蔽;
  • 模块独立(高内聚,低耦合)。

3.2.5 系统设计原则

  • 保持模块的大小适中;尽可能减少调用的深度;多扇入,少扇出;
  • 单入口,单出口;
  • 模块的作用域应该在模块之内;
  • 功能应该是可预测的。

3.3 人机界面设计

3.3.1 三大原则

  1. 置于用户控制之下
  • 以不强迫用户进入不必要的或不希望的动作的方式来定义交互方式;
  • 提供灵活的交互;
  • 允许用户交互可以被中断和取消;
  • 当技能级别增加时可以使交互流水化并允许定制交互;
  • 使用户隔离内部技术细节;
  • 设计应允许用户和出现在屏幕上的对象直接交互。
  1. 减少用户的记忆负担
  • 减少对短期记忆的要求;
  • 建立有意义的缺省;
  • 定义直觉性的捷径;
  • 界面的视觉布局应该基于真实世界的隐喻;
  • 以不断进展的方式揭示信息。
  1. 保持界面的一致性。
  • 允许用户将当前任务放入有意义的语境
  • 在应用系列内保持一致性
  • 如过去的交互模型已建立起了用户期望,除非有迫不得已的理由,不要去改变它。

四 🍔测试基础知识

4.1 测试基础

4.1.1 测试原则

  • 应尽早并不断的进行测试;
  • 测试工作应该避免由元开发软件的人或小组承担;
  • 在设计测试方案时,不仅要确定输入数据,而且要根据系统功能确定预期的输出结果;
  • 既包含有效、合理的测试用例,也包含不合理、失效的用例;
  • 检验程序是否做了该做的事,且是否做了不该做的事;
  • 严格按照测试计划进行;
  • 妥善保存测试计划和测试用例;
  • 测试用例可以重复使用或追加测试。

4.1.2 动态测试类型

程序运行时测试,分为

  • 黑盒测试法:功能性测试,不了解软件代码结构,根据功能设计用例,测试软件功能。
  • 白盒测试法:结构性测试,明确代码流程,根据代码逻辑设计用例,进行用例覆盖。
  • 灰盒测试法:即既有黑盒,也有白盒。

4.1.3 静态测试:类型

程序静止时,即对代码进行人工审查,分为

  • 桌前检查:程序员检查自己编写的程序,在程序编译后,单元测试前。
  • 代码审查:由若干个程序员和测试人员组成评审小组,通过召开程序评审会来进行审查。
  • 代码走查:也是采用开会来对代码进行审查,但并非简单的检查代码,而是由测试人员提供测试用例,让程序员扮演计算机的角色,手动运行测试用例,检查代码逻辑。

4.1.4 测试策略

  • 自底向上:从最底层模块开始测试,需要编写驱动程序,而后开始逐一合并模块,最终完成整个系统的测试。优点是较早的验证了底层模块。
  • 自顶向下:先测试整个系统,需要编写桩程序,而后逐步向下直至最后测试最底层模块。优点是较早的验证了系统的主要控制和判断点。
  • 三明治:既有自底向上也有自顶向下的测试方法,二者都包括。兼有二者的优点,缺点是测试工作量大。

4.2 测试阶段

  1. 单元测试:也称为模块测试,测试的对象是可独立编译或汇编的程序模块、软件构件或OO软件中的类(统称为模块),测试依据是软件详细设计说明书。
  2. 集成测试:目的是检查模块之间,以及模块和已集成的软件之间的接口关系,并验证已集成的软件是否符合设计要求。测试依据是软件概要设计文档。
  3. 确认测试:主要用于验证软件的功能、性能和其他特性是否与用户需求一致。 根据用户的参与程度,通常包括以下类型:
  • 内部确认测试:主要由软件开发组织内部按照SRS进行测试。
  • Alpha测试:用户在开发环境下进行测试。
  • Beta测试:用户在实际使用环境下进行测试,通过改测试后,产品才能交付用户。
  • 验收测试:针对SRS,在交付前以用户为主进行的测试。其测试对象为完整的、集成的计算机系统。验收测试的目的是,在真实的用户工作环境下,检验软件系统是否满足开发技术合同或SRS。验收测试的结论是用户确定是否接收该软件的主要依据。除应满足–般测试的准入条件外,在进行验收测试之前,应确认被测软件系统已通过系统测试。
  1. 系统测试:测试对象是完整的、集成的计算机系统;测试的目的是在真实系统工作环境下,验证完成的软件配置项能否和系统正确连接,并满足系统/子系统设计文档和软件开发合同规定的要求。测试依据是用户需求或开发合同。
`主要内容`包括功能测试、健壮性测试、性能测试、用户界面测试、安全性测试、安装与反安装测试等,其中,最重要的工作是进行功能测试与性能测试。功能测试主要采用黑盒测试方法;性能测试。


`主要指标`有响应时间、吞吐量、并发用户数和资源利用率等。
  1. 配置项测试:测试对象是软件配置项,测试目的是检验软件配置项与SRS的一致性。测试的依据是SRS。在此之间,应确认被测软件配置项已通过单元测试和集成测试。
  2. 回归测试:测试目的是测试软件变更之后,变更部分的正确性和对变更需求的符合性,以及软件原有的、正确的功能、性能和其他规定的要求的不损害性。

4.3测试用例设计

4.3.1 黑盒测试用例

将程序看做-一个黑盒子,只知道输入输出,不知道内部代码,由此设计出测试用例,分为下面几类:

  1. 等价类划分:把所有的数据按照某种特性进行归类,而后在每类的数据里选取一个即可。 等价类测试用例的设计原则:设计一个新的测试用例,使其尽可能多地覆盖尚未被覆盖的有效等价类,重复这一步,直到所有的有效等价类都被覆盖为止;计一个新的测试用例,使其仅覆盖一个尚未被盖的无效等价类,重复这一步,直到所有的无效等价类都被覆盖为止。
  2. 边界值划分:将每类的边界值作为测试用例,边界值一般为范围的两端值以及在此范围之外的与此范围间隔最小的两个值,如年龄范围为0-150,边界值0,150,-1,151 四个。
  3. 错误推测:没有固定的方法,凭经验而言,来推测有可能产生问题的地方,作为测试用例进行测试。
  4. 因果图:由一个结果来反推原因的方法,具体结果具体分析,没有固定方法。

4.3.2 白盒测试用例

知道程序的代码逻辑,按照程序的代码语句,来设计覆盖代码分支的测试用例,覆盖级别从低至高分为下面几种:

  1. 语句覆盖SC:逻辑代码中的所有语句都要被执行一-遍, 覆盖层级最低,因为执行了所有的语句,不代表执行了所有的条件判断。
  2. 判定覆盖DC:逻辑代码中的所有判断语句的条件的真假分支都要覆盖一次。
  3. 条件覆盖CC:针对每一个判断条件内的每一一个独立条件都要执行一-遍真和假。
  4. 条件判定组合覆盖CDC:同时满足判定覆盖和条件覆盖。
  5. 路径覆盖:逻辑代码中的所有可行路径都覆盖了,覆盖层级最高。

4.4调试

测试是发现错误,调试是找出错误的代码和原因。
调试需要确定错误的准确位置;确定问题的原因并设法改正;改正后要进行回归测试。

4.4.1 调试的方法

  • 蛮力法:又称为穷举法或枚举法,穷举出所有可能的方法一一尝试。
  • 回溯法:又称为试探法,按选优条件向前搜索,以达到目标,当发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
  • 演绎法:是由一般到特殊的推理方法,与“归纳法”相反,从一般性的前提出发。得出具体陈述或个别结论的过程。
  • 归纳法:是由特殊到一般的推理方法,从测试所暴露的问题出发,收集所有正确或不正确的数据,分析它们之间的关系,提出假想的错误原因,用这些数据来证明或反驳,从而查出错误所在。

4.4.2 软件度量

McCabe度量法:又称为环路复杂度,假设有向图中有向边数为m,节点数为n,则此有向图的环路复杂度为m-n+2.


注意m和n代表的含义不能混淆,可以用一个最简单的环路来做特殊值记忆此公式,另外,针对一个程序流程图,每-一个分支边(连线)就是一条有向边,每一条语句(语句框)就是一个顶点。

4.4.3 软件的两种属性

  • 外部属性指面向管理者和用户的属性,可直接测量,-般为性能指标。
  • 内部属性指软件产品本身的的属性,如可靠性等,只能间接测量。

五 🍖系统运行与维护

5.1 遗留系统

是指任何基本上不能进行修改和演化以满足新的变化了的业务需求的信息系统,它通常具有以下特点:
  1. 系统虽然完成企业中许多重要的业务管理工作,但仍然不能完全满足要求。-般实现业务处理电子化及部分企业管理功能,很少涉及经营决策。
  2. 系统在性能上已经落后,采用的技术已经过时。例如,多采用主机/终端形式或小型机系统,软件使用汇编语言或第三代程序设计语言的早期版本开发,使用文件系统而不是数据库。
  3. 通常是大型的软件系统,已经融入企业的业务运作和决策管理机制之中,维护工作十分困难。
  4. 没有使用现代信息系统建设方法进行管理和开发,现在基本上已经没有文档,很难理解。

5.2 系统转换

系统转换是指新系统开发完毕,投入运行,取代现有系统的过程,需要考虑多方面的问题,以实现与老系统的交接,有以下`三种转换`计划:
  1. 直接转换:现有系统被新系统直接取代了,风险很大,适用于新系统不复杂,或者现有系统已经不能使用的情况。优点是节省成本。
  2. 并行转换:新系统和老系统并行工作–段时间,新系统经过试运行后再取代,若新系统在试运行过程中有问题,也不影响现有系统的运行,风险极小,在试运行过程中还可以比较新老系统的性能,适用于大型系统。缺点是耗费人力和时间资源,难以控制两个系统间的数据转换。
  3. 分段转换:分期分批逐步转换,是直接和并行转换的集合,将大型系统分为多个子系统,依次试运行每个子系统,成熟-一个子系统,就转换一个子系统。同样适用于大型项目,只是更耗时,而且现有系统和新系统间混合使用,需要协调好接口等问题。

数据转换与迁移:将数据从旧数据库迁移到新数据库中。要在新系统中尽可能的保存旧系统中合理的数据结构,才能降低迁移的难度。也有三种方法:系统切换前通过工具迁移、系统切换前采用手工录入、系统切换后通过新系统生成。

5.3 系统维护概述

系统的可维护性可以定义为维护人员理解、改正、改动和改进这个软件的难易程度,其评价指标
如下:

  1. 易分析性。软件产品诊断软件中的缺陷或失效原因或识别待修改部分的能力。
  2. 易改变性。软件产品使指定的修改可以被实现的能力,实现包括编码、设计和文档的更改。
  3. 稳定性。软件产品避免由于软件修改而造成意外结果的能力。
  4. 易测试性。软件产品使已修改软件能被确认的能力。
  5. 维护性的依从性。软件产品遵循与维护性相关的标准或约定的能力。

系统维护包括硬件维护、软件维护和数据维护,其中软件维护类型如下:,

  1. 正确性维护:发现了bug而进行的修改。
  2. 适应性维护:由于外部环境发生了改变,被动进行的对软件的修改和升级。
  3. 完善性维护:基于用户主动对软件提出更多的需求,修改软件,增加更多的功能,使其比之前的软件功能、性能更高,更加完善。
  4. 预防性维护:对未来可能发生的bug进行预防性的修改。

5.4 系统评价分类

  1. 立项评价:系统开发前的预评价,分析是否立项开发,做可行性评价。
  2. 中期评价:项目开发中期每个阶段的阶段评审。或者项目在开发中途遇到重大变故,评价是否还要继续。
  3. 结项评价:系统投入正式运行后,了解系统是否达到预期的目的和要求而对系统进行的综合评价。

5.5 系统评价的指标

  1. 从信息系统的组成部分出发,信息系统是一个由人机共同组成的系统,所以可以按照运行效果和用户需求(人)、系统质量和技术条件(机)这两架线索构造指标。
  2. 从信息系统的评价对象出发,对于开发方来说,他们所关心的是系统质量和技术水平;对于用户方而言,关心的是用户需求和运行质量:系统外部环境则主要通过社会效益指标来反映。
  3. 从经济学角度出发,分别按系统成本、系统效益和财务指标3条线索建立指标。

本文转载自: 掘金

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

考研数据结构第一讲-C语言基础(2)

发表于 2021-08-11

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

  1. 变量
  2. 运算符
  3. C语言语句
  4. 数组
  5. 函数
  6. 指针
  7. 结构体

05 Part Five 变量

变量就是在程序运行中可以变化的量,如y,z,w等。

  • 定义方法:
    数据类型 变量1[,变量2….]
    如 :
1
2
3
4
c复制代码int a=0;
double b=0.23;
char s=‘A’;
int *a =0x2323;
  • 初始化:定义时给出初始值
  • 变量定义位置:一般在函数开头

06 Part Six 运算符

算术运算符

假设变量a = 10, 变量b = 20,则:

image.png

位运算符

按位与&、按位或| 和 按位异或^的真值表如下:

image.png

赋值运算符

赋值运算符指的是将右边的值赋给左边的变量。

image.png

其他运算符

除此外,还有关系运算符,即> , ≥ , ≤ , < ,! = , == (一个等号是?两个等号又是?),其中优先级
为前面四个优先于后两个,特别的,优先级从高到低,有;
算术运算符>关系运算符>赋值运算符

image.png

07 Part Seven C语言语句

C语句类型

C语言分为以下五类:

  • 控制语句:如循环语句while,for,如条件语句if,如分支语句switch。
  • 函数调用语句:由函数调用加分号组成。
1
2
3
4
5
c复制代码int main()
{ int Min(int a,int b){
if(a>b){
return b;}
else{return a;}}}
  • 表达式语句:在表达式后面加上分号,如赋值语句a=a+1。
  • 空语句:仅包括一个分号的语句,即{}。
  • 复合语句:由大括号{}括起来的语句序列。

控制语句

if语句:

  • 范例
    { /表达式为真将执行的语句/}
  • 举例:
1
2
3
css复制代码main(){
int a=1, b=2;
if(a < b) { ++a; }}
  • 结果:a小于b,则a自增1,变为2.

if else语句:

  • 范例
    if(expression){/表达式为真将执行的语句/} else{/表达式为假将执行的语句/}
  • 举例
1
2
3
4
c复制代码main(){
int a=1, b=2;
if(a < b) { ++a; }
else{a*=4;}}
  • 结果:a小于b,则a自增1,变为2,反之a等于a乘以4.

转向语句

break语句:

  • 举例:
1
2
3
4
c复制代码for(int i=0; i<N; ++i){ 
if(i == 2)
break;
}
  • 结果:i为2时直接结束循环。

continue语句:

  • 举例
1
2
3
c复制代码for(int i=0; i<N; ++i){
if(i == 2)
continue;}
  • 结果:i为2时结束本次循环,进行下一次判定。

08 Part Eight 数组

数组概念

  • 数组长啥样:int a[100];//定义了一个包括100个整型元素的数组; • 数组:按序排列的同类型数据元素的集合;
  • 数组名:数组中共用的名字;
  • 数组元素:集合中的变量;
  • 数组维数:数组名后所跟下标的个数;
  • 数组元素格式:数组名[下标];
  • 数组定义:<数据类型> <数组名>[<常量表达式>]={<初始值>}

几个注意点

• 赋初始值
int a[5]={1,2,3};//a[0]=1,a[1]=2,a[2]=3,a[3]=0,a[4]=0.
注意:数组可以边定义边初始化,不可以先定义后初始化,如下:

image.png
• 二维数组
<数据类型> <数组名>[<常量表达式1>] [<常量表达式1>]={<初始值>} 如:float c[3][3],那么其元素为:

image.png
考虑到内存存储,二维数组其实是一维数组的数组。

本文转载自: 掘金

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

1…569570571…956

开发者博客

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