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

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


  • 首页

  • 归档

  • 搜索

支付宝开发详细流程【沙箱环境】 支付宝开发详细流程

发表于 2021-11-22

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

支付宝开发详细流程

  1. 正式环境(需要营业执照等信息)

  • 支付宝开放平台-开发文档
  • 这里我们选择电脑网站支付
    在这里插入图片描述
  • 使用的话需要有营业执照等,我们这里使用沙箱环境
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPNVXOR3-1596507916903)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803142637380.png)]
  1. 沙箱环境(模拟真实的环境)

  • 沙箱环境

2.1 申请开通沙箱环境

  • 沙箱环境申请
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiEZSuAO-1596507916905)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803143503806.png)]
  • 信息填写确定后,就能看到如下界面
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P9h2ZufQ-1596507916907)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803144000190.png)]
  • 注册成功之后会获取两个值:
+ APPID:xxxx
+ 支付宝网关
    - [openapi.alipaydev.com/gateway.do(…](https://openapi.alipaydev.com/gateway.do%EF%BC%88%E6%B2%99%E7%AE%B1%EF%BC%89)
    - [openapi.alipay.com/gateway.do(…](https://openapi.alipay.com/gateway.do%EF%BC%88%E6%AD%A3%E5%BC%8F%EF%BC%89)

2.2 生成密钥

  • 密钥用于以后对URL中添加的参数进行加密和校验
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YO5iTGmB-1596507916908)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803144430623.png)]

2.2.1 下载密钥生成器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyagKyfC-1596507916910)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803144329714.png)]

2.2.2 生成密钥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FfjJsu26-1596507916911)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803144950121.png)]

  • 会生成一对密钥,同时生成两个 txt文件
    • 应用公钥
    • 应用私钥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hNTQYsG7-1596507916913)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803145116924.png)]

  • 我们将两个文件放到项目中,方便以后使用
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4R6DkFVE-1596507916914)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803145759859.png)]

2.2.3 上传应用公钥并获得支付宝公钥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndJXW5Jo-1596507916915)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803145605267.png)]

  • 点击 保存设置 后就会生成支付宝公钥
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V3iTt8vD-1596507916917)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803145650359.png)]
  • 然后把支付宝公钥也放在项目中,方便以后使用
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mD8munt5-1596507916919)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803145828421.png)]
  • 至此,我们共获取到三个密钥:
+ 应用公钥
    - 生成支付宝公钥后就没用处了
+ 应用私钥
    - 对以后URL中传入的数据进行签名加密用
+ 支付宝公钥(通过应用公钥生成)
    - 在页面支付成功后跳转回来时,对支付宝给我们传的值进行校验
  1. 账户信息和测试APP

  • 下载沙箱版支付宝app【仅提供Android版本】
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U1qYPvmo-1596507916921)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803150312543.png)]
  • 然后查看沙箱账号登录
+ 买家信息
+ 卖家信息

注意: 不要使用自己的支付宝账号登录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NTaNiVcu-1596507916921)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803151058221.png)]

  1. SDK & API

一般都会有两个支持

  • SDK,现成的Python模块【优先使用】
1
2
3
4
> markdown复制代码1. 安装模块
> 2. 基于模块实现想要的功能
>
>
  • API,提供一个URL
1
2
3
> markdown复制代码1. 自己手动对URL进行处理和加密
>
>

4.1 SDK

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyruWYLB-1596507916922)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803151513182.png)]

  • 接入文档中都是通过工具实现的,而我们需要通过代码进行实现,点击下载开发工具包
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUbSLdP0-1596507916923)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803151821254.png)]

4.2 API

  • 支付API
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6Zz6iCY-1596507916924)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803152603186.png)]
  • 要使用支付功能,所以这里选择统一收单下单并支付页面接口
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EBjWnRl9-1596507916926)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803152919532.png)]

参数构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码# 跳转到这个地址: 【网关?参数】组成
网关 = https://openapi.alipaydev.com/gateway.do
params = {
'app_id': 'xxxx',
'method': 'alipay.trade.page.pay',
'format': 'JSON',
'return_url': '支付成之后跳转到的页面地址(GET请求)',
'notify_url': '跳转到return_url的同时向这个地址发送POST请求',
'charset': 'utf-8',
'sign_type': 'RSA2',
'sign': '签名',
'timestamp': 'yyyy-MM-dd HH:mm:ss',
'version': '1.0',
'biz_content': {
'out_trade_no': '订单号',
'product_code': 'FAST_INSTANT_TRADE_PAY',
'total_amount': 88.88,
'subject': '订单标题'
}
}

如果支付成功之后,服务器宕机,如何处理?
向notify_url发请求,支付成功,请求更新状态,
服务器宕机,支付宝访问不到,则会在24小时以内:支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
接收到支付宝请求之后,返回的数据不正确,同上。
返回一个 success

支付结果异步通知

  1. 支付宝签名

  • 对参数进行处理,处理完之后和网关进行拼接
  • 使用生成密钥请求签名
  • 自行实现签名

5.1 签名原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码1. 将参数中 空、文件、字节、sign 剔除
params.pop(sign)

2. 排序,对参数中所有的key进行从小大到大排序 sort(params)
按照第一个字符的键值 ASCII 码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值 ASCII 码递增排序

3. 将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用 & 字符连接起来,此时生成的字符串为待签名字符串。
待签名字符串 = "app_id=xxxx&method=alipay.trade.page.pay&...."
注意: 1. 有字典应该转换为字符串
2. 字符串中间不能有空格(真操蛋的要求,json.dumps(xxx)后默认就会有空格
可以使用 json.dumps(xxx, separators=(',',':')))

4. 使用各自语言对应的 SHA256WithRSA 签名函数并利用商户(应用)私钥对待签名字符串进行签名,并进行 Base64 编码
- result= 使用SHA256WithRSA函数和私钥对待签名字符串进行签名
- 签名 = 对result进行Base64编码

把签名再添加回params字典中 params[sign] = 签名
注意: base64编码之后,内部不能有换行符
签名.replace('\n', '')

5. 再将所有的参数拼接起来

注意: 在拼接URL时不能出现 ;,( 等字符,提前将特殊字符转换为URL转义的字符(URL编码)
`from urllib.parse import quote_plus`

5.2 签名实现

1
2
3
4
5
python复制代码# pip install pycrypto
# windows 安装可能会报错【如下图】
# 可以下载安装 pycryptodome 这个库
# pycryptodome.xxx.whl 安装方法:
# 进入安装目录 pip install pycryptodome.xxx.whl

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xHvAa1d9-1596507916927)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20200803171241856.png)]

下载也有点麻烦,我这里把下载好的几个文件放在网盘,需要自行下载即可,我这里放的版本有 【py27、py35、py36】,其他版本可自行下载

注意:根据自己的python版本安装,例如: 文件名中的py35代表python3.5

链接:pan.baidu.com/s/1z1kT-Qjd…
提取码:kjnd

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
python复制代码# 构造字典
params = {
'app_id': "2021000117635347",
'method': 'alipay.trade.page.pay',
'format': 'JSON',
'return_url': "http://127.0.0.1:8001/pay/notify/",
'notify_url': "http://127.0.0.1:8001/pay/notify/",
'charset': 'utf-8',
'sign_type': 'RSA2',
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'version': '1.0',
'biz_content': json.dumps({
'out_trade_no': xxx,
'product_code': 'FAST_INSTANT_TRADE_PAY',
'total_amount': xxx,
'subject': "tracer payment"
}, separators=(',', ':'))
}


# 获取待签名的字符串
unsigned_string = "&".join(["{0}={1}".format(k, params[k]) for k in sorted(params)])

# 签名 SHA256WithRSA(对应sign_type为RSA2)
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import decodebytes, encodebytes

# SHA256WithRSA + 应用私钥 对待签名的字符串 进行签名
private_key = RSA.importKey(open("files/应用私钥2048.txt").read())
signer = PKCS1_v1_5.new(private_key)
signature = signer.sign(SHA256.new(unsigned_string.encode('utf-8')))

# 对签名之后的执行进行base64 编码,转换为字符串
sign_string = encodebytes(signature).decode("utf8").replace('\n', '')

# 把生成的签名赋值给sign参数,拼接到请求参数中。

from urllib.parse import quote_plus
result = "&".join(["{0}={1}".format(k, quote_plus(params[k])) for k in sorted(params)])
result = result + "&sign=" + quote_plus(sign_string)

gateway = "https://openapi.alipaydev.com/gateway.do"
pay_url = "{}?{}".format(gateway, result)

最后,欢迎大家关注我的个人微信公众号 『小小猿若尘』,获取更多IT技术、干货知识、热点资讯

本文转载自: 掘金

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

小码农二叉树OJ淬体 二叉树OJ淬体

发表于 2021-11-22

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

二叉树OJ淬体

例1:单值二叉树

题目

image-20211114094938096

image-20211114100001843

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码bool isUnivalTree(struct TreeNode* root){
//空树直接就是单值
if(!root)
return true;
//假如仅有一个左节点的时候
if(root->left && root->left->val != root->val)
return false;
//假如仅有一个右节点的时候
if(root->right && root->right->val != root->val)
return false;
return isUnivalTree(root->left) && isUnivalTree(root->right);
}

例2:二叉树的前序遍历

题目

image-20211114101027475

image-20211114211340327

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
c复制代码//我们先把二叉树的节点个数求出来
int BinaryTreeSize(struct TreeNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left)+BinaryTreeSize(root->right)+1;
}

void _preorderTraversal(struct TreeNode* root,int* a,int* i)
{
if(!root)
return;
a[(*i)++] = root->val;
_preorderTraversal(root->left,a,i);
_preorderTraversal(root->right,a,i);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize){
//我们知道二叉树节点个数就好开辟数组大小了
int size = BinaryTreeSize(root);
int* a = (int*)malloc(sizeof(int)*size);
//我们直接递归preorderTraversal它,是不好递归的因为每次递归你都开辟一个数组吗,不现实
//我们应该递归他的类似性质的函数,不过不可以次次开辟数组,应该传递数组再给一个下标
int i = 0;
_preorderTraversal(root,a,&i);
*returnSize = size;
return a;
}

例3:二叉树的中序遍历

题目

image-20211116214530749

image-20211116214359131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码 //二叉树节点个数函数
int BinaryTreeSize(struct TreeNode* root){
if(!root)
return 0;
return BinaryTreeSize(root->left)+BinaryTreeSize(root->right)+1;
}
//子函数
void _inorderTraversal(struct TreeNode* root,int* a,int* pi){
if(!root)
return;
_inorderTraversal(root->left,a,pi);
a[(*pi)++] = root->val;
_inorderTraversal(root->right,a,pi);
}
int* inorderTraversal(struct TreeNode* root, int* returnSize){
//把节点个数赋给数组大小
int size = BinaryTreeSize(root);
//创建合适的数组
int* a = (int*)malloc(sizeof(int)*size);
int i = 0;
_inorderTraversal(root,a,&i);
*returnSize = size;
return a;
}

例4:二叉树的后序遍历

题目

image-20211116214847981

image-20211116221210027

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码//二叉树
int BinaryTreeSize(struct TreeNode* root){
return root == NULL ? 0 : BinaryTreeSize(root->left)+BinaryTreeSize(root->right)+1;
}
//子函数
void _postorderTraversal(struct TreeNode* root,int* a,int* pi){
if(!root)
return;
_postorderTraversal(root->left,a,pi);
_postorderTraversal(root->right,a,pi);
a[(*pi)++] = root->val;
}
int* postorderTraversal(struct TreeNode* root, int* returnSize){
//数组大小传过来
int size = BinaryTreeSize(root);
//创建合适的数组
int* a = (int*)malloc(sizeof(int)*size);
int i = 0;
_postorderTraversal(root,a,&i);
*returnSize = size;
return a;
}

