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

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


  • 首页

  • 归档

  • 搜索

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

发表于 2021-10-25

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

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

这篇主要讲Nginx中间件优化

Nginx主要从负载均衡,限流,限制单个IP的并发数等方面来讲解

一、Nginx负载均衡

负载均衡,用白话讲,就是一个机器分身成几个机器,有一样的效果,帮你分担活。
以前的文章讲了负载均衡SpringBoot项目在Linux部署用Nginx实现负载均衡Https访问,这篇当作复习,大概讲解一下。

1、假如把java项目打包成3个,3个端口不一

2、nginx配置信息,主要配置ip+端口号

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
java复制代码upstream my_blog { #upstream节点名称,my_blog给后面负载均衡使用
server 193.112.29.147:14341;# ip+端口号
server 193.112.29.147:14342;
server 193.112.29.147:14343;
}
server {
listen 443;# 新增443端口
server_name xuluowuhen.com www.xuluowuhen.com;# 域名
# 开启ssl
ssl on;
# 配置ssl证书(文件在相同目录下,也可以不同目录,路径要写绝对路径)
ssl_certificate 1_www.xuluowuhen.com_bundle.crt;
# 配置证书秘钥
ssl_certificate_key 2_www.xuluowuhen.com.key;
# ssl会话cache
ssl_session_cache shared:SSL:1m;
# ssl会话超时时间
ssl_session_timeout 5m;
# 配置加密套件,写法遵循 openssl 标准
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
location / {
proxy_ssl_server_name on;
proxy_pass http://my_blog; # 跟upstream节点名称节点对应
index index.html index.htm;
}
error_page 404 /404.html;

error_page 403 /403.html;
}

3、检验并修改好配置信息,启动Nginx

1
2
3
4
5
6
7
8
java复制代码进入配置文件目录
cd /usr/local/nginx/conf/
检测配置是否ok
../sbin/nginx -t
重新加载
../sbin/nginx -s reload
查看nginx运行状态
ps -A | grep nginx

总的思路是这些,具体可以看以前写的博客

二、限流,禁封IP

ip黑名单,如果发现某个ip持续不断的访问,甚至他的访问造成你的服务器压力很大,CPU持续增加,那么你可以拉近你的黑名单里。

1
2
java复制代码# 在http模块添加以下配置即可
deny 192.168.32.14

甚至可以限制用户访问的次数,并发数,等等,如果别人用Jmeter做压测,那么你的服务器很大程度扛不住。

具体配置如下

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
java复制代码#定义一个名为allips的limit_req_zone用来存储session,大小是10M内存,
#以$binary_remote_addr 为key,限制平均每秒的请求为5个,
#1M能存储16000个状态,rete的值必须为整数,
#如果限制两秒钟一个请求,可以设置成30r/m
limit_req_zone $binary_remote_addr zone=allips:10m rate=5r/s;
server {
# listen 443;
listen 441;
server_name xuluowuhen.com www.xuluowuhen.com;
# 开启ssl
ssl on;
# 配置ssl证书
ssl_certificate 1_www.xuluowuhen.com_bundle.crt;
# 配置证书秘钥
ssl_certificate_key 2_www.xuluowuhen.com.key;
# ssl会话cache
ssl_session_cache shared:SSL:1m;
# ssl会话超时时间
ssl_session_timeout 5m;
# 配置加密套件,写法遵循 openssl 标准
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
location / {
proxy_ssl_server_name on;
proxy_pass http://my_blog;
index index.html index.htm;

#限制每ip每秒不超过20个请求,漏桶数burst为5
#brust的意思就是,如果第1秒、2,3,4秒请求为19个,
#第5秒的请求为25个是被允许的。
#但是如果你第1秒就25个请求,第2秒超过20的请求返回503错误。
#nodelay,如果不设置该选项,严格使用平均速率限制请求数,
#第1秒25个请求时,5个请求放到第2秒执行,
#设置nodelay,25个请求将在第1秒执行。

limit_req zone=allips burst=5 nodelay;
}
error_page 404 /404.html;

error_page 403 /403.html;
}

本文转载自: 掘金

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

Python yaml模块

发表于 2021-10-25

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

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

复习回顾

关于对数据进行序列化和反序列化操作,Python不仅提供内置库还有支持第三方库

Python 内置库

  • 使用二进制协议对数据进行序列和反序列化操作-pickle模块
  • 对数据进行转换成JSON格式和反序列化操作-json模块
  • 使用类似字典键值对数据进行序列和反序列化操作-shelve模块
  • 对命令窗口的命令及选项进行解析操作-argparse模块

Python 第三方库

  • 第三方库,是json模块的升级版对无用字符进行优化-messagepack模块
  • Yaml配置文件数据的序列与反序列操作-yaml模块

序列化模块

本期,我们继续学习行业里流行的yaml模块相关方法的学习和使用,Let’s go~

  1. yaml 简介

yaml 是一种专门专门用于配置文件或者存储数据的语言,其文件以.yaml结尾

yaml 特点

  1. yaml 语言具有易读性易写性
  2. yaml 可用于描述型的数据结构如脚本
  3. yaml 适用在不同程序间的数据交换
  4. yaml 可扩展性强,不受平台和语言限制

yaml安装方法:

1
python复制代码pip install pyyaml

yaml模块导入:

1
python复制代码import yaml
  1. yaml 常用语法

yaml 文档由一个或多个文档组成,文档间区分如下

  • 每个文档开头使用:---。如果是单个文档,可省略
  • 每个文档结束使用:...。可选项

yaml 支持的数据结构有三种

  • 纯量:字符串、布尔值、整数、Null、时间、日期
  • 数组:list
  • 键值表:字典

yaml 使用 & 和 * 引用

1
2
yaml复制代码name: &name Juejin
user: *name

yaml 使用 !!进行强制转换类型

1
2
yaml复制代码String: !!str 3.14
Int: !!int "1234"

PS:更多关于yaml语法的yaml官方文档

  1. yaml 常用方法

yaml 与其他序列化模块json、pickle一样,同样具有dump、load方法

方法 说明
yaml.dump(obj,fp) 将Python对象转换并输入到yaml文档
yaml.dump_all(obj,fp) 将多个Python对象输入到yaml文档
yaml.load() 将yaml文件转换成Python对象
yaml.load_all() 将多段yaml语句转换成Python对象
  1. yaml Vs json

yaml和json都是数据序列化语言.

  • yamls不是标记语言,强调以数据作为重心
  • Json 是js对象标记,一种轻量级的数据交换格式
1
2
3
4
5
6
7
8
9
10
json复制代码{
"juejiner one": {
"name": "Tom",
"age": 21
},
"juejiner two": {
"name": "Tony",
"age": 12
}
}

以上是JSON格式的,我们来看一下yaml转换效果

1
2
3
4
5
6
7
8
yaml复制代码# yaml
---
juejiner one:
name: Tom
age: 21
juejiner two:
name: Tony
age: 12

yaml 和 Json 使用场景划分

  • json 比较适合网络请求返回的数据
  • yaml 比较适合数据存放读取,常用于在自动化测试导入数据中

PS:我们在进行调试时可以,可以使用在线转换工具

  1. 小试牛刀

我们来对前面的yaml数据,使用python进行解析操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码import yaml

Juer = {
"juejiner one": {
"name": "Tom",
"age": 21
},
"juejiner two": {
"name": "Tony",
"age": 12
}
}

# 将Python 对象转换成 yaml
yaml_text = yaml.dump(Juer)
print(yaml_text)

print("------------")

# 将yaml对象转换成Python对象

py = yaml.load(yaml_text,Loader=yaml.FullLoader)

print(py)

总结

本期,我们学习 yaml 模块方法简单使用