例5:相同的树

题目

image-20211114214742547

image-20211114233423560

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//都为空
if(!p && !q)
return true;
//有且只有一个是空
if(!p || !q)
return false;
//没有空树
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

例6:对称二叉树

题目

image-20211116072024229

image-20211116073040713

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码bool _isSymmetricTree(struct TreeNode* root1,struct TreeNode* root2)
{
//两个都是空就返回真
if(!root1 && !root2)
return true;
//只有一个空直接假
if(!root1 || !root2)
return false;
if(root1->val != root2->val)
return false;
return _isSymmetricTree(root1->left,root2->right)
&& _isSymmetricTree(root1->right,root2->left);
}

bool isSymmetric(struct TreeNode* root){
//空树就是对称
if(!root)
return true;
//返回他们判断结果
return _isSymmetricTree(root->left,root->right);
}

例7:另一棵树的子树

题目

image-20211116074903027

image-20211116202318803

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码 //判断是否是相同的树
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//都为空
if(!p && !q)
return true;
//有且只有一个是空
if(!p || !q)
return false;
//没有空树
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
if(!root)
return false;
if(isSameTree(root,subRoot))
return true;
return isSubtree(root->left,subRoot)
|| isSubtree(root->right,subRoot);
}

例8:二叉树遍历

题目

image-20211118001943031

image-20211118231042018

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
c复制代码#include <stdio.h>
#include <stdlib.h>

//树节点
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* right;
struct BinaryTreeNode* left;
int val;
}BTNode;

//创建树
BTNode* CreateTree(char* str,int* pi)
{
//#就返回空
if(str[*pi] == '#')
{
(*pi)++;
return NULL;
}
//不是空开始建树
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->val = str[(*pi)++];
//递归创建
root->right = CreateTree(str,pi);
root->left = CreateTree(str,pi);
return root;
}
//中序遍历
void InOrder(BTNode* root)
{
//空就返回
if(!root)
return;
//先走左
InOrder(root->right);
//打印中
printf("%c ",root->val);
//再走右
InOrder(root->left);
}

int main()
{
//因为不超过100
char str[100] = {0};
//多组输入
while(scanf("%s",str) != EOF)
{
//创建树
int i = 0;
BTNode* root = CreateTree(str,&i);
InOrder(root);
}
return 0;
}

本文转载自: 掘金

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

Spring Cloud Gateway源码解析-06-内置

发表于 2021-11-22

SCG的Predicate是使用工厂方法模式来实现的,类关系如下。
在这里插入图片描述

SCG包括了很多内置的Predicate工厂,如下
在这里插入图片描述

在每个RoutePredicateFactory中都有一个Config类,该类用于存储对应RoutePredicate的配置

AfterRoutePredicateFactory

匹配请求时间满足在配置时间之后的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
json复制代码public class AfterRoutePredicateFactory
extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
final ZonedDateTime now = ZonedDateTime.now();
return now.isAfter(config.getDatetime());
}

@Override
public String toString() {
return String.format("After: %s", config.getDatetime());
}
};
}
}

代码很简单,获取到我们配置的时间,判断当前时间是否大于配置的时间。

Test

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- After=2021-03-26T16:05:22.631+08:00[Asia/Shanghai]

当前时间16:11,小于我们配置的时间,当进行断言的时候,返回了true,表示符合条件。
在这里插入图片描述

当配置日期为27号时,则不符合条件,返回false,不匹配该路由。

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- After=2021-03-27T16:05:22.631+08:00[Asia/Shanghai]

在这里插入图片描述

BeforeRoutePredicateFactory

匹配请求时间满足在配置时间之前的请求。与AfterRoutePredicateFactory类似,不做过多阐述。

配置

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Before=2021-03-27T16:05:22.631+08:00[Asia/Shanghai]

BetweenRoutePredicateFactory

匹配请求时间满足在配置时间之间的请求。与上边两种类似,不做过多阐述

配置

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Between=2021-03-26T16:05:22.631+08:00[Asia/Shanghai],2021-03-27T16:05:22.631+08:00[Asia/Shanghai]

CookieRoutePredicateFactory

匹配带有指定cookie的请求,key需要相等,value满足配置的正则表达式.

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
json复制代码public class CookieRoutePredicateFactory
extends AbstractRoutePredicateFactory<CookieRoutePredicateFactory.Config> {

/**
* Name key.
*/
public static final String NAME_KEY = "name";

/**
* Regexp key.
*/
public static final String REGEXP_KEY = "regexp";

@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
List<HttpCookie> cookies = exchange.getRequest().getCookies()
.get(config.name);
if (cookies == null) {
return false;
}
for (HttpCookie cookie : cookies) {
if (cookie.getValue().matches(config.regexp)) {
return true;
}
}
return false;
}
};
}
}

配置

CookieRoutePredicateFactory需要配置两个参数,name和value,通过逗号分隔,value支持正则表达式。

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Cookie=502819cookie,502819.*

在这里插入图片描述

在这里插入图片描述

HeaderRoutePredicateFactory

匹配带有指定header的请求,key需要相等,value满足配置的正则表达式.与CookieRoutePredicateFactory类似,不过多阐述。

配置

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Header=502819header,502819.*

HostRoutePredicateFactory

匹配请求Host复合配置的正则表达式。可以配置多个

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
json复制代码public class HostRoutePredicateFactory
extends AbstractRoutePredicateFactory<HostRoutePredicateFactory.Config> {

.............省略部分代码............

@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
String host = exchange.getRequest().getHeaders().getFirst("Host");
Optional<String> optionalPattern = config.getPatterns().stream()
.filter(pattern -> pathMatcher.match(pattern, host)).findFirst();

if (optionalPattern.isPresent()) {
Map<String, String> variables = pathMatcher
.extractUriTemplateVariables(optionalPattern.get(), host);
ServerWebExchangeUtils.putUriTemplateVariables(exchange, variables);
return true;
}

return false;
}
};
.............省略部分代码............
}
public static class Config {
//因为是List,所以可以配置多个
private List<String> patterns = new ArrayList<>();
.............省略部分代码............
}

}

Test

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Host=*.spring.io,*.502819lhj.*

在这里插入图片描述

在这里插入图片描述

MethodRoutePredicateFactory

匹配指定HTTP Method的请求。

1
2
3
4
json复制代码public enum HttpMethod {

GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}

Test

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Method=POST,DELETE

在这里插入图片描述

当时用GET请求时,断言失败。
在这里插入图片描述

PathRoutePredicateFactory

匹配复合配置Path的请求。

PathRoutePredicateFactory可以设置模板变量,模板变量会被放入ServerWebExchange的attributes中,可以供GatewayFilter使用。

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
json复制代码public class PathRoutePredicateFactory
extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
@Override
public Predicate<ServerWebExchange> apply(Config config) {
..........省略部分代码.........
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
PathContainer path = parsePath(
exchange.getRequest().getURI().getRawPath());

Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
.filter(pattern -> pattern.matches(path)).findFirst();

if (optionalPathPattern.isPresent()) {
PathPattern pathPattern = optionalPathPattern.get();
traceMatch("Pattern", pathPattern.getPatternString(), path, true);
PathMatchInfo pathMatchInfo = pathPattern.matchAndExtract(path);
//关注点
putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
return true;
}
else {
traceMatch("Pattern", config.getPatterns(), path, false);
return false;
}
}
};
}
}
class ServerWebExchangeUtils {
public static void putUriTemplateVariables(ServerWebExchange exchange,
Map<String, String> uriVariables) {
if (exchange.getAttributes().containsKey(URI_TEMPLATE_VARIABLES_ATTRIBUTE)) {
Map<String, Object> existingVariables = (Map<String, Object>) exchange
.getAttributes().get(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
HashMap<String, Object> newVariables = new HashMap<>();
newVariables.putAll(existingVariables);
newVariables.putAll(uriVariables);
//放入ServerWebExchange中
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, newVariables);
}
else {
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
}
}
}

配置

1
2
3
4
5
6
7
8
9
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Path=/api/{hello}
- Path=/api/hello

QueryRoutePredicateFactory

匹配带有指定请求参数的请求。

可以只配置需要匹配的请求参数名称,也可以同时配置需要匹配的请求参数名称和请求参数值,请求参数值支持正则表达式。

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
json复制代码public class QueryRoutePredicateFactory
extends AbstractRoutePredicateFactory<QueryRoutePredicateFactory.Config> {
.......省略部分代码.......
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
if (!StringUtils.hasText(config.regexp)) {
//如果参数值正则为空则只匹配参数名
// check existence of header
return exchange.getRequest().getQueryParams()
.containsKey(config.param);
}

List<String> values = exchange.getRequest().getQueryParams()
.get(config.param);
if (values == null) {
return false;
}
for (String value : values) {
//判断参数值是否match配置的正则
if (value != null && value.matches(config.regexp)) {
return true;
}
}
return false;
}
};
}
}

Test

1
2
3
4
5
6
7
8
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Query=name,502819.*

在这里插入图片描述

ReadBodyRoutePredicateFactory

用来检查请求体的内容的断言,属于测试版本,未来可能会更改,不做过多的阐述。

RemoteAddrRoutePredicateFactory

匹配请求客户端的IP符合指定的IP地址。可以配置IP段也可以通过逗号配置多个。
源代码没什么难度,就不多说了。

Test

1
2
3
4
5
6
7
8
9
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- RemoteAddr=169.254.183.16,192.168.5.14
- RemoteAddr=169.254.183.1/18 #表示1到18都可以访问

WeightRoutePredicateFactory

WeightRoutePredicateFactory可以配置两个参数,group和weight。

配置

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- Weight=group1,2
- id: hello_route2
uri: http://localhost:8089/api/hello
predicates:
- Weight=group1,8

WeightRoutePredicateFactory会对同一组内的路由进行权重计算,根据配置的权重进行访问。
上边的配置下,访问 http://localhost:8088/api/hello的概率为20%,访问http://localhost:8089/api/hello的概率为80%。

解析

WeightRoutePredicateFactory的设计较为复杂且有趣。

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
json复制代码public class WeightRoutePredicateFactory
extends AbstractRoutePredicateFactory<WeightConfig>
implements ApplicationEventPublisherAware {
@Override
public Predicate<ServerWebExchange> apply(WeightConfig config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
//获取到所有的权重信息,key:group ,value:路由ID
//WEIGHT_ATTR由WeightCalculatorWebFilter放入
Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
Collections.emptyMap());
//获取到当前遍历的路由ID
String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);

// all calculations and comparison against random num happened in
// WeightCalculatorWebFilter
//获取到当前路由的group
String group = config.getGroup();
//判定权重信息中是否包含当前路由的group
if (weights.containsKey(group)) {
//根据group获取权重信息中的路由ID
String chosenRoute = weights.get(group);
if (log.isTraceEnabled()) {
log.trace("in group weight: " + group + ", current route: "
+ routeId + ", chosen route: " + chosenRoute);
}
//判断当前路由的ID与权重信息中的路由ID是否相等,如果不相等,不匹配当前路由
//到这里其实能够看出来,weights中就是当前请求应该请求的route
return routeId.equals(chosenRoute);
}
else if (log.isTraceEnabled()) {
log.trace("no weights found for group: " + group + ", current route: "
+ routeId);
}

return false;
}
};
}
}

接下来解析第10行的weights是怎么来的,SCG定义了一个WebFilter(WeightCalculatorWebFilter),会先对请求进行处理。
在RouteDefinitionRouteLocator#lookup方法中,会在生成配置信息的时候发布一个PredicateArgsEvent事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码@SuppressWarnings("unchecked")
private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
PredicateDefinition predicate) {
...........省略部分代码...............
//每个RoutePredicateFactory实现中都有Config,可以理解为我们配置的参数规则,生成此Config
// @formatter:off
Object config = this.configurationService.with(factory)
.name(predicate.getName())
.properties(predicate.getArgs())
//发布事件
.eventFunction((bound, properties) -> new PredicateArgsEvent(
RouteDefinitionRouteLocator.this, route.getId(), properties))
.bind();
// @formatter:on
//生成异步断言
return factory.applyAsync(config);
}

在WeightCalculatorWebFilter中监听了这个事件。

WeightCalculatorWebFilter

1
2
3
4
5
6
7
8
9
json复制代码@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PredicateArgsEvent) {
//监听到RouteDefinitionRouteLocator发布的事件
handle((PredicateArgsEvent) event);
}
...........省略部分代码...............

}

在handle方法中会获取到发布事件的路由的ID,并且通过configurationService获取到路由的配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码public void handle(PredicateArgsEvent event) {
Map<String, Object> args = event.getArgs();

if (args.isEmpty() || !hasRelevantKey(args)) {
return;
}

WeightConfig config = new WeightConfig(event.getRouteId());
//获取到当前路由的配置信息
this.configurationService.with(config).name(WeightConfig.CONFIG_PREFIX)
.normalizedProperties(args).bind();

addWeightConfig(config);
}
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
json复制代码void addWeightConfig(WeightConfig weightConfig) {
//获取当前路由的group
String group = weightConfig.getGroup();
GroupWeightConfig config;
// only create new GroupWeightConfig rather than modify
// and put at end of calculations. This avoids concurency problems
// later during filter execution.
//判断groupWeights是否包含了已经包含了同group的权重配置,此处的groupWeights是所有的路由权重信息
if (groupWeights.containsKey(group)) {
//如果有也创建一个信息,并将前边的该group下路由权重信息初始化进去
config = new GroupWeightConfig(groupWeights.get(group));
}
else {
config = new GroupWeightConfig(group);
}
//添加当前路由+路由权重
config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());

// recalculate

// normalize weights
int weightsSum = 0;
//计算配置的所有的路由权重和
//假设,我们配置rout1、route2、route3三个路由的权重分别为2,7,1,那么weightSum计算后为10
for (Integer weight : config.weights.values()) {
weightsSum += weight;
}

final AtomicInteger index = new AtomicInteger(0);
//遍历
for (Map.Entry<String, Integer> entry : config.weights.entrySet()) {
//获取到路由ID
String routeId = entry.getKey();
//获取到路由的权重
Integer weight = entry.getValue();
//计算出当前路由的权重占比
//rout1:0.2,route2:0.7,route3:0.1
Double nomalizedWeight = weight / (double) weightsSum;
//放入normalizedWeights
config.normalizedWeights.put(routeId, nomalizedWeight);

// recalculate rangeIndexes
config.rangeIndexes.put(index.getAndIncrement(), routeId);
}

// TODO: calculate ranges
config.ranges.clear();
//放入0号位置数0.0
config.ranges.add(0.0);
/**
* normalizedWeights:rout1:0.2,route2:0.7,route3:0.1
*/
List<Double> values = new ArrayList<>(config.normalizedWeights.values());
for (int i = 0; i < values.size(); i++) {
Double currentWeight = values.get(i);
Double previousRange = config.ranges.get(i);
Double range = previousRange + currentWeight;
config.ranges.add(range);
}
//ranges :大约为 0.0, 0.2, 0.9, 1.0
//相邻两个index之间代表的是一个路由的范围,
//如rout1:0.2,route2:0.7,route3:0.1 那ranges的元素为0.0, 0.2, 0.9, 1.0
//0.0到0.2则表示route1的权重范围,0.2到0.9表示的route2的权重范围以此类推
if (log.isTraceEnabled()) {
log.trace("Recalculated group weight config " + config);
}
// only update after all calculations
//添加权重分组,key:分组 value:分组下的所有路由的权重信息
groupWeights.put(group, config);
}


static class GroupWeightConfig {

String group;
//key:路由ID value:权重
LinkedHashMap<String, Integer> weights = new LinkedHashMap<>();
//路由的权重占比
LinkedHashMap<String, Double> normalizedWeights = new LinkedHashMap<>();

LinkedHashMap<Integer, String> rangeIndexes = new LinkedHashMap<>();
//相邻两个index之间代表的是一个路由的范围,
//如rout1:0.2,route2:0.7,route3:0.1 那ranges的元素为0.0, 0.2, 0.9, 1.0
//0.0到0.2则表示route1的权重范围,0.2到0.9表示的route2的权重范围以此类推
List<Double> ranges = new ArrayList<>();

GroupWeightConfig(String group) {
this.group = group;
}

GroupWeightConfig(GroupWeightConfig other) {
this.group = other.group;
this.weights = new LinkedHashMap<>(other.weights);
this.normalizedWeights = new LinkedHashMap<>(other.normalizedWeights);
this.rangeIndexes = new LinkedHashMap<>(other.rangeIndexes);
}
}

filter方法

此方法会被WebFilterChain调用。用来为当前请求添加group及路由,方便WeightRoutePredicateFactory获取及匹配。

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
json复制代码@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Map<String, String> weights = getWeights(exchange);

for (String group : groupWeights.keySet()) {
//获取到当前分组的所有路由及权重信息
GroupWeightConfig config = groupWeights.get(group);

if (config == null) {
if (log.isDebugEnabled()) {
log.debug("No GroupWeightConfig found for group: " + group);
}
continue; // nothing we can do, but this is odd
}
//生成随机数
double r = this.random.nextDouble();
//获取到当前分组的所有路由的权重范围
List<Double> ranges = config.ranges;

if (log.isTraceEnabled()) {
log.trace("Weight for group: " + group + ", ranges: " + ranges + ", r: "
+ r);
}

for (int i = 0; i < ranges.size() - 1; i++) {
//如果生成的随机数大于等于当前的元素,并且小于下一元素,说明属于当前路由,则获取到路由ID放入weights中返回

//WeightRoutePredicateFactory只需要判断weights中是否有当前路由的group,
//如果有,则进一步判断当前路由ID是否为这里计算出来的路由id即可
if (r >= ranges.get(i) && r < ranges.get(i + 1)) {
String routeId = config.rangeIndexes.get(i);
weights.put(group, routeId);
break;
}
}
}

if (log.isTraceEnabled()) {
log.trace("Weights attr: " + weights);
}

return chain.filter(exchange);
}

总结

目前为止,2.2.6.RELEASE版本的SCG内置的RoutePredicateFactory我们就解析完了,基本上能够满足我们平时的业务场景,也可以根据需要进行自定义。后边会解析GatewayFilterFactory。加油!

本文转载自: 掘金

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

Spring Boot整合Log4j2将日志输出至WebSo

发表于 2021-11-22

需求

需要将后台产生的日志试试通过WebSocket发送,在前端建立连接后可实时查看当前系统产生的日志

思路

先进行一番搜索看看有没有现成的轮子

看到一篇博客与我的想法接近,但是他使用的是Logback

cloud.tencent.com/developer/a…

在Log4j2的官方文档中给出了不同场景下各个日志框架的性能区别logging.apache.org/log4j/2.x/p…

秉持能用好的用好的能用快的用快的的观念,我选择Log4j2

对于这个问题我有两个思路

  1. 将日志文件写到文件中,监控文件变化,变化时,读取一行交给WebSocket发送
  2. 实现一个Appender直接将日志交给WebSocket

总感觉第一种方法不太优雅,需要指定配置文件所在的地址

这里详细说一下第二种方法

步骤

引入Log4j2依赖

spring-boot-starter包下有一个spring-boot-starter-logging内包含了Logback相关的依赖,需要将其排除

image-20211122122228342

再引入依赖

image-20211122122322354

引入WebSocket依赖

image-20211122122357493

什么是WebSocket,WebSocket怎么用等等一系列问题可以看www.mydlq.club/article/86/

写的非常好,非常全面

Spring Boot支持使用 STOMP,我们这里也使用的是STOMP,关于STOMP上面的博客也有提到。

配置WebSocket

新建一个WebSocketConfig类用来配置WebSocket的基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
   /**
    * 配置Broker,表明可以在topic域上可以向客户端发送消息
    * 当客户端向服务端发起请求是需要/app前缀
    */
   @Override
   public void configureMessageBroker(MessageBrokerRegistry config) {
       config.enableSimpleBroker("/topic");
       config.setApplicationDestinationPrefixes("/app");
  }
​
  /**
    * 配置WebSocket连接的端点
    */
   @Override
   public void registerStompEndpoints(StompEndpointRegistry registry) {
       registry.addEndpoint("/websocket")
              .withSockJS();
  }
}

到此WebSocket配置完毕

实现一个WebSocketAppender将日志记录到WebSocket

如何自己实现appender可以看

logging.apache.org/log4j/2.x/m…

logging.apache.org/log4j/2.x/m…

官网的两节内容

也可以搜索关键字”Log4j2 插件“进行学习

为什么要自己实现是因为Log4j2本身提供的appender都没有契合这个需求的(貌似)

appender实现