以上是本期内容,欢迎大佬们点赞评论指正,下次见~ღ( ´・ᴗ・` )比心🌹🌹🌹🌹🌹✈️

本文转载自: 掘金

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

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

发表于 2021-10-25

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

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

这篇主要讲数据库优化

数据库优化主要从两方面讲,索引优化,减少数据库的交互这两方面讲
索引优化

一、索引优化

article表的字段有这些,这个表存的文章的相关信息,包括主键id,文章id,作者,标题,文本内容,标签,发布时间,更新时间,上一篇文章id,下一篇文章id等等
在这里插入图片描述
当初设计表的时候,只有id一个主键,根据id查找该篇文章的信息。后期根据这篇文章的id,找到上一篇文章和下一篇文章的信息。这里经常会用到文章的id,所以id也要加个索引,加快查询的速度,当文章的数量越来越多时,索引的优点就会更加突显出来。
在这里插入图片描述

二、减少数据库交互

根据文章id查询一篇文章,查询出来,会把该文章的上一篇文章id(lastArticleId),查询上一篇文章信息来,会把该文章的下一篇文章id(nextArticleId),查询下一篇文章信息来。

service层

1
2
3
4
5
java复制代码Article realArticle = articleMapper.findArticleByArticleId(article.getId());
if(realArticle != null) {
Article lastArticle = articleMapper.findArticleByArticleId(realArticle.getLastArticleId());
Article nextArticle = articleMapper.findArticleByArticleId(realArticle.getNextArticleId());
}

mapper层

1
2
java复制代码@Select("select articleId,articleTitle from article where articleId=#{articleId}")
Article findArticleByArticleId(@Param("articleId") long articleId);

很明显,查询一篇文章信息,就要涉及到数据库的三次交互,减少数据库的交互,是为了减轻数据库的压力,为此,要优化sql语句,根据一篇文章id,除了查出该篇文章信息,还要查询出上一篇文章和下一篇文章信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public JSONObject getArticleByArticleId(long articleId, String username) {
// 通过文章id,拿到当前文章,上一篇文章,下一篇文章,减少与数据库的交互
List<Article> articleObject = articleMapper.getCurrentLastNextArticleByArticleId(articleId);
if (CollectionUtils.isEmpty(articleObject)) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("status", "500");
jsonObject.put("errorInfo", "获取文章信息失败");
logger.error("获取文章id " + articleId + " 失败");
return jsonObject;
}
Article currentArticle = new Article();
Article lastArticle = new Article();
Article nextArticle = new Article();
genArticle(articleObject, currentArticle, lastArticle, nextArticle);
return genJsonObject(articleId, username, currentArticle, lastArticle, nextArticle);
}

这是优化的sql语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Select("SELECT 1 AS category,a1.id,a1.articleId,a1.author,a1.originalAuthor,a1.articleTitle,a1.articleContent,  " +
"a1.articleTags,a1.articleType,a1.articleCategories,a1.publishDate,a1.updateDate,a1.articleUrl, " +
"a1.articleTabloid,a1.likes,a1.lastArticleId,a1.nextArticleId " +
"FROM article a1 WHERE a1.articleId=#{articleId} " +
"UNION SELECT 2 AS category,a2.id,a2.articleId,a2.author,a2.originalAuthor,a2.articleTitle,a2.articleContent, " +
"a2.articleTags,a2.articleType,a2.articleCategories,a2.publishDate,a2.updateDate,a2.articleUrl, " +
"a2.articleTabloid,a2.likes,a2.lastArticleId,a2.nextArticleId " +
"FROM article a2 " +
" LEFT JOIN article a3 ON a2.articleId = a3.lastArticleId " +
" WHERE a3.articleId=#{articleId} " +
" UNION SELECT 3 AS category,a4.id,a4.articleId,a4.author,a4.originalAuthor,a4.articleTitle,a4.articleContent, " +
"a4.articleTags,a4.articleType,a4.articleCategories,a4.publishDate,a4.updateDate,a4.articleUrl, " +
"a4.articleTabloid,a4.likes,a4.lastArticleId,a4.nextArticleId " +
"FROM article a4 " +
" LEFT JOIN article a5 ON a4.articleId = a5.nextArticleId " +
" WHERE a5.articleId=#{articleId}")
List<Article> getCurrentLastNextArticleByArticleId(@Param("articleId") long articleId);

相比之前的sql语句,长了很多,但是减少了跟数据库的交互,一篇文章与数据库交互3次,当数目越多,数据库压力越大。

总结:数据库优化方面,从索引,从sql语句优化,减少与数据库的交互

本文转载自: 掘金

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

从零开始学设计模式(三):原型模式(Prototype Pa

发表于 2021-10-25

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

作者的其他平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概8287字,读完共需15分钟

1 前言

前面的一篇文章从零开始学设计模式(二):单例模式介绍了什么是单例模式以及单例模式的几种常见的实现方式。今天这篇文章接着介绍设计模式中的原型模式Prototype。

2 原型模式Prototype Pattern

1、什么是原型模式

原型模式(Prototype Pattern)是 Java 中最简单的设计模式之一,属于创建型模式。原型模式使用原型实例指定创建对象的种类,并且通过拷贝原型对象创建新的对象。原型模式实际上就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。这就好比模具的使用,我们可以通过螺丝的模具(原型实例)创建一个个具体的螺丝(新的对象),而不需要知道螺丝具体的创建过程。下图就很好的表现了原型模式使用的过程,图源自网络,侵删:

2、原型模式的优点

a、使用原型模型创建一个对象比直接new一个对象更有效率,因为new产生一个对象需要非常繁琐的数据准备或访问权限,原型模式则直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

b、原型模式隐藏了制造新实例的复杂性,使得创建对象就像复制粘贴一样简单,所以效率高。

3、原型模式的缺点

a、由于使用原型模式复制对象时不会调用类的构造方法,所以原型模式无法和单例模式组合使用,因为原型类需要将clone方法的作用域修改为public类型,那么单例模式的条件就无法满足了。

b、使用原型模式时不能有final对象。

c、Object类的clone方法只会拷贝对象中的基本数据类型,对于数组,引用对象等只能另行拷贝。这里涉及到深拷贝和浅拷贝的概念。

3 原型模式实现方式

前面说到原型模式的优点就是原型模式隐藏了制造新实例的复杂性,使得创建对象就像复制粘贴一样简单,所以效率高。 所以原型模式中实现起来最困难的地方就是内存复制操作,但是Java中就提供了clone()方法替我们做了绝大部分事情,不需要我们自己操作。所以原型模式的实现方式是:实现Cloneable接口和Object类中的clone方法:

1、实现Cloneable接口

克隆类似于new,但是又不同于new。new创建新的对象属性采用的是默认值;而克隆出的对象的属性值完全和原型对象相同,然后,再修改克隆对象的值,并且克隆出的新对象改变不会影响原型对象。

Cloneable接口的作用是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。

2、重写Object类中的clone方法

Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,原型类需要将clone方法的作用域修改为public类型。

栗子:

作为一个程序员需要女朋友,我们可以随时给自己new一个prefect的对象,我们知道作为女朋友,那么性别肯定是女的(当然也可能不是),但是如果换了女朋友之后姓名、年龄、身高、体重等,这些数据一般来说是不一样的,那么我们可以通过原型模式来给自己创建对象了:

定义一个女朋友类:

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
typescript复制代码package com.jiangxia.Prototype;

/**
* @Author: 江夏
* @Date: 2021/10/24/9:17
* @Description:
*/
public class PrototypeDemo1 implements Cloneable{
//性别
private String gender;
//年龄
private int age;
//姓名
private String name;
//体重
private int weight;
//身高
private int height;
public PrototypeDemo1(String gender){
this.gender = gender;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getWeight() {
return weight;
}

public void setWeight(int weight) {
this.weight = weight;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public PrototypeDemo1 clone(){
PrototypeDemo1 prototypeDemo1 = null;
try{
//Object类的clone方法来完成内存中复制数据
prototypeDemo1 = (PrototypeDemo1) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return prototypeDemo1;
}
}

测试下:

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复制代码package com.jiangxia.Prototype;

/**
* @Author: 江夏
* @Date: 2021/10/24/9:27
* @Description:
*/
public class PrototypeTest {
public static void main(String[] args) {
//对象性别肯定是女,这个是一致的不变的数据
String gender = "女";
PrototypeDemo1 prototypeDemo1 = new PrototypeDemo1(gender);
//clone prototypeDemo1 并且设置clone部分的值,这块数据是可变的,每个地方不一样
PrototypeDemo1 cloneprototypeDemo1 = prototypeDemo1.clone();
cloneprototypeDemo1.setAge(20);
cloneprototypeDemo1.setHeight(160);
cloneprototypeDemo1.setWeight(50);
cloneprototypeDemo1.setName("韩梅梅");
PrototypeDemo1 cloneprototypeDemo2 = prototypeDemo1.clone();
cloneprototypeDemo2.setAge(19);
cloneprototypeDemo2.setHeight(168);
cloneprototypeDemo2.setWeight(45);
cloneprototypeDemo2.setWeight(45);
cloneprototypeDemo2.setName("李华");
System.out.println("第一个女朋友的数据是:她叫:"+cloneprototypeDemo1.getName()+";年龄:"+cloneprototypeDemo1.getAge()+";身高:"+cloneprototypeDemo1.getHeight()+";体重:"+cloneprototypeDemo1.getWeight()+";性别是:"+cloneprototypeDemo1.getGender());
System.out.println("新女朋友的数据是:她叫:"+cloneprototypeDemo2.getName()+";年龄:"+cloneprototypeDemo2.getAge()+";身高:"+cloneprototypeDemo2.getHeight()+";体重:"+cloneprototypeDemo2.getWeight()+";性别肯定还是:"+cloneprototypeDemo2.getGender());
}
}

那么你女朋友的结果如下:

4 深拷贝与浅拷贝

在原型模式中还有两个概念:深拷贝和浅拷贝,也叫深克隆和浅克隆!这里克隆和拷贝是一样的!

浅拷贝:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,而所有的对其他对象的引用都仍然指向原来的对象,所以这样是不安全的。

深拷贝:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。把引用的变量指向复制过的新对象,而不是原有的被引用的对象。

那么深拷贝如何具体实现呢?

基本数据类型和String能够自动实现深拷贝(值的复制),其他的引用类型可以让已实现Clonable接口的类中的属性也实现Clonable接口。深拷贝实现方式有两种:

1、重写 clone 方法来实现深拷贝

2:通过对象序列化实现深拷贝

还是上面的例子,先看第一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
arduino复制代码package com.jiangxia.Prototype;

import java.io.Serializable;

/**
* @Author: 江夏
* @Date: 2021/10/24/10:21
* @Description: 深拷贝重写clone方法来实现深拷贝
*/
public class DeepClonePrototypeDemo2 implements  Cloneable{
//年龄
private int age;
//姓名
private String name;
//体重
private int weight;
//身高
private int height;

//构造器
public DeepClonePrototypeDemo2(int age, String name, int weight, int height) {
this.age = age;
this.name = name;
this.weight = weight;
this.height = height;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getWeight() {
return weight;
}

public void setWeight(int weight) {
this.weight = weight;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
    }
}
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复制代码package com.jiangxia.Prototype;

import java.io.Serializable;

/**
* @Author: 江夏
* @Date: 2021/10/24/10:24
* @Description:
*/
public class DeepClonePrototypeDemo22 implements Cloneable,Serializable {
//性别字段
String gender;
//其他数据引用类型
DeepClonePrototypeDemo2 deepClonePrototypeDemo2;

public DeepClonePrototypeDemo22(){
super();
}

//深拷贝第一种方式:重写clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deepclone = null;
//这里完成了对基本数据类型(属性)和 String 的克隆,也就是值的复制
deepclone = super.clone();
//这里进行对引用类型的属性的复制进行处理
DeepClonePrototypeDemo22 deepClonePrototypeDemo22 = (DeepClonePrototypeDemo22) deepclone;
deepClonePrototypeDemo22.deepClonePrototypeDemo2 = (DeepClonePrototypeDemo2) deepClonePrototypeDemo2.clone();
return deepClonePrototypeDemo22;
}

}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.jiangxia.Prototype;

/**
* @Author: 江夏
* @Date: 2021/10/24/10:36
* @Description: 深拷贝测试代码
*/
public class DeepCloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
DeepClonePrototypeDemo22 deep = new DeepClonePrototypeDemo22();
deep.gender = "女";
deep.deepClonePrototypeDemo2 = new DeepClonePrototypeDemo2(21, "韩梅梅",50,168);

//重写clone方法 完成深拷贝
DeepClonePrototypeDemo22 deep2 = (DeepClonePrototypeDemo22) deep.clone();

System.out.println("性别:" + deep.gender + ";姓名:" +deep.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep.deepClonePrototypeDemo2.getWeight()+"||||"+deep.deepClonePrototypeDemo2.hashCode());
System.out.println("深拷贝后性别:" + deep.gender + ";姓名:" +deep2.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep2.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep2.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep2.deepClonePrototypeDemo2.getWeight()+"||||"+deep2.deepClonePrototypeDemo2.hashCode());


}
}

结果如下:

可以发现其他属性都一样,但是hashcode的值已经变了,不一样,说明对象的地址不是同一个,引用的已经不是同一个对象了!

继续上面的例子,看看使用序列化实现深拷贝:

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
ini复制代码//深拷贝第一种方式:使用序列化和反序列化实现深复制
public Object deepClone() {
//创建流对象,需要继承Serializable接口
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
//当前这个对象以对象流的方式输出
oos.writeObject(this);

//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepClonePrototypeDemo22 copyObj = (DeepClonePrototypeDemo22) ois.readObject();

return copyObj;
}

catch (Exception e) {
return null;
}
finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
}
catch (Exception e2) {
System.out.println(e2.getMessage());
}
        }
}

测试用例代码:

1
2
3
4
less复制代码//序列化实现深拷贝
DeepClonePrototypeDemo22 p3 = (DeepClonePrototypeDemo22) deep.deepClone();
System.out.println("性别:" + deep.gender + ";姓名:" +deep.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep.deepClonePrototypeDemo2.getWeight()+"||||"+deep.deepClonePrototypeDemo2.hashCode());
System.out.println("第二种深拷贝后性别:" + deep3.gender + ";姓名:" +deep3.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep3.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep3.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep3.deepClonePrototypeDemo2.getWeight()+"||||"+deep3.deepClonePrototypeDemo2.hashCode());

运行结果如下:

5 常见应用场景

原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。在spring中bean的创建实际就是两种:单例模式和原型模式,并且原型模式需要和工厂模式搭配起来。

6 总结

以上就是我对于原型模式的一些简单的理解。

使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

如果你觉得本文不错,就点赞分享给更多的人吧!

如果你觉得文章有不足之处,或者更多的想法和理解,欢迎指出讨论!

最后本文的测试代码都会同步至github:github.com/JiangXia-10…

其他推荐:

  • Spring注解(三):@scope设置组件作用域
  • Spring常用注解大全,值得你的收藏!!!
  • Spring注解(七):使用@Value对Bean进行属性赋值
  • SpringBoot开发Restful风格的接口实现CRUD功能
  • Spring注解(六):Bean的生命周期中自定义初始化和销毁方法的四种方式

本文转载自: 掘金

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

测试小姐姐问我 gRPC 怎么用,我直接把这篇文章甩给了她

发表于 2021-10-25

原文链接: 测试小姐姐问我 gRPC 怎么用,我直接把这篇文章甩给了她

上篇文章 gRPC,爆赞 直接爆了,内容主要包括:简单的 gRPC 服务,流处理模式,验证器,Token 认证和证书认证。

在多个平台的阅读量都创了新高,在 oschina 更是获得了首页推荐,阅读量到了 1w+,这已经是我单篇阅读的高峰了。

看来只要用心写还是有收获的。

这篇咱们还是从实战出发,主要介绍 gRPC 的发布订阅模式,REST 接口和超时控制。

相关代码我会都上传到 GitHub,感兴趣的小伙伴可以去查看或下载。

发布和订阅模式

发布订阅是一个常见的设计模式,开源社区中已经存在很多该模式的实现。其中 docker 项目中提供了一个 pubsub 的极简实现,下面是基于 pubsub 包实现的本地发布订阅代码:

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
go复制代码package main

import (
"fmt"
"strings"
"time"

"github.com/moby/moby/pkg/pubsub"
)

func main() {
p := pubsub.NewPublisher(100*time.Millisecond, 10)

golang := p.SubscribeTopic(func(v interface{}) bool {
if key, ok := v.(string); ok {
if strings.HasPrefix(key, "golang:") {
return true
}
}
return false
})
docker := p.SubscribeTopic(func(v interface{}) bool {
if key, ok := v.(string); ok {
if strings.HasPrefix(key, "docker:") {
return true
}
}
return false
})

go p.Publish("hi")
go p.Publish("golang: https://golang.org")
go p.Publish("docker: https://www.docker.com/")
time.Sleep(1)

go func() {
fmt.Println("golang topic:", <-golang)
}()
go func() {
fmt.Println("docker topic:", <-docker)
}()

<-make(chan bool)
}

这段代码首先通过 pubsub.NewPublisher 创建了一个对象,然后通过 p.SubscribeTopic 实现订阅,p.Publish 来发布消息。

执行效果如下:

1
2
3
4
5
6
7
8
go复制代码docker topic: docker: https://www.docker.com/
golang topic: golang: https://golang.org
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
/Users/zhangyongxin/src/go-example/grpc-example/pubsub/server/pubsub.go:43 +0x1e7
exit status 2

订阅消息可以正常打印。

但有一个死锁报错,是因为这条语句 <-make(chan bool) 引起的。但是如果没有这条语句就不能正常打印订阅消息。

这里就不是很懂了,有没有大佬知道,欢迎留言,求指导。

接下来就用 gRPC 和 pubsub 包实现发布订阅模式。

需要实现四个部分:

  1. proto 文件;
  2. 服务端: 用于接收订阅请求,同时也接收发布请求,并将发布请求转发给订阅者;
  3. 订阅客户端: 用于从服务端订阅消息,处理消息;
  4. 发布客户端: 用于向服务端发送消息。

proto 文件

首先定义 proto 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码syntax = "proto3";

package proto;

message String {
string value = 1;
}

service PubsubService {
rpc Publish (String) returns (String);
rpc SubscribeTopic (String) returns (stream String);
rpc Subscribe (String) returns (stream String);
}

定义三个方法,分别是一个发布 Publish 和两个订阅 Subscribe 和 SubscribeTopic。

Subscribe 方法接收全部消息,而 SubscribeTopic 根据特定的 Topic 接收消息。

服务端

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
go复制代码package main

import (
"context"
"fmt"
"log"
"net"
"server/proto"
"strings"
"time"

"github.com/moby/moby/pkg/pubsub"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)

type PubsubService struct {
pub *pubsub.Publisher
}

func (p *PubsubService) Publish(ctx context.Context, arg *proto.String) (*proto.String, error) {
p.pub.Publish(arg.GetValue())
return &proto.String{}, nil
}

func (p *PubsubService) SubscribeTopic(arg *proto.String, stream proto.PubsubService_SubscribeTopicServer) error {
ch := p.pub.SubscribeTopic(func(v interface{}) bool {
if key, ok := v.(string); ok {
if strings.HasPrefix(key, arg.GetValue()) {
return true
}
}
return false
})

for v := range ch {
if err := stream.Send(&proto.String{Value: v.(string)}); nil != err {
return err
}
}
return nil
}

func (p *PubsubService) Subscribe(arg *proto.String, stream proto.PubsubService_SubscribeServer) error {
ch := p.pub.Subscribe()

for v := range ch {
if err := stream.Send(&proto.String{Value: v.(string)}); nil != err {
return err
}
}
return nil
}

func NewPubsubService() *PubsubService {
return &PubsubService{pub: pubsub.NewPublisher(100*time.Millisecond, 10)}
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 简单调用
server := grpc.NewServer()
// 注册 grpcurl 所需的 reflection 服务
reflection.Register(server)
// 注册业务服务
proto.RegisterPubsubServiceServer(server, NewPubsubService())

fmt.Println("grpc server start ...")
if err := server.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

对比之前的发布订阅程序,其实这里是将 *pubsub.Publisher 作为了 gRPC 的结构体 PubsubService 的一个成员。

然后还是按照 gRPC 的开发流程,实现结构体对应的三个方法。

最后,在注册服务时,将 NewPubsubService() 服务注入,实现本地发布订阅功能。

订阅客户端

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
go复制代码package main

import (
"client/proto"
"context"
"fmt"
"io"
"log"

"google.golang.org/grpc"
)

func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()

client := proto.NewPubsubServiceClient(conn)
stream, err := client.Subscribe(
context.Background(), &proto.String{Value: "golang:"},
)
if nil != err {
log.Fatal(err)
}

go func() {
for {
reply, err := stream.Recv()
if nil != err {
if io.EOF == err {
break
}
log.Fatal(err)
}
fmt.Println("sub1: ", reply.GetValue())
}
}()

streamTopic, err := client.SubscribeTopic(
context.Background(), &proto.String{Value: "golang:"},
)
if nil != err {
log.Fatal(err)
}

go func() {
for {
reply, err := streamTopic.Recv()
if nil != err {
if io.EOF == err {
break
}
log.Fatal(err)
}
fmt.Println("subTopic: ", reply.GetValue())
}
}()

<-make(chan bool)
}

新建一个 NewPubsubServiceClient 对象,然后分别实现 client.Subscribe 和 client.SubscribeTopic 方法,再通过 goroutine 不停接收消息。

发布客户端

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
go复制代码package main

import (
"client/proto"
"context"
"log"

"google.golang.org/grpc"
)

func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := proto.NewPubsubServiceClient(conn)

_, err = client.Publish(
context.Background(), &proto.String{Value: "golang: hello Go"},
)
if err != nil {
log.Fatal(err)
}

_, err = client.Publish(
context.Background(), &proto.String{Value: "docker: hello Docker"},
)
if nil != err {
log.Fatal(err)
}

}

新建一个 NewPubsubServiceClient 对象,然后通过 client.Publish 方法发布消息。

当代码全部写好之后,我们开三个终端来测试一下:

终端1 上启动服务端:

1
go复制代码go run main.go

终端2 上启动订阅客户端:

1
go复制代码go run sub_client.go

终端3 上执行发布客户端:

1
go复制代码go run pub_client.go

这样,在 终端2 上就有对应的输出了:

1
2
3
makefile复制代码subTopic:  golang: hello Go
sub1: golang: hello Go
sub1: docker: hello Docker

也可以再多开几个订阅终端,那么每一个订阅终端上都会有相同的内容输出。

源码地址: GitHub

REST 接口

gRPC 一般用于集群内部通信,如果需要对外提供服务,大部分都是通过 REST 接口的方式。开源项目 grpc-gateway 提供了将 gRPC 服务转换成 REST 服务的能力,通过这种方式,就可以直接访问 gRPC API 了。

但我觉得,实际上这么用的应该还是比较少的。如果提供 REST 接口的话,直接写一个 HTTP 服务会方便很多。

proto 文件

第一步还是创建一个 proto 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码syntax = "proto3";

package proto;

import "google/api/annotations.proto";

message StringMessage {
string value = 1;
}

service RestService {
rpc Get(StringMessage) returns (StringMessage) {
option (google.api.http) = {
get: "/get/{value}"
};
}
rpc Post(StringMessage) returns (StringMessage) {
option (google.api.http) = {
post: "/post"
body: "*"
};
}
}

定义一个 REST 服务 RestService,分别实现 GET 和 POST 方法。

安装插件:

1
arduino复制代码go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

生成对应代码:

1
2
3
4
5
6
bash复制代码protoc -I/usr/local/include -I. \
-I$GOPATH/pkg/mod \
-I$GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
--grpc-gateway_out=. --go_out=plugins=grpc:.\
--swagger_out=. \
helloworld.proto

--grpc-gateway_out 参数可生成对应的 gw 文件,--swagger_out 参数可生成对应的 API 文档。

在我这里生成的两个文件如下:

1
2
go复制代码helloworld.pb.gw.go
helloworld.swagger.json

REST 服务

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
go复制代码package main

import (
"context"
"log"
"net/http"

"rest/proto"

"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
)

func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

mux := runtime.NewServeMux()

err := proto.RegisterRestServiceHandlerFromEndpoint(
ctx, mux, "localhost:50051",
[]grpc.DialOption{grpc.WithInsecure()},
)
if err != nil {
log.Fatal(err)
}

http.ListenAndServe(":8080", mux)
}

这里主要是通过实现 gw 文件中的 RegisterRestServiceHandlerFromEndpoint 方法来连接 gRPC 服务。

gRPC 服务

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
go复制代码package main

import (
"context"
"net"

"rest/proto"

"google.golang.org/grpc"
)

type RestServiceImpl struct{}

func (r *RestServiceImpl) Get(ctx context.Context, message *proto.StringMessage) (*proto.StringMessage, error) {
return &proto.StringMessage{Value: "Get hi:" + message.Value + "#"}, nil
}

func (r *RestServiceImpl) Post(ctx context.Context, message *proto.StringMessage) (*proto.StringMessage, error) {
return &proto.StringMessage{Value: "Post hi:" + message.Value + "@"}, nil
}

func main() {
grpcServer := grpc.NewServer()
proto.RegisterRestServiceServer(grpcServer, new(RestServiceImpl))
lis, _ := net.Listen("tcp", ":50051")
grpcServer.Serve(lis)
}

gRPC 服务的实现方式还是和以前一样。

以上就是全部代码,现在来测试一下:

启动三个终端:

终端1 启动 gRPC 服务:

1
go复制代码go run grpc_service.go

终端2 启动 REST 服务:

1
go复制代码go run rest_service.go

终端3 来请求 REST 服务:

1
2
3
4
5
bash复制代码$ curl localhost:8080/get/gopher
{"value":"Get hi:gopher"}

$ curl localhost:8080/post -X POST --data '{"value":"grpc"}'
{"value":"Post hi:grpc"}

源码地址: GitHub

超时控制

最后一部分介绍一下超时控制,这部分内容是非常重要的。

一般的 WEB 服务 API,或者是 Nginx 都会设置一个超时时间,超过这个时间,如果还没有数据返回,服务端可能直接返回一个超时错误,或者客户端也可能结束这个连接。

如果没有这个超时时间,那是相当危险的。所有请求都阻塞在服务端,会消耗大量资源,比如内存。如果资源耗尽的话,甚至可能会导致整个服务崩溃。

那么,在 gRPC 中怎么设置超时时间呢?主要是通过上下文 context.Context 参数,具体来说就是 context.WithDeadline 函数。

proto 文件

创建最简单的 proto 文件,这个不多说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码syntax = "proto3";

package proto;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
go复制代码package main

import (
"client/proto"
"context"
"fmt"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func main() {
// 简单调用
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(3*time.Second)))
defer cancel()

client := proto.NewGreeterClient(conn)
// 简单调用
reply, err := client.SayHello(ctx, &proto.HelloRequest{Name: "zzz"})
if err != nil {
statusErr, ok := status.FromError(err)
if ok {
if statusErr.Code() == codes.DeadlineExceeded {
log.Fatalln("client.SayHello err: deadline")
}
}

log.Fatalf("client.SayHello err: %v", err)
}
fmt.Println(reply.Message)
}

通过下面的函数设置一个 3s 的超时时间:

1
2
go复制代码ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(3*time.Second)))
defer cancel()

然后在响应错误中对超时错误进行检测。

服务端

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
go复制代码package main

import (
"context"
"fmt"
"log"
"net"
"runtime"
"server/proto"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
)

type greeter struct {
}

func (*greeter) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
data := make(chan *proto.HelloReply, 1)
go handle(ctx, req, data)
select {
case res := <-data:
return res, nil
case <-ctx.Done():
return nil, status.Errorf(codes.Canceled, "Client cancelled, abandoning.")
}
}

func handle(ctx context.Context, req *proto.HelloRequest, data chan<- *proto.HelloReply) {
select {
case <-ctx.Done():
log.Println(ctx.Err())
runtime.Goexit() //超时后退出该Go协程
case <-time.After(4 * time.Second): // 模拟耗时操作
res := proto.HelloReply{
Message: "hello " + req.Name,
}
// //修改数据库前进行超时判断
// if ctx.Err() == context.Canceled{
// ...
// //如果已经超时,则退出
// }
data <- &res
}
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 简单调用
server := grpc.NewServer()
// 注册 grpcurl 所需的 reflection 服务
reflection.Register(server)
// 注册业务服务
proto.RegisterGreeterServer(server, &greeter{})

fmt.Println("grpc server start ...")
if err := server.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

服务端增加一个 handle 函数,其中 case <-time.After(4 * time.Second) 表示 4s 之后才会执行其对应代码,用来模拟超时请求。

如果客户端超时时间超过 4s 的话,就会产生超时报错。

下面来模拟一下:

服务端:

1
2
3
go复制代码$ go run main.go
grpc server start ...
2021/10/24 22:57:40 context deadline exceeded

客户端:

1
2
3
go复制代码$ go run main.go
2021/10/24 22:57:40 client.SayHello err: deadline
exit status 1

源码地址: GitHub

总结

本文主要介绍了 gRPC 的三部分实战内容,分别是:

  1. 发布订阅模式
  2. REST 接口
  3. 超时控制

个人感觉,超时控制还是最重要的,在平时的开发过程中需要多多注意。

结合上篇文章,gRPC 的实战内容就写完了,代码全部可以执行,也都上传到了 GitHub。

大家如果有任何疑问,欢迎给我留言,如果感觉不错的话,也欢迎关注和转发。


源码地址:

  • github.com/yongxinz/go…
  • github.com/yongxinz/go…

推荐阅读:

  • gRPC,爆赞
  • 使用 grpcurl 通过命令行访问 gRPC 服务
  • 听说,99% 的 Go 程序员都被 defer 坑过

参考:

  • chai2010.cn/advanced-go…
  • codeleading.com/article/946…
  • juejin.cn/post/684490…
  • www.cnblogs.com/FireworksEa…

本文转载自: 掘金

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

手把手教你做微信小程序授权登录交互(一)

发表于 2021-10-25

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

  • 一、uni.login请求临时code
  • 二、uni.request向后台交换数据
  • 三、源代码
  • 前台:在GetUserInfo中添加接口
  • 后台:SpringBoot后台数据处理
  • 四、实现效果

开发需求:
我们团队在开发微信小程序过程中,需要绑定微信用户的信息到数据库里,那么就需要获得用户的唯一标识openid,而微信为了安全,是禁止小程序直接访问该接口,因此我们不能直接拿到用户的openid,从而需要通过调用微信接口实现授权登录。

我是孙不坚1208,这篇文章是在2021年暑假参加山东省大学生软件设计大赛时所写,主要是第一次接触小程序开发(uniapp+springboot),遇到的问题都记下来了,后面我也会持续更新出我的专栏《微信小程序开发指南》,欢迎与我一起学习,希望我的文章能够帮助到大家。

这篇文章是基于uniapp+springboot的微信小程序授权登录交互,对uniapp不熟悉的可以去这篇两万字的博客(【前端之旅】uni-app学习笔记)了解一下。

官方登录流程图(逻辑流程):

login

主要步骤:

  1. 前端获取到code(wx.login),传入服务器。
  2. 服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId。
  3. 服务器将OpenId进行相应的业务处理并返回给前端。

注意事项

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
  2. 临时登录凭证 code 只能使用一次。

一、uni.login请求临时code

在微信小程序中,使用微信开放接口:wx.login(Object object),调用接口获取登录凭证(code)

在uniapp中,我们通过uni.login(OBJECT),调用接口获取登录凭证(code)。

OBJECT 参数说明

参数名 类型 必填 说明 平台差异说明
provider String 否 登录服务提供商,通过 uni.getProvider 获取,如果不设置则弹出登录列表选择界面
scopes String/Array 见平台差异说明 授权类型,默认 auth_base。支持 auth_base(静默授权)/ auth_user(主动授权) / auth_zhima(芝麻信用) 支付宝小程序
timeout Number 否 超时时间,单位ms 微信小程序、百度小程序
univerifyStyle Object 否 一键登录页面样式 App 3.0.0+
success Function 否 接口调用成功的回调
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)

success 返回参数说明

参数名 说明
authResult 登录服务商提供的登录信息,服务商不同返回的结果不完全相同
code 小程序专有,用户登录凭证。开发者需要在开发者服务器后台,使用 code 换取 openid 和 session_key 等信息
errMsg 描述信息

示例

1
2
3
4
5
6
javascript复制代码uni.login({
provider: 'weixin',
success: function (res) {
console.log(res.Result);
}
});

二、uni.request向后台交换数据

我们通过 uni.request(OBJECT):向后台发起请求,后台通过前台发送得到的凭证(code),需要后台发送请求到微信接口,然后微信返回一个json格式的字符串到后台,后台处理之后,进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台帐号下的唯一标识(unionid)及本次登录的会话密钥(session_key)等, 再返回到前台。

uni.request(OBJECT):发起网络请求。

在各个小程序平台运行时,网络相关的 API 在使用前需要配置域名白名单。

OBJECT 参数说明

参数名 类型 必填 默认值 说明 平台差异说明
url String 是 开发者服务器接口地址
data Object/String/ArrayBuffer 否 请求的参数 App(自定义组件编译模式)不支持ArrayBuffer类型
header Object 否 设置请求的 header,header 中不能设置 Referer。 App、H5端会自动带上cookie,且H5端不可手动修改
method String 否 GET 有效值详见下方说明
timeout Number 否 60000 超时时间,单位 ms H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
dataType String 否 json 如果设为 json,会尝试对返回的数据做一次 JSON.parse
responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 支付宝小程序不支持
sslVerify Boolean 否 true 验证 ssl 证书 仅App安卓端支持(HBuilderX 2.3.3+)
withCredentials Boolean 否 false 跨域请求时是否携带凭证(cookies) 仅H5支持(HBuilderX 2.6.15+)
firstIpv4 Boolean 否 false DNS解析时优先使用ipv4 仅 App-Android 支持 (HBuilderX 2.8.0+)
success Function 否 收到开发者服务器成功返回的回调函数
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)

method 有效值

必须大写,有效值在不同平台差异说明不同。

method App H5 微信小程序 支付宝小程序 百度小程序 字节跳动小程序
GET √ √ √ √ √ √
POST √ √ √ √ √ √
PUT √ √ √ x √ √
DELETE √ √ √ x √ x
CONNECT x √ √ x x x
HEAD x √ √ x √ x
OPTIONS √ √ √ x √ x
TRACE x √ √ x x x

success 返回参数说明

参数 类型 说明
data Object/String/ArrayBuffer 开发者服务器返回的数据
statusCode Number 开发者服务器返回的 HTTP 状态码
header Object 开发者服务器返回的 HTTP Response Header
cookies Array.<string> 开发者服务器返回的 cookies,格式为字符串数组

data 数据说明

最终发送给服务器的数据是 String 类型,如果传入的 data 不是 String 类型,会被转换成 String。转换规则如下:

  • 对于 GET 方法,会将数据转换为 query string。例如 { name: 'name', age: 18 } 转换后的结果是 name=name&age=18。
  • 对于 POST 方法且 header['content-type'] 为 application/json 的数据,会进行 JSON 序列化。
  • 对于 POST 方法且 header['content-type'] 为 application/x-www-form-urlencoded 的数据,会将数据转换为 query string。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
data: {
text: 'uni.request'
},
header: {
'custom-header': 'hello' //自定义请求头信息
},
success: (res) => {
console.log(res.data);
this.text = 'request success';
}
});

返回值

如果希望返回一个 requestTask 对象,需要至少传入 success / fail / complete 参数中的一个。例如:

1
2
3
4
5
javascript复制代码var requestTask = uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
complete: ()=> {}
});
requestTask.abort();

如果没有传入 success / fail / complete 参数,则会返回封装后的 Promise 对象:Promise 封装

通过 requestTask,可中断请求任务。

requestTask 对象的方法列表

方法 参数 说明
abort 中断请求任务
offHeadersReceived 取消监听 HTTP Response Header 事件,仅微信小程序平台支持,文档详情
onHeadersReceived 监听 HTTP Response Header 事件。会比请求完成事件更早,仅微信小程序平台支持,文档详情

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码const requestTask = uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
data: {
name: 'name',
age: 18
},
success: function(res) {
console.log(res.data);
}
});

// 中断请求任务
requestTask.abort();

Tips

  • 请求的 header 中 content-type 默认为 application/json。
  • 避免在 header 中使用中文,或者使用 encodeURIComponent 进行编码,否则在百度小程序报错。
  • 网络请求的 超时时间 可以统一在 manifest.json 中配置 networkTimeout。
  • H5 端本地调试需注意跨域问题,参考:调试跨域问题解决方案
  • H5端 cookie 受跨域限制(和平时开发网站时一样),旧版的 uni.request 未支持 withCredentials 配置,可以直接使用 xhr 对象或者其他类库。
  • uni-app 插件市场有flyio、axios等三方封装的拦截器可用
  • localhost、127.0.0.1等服务器地址,只能在电脑端运行,手机端连接时不能访问。请使用标准IP并保证手机能连接电脑网络
  • 单次网络请求数据量建议控制在50K以下(仅指json数据,不含图片),过多数据应分页获取,以提升应用体验。

附:了解各种平台的ID,它们都有相同的功能。

  • OpenID:在微信应用(公众号、小程序等)默认使用 OpenID,在开发中请求的接口返回的一般都是OpenID。在小程序或微信网页里不用做授权,静默情况下也能拿到 OpenID。可以说 OpenID 是微信生态里最重要的一个 ID。可以理解 OpenID 是通过 AppID 和微信用户 ID 加密得到的,其与微信应用(每个应用会有 AppID)相关,每个微信应用都会生成一个唯一的用户的识别。
  • AppID 和 AppSecret:公众号和小程序都会有一个 AppID 用来标识当前的微信应用,而如果需要开发的话,接口的请求都需要用到 AppSecret。
  • 微信用户 ID:微信用户 ID 是有加密的,是无法拿到的。一般我们使用微信机器人开发的话,常用的是微信号或者微信昵称作为 ID。
  • UnionID:在微信开放平台里面,做了账号绑定后,就会生成一个统一的 UnionID,绑定后的微信应用(小程序、公众号等)都可以使用一个 ID。获取 UnionID 需要经过用户授权。
  • UUID:主要是针对于前端的设备,比如小程序或者网页的,因为获取 OpenID 需要一定的开发,所以如果在获取不到的情况下,我们一般会给当前浏览器或者小程序生成一个随机的 ID。
  • UserID:用户的真实 ID,一般是存在数据库的 ID。

本文转载自: 掘金

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

大数据日志收集Flume

发表于 2021-10-25

一、Flume概述

简介:Flume是一个分布式、可靠、高可用的海量日志聚合系统,目前最新版本称为Flume NG(next generation)

功能:Flume能将不同数据源的海量日志数据进行高效收集、聚合、移动,最后存储到一个中心化的数据存储系统中

使用案例:利用Flume高效地采集日志,同时对日志进行聚合,避免小文件的产生,然后将聚合后的数据通过管道移动到存储系统进行后续的数据分析和挖掘。

二、Flume设计概览

Agent是Flume的核心设计,也是Flume一个基本单元;Agent由三部分组成Source、Channel、Sink(如下图)

1635147289044.png
Flume NG数据采集的工作机制如下。

1)Source:接收数据,生产者

可以接收外部源发送过来的数据。不同的Source可以接收不同的数据格式。比如目录池(Spooling Directory)数据源,可以监控指定文件夹中的新文件变化,如果目录中有新文件产生,就会立刻读取其内容。

2)Channel:管道,生产消费者中的存储媒介

1.接收Source的输出,直到有Sink消费掉Channel中的数据。

2.Channel中的数据直到进入下一个Agent的Channel中或进入终端系统才会被删除

3.当Sink写入失败后可以自动重启,不会造成数据丢失,因此很可靠。

上面的2、3点都保证了数据的可靠性

3)Sink:消费者

会消费Channel中的数据,然后发送给外部源(比如数据可以写入HDFS或HBase中)或下一个Agent的Source。

Agent核心组成

1.Source

通过Flume自带的Source(如下图)可以支持多种数据来源,比如Avro、Log4j、Syslog和HTTP,也可以自定义Souce,以IPC或RPC的方式接入自己的应用

1635149361309.png
Source也可以直接读取文件,有两种方式Exec Source、Spool Source

  • Exec Source:通过Unix文件操作指令tail -f 文件名
  • Spool Source:监测新增文件,并采集文件中的数据

一般常用的就是Kafka Source

2.Channel

Channel是中转Event的一个临时存储,保存由Source组件传递过来的Event,目前比较常用的Channel有两种。

  • Memory Channel:内存中存储Event,不稳定,但是速度快
  • File Channel:持久化隧道,它将持久化所有的Event到磁盘中

1635149747745.png

3.Sink

Sink在设置存储数据时,可以向文件系统、数据库、Hadoop中存储数据。

在日志数据较少时,可以将数据存储在文件系统中,并且设定一定的时间间隔保存数据。在日志数据较多时,可以将相应的日志数据存储到Hadoop中,便于日后进行相应的数据分析。

1635149871245.png

4.Event

上面还谈到了Event,也就是所说到的数据。Flume是一个基于事件驱动的架构,Event产生后,监听数据变换,此后Agent才会做之后的数据处理工作

Event在代码中的表现形式如下

1
2
3
4
5
6
7
8
arduino复制代码package org.apache.flume;
import java.util.Map;
public interface Event {
   Map<String, String> getHeaders();
   void setHeaders(Map<String, String> var1);
   byte[] getBody();
   void setBody(byte[] var1);
}

创建Event常用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码public class EventBuilder {
   public EventBuilder() {
  }
   public static Event withBody(byte[] body, Map<String, String> headers) {
       Event event = new SimpleEvent();
       if (body == null) {
           body = new byte[0];
      }
       event.setBody(body);
       if (headers != null) {
           event.setHeaders(new HashMap(headers));
      }
       return event;
  }
   public static Event withBody(byte[] body) {
       return withBody((byte[])body, (Map)null);
  }
   public static Event withBody(String body, Charset charset, Map<String, String> headers) {
       return withBody(body.getBytes(charset), headers);
  }
   public static Event withBody(String body, Charset charset) {
       return withBody(body, charset, (Map)null);
  }
}

本文转载自: 掘金

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

MyBatis-Ext 入门实战

发表于 2021-10-25

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

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


最近在工作中,接触到了一个MyBatis扩展工具包MyBatis-Ext,可以说很大程度上减轻了使用mybatis时的工作量,本文就和大家来分享一下这个轻量的扩展工具。

MyBatis-Ext是MyBatis的增强扩展,和我们平常用的Mybatis-plus非常类似,简化了MyBatis对单表增删改查的操作,提供通用的增删改查,支持函数式编程,支持分页查询,支持用户自定义通用方法,并且能够防止SQL注入。集成起来也非常简单,对MyBatis只做增强不做修改。

以spring-boot项目为例,集成非常简单。pom导入核心依赖:

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>tech.wetech.mybatis</groupId>
<artifactId>mybatis-ext-core</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>tech.wetech.mybatis</groupId>
<artifactId>mybatis-ext-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>

需要注意的是,引入mybatis-ext-spring-boot-starter后无需再引入mybatis-spring-boot-starter。

和以往一样,在application.yml配置一下数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
yml复制代码spring:
datasource:
username: dater
password: 123456
url: jdbc:mysql://127.0.0.1:3306/datacenter?useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 8
min-idle: 1
max-active: 20
mybatis:
mapper-locations: classpath:mapping/*Mapper.xml
type-aliases-package: com.mybatis.ext.test.mybatisexttest.entity
spring:
datasource:
username: dater
password: 123456
url: jdbc:mysql://127.0.0.1:3306/datacenter?useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 8
min-idle: 1
max-active: 20
mybatis:
mapper-locations: classpath:mapping/*Mapper.xml
type-aliases-package: com.mybatis.ext.test.mybatisexttest.entity

创建一个映射的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Data
@Table(name = "user")
public class User {
@Id
String identifycard;
@Column(name="name")
String name;
String money;
String card;
String phone;
String rate;
}

mybatis-ext使用了Jpa的注解,目前实现了@Table、@Id、@Column、@Transient、@Version。其中@Table、@Id是必须添加的注解,其他非必须添加。使用@Table指定数据表名,@Id指定数据表主键。

查询的Mapper接口继承BaseMapper接口,泛型中填写实体类:

1
2
java复制代码public interface UserMapper extends BaseMapper<User> {
}

我们来看一下能够直接调用的方法,为在BaseMapper中内置了很多通用方法,可以直接调用,非常简便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码int deleteByPrimaryKey(PK id);
<S extends T> int insert(S record);
<S extends T> int insertAll(Iterable<S> record);
<S extends T> int insertSelective(S record);
<S extends T> S selectByPrimaryKey(PK id);
<S extends T> Optional<S> selectByPrimaryKeyWithOptional(ID id);
<S extends T> int updateByPrimaryKey(S record);
<S extends T> int updateByPrimaryKeySelective(S record);
<S extends T> List<S> selectAll();
<S extends T> List<S> selectList(S record);
<S extends T> S selectOne(S record);
<S extends T> S selectOneWithOptional(S record);
boolean existsByPrimaryKey(PK id);
<S extends T> int count(S record);
<S extends T> List<S> selectByExample(Example<S, Object> example);
<S extends T> int countByExample(Example<S, Object> example);
<S extends T> int deleteByExample(Example<S, Object> example);
<S extends T> int updateByExample(@Param("record") S record, @Param("example") Example<S, Object> example);
<S extends T> int updateByExampleSelective(@Param("record") S record, @Param("example") Example<S, Object> example);

来进行一下接口调用的测试,先试一下selectAll方法:

1
2
3
4
5
6
7
java复制代码@GetMapping("getUser")
public void getUser(){
List<User> users = userMapper.selectAll();
for (User user : users) {
System.out.println(user.getName()+" "+user.getIdentifycard());
}
}

测试结果:

这样,通过调用内置方法就实现了不写sql语句直接进行查询。同样,如果想根据主键进行查询也很简单,直接调用selectByPrimaryKey方法:

1
2
3
4
5
java复制代码@PostMapping("getUserById")
public void getUserByIdentifycard(@RequestBody User user){
User retUser = userMapper.selectByPrimaryKey(user);
System.out.println(retUser.toString());
}

查询结果:

图片

另外,还可以使用Optional包裹查询,修改一下上面主键查询的方法:

1
2
3
4
5
6
java复制代码@PostMapping("getUserById")
public void getUserByIdentifycard(@RequestBody User user){
User retUser = userMapper.selectByPrimaryKeyWithOptional(user)
.orElseThrow(()->new RuntimeException("未查到数据"));
System.out.println(retUser.toString());
}

这样,在传入一个不存在的主键时,就会直接抛出自定义的异常:

图片

还有其他很多简单的查询,大家可以根据上面列出api自行测试一下。此外,还可以使用Criteria,使用逻辑组合,进行函数式查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@GetMapping("criteriaTest")
public void testCreateCriteria(){
List<User> list = userMapper.createCriteria()
.andEqualTo(User::getName, "Trunks")
.andBetween(User::getMoney, 100, 300)
.andNotLike(User::getRate, "6")
.orIn(User::getCard, Arrays.asList("10"))
.selectList();

list.forEach(user -> {
System.out.println(user.toString());
});
}

查询结果:

图片

也可以使用Example进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@GetMapping("exampleTest")
public void testExample(){
Example<User> example=Example.of(User.class);
example.createCriteria()
.andEqualTo(User::getName, "Trunks")
.andBetween(User::getMoney, 100, 300)
.andNotLike(User::getRate, "6")
.orIn(User::getCard, Arrays.asList("10"));

example.setDistinct(true);
List<User> list = userMapper.selectByExample(example);
list.forEach(user -> {
System.out.println(user.toString());
});
}

结果与使用Criteria结果相同。另外,还可以将多个条件组合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码GetMapping("testExampleWithSub")
public void selectByExampleWithSub(){
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper1 = session.getMapper(UserMapper.class);
Example<User> example=Example.of(User.class);
example.and()
.andEqualTo(User::getName, "Trunks");
example.and()
.andEqualTo(User::getCard,"10");
example.and()
.andLessThanOrEqualTo(User::getRate,300);

Criteria<User> criteria=new Criteria<>();
criteria.andIsNotNull(User::getPhone);
example.and(criteria);
List<User> list = userMapper1.selectByExample(example);

list.forEach(user -> {
System.out.println(user.toString());
});
}
}

结果:

图片

除了上面介绍的这些功能与基础的sql增删改查外,MyBatis-Ext还实现了很多其他功能,例如排序和分页,并且支持自定义通用接口方法等等,大家可以在使用中通过实践继续探索一下。

最后

如果觉得对您有所帮助,小伙伴们可以点赞、转发一下,非常感谢

公众号码农参上,加个好友,做个点赞之交啊

本文转载自: 掘金

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

python 操作 Excel 的基本操作——模块化

发表于 2021-10-25

因最近工作需要将数据写入excel,并设置格式,编写此Excel操作模块,以便未来还要用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
python复制代码# !/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2021/7/21 11:31
@Author : luoshixiang
@Email : luoshixiang@shsnc.com
@File : Parse_Excel.py
"""

import os
import openpyxl
from openpyxl.styles import PatternFill, Border, Side, Alignment, Protection, Font,colors
from openpyxl.drawing.image import Image
from openpyxl.chart import Series,LineChart,Reference

def create_excel(sheetname, filename):
# 创建一个workbook对象
wb = openpyxl.Workbook()
# 激活一个sheet
sheet = wb.active
# 为sheet设置表名sheetname
# sheet.title = 'Sheet1'
for i in range(len(sheetname)):
wb.create_sheet(sheetname[i])
ws = wb['Sheet']
wb.remove(ws)
wb.save(filename)

import openpyxl
def write_excel(title, sheet_name, data, filename):
# 创建一个workbook对象
wb = openpyxl.Workbook()
# 激活一个sheet
sheet = wb.active
# 为sheet设置一个表名sheetname
sheet.title = sheet_name
# 添加表头(不需要表头可以不用加)
for i in range(len(title)):
sheet.cell(row=1, column=i + 1, value=title[i])
# 添加表内容
for row_index, row_item in enumerate(data): # 获取索引和内容
for col_index, col_item in enumerate(row_item):
sheet.cell(row=row_index + 2, column=col_index + 1, value=col_item) # 写入
wb.save(filename)
return filename

class Parse_Excel(object):
"""解析excel文件"""
def __init__(self, filename, sheet_name = None):
try:
if os.path.exists(filename) == True:
# 获取绝对路径下的文件名称
self.filename = os.path.realpath(filename)
# 打开文件,获取excel文件的workbook(工作簿)对象
self.__wb = openpyxl.load_workbook(self.filename, data_only=True)
if sheet_name:
self.__sheet_name = sheet_name
else:
# 获取活跃表单
self.__ws = self.__wb.active # self.__ws的值与self.__wb[sheet_name]的值相同
# 获取活跃表名称
self.__sheet_name = self.__ws.title
else:
# 创建一个workbook对象
self.__wb = openpyxl.Workbook()
# 激活一个sheet
self.__ws = self.__wb.active
# 为sheet设置表名sheetname
if sheet_name:
self.__wb.create_sheet(sheet_name)
self.__sheet_name = sheet_name
# self.__wb.save(filename)
else:
self.__ws.title = 'Sheet1'
self.__ws = self.__wb['Sheet']
self.__wb.remove(self.__ws)
self.__wb.save(filename)
# 获取绝对路径下的文件名称
self.filename = os.path.realpath(filename)
except FileNotFoundError as e:
raise e
def save(self):
self.__wb.save(self.filename)

def create_sheets(self, sheet_name):
for i in range(len(sheet_name)):
self.__wb.create_sheet(sheet_name[i])

def remove_sheets(self, sheet_name):
for i in range(len(sheet_name)):
self.__wb.remove(sheet_name[i])

def get_max_row_num(self):
"""获取最大行号"""
sheet_name = self.__sheet_name
max_row_num = self.__wb[sheet_name].max_row
return max_row_num
def get_max_column_num(self):
"""获取最大列号"""
sheet_name = self.__sheet_name
max_column = self.__wb[sheet_name].max_column
return max_column
def get_row_value(self, row):
"""获取某一行的数据"""
sheet_name = self.__sheet_name
column_num = self.get_max_column_num()
row_value = []
if isinstance(row, int):
for column in range(1, column_num + 1):
values_row = self.__wb[sheet_name].cell(row, column).value
row_value.append(values_row)
# while None in values_row: # 去除列表中的None值和nall值
# values_row.remove(None)
return row_value
else:
raise TypeError('row must be type int')

def get_column_value(self, column):
"""获取某一列数据"""
sheet_name = self.__sheet_name
row_num = self.get_max_row_num()
column_value = []
if isinstance(column, int):
for row in range(1, row_num + 1):
values_column = self.__wb[sheet_name].cell(row, column).value
column_value.append(values_column)
# while None in column_value: # 去除列表中的None值和nall值
# column_value.remove(None)
return column_value
else:
raise TypeError('column must be type int')
def get_all_value(self):
"""获取指定表单的所有数据(除去表头)"""
sheet_name = self.__sheet_name
max_row_num = self.get_max_row_num()
max_column = self.get_max_column_num()
values = []
for row in range(2, max_row_num + 1):
value_list = []
for column in range(1, max_column + 1):
value = self.__wb[sheet_name].cell(row, column).value
value_list.append(value)
values.append(value_list)
return values

def get_all_value2(self):
"""获取指定表单的所有数据(包含表头)"""
sheet_name = self.__sheet_name
max_row_num = self.get_max_row_num()
max_column = self.get_max_column_num()
values = []
for row in range(1, max_row_num + 1):
value_list = []
for column in range(1, max_column + 1):
value = self.__wb[sheet_name].cell(row, column).value
value_list.append(value)
values.append(value_list)
return values
def get_excel_title(self):
"""获取sheet表头"""
sheet_name = self.__sheet_name
title_key = tuple(self.__wb[sheet_name].iter_rows(max_row=1, values_only=True))[0]
return title_key
def get_cell(self,location):
sheet_name = self.__sheet_name
cell_value = self.__wb[sheet_name][location].value
return cell_value
def get_cell_value(self,row,column):
sheet_name = self.__sheet_name
cell_value = self.__wb[sheet_name].cell(row,column).value
return cell_value

def my_border(self,t_border, b_border, l_border, r_border):
# 定义边框样式
border = Border(top=Side(border_style=t_border, color=colors.BLACK),
bottom=Side(border_style=b_border, color=colors.BLACK),
left=Side(border_style=l_border, color=colors.BLACK),
right=Side(border_style=r_border, color=colors.BLACK))
return border
# 初始化制定区域边框为所有框线
def format_border(self,s_column, s_index, e_column, e_index):
sheet_name = self.__sheet_name
for row in tuple(self.__wb[sheet_name][s_column + str(s_index):e_column + str(e_index)]):
for cell in row:
cell.border = self.my_border('thin', 'thin', 'thin', 'thin')

def set_solid_border(self,area_list):
"""给指定区域设置粗匣框线"""
# 调用方式:pe.set_solid_border([['A', 3, 'D', 5], ['A', 6, 'D', 7],['A', 3, 'A', 10], ['B', 3, 'C', 10]])
sheet_name = self.__sheet_name
for area in area_list:
s_column = area[0]
s_index = area[1]
e_column = area[2]
e_index = area[3]
#设置左粗框线
for cell in self.__wb[sheet_name][s_column][s_index - 1:e_index]:
cell.border = self.my_border(cell.border.top.style, cell.border.bottom.style,
'medium', cell.border.right.style)
# 设置右粗框线
for cell in self.__wb[sheet_name][e_column][s_index - 1:e_index]:
cell.border = self.my_border(cell.border.top.style, cell.border.bottom.style,
cell.border.left.style, 'medium')
# 设置上粗框线
for row in tuple(self.__wb[sheet_name][s_column + str(s_index):e_column + str(s_index)]):
for cell in row:
cell.border = self.my_border('medium', cell.border.bottom.style,
cell.border.left.style, cell.border.right.style)
# 设置下粗框线
for row in tuple(self.__wb[sheet_name][s_column + str(e_index):e_column + str(e_index)]):
for cell in row:
cell.border = self.my_border(cell.border.top.style, 'medium',
cell.border.left.style, cell.border.right.style)

def pattern_fill(self,rows,columns):
"""将单元格背景填充红色"""
sheet_name = self.__sheet_name
fill_set = PatternFill("solid", fgColor="00FF0000")
# fill_set = PatternFill("solid", fgColor="00FFFFFF")
font_set = Font(bold=True)
for i in range(2, columns ):
if int(self.__wb[sheet_name].cell(row = rows, column = i).value) >= 1:
self.__wb[sheet_name].cell(row = rows, column = i).fill = fill_set
self.__wb[sheet_name].cell(row=rows, column=i).font = font_set

def pattern_fill_cell(self, row, column):
# 将单元格标红
sheet_name = self.__sheet_name
fill_set = PatternFill("solid", fgColor="00FF0000")
font_set = Font(bold=True)
self.__wb[sheet_name].cell(row, column).border = self.my_border('thin', 'thin', 'thin', 'thin')
self.__wb[sheet_name].cell(row, column).fill = fill_set
self.__wb[sheet_name].cell(row, column).font = font_set

def set_border_cells(self,rows,columns):
"""将rows行加粗"""
sheet_name = self.__sheet_name
fill_set = PatternFill("solid", fgColor="00FF0000")
font_set = Font(bold=True)
for i in range(1, columns + 1):
self.__wb[sheet_name].cell(row=rows, column=i).font = font_set

def write_cell(self, row, column, value):
sheet_name = self.__sheet_name
if isinstance(row, int) and isinstance(column, int):
try:
cell_obj = self.__wb[sheet_name].cell(row, column)
cell_obj.value = value
except Exception as e:
raise e
else:
raise TypeError('row and column must be type int')

def write_sheet(self, data, title=None, value=None, bold=False):
sheet_name = self.__sheet_name
if title:
# 添加表头(不需要表头可以不用加)
self.write_sheet_title(title)
# 开始遍历数组
for row_index, row_item in enumerate(data): # 获取索引和内容
for col_index, col_item in enumerate(row_item):
# 写入
self.__wb[sheet_name].cell(row=row_index + 2, column=col_index + 1, value=col_item)


def write_sheet_from_column(self, data, column, title=None, value=None, bold=False):
sheet_name = self.__sheet_name
if title:
# 添加表头(不需要表头可以不用加)
self.write_sheet_title(title)
# 开始遍历数组
for row_index, row_item in enumerate(data): # 获取索引和内容
for col_index, col_item in enumerate(row_item):
# 写入
self.__wb[sheet_name].cell(row=row_index + 2, column=col_index +1 + column, value=col_item)


def write_sheet_from_positive_cell_row(self, data, row=2, column=1, title=None, value=None, bold=False):
sheet_name = self.__sheet_name
if title:
# 添加表头(不需要表头可以不用加)
self.write_sheet_title(title)
# 开始遍历数组
for row_index, row_item in enumerate(data): # 获取索引和内容
for col_index, col_item in enumerate(row_item):
# 写入
self.__wb[sheet_name].cell(row=row_index + row, column=col_index + column, value=col_item)

def write_sheet_from_positive_cell_col(self, data, row=2, column=1, title=None, value=None, bold=False):
sheet_name = self.__sheet_name
if title:
# 添加表头(不需要表头可以不用加)
self.write_sheet_title(title)
# 开始遍历数组
for col_index, row_item in enumerate(data): # 获取索引和内容
for row_index, col_item in enumerate(row_item):
# 写入
self.__wb[sheet_name].cell(row=row_index + row, column=col_index + column, value=col_item)


def write_sheet_title(self,sheet_title):
# 添加表头
sheet_name = self.__sheet_name
for i in range(len(sheet_title)):
self.__wb[sheet_name].cell(row=1, column=i + 1, value=sheet_title[i])

def write_row_data(self,rows, sheet_data):
# 追加一行数据
sheet_name = self.__sheet_name
for i in range(len(sheet_data)):
self.__wb[sheet_name].cell(row=rows + 1, column=i+1, value=sheet_data[i])

def write_column_data(self,columns, sheet_data):
# 向max_column右边追加一列数据,从第二行开始写
sheet_name = self.__sheet_name
for i in range(len(sheet_data)):
self.__wb[sheet_name].cell(row=i + 2, column=columns+1, value=sheet_data[i])

def write_column_data2(self,columns, sheet_data):
# 向max_column右边追加一列数据,从第一行开始写
sheet_name = self.__sheet_name
for i in range(len(sheet_data)):
self.__wb[sheet_name].cell(row=i + 1, column=columns+1, value=sheet_data[i])

def delete_row_v(self, rows, columns, values):
sheet_name = self.__sheet_name
for i in range(2, rows +1):
if self.__wb[sheet_name].cell(row = i, column = columns).value == values:
self.__wb[sheet_name].delete_rows(i, 1)

def delete_row(self, row):
sheet_name = self.__sheet_name
self.__wb[sheet_name].delete_rows(row, 1)

def delete_all_value(self,row=1):
sheet_name = self.__sheet_name
max_row = self.get_max_row_num()
self.__wb[sheet_name].delete_rows(row,max_row)
# for i in range(max_row+1):
# self.__wb[sheet_name].delete_rows(i)

def get_tail10_value(self, row_num , num):
"""获取后10行的数据"""
sheet_name = self.__sheet_name
column_num = self.get_max_column_num()
if isinstance(row_num, int):
if row_num < num:
for row in range(2, row_num + 1):
row_value = []
for column in range(1, column_num + 1):
values_row = self.__wb[sheet_name].cell(row, column).value
row_value.append(values_row)
print(row_value)
else:
for row in range(row_num - num, row_num + 1):
row_value = []
for column in range(1, column_num + 1):
values_row = self.__wb[sheet_name].cell(row, column).value
row_value.append(values_row)
print(row_value)
else:
raise TypeError('row must be type int')

def get_bf_value(self, row_num):
"""获取后10行的数据"""
sheet_name = self.__sheet_name
column_num = self.get_max_column_num()
if isinstance(row_num, int):
row_value = []
for row in range(row_num - 11, row_num + 1):
col_value = []
for column in range(1, column_num + 1):
value_col = self.__wb[sheet_name].cell(row, column).value
col_value.append(value_col)
row_value.append(col_value)
return row_value
else:
raise TypeError('row must be type int')

def get_bf_value2(self, row_num):
"""获取后10行的数据"""
sheet_name = self.__sheet_name
column_num = self.get_max_column_num()
if isinstance(row_num, int):
row_value = []
for row in range(row_num - 11, row_num + 1):
col_value = []
for column in range(1, column_num + 1):
value_col = self.__wb[sheet_name].cell(row, column).value
if type(value_col) == int:
value_col = "%.2f%%" % (value_col * 100)
col_value.append(value_col)
row_value.append(col_value)
return row_value
else:
raise TypeError('row must be type int')

def get_bf_value3(self,row_begin, row_end, col_begin, col_end):
"""获取后10行的数据"""
sheet_name = self.__sheet_name
# column_num = self.get_max_column_num()
if isinstance(row_end, int):
row_value = []
for row in range(row_begin, row_end + 1):
col_value = []
for column in range(col_begin, col_end + 1):
value_col = self.__wb[sheet_name].cell(row, column).value
if type(value_col) == float:
value_col = "%.2f%%" % (value_col * 100)
col_value.append(value_col)
row_value.append(col_value)
return row_value
else:
raise TypeError('row must be type int')

def set_percent(self,row,column,row2=None, column2=None):
# 将单元格格式设置为百分比显示
sheet_name = self.__sheet_name
fill_set = PatternFill("solid", fgColor="00FF0000")
font_set = Font(bold=True)
if not row2:
row2 = self.get_max_row_num()
if not column2:
column2 = self.get_max_column_num()
for i in range(row, row2 + 1):
for j in range(column, column2 +1):
self.__wb[sheet_name].cell(i, j).number_format = '0.00%'

def set_percent_column(self,column):
# 将单元格格式设置为百分比显示
sheet_name = self.__sheet_name
max_row =self.get_max_row_num()
for i in range(1,max_row + 1):
self.__wb[sheet_name].cell(i, column).number_format = '0.00%'

def sum_column(self,column):
# 对列中数字类型单元格进行求和,将结果写在列的最后一行
data = self.get_column_value(column)
sum = 0
for i in data:
if type(i) == int:
sum += i
return sum

def set_date_row(self,row):
# 将单元格格式设置为百分比显示
sheet_name = self.__sheet_name
# 设置字体
font_set = Font(name='宋体', color='000000', size=12, bold=True)
max_col =self.get_max_column_num()
for i in range(1,max_col + 1):
self.__wb[sheet_name].cell(row, i).font = font_set
self.__wb[sheet_name].cell(row, i).number_format = 'mm月dd日'

def merge_cells(self,loc1,loc2): # loc1示例:'A1'
# 合并单元格
sheet_name = self.__sheet_name
# 设置对齐方式,水平是右对齐,垂直是居中
align = Alignment(horizontal='center', vertical='center', wrap_text=True)
# 设置字体
font_set = Font(name='宋体', color='000000', size=11, bold=False)
self.__wb[sheet_name].merge_cells(loc1+":"+loc2)
# self.__wb[sheet_name].merge_cells(start_row=2, start_column=1, end_row=4, end_column=4) # 与上一条等价
self.__wb[sheet_name][loc1].alignment = align
self.__wb[sheet_name][loc1].font = font_set

def unmerge_cells(self,sr,sc,er,ec):
# 分割单元格
sheet_name = self.__sheet_name
# self.__wb[sheet_name].unmerge_cells('A2:D2')
# self.__wb[sheet_name].unmerge_cells(start_row=2, start_column=1, end_row=4, end_column=4) # 与上一条等价
self.__wb[sheet_name].unmerge_cells(start_row=sr, start_column=sc, end_row=er, end_column=ec)

def add_image(self,img_path,loc): # loc示例:'A1'
# 插入图像
sheet_name = self.__sheet_name
# 制作一个图片
img = Image(img_path)
# 在单元格旁边添加工作表和锚
self.__wb[sheet_name].add_image(img, loc)

def column_dimensions(self,col_name1,col_name2): # col_name1示例:'A'
# 折叠列
sheet_name = self.__sheet_name
self.__wb[sheet_name].column_dimensions.group(col_name1,col_name2, hidden=True)

def add_formula(self,loc,formula):
# 添加公式
sheet_name = self.__sheet_name
self.__wb[sheet_name][loc] = formula # ws["A1"] = "=SUM(1, 1)"


def add_line_chart(self,title, min_row, min_col, line_name, save_img, max_row=None, max_col=None):
sheet_name = self.__sheet_name
self.chart = LineChart()
self.chart.title = title # 图的标题
self.chart.style = 2 # 线条的style
self.chart.width = 17
self.chart.height = 6.5
# self.chart.y_axis.title = '次数' # y坐标的标题
# self.chart.x_axis.number_format = 'mm-dd' # 规定日期格式 这是月,年格式
# self.chart.x_axis.majorTimeUnit = "Months" # 规定日期间隔 注意days;Months大写
# self.chart.x_axis.title = "时间点" # x坐标的标题
if not max_row:
max_row = self.get_max_row_num()
if not max_col:
max_col = self.get_max_column_num()

for i in range(2,max_col+1):
# 选中要画图的数据列(Y轴)
data = Reference(self.__wb[sheet_name], min_col=i, min_row=min_row, max_col=i,
max_row=max_row) # 图像的数据 起始行、起始列、终止行、终止列
seriesObj = Series(data, title="'" + line_name[i - 2])
self.chart.append(seriesObj)
# self.chart.add_data(data, titles_from_data=True, from_rows=True)
# 指定X轴选取的数据列
dates = Reference(self.__wb[sheet_name], min_col=1, min_row=min_row, max_col=1, max_row=max_row)
self.chart.set_categories(dates)
self.__wb[sheet_name].add_chart(self.chart, save_img) # 将图表添加到 sheet中的:A1

其中有一部分内容:向excel中输入公式,仅适用于结论,不适用于中间处理过程,若excel公式处理结果还被调用的话,需要另想他法。

本文转载自: 掘金

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

使用 Laravel Horizon 优雅的终止进程(1)

发表于 2021-10-25

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

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

抛出问题

我们发布新版本代码时,如果优雅的终止运行中的异步任务,规避异步任务运行到一半被kill掉的情况。

解决办法

通过调研之后发现,laravel 的 horizon 扩展可以解决这个问题,使用下面的命令可以优雅的结束进程:

  1. 确保进行中进程不会被kill掉,执行结束后才允许被kill;
  2. 非进行的任务等候,不会加入到队列中;

优雅的解决了这个问题,思路就是这么的朴实无华。

1
c复制代码php artisan horizon:terminate

下面开始隆重介绍 Horizon

简介

Horizon 提供了一个漂亮的仪表盘,可以通过代码配置 Laravel Redis 队列,同时它允许你轻易的监控你的队列系统中诸如任务吞吐量,运行时间和失败任务等关键指标。

image.png

image.png

安装

注意:确保 queue 配置文件中设置了 redis 队列驱动。

使用 Composer 为 Laravel 项目安装 Horizon:

1
bash复制代码composer require laravel/horizon

安装完成后,使用 horizon:install 发布 Artisan 命令:

1
复制代码php artisan horizon:install

需要创建 failed_jobs 表,Laravel 将使用该表来存储任何 failed queue jobs:

1
2
3
arduino复制代码php artisan queue:failed-table

php artisan migrate

升级 Horizon

当准备升级到 Horizon 的一个新的主要版本之前,一定好好看看更新声明,熟悉新特性,结合业务场景思考有没有坑。

注意,升级之后记得要重新发布 Horizon 的资源文件:

1
复制代码php artisan horizon:assets

配置

发布 Horizon 相关文件后,他的主要配置文件会放在 config/horizon.php。

可以在这个文件中配置队列相关选项,并且每个配置项都有详细的使用说明。

注意:确保 horizon 配置文件的 environments 部分包含你计划在其上运行 Horizon 的每个环境的条目。

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
dart复制代码'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => [
'high_{0}',
],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 2,
'tries' => 1
],
],

'test' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => [
'high_{0}',
],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 2,
'tries' => 1
],
],

'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => [
'high_{0}',
],
'balance' => 'auto',
'minProcesses' => 3,
'maxProcesses' => 11,
'tries' => 1
],
],
],

均衡配置

Horizon 提供了三种均衡策略: simple , auto ,和 false 。

  1. 默认的是 simple , 会将收到的任务均分给队列进程:
1
ini复制代码'balance' => 'simple',
  1. auto 策略会根据当前的工作量调整每个队列的工作进程任务数量。

例如:如果 A 队列有 1000 个待执行任务,但是你的 B 队列是空的,
Horizon 会分配更多的工作进程给 B 队列,直到 A 队列中所有任务执行完成。

  1. 当配置项 balance 配置为 false ,Horizon 会使用 Laravel 默认执行行为,它将按照配置中列出的顺序处理队列任务。

当使用 auto 策略时,您可以定义 minProcesses 和 maxProcesses 配置选项,以控制最小和最大进程数范围应向上和向下扩展到:

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
],

失败任务记录

horizon 配置文件允许你配置应保留最近和失败任务的时间(以分钟为单位)。 默认情况下,最近的任务保留一小时,而失败的任务保留一周:

1
2
3
4
dart复制代码'trim' => [
'recent' => 60,
'failed' => 10080,
],

image.png

仪表盘权限验证

Horizon 仪表盘路由是 /horizon 。

默认情况下,你只能在 local 环境下访问仪表盘。

在你的 app/Providers/HorizonServiceProvider.php 文件中,有一个 gate 方法。

这里授权控制 非本地 环境中对 Horizon 的访问。

可以根据需要随意修改此门面,以限制对 Horizon 安装的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
php复制代码/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*
* @return void
*/
protected function gate()
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'xxxx@xxx.com',
]);
});
}

注意:记住,Laravel 会自动将 authenticated 用户注入到 gate 方法。

如果应用程序通过其他方法(如 IP 限制)提供 Horizon 安全,那么 Horizon 用户可能不需要 “登录”。

总结

文章开头抛出的问题是我开发过程中没考虑到的,

初识文档也没体会到 php artisan horizon:terminate 的妙处。

随着项目的不断成长,自己的技术水平和编程思想也在随之成长。

下期再见

下一篇文章会总结 Horizon 进阶知识点,包括:安装、部署、标签、通知、Metrics等。

本文转载自: 掘金

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

1…469470471…956

开发者博客

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