这里直接给我我的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码@Plugin(name = "WebSocketAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public class WebSocketAppender extends AbstractAppender {
​
   // 一个阻塞队列
   private LoggerQueue loggerQueue  = LoggerQueue.getInstance();
​
   protected WebSocketAppender(String name,
                               Filter filter,
                               Layout<? extends Serializable> layout,
                               boolean ignoreExceptions,
                               Property[] properties) {
       super(name, filter, layout, ignoreExceptions, properties);
  }
​
   // TODO:未考虑并发
   // 这个方法就是将日志文件放到哪的具体实现
   // 这里将日志文件转换为字符串后并没有直接给WebSocket而是给一个阻塞队列进行缓冲
   @Override
   public void append(LogEvent event) {
       loggerQueue.push(new String(getLayout().toByteArray(event)));
  }
​
   // 用来构造这个类
   @PluginFactory
   public static WebSocketAppender createAppender(@PluginAttribute("name") String name,
                                                  @PluginAttribute("ignoreExceptions") boolean ignoreExceptions,
                                                  @PluginElement("Layout") Layout layout,
                                                  @PluginElement("Filters") Filter filter) {
​
​
       if (name == null) {
           LOGGER.error("No name provided for WebSocketAppender");
           return null;
      }
​
       if (layout == null) {
           layout = PatternLayout.createDefaultLayout();
      }
       return new WebSocketAppender(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
  }
}

我append的方法不够完善,并没有考虑到并发的情况,可能出现许多问题

阻塞队列的实现

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
java复制代码public class LoggerQueue {
   //队列大小
   public static final int QUEUE_MAX_SIZE = Integer.MAX_VALUE;
   private static final LoggerQueue alarmMessageQueue = new LoggerQueue();
   //阻塞队列
   private final BlockingQueue<String> queue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
​
   public static LoggerQueue getInstance() {
       return alarmMessageQueue;
  }
   
   /**
    * 消息入队
    * @param log
    * @return
    */
   public boolean push(String log) {
       return this.queue.add(log);
  }
   
   /**
    * 消息出队
    * @return
    */
   public String pop() {
       String result = null;
       try {
           result = this.queue.take();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       return result;
  }
}

日志转发实现

创建一个LogForward用以将队列中的方法转发到WebSocket中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Component
// 开启定时任务用来测试
@EnableScheduling
public class LogForward {
   protected final Logger logger = LoggerFactory.getLogger(this.getClass());
   // 这个类是Spring提供用来发送消息的类
   @Autowired
   private SimpMessagingTemplate messagingTemplate;
   // 线程池,没有的话可以直接new Thread
   @Autowired
   private ThreadPoolExecutor threadPoolExecutor;
​
   // 获取阻塞队列的实例
   private final LoggerQueue loggerQueue = LoggerQueue.getInstance();
​
   // 每5s打印一条日志用以测试
   @Scheduled(cron = "*/5 * * * * *")
   public void ok(){
       logger.info("ok");
  }
​
   @Bean
   public void pushLogs() {
       threadPoolExecutor.execute(() -> {
           // 从队列中读取日志并发送
           while (true) {
               String message = loggerQueue.pop();
               messagingTemplate.convertAndSend("/topic/log", message);
          }
      });
  }
}

Log4j2.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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--packages="cn.xxx.xxx.xxx"用来配置自定义插件所在的包-->
<Configuration status="WARN" packages="cn.xxx.xxx.xxx" strict="true">
   <Appenders>
       <!--*********************WebSocket日志***********************-->
       <Appender type="WebSocketAppender" name="webSocketAppender">
           <Layout type="PatternLayout"
                   pattern="%d [%t] %-5level: %msg%n%throwable"/>
           <!--TODO:临时使用字符串匹配关键字过滤,不能保证完全避免问题,而且并不优雅,后期可以考虑别的办法-->
           <!--问题: 在debug级别下,当WebSocket消息发出后会产生一条日志,这条日志会导致WebSocketAppender又发送日志,就导致死循环-->
           <Filters>
               <StringMatchFilter text="Processing MESSAGE destination=" onMatch="DENY" onMismatch="NEUTRAL"/>
               <StringMatchFilter text="Broadcasting to" onMatch="DENY" onMismatch="NEUTRAL"/>
           </Filters>
       </Appender>
​
       <!--*********************控制台日志***********************-->
       <Appender type="Console" name="consoleAppender" target="SYSTEM_OUT">
           <PatternLayout
                   pattern="%style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
                   disableAnsi="false"
                   noConsoleNoAnsi="false"/>
​
       </Appender>
   </Appenders>
   
   <Loggers>
       <Root level="debug">
           <AppenderRef ref="consoleAppender"/>
           <AppenderRef ref="webSocketAppender"/>
       </Root>
   </Loggers>
</Configuration>

这里有个很坑的点就是

在DEBUG级别下WebSocket发消息会产生一条日志,这条日志又会导致发消息,就形成了循环

这里用了一个StringMatchFilter通过匹配字符串来过滤那两条日志,但这样不优雅,也不完美,但暂时想不到别的办法

至此,后端部分全部完成

搭建前端测试

这里直接复制了Spring官方文档的代码稍作修改

index.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
html复制代码<!DOCTYPE html>
<html>
<head>
   <title>Hello WebSocket</title>
   <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
   <link href="main.css" rel="stylesheet">
   <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
   <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
   <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
   <script src="app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
  enabled. Please enable
  Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
   <div class="row">
       <div class="col-md-6">
           <form class="form-inline">
               <div class="form-group">
                   <label for="connect">WebSocket connection:</label>
                   <button id="connect" class="btn btn-default" type="submit">Connect</button>
                   <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                   </button>
               </div>
           </form>
       </div>
       <div class="col-md-6">
           <form class="form-inline">
               <div class="form-group">
                   <label for="name">What is your name?</label>
                   <input type="text" id="name" class="form-control" placeholder="Your name here...">
               </div>
               <button id="send" class="btn btn-default" type="submit">Send</button>
           </form>
       </div>
   </div>
   <div class="row">
       <div class="col-md-12">
           <table id="conversation" class="table table-striped">
               <thead>
               <tr>
                   <th>Greetings</th>
               </tr>
               </thead>
               <tbody id="greetings">
               </tbody>
           </table>
       </div>
   </div>
</div>
</body>
</html>

app.js

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
javascript复制代码var stompClient = null;
​
function setConnected(connected) {
   $("#connect").prop("disabled", connected);
   $("#disconnect").prop("disabled", !connected);
   if (connected) {
       $("#conversation").show();
  }
   else {
       $("#conversation").hide();
  }
   $("#greetings").html("");
}
​
function connect() {
   var socket = new SockJS('websocket');
   stompClient = Stomp.over(socket);
   stompClient.connect({}, function (frame) {
       setConnected(true);
       console.log('Connected: ' + frame);
       stompClient.subscribe('/topic/log', function (greeting) {
           console.log(greeting)
           showGreeting(greeting.body);
      });
  });
}
​
function disconnect() {
   if (stompClient !== null) {
       stompClient.disconnect();
  }
   setConnected(false);
   console.log("Disconnected");
}
​
function sendName() {
   stompClient.send("/app/hello", {}, JSON.stringify({'ok': $("#name").val()}));
}
​
function showGreeting(message) {
   $("#greetings").append("<tr><td>" + message + "</td></tr>");
}
​
$(function () {
   $("form").on('submit', function (e) {
       e.preventDefault();
  });
   $( "#connect" ).click(function() { connect(); });
   $( "#disconnect" ).click(function() { disconnect(); });
   $( "#send" ).click(function() { sendName(); });
});

main.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码body {
   background-color: #f5f5f5;
}
​
#main-content {
   max-width: 940px;
   padding: 2em 3em;
   margin: 0 auto 20px;
   background-color: #fff;
   border: 1px solid #e5e5e5;
   -webkit-border-radius: 5px;
   -moz-border-radius: 5px;
   border-radius: 5px;
}

讲这三个文件保存到resource/static下即可

测试

image-20211122130035879

点击Connect建立连接,就可以看到日志输出

本文转载自: 掘金

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

Golang测试第一弹:单元测试 热身 单元测试 继续探索

发表于 2021-11-22

热身

单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。Golang当然也有自带的测试包testing,使用该包可以进行自动化的单元测试,输出结果验证。

如果之前从没用过golang的单元测试的话,可以输入命令 go help test,看看官方的介绍。
这里只打印一些关键信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码E:\mygolandproject\MyTest>go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:

ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...

followed by detailed output for each failed package.
// ......
The go tool will ignore a directory named "testdata", making it available
to hold ancillary data needed by the tests.
// ......
'Go test' recompiles each package along with any files with names matching
the file pattern "*_test.go".
These additional files can contain test functions, benchmark functions, and
example functions. See 'go help testfunc' for more.
// ......

再执行 go help testfunc 看看

1
2
3
4
5
6
7
8
9
10
go复制代码E:\mygolandproject\MyTest1>go help testfunc
The 'go test' command expects to find test, benchmark, and example functions
in the "*_test.go" files corresponding to the package under test.

A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,

func TestXxx(t *testing.T) { ... }
// ......
See the documentation of the testing package for more information.

现在应该清楚了,要编写一个测试套件,首先需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数:

1
go复制代码func TestXxx(*testing.T)    // Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。

go test的基本格式是:

1
go复制代码go test [build/test flags] [packages] [build/test flags & test binary flags]

执行 go test 命令后,就会在指定的包下寻找 *_test.go 文件中的 TestXxx 函数来执行。
除了一些可选的 flags 外,需要注意一下 packages 的填写。该*_test.go测试文件必须要与待测试的文件置于同一包下,执行 go test 或 go test . 或 go test ./xxx_test.go 都可以运行测试套。测试文件不会参与正常源码编译,不会被包含到可执行文件中。

go test 命令会忽略 testdata 目录,该目录是用来保存测试需要用到的辅助数据。

执行完成后就会打印结果信息:

1
2
3
go复制代码ok   archive/tar   0.011s
FAIL archive/zip 0.022s
...

单元测试

要测试的代码:

1
2
3
4
5
6
go复制代码func Fib(n int) int {
if n < 4 {
return n
}
return Fib(n-1) + Fib(n-2)
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func TestFib(t *testing.T) {
var (
in = 7
expected = 13
)
actual := Fib(in)
fmt.Println(actual)
if actual != expected {
// Errorf()函数是单元测试中用于打印格式化的错误信息。
t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
}
}

执行结果如下:

1
2
3
go复制代码E:\myGolandProject\MyTest>go test
PASS
ok gin/MyTest 0.670s

把 expected 改为14,执行结果如下:

1
2
3
4
5
6
go复制代码E:\myGolandProject\MyTest>go test
--- FAIL: TestFib (0.00s)
first_test.go:15: Fib(7) = 13; expected 14
FAIL
exit status 1
FAIL gin/MyTest 0.585s

测试讲究 case 覆盖,按上面的方式,当我们要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func TestFib(t *testing.T) {
var fibTests = []struct {
in int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}

for _, tt := range fibTests {
actual := Fib(tt.in)
if actual != tt.expected {
t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
}
}
}

上面例子中,即使其中某个 case 失败,也不会终止测试执行。

不过可能有小伙伴会觉得为了测试一个简单的函数就要写这么长一段代码,太麻烦了吧!

不用担心,Goland已经具备了一键生成单元测试代码的功能。

图片.png

图片.png

如图所示,光标置于函数名之上,右键选择Generate,我们可以选择生成整个package、当前file或者当前选中函数的测试代码。以当前选中函数为例,Goland会自动在当前目录下生成测试文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func TestFib(t *testing.T) {
type args struct {
n int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Fib(tt.args.n); got != tt.want {
t.Errorf("Fib() = %v, want %v", got, tt.want)
}
})
}
}

我们只需要把测试用例添加到TODO中即可。

这里有个坑需要注意一下,假设原文件是Fib.go,生成的测试文件是Fib_test.go。如果我们直接构造测试用例,然后运行go test ./Fib_test.go的话会报如下错误:

1
2
3
4
go复制代码# command-line-arguments [command-line-arguments.test]
.\Fib_test.go:26:14: undefined: Fib
FAIL command-line-arguments [build failed]
FAIL

解决方法:测试单个文件,需要要带上被测试的原文件,如果原文件有其他引用,也需一并带上。
将go test ./Fib_test.go改为go test ./Fib.go ./Fib_test.go即可

继续探索

到这里已经基本介绍了 Golang单元测试的基本流程。但是还有个疑问没解开,就是*testing.T

函数TestFib(t *testing.T)中的入参 *testing.T 是个啥东西?我们进去源码瞧瞧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// T is a type passed to Test functions to manage test state and support formatted test logs.
//
// A test ends when its Test function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as
// the Parallel method, must be called only from the goroutine running the
// Test function.
//
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
type T struct {
common
isParallel bool
context *testContext // For running tests and subtests.
}

可以看到,T是传递给Test函数的类型,用于管理测试状态并支持格式化的测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。

当测试函数返回时,或者当测试函数调用 FailNow、 Fatal、Fatalf、SkipNow、Skip、Skipf 中的任意一个时,则宣告该测试函数结束。跟 Parallel 方法一样,以上提到的这些方法只能在运行测试函数的 goroutine 中调用。

至于其他报告方法,比如 Log 以及 Error 的变种, 则可以在多个 goroutine 中同时进行调用。

T 类型内嵌了 common 类型,common 提供这一系列方法,我们经常会用到的(注意,这里说的测试中断,都是指当前测试函数,并不是中断整个测试文件的执行):

  1. 当我们遇到一个断言错误的时候,标识这个测试失败,会使用到:
1
2
yaml复制代码Fail : 测试失败,测试继续,也就是之后的代码依然会执行
FailNow : 测试失败,测试函数中断

在 FailNow 方法实现的内部,是通过调用 runtime.Goexit() 来中断测试的。

  1. 当我们遇到一个断言错误,只希望跳过这个错误并中断,但是不希望标识测试失败,会使用到:
1
yaml复制代码SkipNow : 跳过测试,测试中断

在 SkipNow 方法实现的内部,是通过调用 runtime.Goexit() 来中断测试函数的。

  1. 当我们只希望打印信息,会用到 :
1
2
yaml复制代码Log : 输出信息
Logf : 输出格式化的信息

注意:默认情况下,单元测试成功时,它们打印的信息不会输出,可以通过加上 -v 选项,输出这些信息。但对于基准测试,它们总是会被输出。

  1. 当我们希望跳过这个测试函数,并且打印出信息,会用到:
1
2
yaml复制代码Skip : 相当于 Log + SkipNow
Skipf : 相当于 Logf + SkipNow
  1. 当我们希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试函数继续执行,会用到:
1
2
yaml复制代码Error : 相当于 Log + Fail
Errorf : 相当于 Logf + Fail
  1. 当我们希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试函数,会用到:
1
2
yaml复制代码Fatal : 相当于 Log + FailNow
Fatalf : 相当于 Logf + FailNow

接着来看一下runtime.Goexit()的定义:

1
2
3
4
5
6
7
8
9
10
11
go复制代码// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.
func Goexit(){
...
}

函数头第一句注释就说明了Goexit会终止调用它的goroutine。那问题来了,当某个测试函数断言失败调用FailNow的时候,为什么后面的测试代码还可以执行呢?难道不是一个Goroutine执行完整个测试文件吗?(菜鸡的我刚开始确实是这么想的..)。其实答案就在testing包!

testing包中有一个Runtest函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// RunTests is an internal function but exported because it is cross-package;
// it is part of the implementation of the "go test" command.
func RunTests(matchString func(pat, str string) (bool, error), tests []InternalTest) (ok bool) {
var deadline time.Time
if *timeout > 0 {
deadline = time.Now().Add(*timeout)
}
ran, ok := runTests(matchString, tests, deadline)
if !ran && !haveExamples {
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
}
return ok
}
  • 原来Runtest函数就是go test命令的实现!
  • tests []InternalTest这个切片入参就是保存着测试文件中所有的测试函数
  • 调用了runTests,tests切片入参也被传了进去

再看看runTests函数内部实现,我把其他的实现细节屏蔽了:

1
2
3
4
5
6
7
8
9
go复制代码func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) {
// ......
tRunner(t, func(t *T) {
for _, test := range tests {
t.Run(test.Name, test.F)
}
})
// ......
}

果然是这样,遍历了tests切片,对每个测试函数都调用了Run这个方法

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
go复制代码// Run runs f as a subtest of t called name. It runs f in a separate goroutine
// and blocks until f returns or calls t.Parallel to become a parallel test.
// Run reports whether f succeeded (or at least did not fail before calling t.Parallel).
//
// Run may be called simultaneously from multiple goroutines, but all such calls
// must return before the outer test function for t returns.
func (t *T) Run(name string, f func(t *T)) bool {
atomic.StoreInt32(&t.hasSub, 1)
testName, ok, _ := t.context.match.fullName(&t.common, name)
if !ok || shouldFailFast() {
return true
}
// Record the stack trace at the point of this call so that if the subtest
// function - which runs in a separate stack - is marked as a helper, we can
// continue walking the stack into the parent test.
var pc [maxStackLen]uintptr
n := runtime.Callers(2, pc[:])
t = &T{
common: common{
barrier: make(chan bool),
signal: make(chan bool, 1),
name: testName,
parent: &t.common,
level: t.level + 1,
creator: pc[:n],
chatty: t.chatty,
},
context: t.context,
}
t.w = indenter{&t.common}

if t.chatty != nil {
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
}
// Instead of reducing the running count of this test before calling the
// tRunner and increasing it afterwards, we rely on tRunner keeping the
// count correct. This ensures that a sequence of sequential tests runs
// without being preempted, even when their parent is a parallel test. This
// may especially reduce surprises if *parallel == 1.
go tRunner(t, f)
if !<-t.signal {
// At this point, it is likely that FailNow was called on one of the
// parent tests by one of the subtests. Continue aborting up the chain.
runtime.Goexit()
}
return !t.failed
}

答案就在这里,对于每个f,也就是测试函数,都起了一个新的Goroutine来执行!所以当某个测试函数断言失败调用FailNow的时候,后面的测试代码是可以执行的,因为每个TestXxx函数跑在不同的Goroutine上。

扩展

在Go1.17中,给go test新增了一个-shuffle选项,shuffle是洗牌的意思,顾名思义就是TestXxx测试方法的执行顺序被打乱了。

截图.PNG

切换到Go1.17,执行go help testflag,找到-shuffle的描述

1
2
3
4
5
6
7
go复制代码// ......
-shuffle off,on,N
Randomize the execution order of tests and benchmarks.
It is off by default. If -shuffle is set to on, then it will seed
the randomizer using the system clock. If -shuffle is set to an
integer N, then N will be used as the seed value. In both cases,
the seed will be reported for reproducibility.

-shuffle默认是off,设置为on就会打开洗牌。

写个简单Demo验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码import (
"testing"
)

func TestFunc1(t *testing.T) {
t.Logf("1")
}

func TestFunc2(t *testing.T) {
t.Logf("2")
}

func TestFunc3(t *testing.T) {
t.Logf("3")
}

func TestFunc4(t *testing.T) {
t.Logf("4")
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码E:\myGolandProject\MyTest>go test -v -shuffle=on .
-test.shuffle 1637545619604654100
=== RUN TestFunc4
fib2_test.go:20: 4
--- PASS: TestFunc4 (0.00s)
=== RUN TestFunc3
fib2_test.go:16: 3
--- PASS: TestFunc3 (0.00s)
=== RUN TestFunc1
fib2_test.go:8: 1
--- PASS: TestFunc1 (0.00s)
=== RUN TestFunc2
fib2_test.go:12: 2
--- PASS: TestFunc2 (0.00s)
PASS
ok command-line-arguments 0.025s

如果按照某种测试顺序会导致错误的话,那么这种错误是很难定位的,这时候就可以利用-shuffle选项来解决这种问题

参考:www.cnblogs.com/Detector/p/…

本文转载自: 掘金

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

Java 处理表格,真的很爽!

发表于 2021-11-22

一个简单又快速的表格处理库

大家好,我是鱼皮。

处理 Excel 表格是开发中经常遇到的需求,比如表格合并、筛选表格中的某些行列、修改单元格数据等。

今天给大家分享一个 Java 处理表格的工具库,不需要任何专业知识,拿来就能用,快速又轻松~

可能有同学说了,用 Python 处理表格不是更方便么?为毛用 Java 啊?

当然是因为企业中大部分后台开发用的都是 Java!如果你要搞一个允许用户自主上传 Excel 进行处理的服务,那显然直接用 Java 来实现最方便~

Easy Excel

要介绍的库是阿里的 Easy Excel,简单、省内存的读写 Excel 的开源项目。

文档地址:www.yuque.com/easyexcel/d…

直接打开官方文档,就能看到项目的使用说明了:

官方文档

首先在项目中引入 Easy Excel(版本号以文档中的最新版本号为主):

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>

然后进入文档的 快速开始 部分,就可以看到读取和写入表格数据的方法了。

下面让我们以一个实际需求为例,试着使用一下这个库。

需求

假设我们有这样一个 Excel 表格:

如果想要调换 姓名列 和 年龄列 的顺序,应该怎么做呢?

读取表格

首先要读取原始表格中的数据。

Easy Excel 提供了两种读取表格的方式:创建对象的读 和 不创建对象的读 。

创建对象的读

如果你已知整个表格的表头信息,比如列名(比如 “姓名”)和列的数据类型(比如字符串),那么可以创建一个对应的类,用来在 Java 中表示表格的元信息。

比如为上述表格创建 YupiData 类,代码如下:

1
2
3
4
5
6
7
8
9
java复制代码@Data
public class YupiData {
// 姓名
private String name;
// 年龄
private Integer age;
// 出生日期
private Date bornDate;
}

默认会根据属性的顺序来关联表格列的顺序,比如 name 对应姓名(第 0 列)、age 对应年龄(第 1 列)。

当然,你也可以使用注解的方式来指定每个属性对应的表格列,支持指定下标和列名,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Data
public class YupiData {
// 强制读取下标为 2 的列(第三列)
@ExcelProperty(index = 2)
// 指定接受日期的格式
@DateTimeFormat("yyyy/MM/dd")
private Date bornDate;

// 用名字去匹配,不能和其他列重复
@ExcelProperty("年龄")
private Integer age;

@ExcelProperty("姓名")
private String name;
}

定义好了表格数据类,就可以开始读取了,该库非常贴心,提供了 同步 和 异步 两种读取方式。

同步是指一次性读取表格中的所有行,以列表的方式完整返回,再整体去处理。由于这种方式会将数据完整加载到内存中,因此只 适用于表格行数比较少 的情况。代码如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 同步读取
*/
public void synchronousRead() {
String fileName = "鱼皮的表格.xlsx";
// 读取到的数据
List<YupiData> list = EasyExcel.read(fileName)
.head(YupiData.class)
.sheet()
.doReadSync();
}

异步方式需要定义一个 监听器 ,每读取一行,就要立即去处理该行数据。这样就不需要将所有数据都加载到内存中,算一行读一行,理论上算完了也可以丢弃。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码/**
* 定义监听器
*/
public class YupiDataListener
implements ReadListener<YupiData> {
/**
* 每读一行数据,都会调用一次
*
* @param data 一行数据
* @param context 上下文
*/
@Override
public void invoke(YupiData data, AnalysisContext context) {
// 输出姓名
System.out.println(data.getName());
}
}

/**
* 开始读取
*/
void assynchronousRead() {
String fileName = "鱼皮的表格.xlsx";
EasyExcel.read(fileName, YupiData.class,
new YupiDataListener())
.sheet()
.doRead();
}

不创建对象的读

如果事先不清楚表格会有哪些列、类型如何(比如让用户自主上传表格),那么可以使用 不创建对象读 的方式,直接用 Map<Integer, String> 泛型类来接收:

1
2
3
4
5
6
7
8
java复制代码List<Map<Integer, String>> list = EasyExcel
.read(fileName)
.sheet()
.doReadSync();
// Map 的 key 为列下标,value 为单元格的值
for (Map<Integer, String> data : list) {
...
}

当然,这种读取方式也同时支持同步和异步,可以根据需求选择方式,灵活的一批!

写入表格

学会读取后,写入表格就更简单了,依然是先定义一个类,用来表示要写入表格的元信息(列名、列数据类型等)。

比如要完成表格列顺序调换的需求,定义表格数据类的时候,把 age 和 name 属性的顺序换一下就好了:

1
2
3
4
5
6
7
8
9
java复制代码@Data
public class YupiWriteData {
// 年龄 ↑
private Integer age;
// 姓名 ↓
private String name;
// 出生日期
private Date bornDate;
}

然后执行 Easy Excel 的 write 方法,就完事了,代码如下:

1
2
3
4
5
6
7
8
java复制代码void doWrite() {
// 已读取和处理后的数据列表
List<YupiWriteData> dataList = xxx;
String fileName = "result.xlsx";
EasyExcel.write(fileName, YupiWriteData.class)
.sheet("工作表1")
.doWrite(dataList);
}

搞定,是不是贼简单!

除了这个库外,Java 处理 Excel 的库还有很多,比如 Apache POI、Hutool 等,大家可以去试试。但我个人感觉还是 Easy Excel 更对我的胃口。


好了,是不是很简单了,有兴趣的话自己写个表格处理程序吧~

学到的话,帮鱼皮点个 赞 呗,感谢!

本文转载自: 掘金

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

AEJoy —— AE 插件开发中的 交互的回调函数 交互的

发表于 2021-11-22

交互的回调函数

PF_InData 中提供了非宏的(un-macro’d)函数指针,使用提供的宏来访问它们。

交互回调函数

PF_ADD_PARAM

在 PF_Cmd_PARAM_SETUP 期间,将插件的参数枚举到 After Effects,多次调用此函数, 。

注意: 不能完全清除 PF_ADD_PARAM() 之前的 PF_ParamDef 的话,会导致很多问题。在添加参数之前总是使用 AEFX_CLR_STRUCT。

1
2
3
4
cpp复制代码PF_Err PF_ADD_PARAM (
PF_InData *in_data,
PF_ParamIndex index,
PF_ParamDefPtr def);

我们在 Utils/ Param_Utils.h 中为特定的参数类型提供了方便的宏:

  • PF_ADD_COLOR,
  • PF_ADD_ARBITRARY,
  • PF_ADD_SLIDER,
  • PF_ADD_FIXED,
  • PF_ADD_FLOAT_SLIDERX,
  • PF_ADD_CHECKBOXX,
  • PF_ADD_BUTTON,
  • PF_ADD_ANGLE,
  • PF_ADD_NULL,
  • PF_ADD_LAYER,
  • PF_ADD_255_SLIDER,
  • PF_ADD_PERCENT,
  • PF_ADD_POINT,
  • PF_ADD_POINT_3D,
  • PF_ADD_TOPICX,
  • PF_END_TOPIC,
  • PF_ADD_POPUPX,
  • PF_ADD_FLOAT_SLIDERX_DISABLED

PF_ABORT

如果用户已取消,返回非零;将该值返回给 After Effects 。将渲染程序包装在一个“当没有要求中止” 的 while 循环中。

1
cpp复制代码PF_Err PF_ABORT (PF_InData *in_data);

PF_PROGRESS

在处理过程中显示进度条; current 和 total 描述完成的百分比。如果您应该暂停或中止当前处理,则返回非零; 将该值返回给 After Effects 。每扫描线调用一次,除非您的效果非常慢。

如果 total 为 0 ,则使用 PF_ABORT(为用户提供不同的选择)。

1
2
3
4
cpp复制代码PF_Err PF_PROGRESS (
PF_InData *in_data,
A_long current,
A_long total );

PF_CHECKOUT_PARAM

在指定的时间获取参数值或源视频层。After Effects 基于参数的检出状态做出缓存决策。

分配一个新的 PF_ParamDef 来保存结果; 传递给插件的文件是只读的。如果您检出一个设置为 的层参数,则返回的层将填充为0 。遮罩不包含在检出层中。

在 UI 事件处理期间不要检出层参数。

1
2
3
4
5
6
7
cpp复制代码PF_Err PF_CHECKOUT_PARAM (
PF_InData *in_data,
PF_ParamIndex index,
A_long what_time,
A_long step,
A_long time_scale,
PF_ParamDef *param);

如果检查出源层,将返回一个去隔行帧。如果您询问引用上字段(upper field)的时间,您将收到返回的上字段和用于生成额外扫描线的过滤器。例如,假设第 0 行和第 2 行是上字段,第 1 行是下字段,如果您查看上字段,第 0 行和第 2 行将直接从源镜头返回,第 1 行将通过对第 0 行和第 2 行进行平均计算。如果您想重新组合一个包含两个字段的完整分辨率源帧,可以调用 PF_CHECKOUT_PARAM 两次来获取两个字段,并重新交错镜头。

当检出一个没有帧对齐的图层时,会发生什么? 所有项目本质上都具有无限的时间分辨率,所以当 AE 询问任何时间值时,它都会渲染该物品。对于一个合成,这涉及到插值所有关键帧的值到子帧时间。对于连续镜头, AE 返回一个与所询问的时间相对应的完整图像,这是离左边最近的帧。如果用户在那个图层上进行帧混合,就会生成一个插值帧。

PF_CHECKIN_PARAM

用 PF_CHECKIN_PARAM 匹配每个 PF_CHECKOUT_PARAM 。

不这样做会导致糟糕的性能和内存泄漏。一旦检入,PF_ParamDef 中的字段将不再有效。

1
2
3
cpp复制代码PF_Err PF_CHECKIN_PARAM (
PF_InData *in_data,
PF_ParamDef *param );

PF_REGISTER_UI

注册一个自定义用户界面元素。参见 Effect UI & Events 。注意:不支持 PF_UIAlignment 标志。

1
2
3
cpp复制代码PF_Err PF_REGISTER_UI (
PF_InData *in_data,
PF_CustomUIInfo *cust_info );

PF_CHECKOUT_LAYER_AUDIO

给定一个 index,start_time, duration, time_scale, rate, bytes_per_sample, num_channels*,和 *fmt_signed, After Effects 将返回一个对应的 PF_LayerAudio 。After Effects 将执行任何必要的重采样。

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码PF_Err PF_CHECKOUT_LAYER_AUDIO (
PF_InData *in_data,
PF_ParamIndex index,
A_long start_time,
A_long duration,
A_u_long time_scale,
PF_UFixed rate,
A_long bytes_per_sample,
A_long num_channels,
A_long fmt_signed,
PF_LayerAudio *audio);

PF_CHECKIN_LAYER_AUDIO

将所有对 PF_CHECKOUT_LAYER_AUDIO 的调用与对PF_CHECKIN_LAYER_AUDIO 的调用进行匹配,无论错误条件如何。

1
2
3
cpp复制代码PF_Err PF_CHECKIN_LAYER_AUDIO (
PF_InData *in_data,
PF_LayerAudio audio );

PF_GET_AUDIO_DATA

返回关于 PF_LayerAudio 的信息。

audio 之后的所有参数都是可选的; 为任何您不感兴趣的值传递 0 。rate0 是无符号的,fmt_signed0 对于有符号应该是非零,对于无符号应该是零。这个回调用于读取音频信息的视觉效果。要改变音频,写一个音频过滤器。

1
2
3
4
5
6
7
8
9
cpp复制代码PF_Err PF_GET_AUDIO_DATA (
PF_InData *in_data,
PF_LayerAudio audio,
PF_SndSamplePtr *data0,
A_long *num_samples0,
PF_UFixed *rate0,
A_long *bytes_per_sample0,
A_long *num_channels0,
A_long *fmt_signed0);

参数检验与参数零

在效果控制(和合成)面板中,效果按 0 到 n 的顺序应用于图像。

effect[n-1] 的输出是 effect[n] 的输入(param[0])。

另一方面,当一个正常效果使用 PF_CHECKOUT_PARAM 检查一个层时,它会收到原始(未添加效果的)源层,而不管它的顺序。

但是,当 SmartFX 效果检出它的输入参数(params[0])时,将应用先前的效果。

参数检出行为

不管图层的进点和出点(in and out point)是否被修剪过,你都会从源画面的开始到结束得到有效的帧,在此之前和之后都是透明的。

与被检出的合成相比,帧率较低的图层参数只在需要时刷新。

在 30 帧/秒的合成中,10 帧/秒的图层只需要每三帧刷新一次。尽管是静态输入层,如果你的效果想要改变它的输出,你只需要设置PF_Outflag_NON_PARAM_VARY。

当一个效果检出一个连续栅格化的 Adobe Illustrator 图层时,After Effects 在合成大小的缓冲区中渲染 Illustrator 图层并应用几何图形。

参数检出与重入

插件在不同时间检出层,可以生成可重入行为。考虑一个实例,其中 Checkout 示例插件应用于合成 B 中的一个层,而 B 被预合成为合成 A , Checkout 也会应用于合成 A。

当合成 A 被渲染时,将会发送 PF_Cmd_RENDER 到 Checkout[A] ,在此期间内,它将从当前时间以外的时间检出一个层(合成 B )。

为了提供这个检出层,After Effects 会发送 PF_Cmd_RENDER 到 Checkout[B]。

转眼间, 递归!

如果要检出参数,效果必须适当地处理可重入的渲染请求。

不要使用全局变量,也不要读写静态变量……但你不会这么做的,对吧?

迭代期间的进度

After Effects 力求尽可能地响应用户交互,甚至在渲染时。通过适当使用PF_ITERATE() 实现相同的操作。例如,您可能在响应 PF_Cmd_RENDER 期间使用了三次 PF_ITERATE 函数。

在这种情况下,你可以这样开始:

1
2
3
cpp复制代码lines_per_iterateL = in_data->extent_hint.top - in_data->extent_hint.bottom;
total_linesL = 3 * lines_per_iterateL;
lines_so_farL = 0;

在每次迭代之后,将已经完成的代码行添加到当前位置:

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
cpp复制代码suites.iterate8suite()->iterate( lines_so_farL,
total_linesL,
input_worldP,
&output->extent_hint,
refcon,
WhizBangPreProcessFun,
output_worldP);

lines_so_farL += lines_per_iterateL;

ERR(PF_PROGRESS(lines_so_farL, total_linesL));

suites.iterate8suite()->iterate( lines_so_farL,
total_linesL,
input_worldP,
&output->extent_hint,
refcon,
WhizBangRenderFunc,
output_worldP);

lines_so_far += lines_per_iterateL;

ERR(PF_PROGRESS(lines_so_farL, total_linesL));

suites.iterate8suite()->iterate( lines_so_farL,
total_linesL,
input_worldP,
&output->extent_hint,
refcon,
WhizBangPostProcessFunc,
output_worldP);

ERR(PF_PROGRESS(lines_so_farL, total_linesL));

本文转载自: 掘金

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

实验三:Linux用户与用户组管理 实验三:Linux用户与

发表于 2021-11-22

实验三:Linux用户与用户组管理

单个、批量添加(删除用户)。

添加、删除用户组。

添加用户

单个添加

1
shell复制代码useradd 选项 用户名
  • 选项:
    • -c comment 指定一段注释性描述。
    • -d 目录 指定用户主目录,如果此目录不存在,则同时使用-m选项,可以创建主目录。
    • -g 用户组 指定用户所属的用户组。
    • -G 用户组,用户组 指定用户所属的附加组。
    • -s Shell文件 指定用户的登录Shell。
    • -u 用户号 指定用户的用户号,如果同时有-o选项,则可以重复使用其他用户的标识号。
1
shell复制代码useradd –d  /home/user01 -m user01

该命令在home文件夹下创建user01用户

image-20211015211720800

image-20211015211734694

批量添加

Linxu下每个用户都在/etc/passwd文件中有一个对应的记录行,首先需要了解/etc/passwd文件中的信息含义

image-20211015215745115

/etc/passwd中一行记录对应着一个用户,每行记录又被冒号(:)分隔为7个字段:

1
复制代码注册名:口令:用户标识号:组标识号:用户名:主目录:登录shell

注册名

代表用户账号的字符串

口令

口令的加密字符

用户标识号

一个整数,系统内部用来标识用户

组标识号

记录的是用户所属的组

用户名

包含有关用户的一些信息,如用户的真实姓名、办公室地址、联系电话等。在Linux系统中,mail和finger等程序利用这些信息来标识系统的用户。

主目录

该字段定义了个人用户的主目录,当用户登录后,他的Shell将把该目录作为用户的工作目录。 在Unix/Linux系统中,超级用户root的工作目录为/root;而其它个人用户在/home目录下均有自己独立的工作环境,系统在该目录下为每个用户配置了自己的主目录。个人用户的文件都放置在各自的主目录下,对自己的主目录有读、写、执行(搜索)权限。

登录shell

动一个进程,负责将用户的操作传给内核,这个进程是用户登录到系统后运行的命令解释器或某个特定的程序。命令解释程序(Shell):Shell是当用户登录系统时运行的程序名称,通常是一个Shell程序的全路径名。

要批量添加,首先创建txt文件,每一行按照/etc/passwd密码文件的格式书写,每个用户的用户名、UID、宿主目录都不可以相同,其中密码栏可以留做空白或输入x号

1
2
3
ruby复制代码user01::901:100:user01:/home/user01:/bin/bash
user02::902:100:user02:/home/user02:/bin/bash
user03::903:100:user03:/home/user03:/bin/bash

导入数据,创建用户

1
复制代码newusers < user.txt

image-20211015221544235

image-20211015221513403

image-20211015222118751

删除用户

单个删除

1
复制代码userdel 选项 用户名
1
shell复制代码userdel -r user01

该命令删除用户user01

image-20211015212607868

image-20211015212617886

批量删除

使用vim创建脚本删除:

1
2
3
4
5
bash复制代码for user in `cat user.txt`
do
userdel -r $user
echo "$user del successfully"
done

image-20211015233852866

image-20211015234802167

添加用户组

1
复制代码groupadd 选项 用户组

可以使用的选项有:

  • -g GID 指定新用户组的组标识号(GID)。
  • -o 一般与-g选项同时使用,表示新用户组的GID可以与系统已有用户组的GID相同。
1
复制代码groupadd -g 10086 group1

添加一个组织编号为10086的用户组group1

image-20211015213356061

删除用户组

1
复制代码groupdel 用户组
1
复制代码groupdel group1

删除用户组group1

image-20211015213553584

image-20211015213630425

本文转载自: 掘金

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

数据结构与算法(二)算法

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战
算法与数据结构是相辅相成的。

算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

举那个最经典的例子:高斯求和, 求1加到100的数字和。

我这里使用PHP来举例:

1
2
3
4
5
6
7
php复制代码<?php
    $sum = 0;
    for($i = 0;$i<=100;$i++)
    {
        $sum += $i;
    }
    echo $sum;die;

上边的例子,我们是实现了0到100的数求和,但是,这个方法循环了100次,在效率上可能是有些不足。

那么高斯帮我们找到了其中的规律。下面这个例子我们使用高斯求和来写:

1
2
3
4
5
php复制代码<?php
    $sum = 0;
    $n = 100;
    $sum = (1 + $n) * $n / 2;
    echo $sum;die;

高斯求和相对于我们使用循环来求和,效率大大提升。

所以算法,现阶段我认为就是数学中的找规律,当然并不是说找不到规律这个问题就解决不了,可能我描述的不是很准确。

算法的特性:

1:输入输出

算法可以有0个或多个输入,至少有一个或多个输出。


2:有穷性

指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成

3:确定性

算法的每一个步骤都具有确定的含义,不会出现二义性。

4:可行性:

算法的每一个步骤都必须是可行的,也就是说,每一个步骤都能够通过执行有限次数完成。

算法设计的要求

1:正确性

算法的正确性是指算法至少应该具有输入输出和加工处理无歧义性、能正确反映问题的需求,能够得到问题的正确答案。

(1):无语法错误

(2):能得到正确的返回值

(3):对异常有处理

2:可读性

算法设计的另一目的是为了便于阅读,理解和交流。可能写出来的代码要保证大多数人能看懂。

3:健壮性

当输入不合法的数据是,算法也能做出相关的处理,而不是直接报错。(异常处理)

4:时间效率高和存储量低

设计算法应该尽量满足时间效率高和存储量低的需求。

比如上边的0到100求和,在时间效率上高斯求和的效率要比循环累加要高得多。

算法函数的渐进增长

通俗点说就是对算法进行大量数据的测试,随着测试数据量的增大,两种算法之间效率的差异就会越来越大,比如上边说到的高斯求和,如果求0到1亿的数字和,那么高斯求和的效率就会比循环不知道高出多少倍。

关于算法时间复杂度的计算这类相关的算法,这个暂时不涉及,我觉得现阶段我可能是用不上,用的时候再说。

以上就是算法的大概内容,有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

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

深入理解C语言指针

发表于 2021-11-22

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

要知道指针的概念,要先了解变量在内存中如何存储的。在存储时,内存被分为一块一块的。每一块都有一个特有的编号。而这个编号可以暂时理解为指针,就像酒店的门牌号一样。

1.1、变量和地址

先写一段简单的代码:

1
2
3
csharp复制代码void main(){
int x = 10, int y = 20;
}

这段代码非常简单,就是两个变量的声明,分别赋值了 10、20。我们把内存当做一个酒店,而每个房间就是一块内存。那么“int x = 10;”和“int y = 20;”的实际含义如下:

  1. 去酒店订了两个房间,门牌号暂时用 px、py 表示
  2. 让 10 住进 px,让 20 住进 py
  3. 其中门牌号就是 px、py 就是变量的地址
  4. x 和 y 在这里可以理解为具体的房间,房间 x 的门牌号(地址)是 px,房间 y 的门牌号(地址)是 py。而 10 和 20,通过 px、py 两个门牌,找到房间,住进 x、y。用户(变量的值)和房间(变量)以及房间号(指针、地址)的关系

1.2、指针变量和指针的类型

指针变量就是一个变量,它存储的内容是一个指针。如果用前面的例子,可以理解为指针变量就是一张房卡,房卡存储了房间号的信息。

在我们定义一个变量的时候,要确定它的类型。int x、char ch、float、、、在定义指针变量时也是一样的,必须确定指针类型。int 变量的指针需要用 int 类型的指针存储,float 变量的指针需要用 float 类型的指针存储。就像你只能用酒店 A 的房卡存储酒店 A 中房间号的信息一样。

二、变量的指针与指针变量

变量的指针就是变量的存储地址,指针变量就是存储指针的变量。

2.1、指针变量的定义及使用

(1)指针变量的定义

指针变量的定义形式如:数据类型 *指针名;例如:

1
2
3
4
sql复制代码//分别定义了 int、float、char 类型的指针变量
int *x;
float *f;
char *ch;

如上面的定义,指针变量名为 x、f、ch。并不是*x、*f、*ch

(2)指针变量的使用

  • 取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。
  • 指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。

代码示例:

1
2
3
4
5
6
7
8
arduino复制代码//声明了一个普通变量 a
int a;
//声明一个指针变量,指向变量 a 的地址
int *pa;
//通过取地址符&,获取 a 的地址,赋值给指针变量
pa = &a;
//通过间接寻址符,获取指针指向的内容
printf("%d", *pa);

(3)“&”和“*”的结合方向

“&”和“*”都是右结合的。假设有变量 x = 10,则*&x 的含义是,先获取变量 x 的地址,再获取地址中的内容。因为“&”和“*”互为逆运算,所以 x = *&x。

接下来做个小练习,输入 x、y 两个整数,然后将其中的值大的赋值给 x,小的赋值给 y。即:假设输入 x = 8,y = 9。就将 9 赋值给 x,8 赋值给 y。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码void main(){
//声明两个普通变量
int x, y;
//声明两个指针变量
int *px, *py;
//声明一个临时变量,用于交换
int t;
//输入两个值,赋值给 x、y
scanf("%d", &x);
scanf("%d", &y);
//给指针变量 px、py 赋初值(关联变量 x、y)
px = &x;
py = &y;
//利用指针来对比 x、y 的值,如果 x 的值比 y 的值小,就交换
if(*px < *py){
//交换步骤,其中*px == x、*py == y
t = *px;
*px = *py;
*py = t;
}
printf("x = %d, y = %d", *px, *py);
}
1
2
ini复制代码输入:23 45
输出结果为:x = 45, y = 23

2.2、指针变量的初始化

指针变量与其它变量一样,在定义时可以赋值,即初始化。也可以赋值“NULL”或“0”,如果赋值“0”,此时的“0”含义并不是数字“0”,而是 NULL 的字符码值。

1
2
3
4
5
ini复制代码//利用取地址获取 x 的地址,在指针变量 px 定义时,赋值给 px
int x;
int *px = &x;
//定义指针变量,分别赋值“NULL”和“0”
int *p1= NULL, *p2 = 0;

2.3、指针运算

(1)赋值运算

指针变量可以互相赋值,也可以赋值某个变量的地址,或者赋值一个具体的地址

1
2
3
4
5
6
7
ini复制代码int *px, *py, *pz, x = 10;
//赋予某个变量的地址
px = &x;
//相互赋值
py = px;
//赋值具体的地址
pz = 4000;

(2)指针与整数的加减运算

  1. 指针变量的自增自减运算。指针加 1 或减 1 运算,表示指针向前或向后移动一个单元(不同类型的指针,单元长度不同)。这个在数组中非常常用。
  2. 指针变量加上或减去一个整形数。和第一条类似,具体加几就是向前移动几个单元,减几就是向后移动几个单元。
1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码//定义三个变量,假设它们地址为连续的,分别为 4000、4004、4008
int x, y, z;

//定义一个指针,指向 x
int *px = &x;

//利用指针变量 px 加减整数,分别输出 x、y、z
printf("x = %d", *px); //因为 px 指向 x,所以*px = x

//px + 1,表示,向前移动一个单元(从 4000 到 4004)
//这里要先(px + 1),再*(px + 1)获取内容,因为单目运算符“*”优先级高于双目运算符“+”
printf("y = %d", *(px + 1));
printf("z = %d", *(px + 2));

(3)关系运算

假设有指针变量 px、py。

  1. px > py 表示 px 指向的存储地址是否大于 py 指向的地址
  2. px == py 表示 px 和 py 是否指向同一个存储单元
  3. px == 0 和 px != 0 表示 px 是否为空指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码//定义一个数组,数组中相邻元素地址间隔一个单元
int num[2] = {1, 3};

//将数组中第一个元素地址和第二个元素的地址赋值给 px、py
int *px = &num[0], *py = &num[1];
int *pz = &num[0];
int *pn;

//则 py > px
if(py > px){
printf("py 指向的存储地址大于 px 所指向的存储地址");
}

//pz 和 px 都指向 num[0]
if(pz == px){
printf("px 和 pz 指向同一个地址");
}

//pn 没有初始化
if(pn == NULL || pn == 0){
printf("pn 是一个空指针");
}

三、指针与数组

之前我们可以通过下标访问数组元素,学习了指针之后,我们可以通过指针访问数组的元素。在数组中,数组名即为该数组的首地址,结合上面指针和整数的加减,我们就可以实现指针访问数组元素。

3.1、指向数组的指针

如以下语句:

1
css复制代码int nums[10], *p;

上面语句定义了一个数组 nums,在定义时分配了 10 个连续的int 内存空间。而一个数组的首地址即为数组名nums,或者第一个元素的首地址也是数组的首地址。那么有两种方式让指针变量 p 指向数组 nums:

1
2
3
4
ini复制代码//数组名即为数组的首地址
p = nums;
//数组第一个元素的地址也是数组的首地址
p = &nums[0];

上面两句是等价的。
如下几个操作,用指针操作数组:

  1. *p = 1,此操作为赋值操作,即将指针指向的存储空间赋值为 1。此时 p 指向数组 nums 的第一个元素,则此操作将 nums 第一个元素赋值为 0,即 nums[0] = 1。
  2. p + 1,此操作为指针加整数操作,即向前移动一个单元。此时 p + 1 指向 nums[0]的下一个元素,即 nums[1]。通过p + 整数可以移动到想要操作的元素(此整数可以为负数)。
  3. 如上面,p(p + 0)指向 nums[0]、p + 1 指向 nums[1]、、、类推可得,p+i 指向 nums[i],由此可以准确操作指定位置的元素。
  4. 在 p + 整数的操作要考虑边界的问题,如一个数组长度为 2,p+3 的意义对于数组操作来说没有意义。

下面写一段代码,用指针访问数组的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
css复制代码//定义一个整形数组,并初始化
int nums[5] = {4, 5, 3, 2, 7};

//定义一个指针变量 p,将数组 nums 的首地址赋值给 p,也可以用p = &nums[0]赋值
int *p = nums, i; //i 作为循环变量

//p 指向数组第一个元素(数组首地址),我们可以直接用间接寻址符,获取第一个元素的内容
printf("nums[0] = %d\n", *p); //输出结果为 nums[0] = 4

//我们可以通过“p + 整数”来移动指针,要先移动地址,所以 p + 1 要扩起来
printf("nums[1] = %d\n", *(p + 1)); //输出结果为 nums[1] = 5

//由上面推导出*(p + i) = nums[i],所以我们可以通过 for 循环变量元素
for(i = 0; i < 5; i++){
printf("nums[%d] = %d", i, *(p + i));
}

注:数组名不等价于指针变量,指针变量可以进行 p++和&操作,而这些操作对于数组名是非法的。数组名在编译时是确定的,在程序运行期间算一个常量。

3.2、字符指针与字符数组

在 C 语言中本身没有提供字符串数据类型,但是可以通过字符数组和字符指针的方式存储字符串。

(1)字符数组方式

这个在前面应该学习过,这里就不赘述了。

1
2
arduino复制代码char word[] = "zack";
printf("%s", word);

(2)字符指针方式

指针方式操作字符串和数组操作字符串类似,可以把定义的指针看做是字符数组的数组名。在内存中存储大致如下,这里为了方便换了个字符串:在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码//除了定义一个字符数组外,还可以直接定义一个字符指针存储字符串
char *sentence = "Do not go gentle into that good night!";

//此时可以做字符串的操作
//输出
printf("%s", sentence);

//通过下标取字符
printf("%c", sentence[0]);

//获取字符串长度,其中 strlen 是 string.h 库中的方法
printf("%d", strlen(sentence));

注:字符指针方式区别于字符数组方式,字符数组不能通过数组名自增操作,但是字符指针是指针,可以自增操作。自增自减少会实现什么效果大家可以自己尝试运行一下

下面做个小练习,利用字符指针将字符数组 sentence 中的内容复制到字符数组 word 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码//定义字符数组 sentence 和 word,给 sentence 赋初值
char sentence[] = "Do not go gentle into that good night!", word[100];

//定义字符指针,指向 word
char *ch = word;
int i;

//循环赋值
for(i = 0; sentence[i] != '\0'; i++){
*(ch + i) = sentence[i];
}

//在当 i 等于 sentence 的长度(sentence 的长度不包含'\0')时,
//i 继续自增,此时判断 sentence[0] != '\0'不符合,跳出循环,则 i 比 sentence 长度大 1
*(ch + i) = '\0';

//输出字符串,因为 ch 指向 word,所以输出结果是一样的
printf("ch = %s, word = %s", ch, word);

注:指针变量必须初始化一个有效值才能使用

3.3、多级指针及指针数组

(1)多级指针

指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。我们先看看二级指针,它们关系如下:指针变量 p 指向变量 x,二级指针变量指向指针变量 p
其中 p 为一级指针,pp 为二级指针。二级指针定义形式如下:

1
ini复制代码数据类型 **二级指针名;

和指针变量的定义类似,由于*是右结合的,所以*pp 相当于*(*p)。在本次定义中,二级指针的变量名为 pp,而不是**p。多级指针的定义就是定义时使用多个“*”号。下面用一个小程序给大家举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码//定义普通变量和指针变量
int *pi, i = 10;
//定义二级指针变量
int **ppi;

//给指针变量赋初值
pi = &i;

//给二级指针变量赋初值
ppi = &pi;

//我们可以直接用二级指针做普通指针的操作
//获取 i 的内容
printf("i = %d", **ppi);
//获取 i 的地址
printf("i 的地址为%d", *ppi);

注:在初始化二级指针 ppi 时,不能直接 ppi = &&i,因为&i 获取的是一个具体的数值,而具体数字是没有指针的。

(2)指针数组

指针变量和普通变量一样,也能组成数组,指针数组的具体定义如下:

1
ini复制代码数据类型 *数组名[指针数组长度];

下面举一个简单的例子熟悉指针数组:

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
css复制代码//定义一个数组
int nums[5] = {2, 3, 4, 5, 2}, i;

//定义一个指针数组
int *p[5];

//定义一个二级指针
int **pp;

//循环给指针数组赋值
for(i = 0; i < 5; i++){
p[i] = &nums[i];
}

//将指针数组的首地址赋值给 pp,数组 p 的数组名作为 p 的首地址,也作为 p 中第一个元素的地址。
//数组存放的内容为普通变量,则数组名为变量的指针;数组存放的内容为指针,则数组名为指针的指针。
pp = p;

//利用二级指针 pp 输出数组元素
for(i = 0; i < 5; i++){
//pp == &p[0] == &&nums[0],nums[0] == *p[0] == **pp
printf("%d", **pp);

//指针变量+整数的操作,即移动指针至下一个单元
pp++;
}

3.4、指针与多维数组

讲多维数组是个麻烦的事,因为多维数组和二维数组没有本质的区别,但是复杂度倒是高了许多。这里我主要还是用二维数组来举例,但是还是会给大家分析多维数组和指针的关系。

(1)多维数组的地址

先用一个简单的数组来举例:

1
2
3
4
ini复制代码int nums[2][2] = {
{1, 2},
{2, 3}
};

我们可以从两个维度来分析:

  1. 先是第一个维度,将数组当成一种数据类型 x,那么二维数组就可以当成一个元素为 x 的一维数组。
  2. 如上面的例子,将数组看成数据类型 x,那么 nums 就有两个元素。nums[0]和 nums[1]。
  3. 我们取 nums[0]分析。将 nums[0]看做一个整体,作为一个名称可以用 x1 替换。则 x1[0]就是 nums[0][0],其值为 1。
    在这里插入图片描述

我们知道数组名即为数组首地址,上面的二维数组有两个维度。首先我们把按照上面 1 来理解,那么 nums 就是一个数组,则nums 就作为这个数组的首地址。第二个维度还是取 nums[0],我们把 nums[0]作为一个名称,其中有两个元素。我们可以尝试以下语句:

1
perl复制代码printf("%d", nums[0]);

此语句的输出结果为一个指针,在实验过后,发现就是 nums[0][0]的地址。即数组第一个元素的地址。

如果再多一个维度,我们可以把二维数组看做一种数据类型 y,而三维数组就是一个变量为 y 的一维数组。而数组的地址我们要先确定是在哪个维度,再将数组某些维度看成一个整体,作为名称,此名称就是该维度的地址(这里有些绕)。

例:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码//假设已初始化,二维数组数据类型设为 x,一维数组数据类型设为 y
int nums[2][2][2];

//此数组首地址为该数组名称
printf("此数组首地址为%d", nums);

//此数组可以看做存储了两个 x 类型元素的一维数组,则 nums[0] = x1 的地址为
printf("第二个维度的首地址为%d", nums[0]);

//而 x1 可以看做存储了两个 y 类型元素的一维数组,则 y1 = x1[0] = nums[0][0]
printf("第三个维度的首地址为%d", nums[0][0]);

三维数组实际存储形式如下:
在这里插入图片描述
实际存储内容的为最内层维度,且为连续的。对于 a 来说,其个跨度为 4 个单元;对 a[0]来说,其跨度为 2 个单元;对 a[0][0]来说,跨度为一个单元。有上面还可以得出:

1
css复制代码a == a[0] == a[0][0] == &a[0][0][0];

上面的等式只是数值上相等,性质不同。

(2)多维数组的指针

在学习指针与数组的时候,我们可以如下表示一个数组:

1
2
ini复制代码int nums[5] = {2, 4, 5, 6, 7};
int *p = nums;

在前面讲指针数组时,所有指针数组元素都指向一个数字,那么我们现在可以尝试用指针数组的每个元素指向一个数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
css复制代码//定义一个二维数组
int nums[2][2] = {
{1, 2},
{2, 3}
};

//此时 nums[0]、和 nums[1]各为一个数组
int *p[2] = {nums[0], nums[1]};

//我们可以用指针数组 p 操作一个二维数组

//p 为数组 p 的首地址,p[0] = nums[0] = *p,**p = nums[0][0]
printf("nums[0][0] = %d", **p);

//指针 + 整数形式,p+1 移动到 nums 的地址,*(p +1) = nums[1],则**(p + 1) = nums[1][0]
printf("nums[1][0] = %d", **(p + 1));

//先*p = nums[0],再*p + 1 = &nums[0][1],最后获取内容*(*p + 1)即为 nums[0][1]
printf("nums[0][1] = %d", *(*p + 1));

这里可能不能理解为什么*p + 1 = &nums[0][1],而不是 nums[1]。*p 获得的是一个一维数组,而 int 数组 + 1 的跨度只有 4 个字节,也就是一个单元。前面 p 是一维数组的指针,其跨度为一个数组。所以*p + 1 = &nums[0][1],而 p + 1 = nums[1]。

四、指针与函数

前面学习函数学到,函数参数可以为 int、char、float 等,但是在操作时,这些参数只作为形参,所有操作都只在函数体内有效(除对指针的操作外),那么今天来学习一下指针作为函数参数。

4.1、函数参数为指针

我们直接做一个练习,定义一个函数,用来交换两个变量的内容。

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码void swap(int *x, int *y);
void main(){
int x = 20, y = 10;
swap(&x, &y);
printf("x = %d, y = %d", x ,y);
}
void swap(int *x, int *y){
int t;
t = *x;
*x = *y;
*y = t;
}

代码非常简单,我也就不细讲了。这里传入的参数为指针,所以调用 swap 方法后 x,y 的内容发生了交换。如果直接传入 x,y,那么交换只在 swap 中有效,在 main 中并没有交换。

4.2、函数的返回值为指针

返回值为指针的函数声明如下:

1
2
3
4
5
6
7
8
9
arduino复制代码数据类型 *函数名(参数列表){
函数体
}
//例如:
int s;
int *sum(int x, int y){
s = x + y;
return &s;
}

在函数调用前要声明需要对函数声明(有点编译器不需要)

1
2
3
4
5
6
7
8
9
arduino复制代码int s;
void mian(){
int *r = sum(10, 9);
printf("10 + 9 + %d", *r);
}
int *sum(int x, int y){
s = x + y;
return &s;
}

除了上面的操作,更实用的是返回一个指向数组的指针,这样就实现了返回值为数组。

4.3、指向函数的指针

C 语言中,函数不能嵌套定义,也不能将函数作为参数传递。但是函数有个特性,即函数名为该函数的入口地址。我们可以定义一个指针指向该地址,将指针作为参数传递。

函数指针定义如下:

1
scss复制代码数据类型 (*函数指针名)();

函数指针在进行“*”操作时,可以理解为执行该函数。函数指针不同与数据指针,不能进行+整数操作。

下面举个例子,来使用函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include <string.h>
/**
* 定义一个方法,传入两个字符串和一个函数指针 p,用 p 对两个字符串进行操作
*/
void check(char *x, char *y, int (*p)());
void main(){
//string.h 库中的函数,使用之前需要声明该函数。字符串比较函数
int strcmp();
char x[] = "Zack";
char y[] = "Rudy";

//定义一个函数指针
int (*p)() = strcmp;

check(x, y, p);
}
void check(char *x, char *y, int (*p)()){
if(!(*p)(x, y)){
printf("相等");
}else{
printf("不相等");
}
}

利用函数指针调用方法具体操作如下:

1
scss复制代码(*p)(x, y);

指针除了这些地方,还在结构体中用处巨大。今天就先讲到这里~·

本文转载自: 掘金

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

1…239240241…956

开发者博客

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