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

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


  • 首页

  • 归档

  • 搜索

MySql存储过程和函数 流程控制结构

发表于 2021-11-02

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

存储过程

含义:一组预先编译好的的sql语句的集合,理解成批处理语句

1、提高代码的重用性

2、简化操作

3、减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率

创建语法

1
2
3
4
sql复制代码CREATE PROCEDURE 存储过程名(参数列表)
BEGIN
存储过程体(一组合法的sql语句)
END

注意:

1、参数列表包含三部分

参数模式 参数名 参数类型

举例:

1
scss复制代码IN stuname VARCHAR(20)

参数模式:

IN: 该参数可以作为输入,也就是该参数需要调用方传入值

OUT: 该参数可以作为输出,也就是该参数可以作为返回值

INOUT: 该参数既可以作为输入又可以作为输出,也就是该参数既需要传入值,又可以返回值

2、如果存储过程体仅仅只有一句话,BEGIN END可以省略

存储过程体的每条sql语句的结尾要求必须加分号。

存储过程的结尾可以使用DELIMITER重新设置

存储语法:

DELIMITER 结束标记

具体使用:

DELIMITER $

调用语法:

call 存储过程名(实参列表);

创建带in模式参数的存储过程

image.png

创建带out模式参数的存储过程

image-20211031233943287

image.png
创建带inout模式参数的存储过程

image.png

删除存储过程
语法:drop procedure存储过程名

1
sql复制代码DROP PROCEDURE p1;

查看存储过程的信息

1
sql复制代码SHOW CREATE PROCEDURE myp2;

函数

一、含义:

一组预先编译好的额sql语句的集合,理解成批处理语句

1、提高代码的重用性

2、简化操作

3、减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率

与存储过程的区别:

存储过程:可以有0个返回,也可以有多个返回,适合做批量插入、批量跟新

函数:有且仅有1个返回,适合做处理数据后返回一个结果

创建语法

1
2
3
4
sql复制代码CREATE FUNCTION 函数名(参数列表) RETURNS 返回类型
BEGIN
函数体
END

注意:

1、参数列表 包含两部分:

函数名 参数类型

2、函数体:肯定会有return语句,如果没有会报错

如果return 语句没有放在函数体的最后也不报错,但不建议return 值;

3、函数体中仅有一句话,则可以省略begin end

4、使用 delimiter 语句设置结束标记

二、调用语法

1
sql复制代码SELECT 函数名(函数列表)

三、查看函数

1
sql复制代码SHOW CREATE FUNCTION myf3;

四、删除函数

1
sql复制代码DROP FUNCTION myf3

案例:

1
2
3
4
5
6
7
8
9
10
sql复制代码#创建函数,实现传入两个float,返回二者之和
CREATE FUNCTION MY2(A DOUBLE,B DOUBLE) RETURN DOUBLE
BEGIN

DECLARE RESULT INT DEFAULT 0;
SELECT A+B INTO RESULT;
SET RESULT=A+B;
RETURN RESULT;
​
END $

流程控制结构

顺序结构:程序从上往下依次执行

分支结构:程序从两条或多条路径中选择一条去执行

循环结构:程序在满足一定条件的基础上,重复执行一段代码

一、分支结构

if函数

功能:实现简单的双分支

语法:

if(表达式1,表达式2,表达式3)

执行顺序:

如果表达式1成立,则if函数返回表达式2的值,否则返回表达式3的值

应用:任何地方

if结构

功能:实现多重分支

语法:

1
2
3
4
5
arduino复制代码if 条件1 then 语句1;
if 条件 2 then 语句2;
...
else 语句n
end if;

应用在begin end语句块中

case结构

情况1:类似于java中的switch语句,一般用于实现等值判断

语法:

1
2
3
4
5
6
sql复制代码                    CASE   变量|表达式|字段
WHEN   要判断的值 THEN 返回的值1或语句1;
WHEN   要判断的值 THEN 返回的值2或语句1;

ELSE   要返回的值n或语句1;
END CASE;

情况2:类似于java中的多重if语句,一般用于区间判断

语法:

1
2
3
4
5
6
sql复制代码                    CASE
WHEN 要判断的条件1 THEN 返回的值1或语句1;
WHEN 要判断的条件2 THEN 返回的值2或语句1;

ELSE 要返回的值n或语句1;
END CASE;

特点:

1、可以作为表达式,嵌套在其他语句中使用,可以放置在任何地方,BEGIN END中或BEGIN END的外面

可以作为独立的语句去使用,只能放在BEGIN END中

2、如果WHEN中的值满足或条件成立,则执行对应的THEN后面的语句,并且技术CASE

如果都不满足,则执行ELSE中的语句或值

3、ELSE可以省略,如果省略,并且所有的WHEN条件都不满足,则返回NULL

案例:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE PROCEDURE test_case(IN score INT)
BEGIN
CASE
WHEN score>=90 AND score<=100 THEN SELECT 'A';
WHEN score>=80 THEN SELECT 'B';
WHEN score>=60 THEN SELECT 'C';
ELSE SELECT 'D';
END CASE;
END $
​
CALL test_case(95)$

二、循环结构

分类:

while、loop、repeat

循环控制:

iterate 类似于continue 表示继续的意思,结束本次循环 继续下一次

leave 类似于 break跳出 结束当前所在的循环

while结构

语法:

1
2
3
4
5
arduino复制代码【标签】 while 循环条件 do
​
循环体;
​
end while 【标签】

loop结构

语法:

1
2
3
arduino复制代码【标签】 loop
循环体;
end loop 【标签】;

注意:这个循环不会自己结束,需要配合循环控制;常用来模拟死循环;

repeat结构

语法:

1
2
3
4
lua复制代码【标签】 repeat
循环体;
until 结束循环的条件
end repeat 【标签】;

例题:

使用leave控制结构

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码DELIMITER $
DROP PROCEDURE test$
CREATE PROCEDURE test(IN insertcount INT)
BEGIN
DECLARE i INT DEFAULT 0;
a:WHILE i<=insertcount DO
INSERT INTO admin(username ,password) VALUES(CONCAT('小花',i),001);
IF(i>=20) THEN LEAVE a;
END IF;
set insertcount=insertcount+1;
 END WHILE A;
​
END $

使用iterate控制结构

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码DROP PROCEDURE test1$
CREATE PROCEDURE test(IN insertcount INT)
BEGIN
DECLARE i INT DEFAULT 0;
a:WHILE i<=insertcount DO
SET i=i+1;
IF MOD(i,2)!=0 THEN ITERATE a;
END IF;
INSERT INTO admin(username ,password) VALUES(CONCAT('小花',i),001);
 END WHILE a;
​
END $
CALL test(20)$

本文转载自: 掘金

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

Django-ORM详解-字段、属性、操作 Django-O

发表于 2021-11-02

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

Django-ORM详解-字段、属性、操作

常用字段类型

CharField:字符类型,必须提供max_length参数,max_length表示字符长度。

Email Field:邮箱类型,实际上是字符类型,只是提供了邮箱格式检验。

Text Field:文本类型,存储大段文本字符串。字符串如果超过254 个字符建议使用Text Field。

Integer Field:整数类型。

Date Field:日期字段。

Time Field:时间字段。

Date Time Field:日期时间字段,合并了日期字段与时间字段。

File Field:实际上是字符串类型,用来把上传的文件的路径保存在数据库中。

Image Field:实际上是字符串类型,用来把上传的图片的路径保存在数据库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码name=models.Char Field(max_length=32,verbose_name='姓名')
verbose_name在Django Admin管理后台是字段的显示名称,可理解为字段别名,verbose_name在SQL层面没有具体的体现,也就是说加不加verbose_name对数据库中的字段没影响

email=models.Email Field(verbose_name='邮箱')
descript=models.Text Field(verbose_name="简介")
int= models.Integer Field()
date=models.Date Field(auto_now=True, auto_now_add=False)
auto_now参数自动保存当前时间,一般用来表示最后修改时间。在第一次创建记录的时候, Django将auto_now_add字段值自动设置为当前时间,用来表示记录对象的创建时间。

time= models.Time Field(auto_now=False,auto_now_add=False)
datetime=models.Date Time Field(auto_now=False,auto_now_add=False)
filetest =models.Fiel Field (upload_to = 'test/')
picture = models.Image Field(upload_to = 'pic/')

常用字段属性

db_index:db_index=True表示设置此字段为数据库表的索引。

title = models.Char Field(max_length=32, db_index=True)

unique:unique=True表示该字段在数据库表中不能有重复值。

default:设置字段默认值,如default=’good’。

auto_now_add:Datetime Field、Date Field、Time Field 这3 种字段的独用属性, auto_now_add=True表示把新建该记录的时间保存为该字段的值。

auto_now:Datetime Field、Date Field、Time Field这3种字段的独用属性,auto_now= True表示每次修改记录时,把当前时间存储到该字段。

ORM基本数据操作

增加记录

方式一:

1
python复制代码new_emp= models.employee.objects.create(name="tom",email="tom@163.com",dep_id=66)

方式二:

1
2
python复制代码new_emp= models.employee (name="tom",email="tom@163.com",dep_id=66)
new_emp.save()

删除记录

用filter()过滤出符合条件的记录后调用delete()删除

1
python复制代码models. employee.objects.filter(name= "张三").delete()

修改记录

将指定条件的记录更新,并更新指定字段的值

1
python复制代码models.employee.objects.filter(name='tom').update(email="tom2@163.com")

修改单条数据

1
2
3
python复制代码obj = models.employee.objects.get(id=66)
obj.email = "tom2@sina.com"
obj.save()

查询

获取全部

1
python复制代码Emp_list= models.employee.objects.all()

获取单条数据,数据不存在则报错

1
python复制代码Emp=models.employee.objects.get(id=123)

获取指定条件的记录集

1
python复制代码Emp_group=models. employee.objects.filter(name= "张三")

Django ORM数据操作常用函数

all()函数,返回符合条件的全部记录。

1
python复制代码objects = models.employee.objects.all()

filter()函数,返回指定条件的记录。filter后面的括号内为过滤条件,类似于SQL中语句where后面的条件语句。

1
python复制代码objects = models.employee.objects.filter(name='tom')
1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码# 获取name字段包含“Tom”的记录
models. employee.objects.filter(name__contains="Tom")
# 获取name字段包含“tom”的记录,icontains忽略大小写models.employee.objects.filter(name__icontains="tom")
# 获取employee数据表中id等于10、20、66的数据
models. employee.objects.filter(id__in=[10, 20, 66])
# 获取employee数据表中id不等于10、20、66的记录,因为前面用的是exclude
models. employee.objects.exclude(id__in=[10, 20, 66])。
# 获取employee数据表中id大于1 且 小于10的记录,两个过滤条件的关系等价于SQL的and
models. employee.objects.filter(id__gt=1, id__lt=10)
# 获取employee数据表中id在范围1~66内的记录,等价于SQL的id bettwen 1and 66
models. employee.objects.filter(id__range=[1, 66])
# 获取employee数据表中birthday字段中月份为9月的记录,birthday为日期格式
models. employee.objects.filter(birthday__month=9)

order_by()函数,按照order_by后面括号中的字段排序。

1
2
3
python复制代码objects =models.employee.objects.exclude(name='tom').order_by('name','id')
# 字段名中加“-”,表示按该字段倒序排列。如下代码表示,按name字段倒序排列列表。
objects = models.employee.objects.order_by('-name')

distinct()函数,去掉记录集合中完全一样的记录(重复记录),然后返回这个记录集。

1
python复制代码objects = models.employee.objects.filter (name='tom').distinct()

其他函数

values()函数,返回一个字典类型序列。

1
python复制代码objects = models.employee.objects.values('id','name','email')

values_list()函数,返回一个元组类型序列。

1
python复制代码objects = models.employee.objects.values_list('id','name','email')

get()、first()、last()返回单个对象,可以理解为返回数据表中的一条记录。

1
2
3
4
5
6
7
8
python复制代码# 返回id为1的记录,括号内是过滤条件
object1 = models.employee.objects.get(id=1)
# 返回数据集的第一条记录
object2 = models.employee.objects.first()
# 返回数据集的最后一条记录
object3 = models.employee.objects.last()
# 返回数据集的个数
bject4= models.employee.objects.count()

本文转载自: 掘金

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

Java 里的枚举

发表于 2021-11-02

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

枚举类是一个小但有用的结构。

基本概念

枚举类型的英文是:enumerated type。
在 Java 里是一组特殊的类,用 enum 进行声明。
声明的是一组常量。

说明 Java运行的时候,最开始就会把它放在方法区里加载好。

在 main函数 里调用,直接打印就是它本身。

一般我们对于常用的常量,要么就单独建一个类,叫 Constant, 然后在类里这样定义:

1
2
3
4
Java复制代码public final String SPRING = "SPRING";
public final String SUMMER = "SUMMER";
public final String AUTUMN = "AUTUMN";
public final String WINTER = "WINTER";

通过 Constant.XXX 来调用,如定义春天就是:

String Season = Constant.SPRING 这样把 “SPRING” 赋给了 Season 字符串。

但如果 .XXX 中的 XXX 是一个有限集合。像上面的四季,就可以用枚举来减少冗余。

话不多说,来看代码

上面的例子如果在 enum 里就是这样写的:

1
2
3
Java复制代码enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}

类似的,定义星期:

1
2
3
Java复制代码enum WEEK {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

当然还有月份,感兴趣的同学可以自己写写。

怎么用枚举类型呢?

就用 Switch 就好了。这点我和高老师的想法是非常一致的。枚举本来就是为了简单而存在的。非要整枚举的高级方法,还不如新建一个类。

枚举类,虽然特殊了点,但也是一个类。既然是类,就得先声明,再调用。

在刚刚定义了两个 enum 类之后,我们在这个文件里的 main函数里,可以以「类名 对象名 = 类.某一个枚举类型」来初始化一个枚举对象。

1
2
3
4
5
6
7
8
9
10
Java复制代码public class TestEnum {
public static void main(String[] args) {

Season a = Season.AUTUMN;
System.out.println("It is " + a + ".");

WEEK today = WEEK.TUESDAY;
System.out.println("Today is " + today + ".");
}
}

要注意的事情:
枚举的顺序是按照在 enum 里写的顺序来的。

这点和 Python里的 enumerate 非常像。
Python 中对一个可以进行枚举的对象用 for 循环遍历键值对:

1
2
Python复制代码for k,v in enumerate("happy"):
print(k,":",v)

运行结果为:
image.png
可以看到,默认为每一个抛出来的值,匹配的键就是从 0 开始递增的。

枚举 + Switch

来看看枚举大显身手的时刻:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java复制代码
Season a = Season.AUTUMN;
System.out.println("It is " + a + ".");

switch (a) {
case SPRING:
System.out.println("春天来了,又到了动物XX的季节");
break;
case SUMMER:
System.out.println("夏天来了,又到了游泳的季节");
break;
case AUTUMN:
System.out.println("秋天来了,又开学了");
break;
case WINTER:
System.out.println("冬天来了,又到了一天睡12个小时的季节");
break;
}

运行结果:
image.png

本文转载自: 掘金

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

Java 开发必备! I/O与Netty原理精讲(建议收藏)

发表于 2021-11-02

I/O技术在系统设计、性能优化、中间件研发中的使用越来越重要,学习和掌握I/O相关技术已经不仅是一个Java攻城狮的加分技能,而是一个必备技能。本文将带你了解BIO/NIO/AIO的发展历程及实现原理,并介绍当前流行框架Netty的基本原理。

一 Java I/O模型

1 BIO(Blocking IO)

BIO是同步阻塞模型,一个客户端连接对应一个处理线程。在BIO中,accept和read方法都是阻塞操作,如果没有连接请求,accept方法阻塞;如果无数据可读取,read方法阻塞。

image-20211102164418322

2 NIO(Non Blocking IO)

NIO是同步非阻塞模型,服务端的一个线程可以处理多个请求,客户端发送的连接请求注册在多路复用器Selector上,服务端线程通过轮询多路复用器查看是否有IO请求,有则进行处理。

image-20211102164428536

NIO的三大核心组件:

Buffer:用于存储数据,底层基于数组实现,针对8种基本类型提供了对应的缓冲区类。

Channel:用于进行数据传输,面向缓冲区进行操作,支持双向传输,数据可以从Channel读取到Buffer中,也可以从Buffer写到Channel中。

Selector:选择器,当向一个Selector中注册Channel后,Selector 内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel,也可以说管理多个网络连接,因此,Selector也被称为多路复用器。当某个Channel上面发生了读或者写事件,这个Channel就处于就绪状态,会被Selector监听到,然后通过SelectionKeys可以获取就绪Channel的集合,进行后续的I/O操作。

Epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

3 AIO(NIO 2.0)

AIO是异步非阻塞模型,一般用于连接数较多且连接时间较长的应用,在读写事件完成后由回调服务去通知程序启动线程进行处理。与NIO不同,当进行读写操作时,只需直接调用read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。

二 I/O模型演化

1 传统I/O模型

对于传统的I/O通信方式来说,客户端连接到服务端,服务端接收客户端请求并响应的流程为:读取 -> 解码 -> 应用处理 -> 编码 -> 发送结果。服务端为每一个客户端连接新建一个线程,建立通道,从而处理后续的请求,也就是BIO的方式。

image-20211102172948707

这种方式在客户端数量不断增加的情况下,对于连接和请求的响应会急剧下降,并且占用太多线程浪费资源,线程数量也不是没有上限的,会遇到各种瓶颈。虽然可以使用线程池进行优化,但是依然有诸多问题,比如在线程池中所有线程都在处理请求时,无法响应其他的客户端连接,每个客户端依旧需要专门的服务端线程来服务,即使此时客户端无请求,也处于阻塞状态无法释放。基于此,提出了基于事件驱动的Reactor模型。

2 Reactor模型

Reactor模式是基于事件驱动开发的,服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分发(Dispatch给某进程),这是编写高性能网络服务器的必备技术之一。

Reactor模式以NIO为底层支持,核心组成部分包括Reactor和Handler:

  • Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对I/O事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
  • Handlers:处理程序执行I/O事件要完成的实际事件,Reactor通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。类似于客户想要与之交谈的公司中的实际员工。

根据Reactor的数量和Handler线程数量,可以将Reactor分为三种模型:

  • 单线程模型 (单Reactor单线程)
  • 多线程模型 (单Reactor多线程)
  • 主从多线程模型 (多Reactor多线程)

单线程模型

image-20211102173024506

Reactor内部通过Selector监控连接事件,收到事件后通过dispatch进行分发,如果是连接建立的事件,则由Acceptor处理,Acceptor通过accept接受连接,并创建一个Handler来处理连接后续的各种事件,如果是读写事件,直接调用连接对应的Handler来处理。

Handler完成read -> (decode -> compute -> encode) ->send的业务流程。

这种模型好处是简单,坏处却很明显,当某个Handler阻塞时,会导致其他客户端的handler和accpetor都得不到执行,无法做到高性能,只适用于业务处理非常快速的场景,如redis读写操作。

多线程模型

image-20211102173104756

主线程中,Reactor对象通过Selector监控连接事件,收到事件后通过dispatch进行分发,如果是连接建立事件,则由Acceptor处理,Acceptor通过accept接收连接,并创建一个Handler来处理后续事件,而Handler只负责响应事件,不进行业务操作,也就是只进行read读取数据和write写出数据,业务处理交给一个线程池进行处理。

线程池分配一个线程完成真正的业务处理,然后将响应结果交给主进程的Handler处理,Handler将结果send给client。

单Reactor承担所有事件的监听和响应,而当我们的服务端遇到大量的客户端同时进行连接,或者在请求连接时执行一些耗时操作,比如身份认证,权限检查等,这种瞬时的高并发就容易成为性能瓶颈。

主从多线程模型

image-20211102173123531

存在多个Reactor,每个Reactor都有自己的Selector选择器,线程和dispatch。

主线程中的mainReactor通过自己的Selector监控连接建立事件,收到事件后通过Accpetor接收,将新的连接分配给某个子线程。

子线程中的subReactor将mainReactor分配的连接加入连接队列中通过自己的Selector进行监听,并创建一个Handler用于处理后续事件。

Handler完成read -> 业务处理 -> send的完整业务流程。

关于Reactor,最权威的资料应该是Doug Lea大神的Scalable IO in Java,有兴趣的同学可以看看。

三 Netty线程模型

Netty线程模型就是Reactor模式的一个实现,如下图所示:

image-20211102173143532

1 线程组

Netty抽象了两组线程池BossGroup和WorkerGroup,其类型都是NioEventLoopGroup,BossGroup用来接受客户端发来的连接,WorkerGroup则负责对完成TCP三次握手的连接进行处理。

NioEventLoopGroup里面包含了多个NioEventLoop,管理NioEventLoop的生命周期。每个NioEventLoop中包含了一个NIO Selector、一个队列、一个线程;其中线程用来做轮询注册到Selector上的Channel的读写事件和对投递到队列里面的事件进行处理。

Boss NioEventLoop线程的执行步骤:

  • 处理accept事件, 与client建立连接, 生成NioSocketChannel。
  • 将NioSocketChannel注册到某个worker NIOEventLoop上的selector。
  • 处理任务队列的任务, 即runAllTasks。

Worker NioEventLoop线程的执行步骤:

  • 轮询注册到自己Selector上的所有NioSocketChannel的read和write事件。
  • 处理read和write事件,在对应NioSocketChannel处理业务。
  • runAllTasks处理任务队列TaskQueue的任务,一些耗时的业务处理可以放入TaskQueue中慢慢处理,这样不影响数据在pipeline中的流动处理。

Worker NIOEventLoop处理NioSocketChannel业务时,使用了pipeline (管道),管道中维护了handler处理器链表,用来处理channel中的数据。

2 ChannelPipeline

Netty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的双向链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便的新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

ChannelPipeline是一系列的ChannelHandler实例,流经一个Channel的入站和出站事件可以被ChannelPipeline 拦截。每当一个新的Channel被创建了,都会建立一个新的ChannelPipeline并绑定到该Channel上,这个关联是永久性的;Channel既不能附上另一个ChannelPipeline也不能分离当前这个。这些都由Netty负责完成,而无需开发人员的特别处理。

根据起源,一个事件将由ChannelInboundHandler或ChannelOutboundHandler处理,ChannelHandlerContext实现转发或传播到下一个ChannelHandler。一个ChannelHandler处理程序可以通知ChannelPipeline中的下一个ChannelHandler执行。Read事件(入站事件)和write事件(出站事件)使用相同的pipeline,入站事件会从链表head 往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。

image-20211102173230802

ChannelInboundHandler回调方法:

image-20211102173242210

ChannelOutboundHandler回调方法:

image-20211102173252291

3 异步非阻塞

写操作:通过NioSocketChannel的write方法向连接里面写入数据时候是非阻塞的,马上会返回,即使调用写入的线程是我们的业务线程。Netty通过在ChannelPipeline中判断调用NioSocketChannel的write的调用线程是不是其对应的NioEventLoop中的线程,如果发现不是则会把写入请求封装为WriteTask投递到其对应的NioEventLoop中的队列里面,然后等其对应的NioEventLoop中的线程轮询读写事件时候,将其从队列里面取出来执行。

读操作:当从NioSocketChannel中读取数据时候,并不是需要业务线程阻塞等待,而是等NioEventLoop中的IO轮询线程发现Selector上有数据就绪时,通过事件通知方式来通知业务数据已就绪,可以来读取并处理了。

每个NioSocketChannel对应的读写事件都是在其对应的NioEventLoop管理的单线程内执行,对同一个NioSocketChannel不存在并发读写,所以无需加锁处理。

使用Netty框架进行网络通信时,当我们发起I/O请求后会马上返回,而不会阻塞我们的业务调用线程;如果想要获取请求的响应结果,也不需要业务调用线程使用阻塞的方式来等待,而是当响应结果出来的时候,使用I/O线程异步通知业务的方式,所以在整个请求 -> 响应过程中业务线程不会由于阻塞等待而不能干其他事情。

本文转载自: 掘金

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

Dubbo为何不适合大文本传输

发表于 2021-11-02
年初的时候朋友参加面试,回来和我探讨过这个问题。当时只是知道平时使用过程中确实遵守了这一约定,但是真的认真分析起原因,确实没什么头绪,没想到入手的地方。


几个月后的现在我想结合官方文档谈谈我对这个问题的理解。


笔者将从三个方面阐述dubbo不适合大文本传输的具体原因。

I/O限制

这里不仅仅是指网卡写入发出的I/O限制,同时包含网络带宽限制。我们从实际数据出发,计算一下单连接模型在大文本传输下的表现。


 模拟一下计算场景
  • 服务端配置1000兆网卡
  • 客户端配置50兆网络带宽
  • 单个大文本占用512字节

⚠️: 运营商公司,网卡生产方往往单位都是M Bit,而非M Byte

整合单位之后的数据应该是:
  • 1000M Bit ≈ 128 M Byte
  • 50 M Bit ≈ 7 M Byte
通过计算的单个服务提供者的 TPS(每秒处理事务数)最大为:128M Byte / 512K Byte = 256。单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte / 512K Byte = 14。


假设服务端、客户端的CPU能力计算完全可以应付,那么最后服务端的瓶颈在于网卡写出速度,而客户端的瓶颈则在于网络带宽,如果使用者能接受这种TPS,那么可以酌情考虑使用。

序列化限制

Dubbo这一类的Rpc框架执行远程调用的本质就是消费者将执行上下文通过网络IO传输给提供者,提供者能够通过上下文查找到虚拟机内部服务对象,然后通过反射执行对应逻辑最后将结果返还给消费端。


在Dubbo中执行上下文其实就是Invocation,Invocation中会提供明确的调用信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/* 节选部分代码 */
public interface Invocation {

/* 获取方法名 */
String getMethodName();

/* 获取接口名 */
String getServiceName();

/* 获取参数类型数组 */
Class<?>[] getParameterTypes();

/* 获取调用参数 */
Object[] getArguments();

}
消费者在发生远程调用时会将Invocation通过序列化协议转化为字节数组,而提供者则相反,将网络IO过来的字节数组反序列化为对象(decode过程其实比这个复杂的多,笔者省略掉跟本文无关的内容)。


实际处理就是decode过程中解析出此数据包的大小之后需要等待该包所有字节到达然后参与序列化,此时数据全部在内存中,序列化出来的大对象也占用内存,于我们的服务器而言,并不想看到这种情况。

Channel的限制

Dubbo默认使用Netty来实现网络传输,Netty其实还是基于Java NIO,为了接收方能够方便快捷的进行拆包其实要求Channel的写入事件是根据顺序排队处理的。


那么在单连接下,多个请求共用一个Channel实现数据写入;当多个请求到达,如果报文过大,会导致Channe一直在发送这个报文,其他请求的报文写入事件会进行排队,迟迟无法发送,当然也就迟迟没有响应,最坏的情况就是大量请求堆积、超时。

本文转载自: 掘金

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

使用反射摆脱了过长的if/else校验 使用反射摆脱了过长的

发表于 2021-11-02

使用反射摆脱了过长的if/else校验

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

引言

最近做一个业务,需要做额度限制,超出额度做特殊业务处理,封装了一个方法专门做额度限制的校验,由于需要判断的金额字段太多导致if/else判断也随之增加,所以想到使用反射去做处理,减少代码量的同时,后续扩展性也比较强。

业务实体

下面是业务实体类,对字段名称做了特殊处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExpendLimit{
private BigDecimal count1Amt;

private BigDecimal count2Amt;

private BigDecimal count3Amt;

private BigDecimal count4Amt;

....
// 此处目前大概有12个金额字段需要做校验
}

代码对比

封装方法使用两个参数,currentResidue表示当前剩余额度,currentTotal表示本期付款,逻辑为本期付款对象里如果count?Amt大于0,并且大于currentResidue对应字段的金额时,返回false,做超额逻辑处理。

使用反射前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private boolean checkResidue(ExpendLimit currentResidue, ExpendLimit currentTotal){
boolean result = true;
if (currentTotal.getCount1Amt().compareTo(BigDecimal.ZERO)>0){
if (currentTotal.getCount1Amt().compareTo(currentResidue.getCount1Amt())>0){
result = false;
}
} else if (currentTotal.getCount2Amt().compareTo(BigDecimal.ZERO)>0){
if (currentTotal.getCount2Amt().compareTo(currentResidue.getCount2Amt())>0){
result = false;
}
}...
// 如果写完全需要40多行,且如果后续业务增加需要继续加代码
return result;
}

使用反射后

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
java复制代码private boolean checkResidue(ExpendLimit currentResidue, ExpendLimit currentTotal) {
boolean result = true;
Class<? extends ExpendLimit> clazz = currentResidue.getClass();
Field[] fields = clazz.getDeclaredFields();
try {
for (Field field : fields) {
//打开私有访问
field.setAccessible(true);
//获取属性
String name = field.getName();
if (name.contains("Amt")) {
//获取属性值
BigDecimal totalAmt = (BigDecimal) field.get(currentTotal);
if (totalAmt.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal residueAmt = (BigDecimal) field.get(currentResidue);
if (totalAmt.compareTo(residueAmt) > 0) {
result = false;
break;
}
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
}

反射的基本原理

  • 编译阶段:编译期装载所有的类,将每个类的信息保存至Class类对象中,每一个类对应一个Class对象(不懂为啥每个类只有一个Class类对象的可以自己查询一下双亲委派模型)
  • 获取Class对象:调用x.class/x.getClass()/Class.forName() 获取x的Class对象clz
  • 使用反射进行操作:通过clz对象获取Field/Method/Constructor对象进行进一步操作。

反射使用的API

  • java.lang.Class:代表一个类
  • java.lang.reflect.Method:代表类的方法
  • java.lang.reflect.Field:代表类成员变量
  • java.lang.reflect.Constructor:代表类的构造器

反射提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型的信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

总结

通过使用反射,改变了过长的if/else判断。在我的业务里其实只使用了java.lang.reflect.Field类获取对象属性做业务判断。其实反射在很多框架中都有使用,Spring的动态代理也是应用了反射,不管是CGLAB动态代理还是JDK动态代理其根本都是使用反射。

本文转载自: 掘金

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

go极速学-Go Fiber 自搭框架脚手架笔记-(1):-

发表于 2021-11-02

为什么使用go fiber?

出于好奇心,之前就听闻过这个框架,虽然我的不会Express,听说Fiber是一个参考了Express的Web框架,建立在Go语言写的最快的FasthttpHTTP引擎的基础上。

按官网说的:皆在简化 零内存分配和提高性能,以便快速开发。

至于为啥不用Gin,beego,iris,echo,gf·····感觉上面哪些的框架目前市面上应该是已经很多人,有自己的教脚手架了!我自己出于刚重新接触GO回来没多久的!尝鲜的过程中去学东西也是很有感觉的!所以试一试吧!

目前的它已经出到了V2的版本了,和V1的差别还是比较大的!百度一番之后,也没教程!惯例!哈哈搬砖系列~之官网!!

Fiber 的特点(优势)

官网的大佬的给的几个点:

  • 强大的路由
  • 静态文件服务
  • 极限表现
  • 内存占用低
  • API 接口
  • 中间件和Next支持
  • 快速服务器端编程
  • 模版引擎
  • WebSocket 支持
  • 频率限制器
  • 15 种语言

Fiber的限制

由于 Fiber 使用了 unsafe 特性,导致其可能与最新的 Go 版本不兼容。Fiber 2.18.0 已经在 Go 1.14 到 1.17 上验证过。
Fiber 与 net/http 接口不兼容。这意味着你无法使用 gqlen,go-swagger 或者任何其他属于 net/http 生态的项目。

Fiber v2.21.0 版本初步使用

本节内容:

  • 热更新插件使用
  • Fiber app对象的配置项
  • Fiber app路由和路由组
  • Fiber app启动监听自定义(http和https的配置)

1、来自官网的示例代码

1.1 fresh热重启

插件:

1
go复制代码D:\code\go\awesomeProject1>go get github.com/pilu/fresh

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
makefile复制代码D:\code\go\awesomeProject1>fresh
20:23:57 runner | InitFolders
20:23:57 runner | mkdir ./tmp
20:23:57 runner | mkdir ./tmp: Cannot create a file when that file already exists.
20:23:57 watcher | Watching .
20:23:57 main | Waiting (loop 1)...
20:23:57 main | receiving first event /
20:23:57 main | sleeping for 600 milliseconds
20:23:57 main | flushing events
20:23:57 main | Started! (5 Goroutines)
20:23:57 main | remove tmp\runner-build-errors.log: The system cannot find the file specified.
20:23:57 build | Building...
20:23:58 runner | Running...

自己玩的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})
// 定义路由
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("你好小钟同学!")
})

app.Listen(":3000")
}

启动图示:

然后访问接口即可:http://127.0.0.1:3000/

其实对比Fastapi的框架的来说的,其实框架这东西多数蕾西,而且也和我们的GIN其实总体是保持差不多的。


1.2 其他官网示例的安利

📖 基础路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
go复制代码func main() {
app := fiber.New()

// GET /api/register
app.Get("/api/*", func(c *fiber.Ctx) error {
msg := fmt.Sprintf("✋ %s", c.Params("*"))
return c.SendString(msg) // => ✋ register
})

// GET /flights/LAX-SFO
app.Get("/flights/:from-:to", func(c *fiber.Ctx) error {
msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to"))
return c.SendString(msg) // => 💸 From: LAX, To: SFO
})

// GET /dictionary.txt
app.Get("/:file.:ext", func(c *fiber.Ctx) error {
msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext"))
return c.SendString(msg) // => 📃 dictionary.txt
})

// GET /john/75
app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error {
msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age"))
return c.SendString(msg) // => 👴 john is 75 years old
})

// GET /john
app.Get("/:name", func(c *fiber.Ctx) error {
msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name"))
return c.SendString(msg) // => Hello john 👋!
})

log.Fatal(app.Listen(":3000"))
}

📖 静态文件服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码func main() {
app := fiber.New()

app.Static("/", "./public")
// => http://localhost:3000/js/script.js
// => http://localhost:3000/css/style.css

app.Static("/prefix", "./public")
// => http://localhost:3000/prefix/js/script.js
// => http://localhost:3000/prefix/css/style.css

app.Static("*", "./public/index.html")
// => http://localhost:3000/any/path/shows/index/html

log.Fatal(app.Listen(":3000"))
}

📖 中间件和Next

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
go复制代码func main() {
app := fiber.New()

// 全局中间件件,对所有的路由生效
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First handler")
return c.Next()
})

// 匹配所有使用 /api开头的路由
app.Use("/api", func(c *fiber.Ctx) error {
fmt.Println("🥈 Second handler")
return c.Next()
})

// 配置多个中间件一起的使用
app.Use("/api",func(c *fiber.Ctx) error {
c.Set("X-Custom-Header", random.String(32))
return c.Next()
}, func(c *fiber.Ctx) error {
return c.Next()
})

//注册的具体的路由示例,地址为/api/list
app.Get("/api/list", func(c *fiber.Ctx) error {
fmt.Println("🥉 Last handler")
return c.SendString("Hello, World 👋!")
})

log.Fatal(app.Listen(":3000"))
}

📖 根据官网扩展示例-获取路径参数(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
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})


app.Get("/:value", func(c *fiber.Ctx) error {
return c.SendString("value: " + c.Params("value"))
// => Get request with value: hello world
})

app.Listen(":3000")
}

访问地址:http://127.0.0.1:3000/sdf43534
输出的结果是:

1
makefile复制代码value: sdf43534

📖 根据官网扩展示例-获取路径t数(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
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})

//对参数的校验
app.Get("/:name?", func(c *fiber.Ctx) error {
if c.Params("name") != "" {
return c.SendString("Hello " + c.Params("name"))
// => Hello john
}
return c.SendString("Where is john?")
})

app.Listen(":3000")
}

访问的结果:

1
2
3
4
5
csharp复制代码地址:http://127.0.0.1:3000/name
结果:Hello name
============
地址:http://127.0.0.1:3000
结果:Where is john?

📖 根据官网扩展示例-获取路径t数(3)-通配符的形式

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

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})

//对参数的校验
app.Get("/api/*", func(c *fiber.Ctx) error {
return c.SendString("API path: " + c.Params("*"))
// => API path: user/john
})

app.Listen(":3000")
}

访问:

1
2
3
markdown复制代码地址:http://127.0.0.1:3000/api/34534/3453
结果:API path: 34534/3453
============

📖 【新增】多应用和flask和fastapi的多应用有些类似!

1
2
3
4
5
6
7
8
9
css复制代码func main() {
micro := fiber.New()
micro.Get("/doe", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
app := fiber.New()
app.Mount("/john", micro) // GET /john/doe -> 200 OK
log.Fatal(app.Listen(":3000"))
}

上面的访问的的地址则为:GET /john/doe -> 200 OK

2、 Fiber实例的对象的配置

这官网的就有点不地道,锁好翻译很多种语言了!可惜进入官网还是英文!哈哈
那也只好应啃吧!

其实这个的Fiber实例的对象的配置和我们的之前的fastapi的配置是一个概念,就是对我们的Fiber实例的对象进行特殊的参数配置初始化。

2.1 new使用的是默认的配置

1
css复制代码app := fiber.New()

2.2 自定义定制个人的配置初始化信息

1
2
3
4
5
6
php复制代码app := fiber.New(fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
ServerHeader: "Fiber",
})

如上面的开启了多进程之后 Prefork: true,:

2.3 配置初始化信息-配置项有哪些?

挑一些比较值得关注的:


  • Prefork: 是否开启多进程
+ 默认值是 false
+ 作用: 是否开启多进程模式,官网说注意点是:注意:如果启用了,应用程序将需要在shell中运行,因为预叉模式设置了环境变量。如果您使用的是Docker,请确保该应用程序是与CMD ./app或CMD ["sh", "-c", "/app"]
+ 示例,如果开启多进程的情况后:
如上面的开启了多进程之后 Prefork: true,:
![](https://gitee.com/songjianzaina/juejin_p15/raw/master/img/c8bf7e99793fecc8ce840aae18138607de350522dfd553a61625850766844adc)

  • ServerHeader: 定义响应头中的Server的标记头
    • 默认值是 : “”
    • 作用:启用Server具有给定值的http标头。
    • 示例,写入 ServerHeader: “Fiber”,:
      那么我们的请求响应头那就有会:


  • CaseSensitive: 路由定义大小写问题的匹配
    • 默认值是 : false
    • 作用:启用后,/Foo和/foo是不同的路由。

  • Immutable :按描述应该是上下文的值是否可复用的问题,以前gin使用的时候也会有这个问题
    • 默认值是 : false
    • 作用:启用后上下文方法返回的所有值都是不可变的。默认情况下,它们在从处理程序返回之前是有效的

  • UnescapePath: 解决的应该是路由中的所有编码字符编码问题

  • ETag: 应该解决缓存的问题

  • BodyLimit:
  • 默认值是:int类型的4 * 1024 * 1024
  • 作用:为请求体设置允许的最大大小,如果大小超过配置的限制,它将发送413 - Request Entity Too Large回应

  • Concurrency:
+ 默认值是:int类型的256 \* 1024
+ 并发连接的最大数量。
  • ReadTimeout:
+ 默认值是:time.Duration
+ 读取请求完成的超时时间显示,默认是不约束。
  • WriteTimeout:
+ 默认值是:time.Duration
+ 写入最长响应时间,默认是不约束。
  • ReadBufferSize:
    请求读取的每个连接缓冲区大小
  • DisableKeepalive:
    禁用“保持活动连接”,服务器将在向客户端发送第一个响应后关闭传入连接。
  • ErrorHandler
  • 默认的全局的错误的异常处理。

2.4 应用静态文件服务提供

2.4.1 不设置虚拟路径

配置静态文件:

如设置静态文件的目录:

1
vbnet复制代码app.Static("/", "./public")

完整代码:

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

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New(fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
ServerHeader: "Fiber",
})

app.Static("/", "./public")

// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})

//对参数的校验
app.Get("/api/*", func(c *fiber.Ctx) error {
return c.SendString("API path: " + c.Params("*"))
// => API path: user/john
})

app.Listen(":3000")
}

直接的访问:

1
arduino复制代码http://127.0.0.1:3000/

2.4.2 设置虚拟路径

其中路径实际上不存在于文件系统中,静态方法,为静态目录指定前缀路径

配置静态文件:

如设置静态文件的目录:

1
vbnet复制代码	app.Static("/static", "./public")

完整代码:

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

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New(fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
ServerHeader: "Fiber",
})

//app.Static("/", "./public")
app.Static("/static", "./public")
// 定义全局的中间件
app.Use(func(c *fiber.Ctx) error {
fmt.Println("🥇 First 2222handler")
return c.Next()
})

//对参数的校验
app.Get("/api/*", func(c *fiber.Ctx) error {
return c.SendString("API path: " + c.Params("*"))
// => API path: user/john
})

app.Listen(":3000")
}

访问地址:

1
vbnet复制代码http://127.0.0.1:3000/static/hello.html

结果:

2.4.3 更多的静态服务配置项参数

1
2
3
4
5
6
7
8
php复制代码app.Static("/", "./public", fiber.Static{
Compress: true, //是否开启压缩
ByteRange: true, //是否启用字节范围请求。
Browse: true, //是否启用目录浏览
Index: "index.html" //默认的访问
CacheDuration: 10 * time.Second,//缓存时间
MaxAge: 3600,
})

3、 Fiber路由和路由组

3.1 Fiber路由和路由组

其实这个概念和GIN的类似,如果你接触过GIN的话其实都一样的性质。
完整示例:

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

import (
"log"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New(fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
ServerHeader: "Fiber",
})

handler := func(c *fiber.Ctx) error {
c.Set("X-Custom-Header", "saaaaaaaaaa")
return c.SendString("我是你的谁!111111!")
}
api := app.Group("/api", handler) // /api
v1 := api.Group("/v1", handler) // /api/v1
v1.Get("/list", handler) // /api/v1/list
v1.Get("/user", handler) // /api/v1/user
v2 := api.Group("/v2", handler) // /api/v2
v2.Get("/list", handler) // /api/v2/list
v2.Get("/user", handler) // /api/v2/user
log.Fatal(app.Listen(":3000"))

app.Listen(":3000")
}

具体结果:

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

import (
"encoding/json"
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

handler := func(c *fiber.Ctx) error {
c.Set("X-Custom-Header", "saaaaaaaaaa")
return c.SendString("我是你的谁!111111!")
}
api := app.Group("/api", handler) // /api
v1 := api.Group("/v1", handler) // /api/v1
v1.Get("/list", handler) // /api/v1/list
v1.Get("/user", handler) // /api/v1/user
v2 := api.Group("/v2", handler) // /api/v2
v2.Get("/list", handler) // /api/v2/list
v2.Get("/user", handler) // /api/v2/user

//打印
data, _ := json.MarshalIndent(app.Stack(), "", " ")
fmt.Println(string(data))

app.Listen(":3000")
}

输出的结果为:

4、 Fiber启动的监听

http的启动监听:

通常我们的启动的,可以定制器的HOST和PORT,Fiber也一样的可以提供:

1
2
3
4
arduino复制代码// 设置启动的监听端口,默认的HOTS的是本地
app.Listen(":8080")
// 自定义启动额HOTS+端口
app.Listen("127.0.0.1:8080")

https的启动监听:

1
arduino复制代码app.ListenTLS(":443", "./cert.pem", "./cert.key");

还可以对https进行配置:

1
2
3
4
5
6
7
yaml复制代码&tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
Certificates: []tls.Certificate{
cert,
},
}

使用net.Listen来启动

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

import (
"crypto/tls"
"net"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

handler := func(c *fiber.Ctx) error {
c.Set("X-Custom-Header", "saaaaaaaaaa")
return c.SendString("我是你的谁!111111!")
}
api := app.Group("/api", handler) // /api
v1 := api.Group("/v1", handler) // /api/v1
v1.Get("/list", handler) // /api/v1/list
v1.Get("/user", handler) // /api/v1/user
v2 := api.Group("/v2", handler) // /api/v2
v2.Get("/list", handler) // /api/v2/list
v2.Get("/user", handler) // /api/v2/user

//打印
//data, _ := json.MarshalIndent(app.Stack(), "", " ")
//fmt.Println(string(data))

ln, _ := net.Listen("tcp", ":3000")
cer, _ := tls.LoadX509KeyPair("server.crt", "server.key")
ln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cer}})
app.Listener(ln)
}

5、 Fiber请求上下文篇

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

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

app.Get("/", func(c *fiber.Ctx) error {
c.Accepts("png")
//c.Accepts("json", "text") // "json"
//c.Accepts("application/json") // "application/json"

//新增的响应头信息---下面的字段会出现再响应头
c.Append("Link", "Test")
c.Append("Link", "http://google.com", "http://localhost")

//返回的APP路由堆栈
return c.JSON(c.App().Stack())
})

app.Listen(":3000")
}

结果:

image.png

其他设置响应的方法 c.Set():

1
2
3
4
5
6
go复制代码app.Get("/", func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/plain")
// => "Content-type: text/plain"
// ...
c.Vary("Accept-Encoding", "Accept")
})

5.2 返回JSON格式数据

1
javascript复制代码  return c.JSON(c.App().Stack())

完整示例:

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

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

type SomeStruct struct {
Name string
Age uint8
}
app.Get("/json1", func(c *fiber.Ctx) error {
// Create data struct:
data := SomeStruct{
Name: "Grame",
Age: 20,
}
return c.JSON(data)
})
app.Get("/json2", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"name": "Gram222e",
"age": 2000,
})

})

app.Listen(":3000")
}

或者链式的返回:

1
2
3
javascript复制代码return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
})

5.3 获取请求的baseurl和hotsname

1
2
less复制代码fmt.Println("c.BaseURL()", c.BaseURL())
fmt.Println("c.Hostname()", c.Hostname())

image.png

5.4 获取POST提交的body参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main


import (
"fmt"
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

app.Post("/", func(c *fiber.Ctx) error {
// Get raw body from POST request:
return c.Send(c.Body()) // []byte("user=john")
})

app.Listen(":3000")
}

结果:
image.png

5.5 POST中的body参数绑定解析

示例代码:

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

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

type Person struct {
Name string `json:"name" xml:"name" form:"name"`
Pass string `json:"pass" xml:"pass" form:"pass"`
}

app.Post("/", func(c *fiber.Ctx) error {
p := new(Person)
if err := c.BodyParser(p); err != nil {
return err
}
return c.JSON(p)
})

app.Listen(":3000")
}

结果:

image.png

5.6 Cookie设置和删除

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 (
"time"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

type Cookie struct {
Name string `json:"name"`
Value string `json:"value"`
Path string `json:"path"`
Domain string `json:"domain"`
MaxAge int `json:"max_age"`
Expires time.Time `json:"expires"`
Secure bool `json:"secure"`
HTTPOnly bool `json:"http_only"`
SameSite string `json:"same_site"`
}

app.Get("/", func(c *fiber.Ctx) error {
cookie := new(fiber.Cookie)
cookie.Name = "john"
cookie.Value = "doe"
cookie.Expires = time.Now().Add(24 * time.Hour)
// Set cookie
c.Cookie(cookie)
return c.SendString("设置cookie成功!")
})

app.Post("/", func(c *fiber.Ctx) error {
//删除所有的ClearCookie
c.ClearCookie()
// 根据键值对删除
c.ClearCookie("user")
return c.SendString("删除cookie成功!")
})

app.Listen(":3000")
}

5.7 文件下载和发送文件

  • 下载文件示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

app.Get("/", func(c *fiber.Ctx) error {
//return c.Download("./files/report-12345.pdf");
// => Download report-12345.pdf
return c.Download("./小同学.txt", "小钟同学的密码文件.txt")
// => Download report.pdf
})

app.Listen(":3000")
}

结果:

image.png


  • 发送文件示例代码:

从给定路径传输文件。设置内容-类型响应HTTP报头字段

PS:发送文件时候,默认的是开启了gzipping的压缩机制,如果需要关闭,设置为false即可,如下

1
arduino复制代码c.SendFile("./static/index.html", false);

演示文件结构:

image.png
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/sendfile", func(c *fiber.Ctx) error {
return c.SendFile("./public/hello.html")
// Disable compression
//return c.SendFile("./static/index.html", false);
})

app.Listen(":3000")
}

执行:

image.png

5.8 上传文件-获取表单文件内容

番外篇说:

  • 1:form-data主要是以键值对的形式来上传参数,同时参数之间以&分隔符分开,同时也可以上传文件,文件上传要指定文件类型。

image.png

  • 2:x-www-form-urlencode 这种参数的传递与form-data最大的区别是,x-www-form-urlencode只能是以键值对的形式传参,但是不可以上传文件。

按名称检索MultipartForm文件,第一返回来自给定密钥的文件

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
//app.Server().MaxConnsPerIP = 1

app.Post("/", func(c *fiber.Ctx) error {
// Get first file from form field "document":
//MultipartForm()。这将返回一个map[string][]string
file, _ := c.FormFile("5gmsg.conf")
// Save file to root directory:
fmt.Println("文件名称", file.Filename)
return c.SaveFile(file, fmt.Sprintf("./%s", file.Filename))
})

app.Listen(":3000")
}

代码执行结果:

image.png

执行上传后:

image.png

5.9 多文件接收-获取表单文件内容

示例代码:

1
css复制代码

package main

import (
“fmt”

“github.com/gofiber/fiber/v2”
)

func main() {
app := fiber.New()
app.Post(“/“, func(c *fiber.Ctx) error {
//MultipartForm()。这将返回一个map[string][]string
if form, err := c.MultipartForm(); err == nil {
fmt.Println(“输出表单信息,”, form)
files := form.File[“5gmsg.conf”]
fmt.Println(“输出files信息,”, files)
for _, file := range files {
fmt.Println(file.Filename, file.Size, file.Header[“Content-Type”][0])
if err := c.SaveFile(file, fmt.Sprintf(“./%s”, file.Filename)); err != nil {
return c.SendString(“上传失败!”)
}
}
}
return c.SendString(“上传成功!”)

})

app.Listen(“:3000”)
}

1
2


代码执行结果:

image.png

执行上传后:

image.png

5.10 获取表单值内容信息

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Post("/", func(c *fiber.Ctx) error {
//MultipartForm()。这将返回一个map[string][]string
fmt.Println("name表单值信息", c.FormValue("name"))
fmt.Println("age表单值信息", c.FormValue("age"))
return c.SendString(fmt.Sprintf("%s-%s-%s", "表单值信息为:", c.FormValue("name"), c.FormValue("age")))

})

app.Listen(":3000")
}

输出结果:

image.png

5.11 获取自定义的请求头信息

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
//MultipartForm()。这将返回一个map[string][]string

c.Get("Content-Type") // "text/plain"

return c.SendString(fmt.Sprintf("%s-%s-%s", "请求头信息为:", c.Get("Content-Type"), c.Get("xiaozhong")))

})

app.Listen(":3000")
}

示例:

image.png

5.12 获取客户端请求IP信息

1
scss复制代码c.IP(), c.IPs()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
//MultipartForm()。这将返回一个map[string][]string

return c.SendString(fmt.Sprintf("%s-%s-%s", "请求IP信息为:", c.IP(), c.IPs()))

})

app.Listen(":3000")
}

执行截图:

image.png

5.13 判断Content-Type类型和是否XHR请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

app.Get("/", func(c *fiber.Ctx) error {
c.Is("html") // true
c.Is(".html") // true
c.Is("json") // false
c.XHR() // 判断是否XHRjQuery提交
return c.SendString(fmt.Sprintf("%s-%s-%s", "请求IP信息为:", c.Is("html"), c.Is(".html"), c.Is("json")))
})

app.Listen(":3000")
}

5.14 存贮变量传递(中间件之间的信息传递)

这个其实对后续的鉴权非常有用滴,比如我的鉴权完成后,穿得TOKEN到下一个接口!

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

//全局的中间件中我们的设置一个参数值信息---
app.Use(func(c *fiber.Ctx) error {
c.Locals("user", "小钟同学-变量存贮传递")
return c.Next()
})
app.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"name": c.Locals("user"),
"age": 2000,
})
})

app.Listen(":3000")
}

执行结果:

image.png

5.15 301或302的重定向


示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

//全局的中间件中我们的设置一个参数值信息---
app.Get("/coffee", func(c *fiber.Ctx) error {
return c.Redirect("/teapot")
})
app.Get("/teapot", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTeapot).SendString("我是重定向过来的!")
})

app.Listen(":3000")
}

访问地址:http://127.0.0.1:3000/coffee
执行结果:

image.png


其他示例:

1
2
3
4
5
6
kotlin复制代码app.Get("/", func(c *fiber.Ctx) error {
return c.Redirect("/foo/bar")
return c.Redirect("../login")
return c.Redirect("http://example.com")
return c.Redirect("http://example.com", 301)
})

5.16 Params参数-获取path参数


代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/user/:name", func(c *fiber.Ctx) error {
c.Params("name")
return c.SendString(fmt.Sprintf("%s-%s", "获取路径参数上的name值:", c.Params("name")))
})

app.Listen(":3000")
}

访问地址:

1
arduino复制代码http://127.0.0.1:3000/user/nihao

执行结果:

image.png


复杂匹配示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/user/:name", func(c *fiber.Ctx) error {
c.Params("name")
return c.SendString(fmt.Sprintf("%s-%s", "获取路径参数上的name值:", c.Params("name")))
})
app.Get("/user2/*", func(c *fiber.Ctx) error {
return c.SendString(fmt.Sprintf("%s-%s-%s", "获取路径参数上的name值:", c.Params("*"), c.Params("*1")))
})

app.Listen(":3000")
}

可以访问的地址:

1
2
3
bash复制代码http://127.0.0.1:3000/user2/nihao/3453
http://127.0.0.1:3000/user2/nihao3453
http://127.0.0.1:3000/user2/nihao=3453

image.png
image.png

image.png

5.17 获取Query参数和QueryParser参数解析


获取参数示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/user", func(c *fiber.Ctx) error {
return c.SendString(fmt.Sprintf("%s-%s-%s", "获取路径参数上的name值:", c.Query("name"), c.Query("age")))
})

app.Listen(":3000")
}

请求返回:

image.png


示例绑定解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
)

type Person struct {
Name string `query:"name"`
Age string `query:"age"`
}

func main() {
app := fiber.New()
app.Get("/user", func(c *fiber.Ctx) error {
p := new(Person)
if err := c.QueryParser(p); err != nil {
return err
}
return c.JSON(p)
})

app.Listen(":3000")
}

请求执行:

image.png


获取是有请求路径和参数信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/user", func(c *fiber.Ctx) error {

return c.SendString(fmt.Sprintf("%s-%s", "获取访问地址信息:", c.OriginalURL()))
})

app.Listen(":3000")
}

执行结果:

image.png

5.18 响应字节流数据

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"bytes"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return c.Send([]byte("Hello, World!")) // => "Hello, World!"
})
app.Get("/user", func(c *fiber.Ctx) error {
return c.SendStream(bytes.NewReader([]byte("Hello, World!"))) // => "Hello, World!"
})

app.Listen(":3000")
}

字节流信息的追加写入:

1
2
3
4
5
6
7
8
go复制代码
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
c.Write([]byte("Hello, World!111")) // => "Hello, World!"
c.Write([]byte("Hello, World!222")) // => "Hello, World!"
c.Status(200)
return nil
})

执行结果:

image.png

5.19 响应码和响应体同时设置

示例代码:

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

import (
"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
c.Status(200)
return nil
})

app.Get("/ok2", func(c *fiber.Ctx) error {
return c.Status(400).SendString("Bad Request")
})

app.Get("/ok3", func(c *fiber.Ctx) error {
return c.Status(404).SendFile("./public/gopher.png")
})
app.Listen(":3000")
}

5.20 自定义404的错误响应处理

PS:关键坑点:这个的404自定义的话,你需要放到当所有的路由都注册完成后才可以天机,不然它所有的地址都会找不到!
PS:关键坑点:这个的404自定义的话,你需要放到当所有的路由都注册完成后才可以天机,不然它所有的地址都会找不到!
PS:关键坑点:这个的404自定义的话,你需要放到当所有的路由都注册完成后才可以天机,不然它所有的地址都会找不到!

主要还是使用中间件的方式来处理。

错误的示例代码:

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

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
app := fiber.New()
//坑点注意了!!!!!
//坑点注意了!!!!!
//坑点注意了!!!!!
app.Use(func(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"code": "404",
"msg": "找不到这个地址啊!你估计是搞错地址了!",
})
})
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

// 设置全局错误
app.Use(recover.New(recover.Config{
//Next: func(c *fiber.Ctx) bool {
// return c.Query("refresh") == "true"
//},
EnableStackTrace: true,
StackTraceHandler: func(e interface{}) {
//堆栈信息

},
}))
// This panic will be catch by the middleware
app.Get("/", func(c *fiber.Ctx) error {
panic("I'm an error")
})
app.Listen(":3000")
}

正确的应该是放最后:

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

import (
"fmt"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {

app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {

return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"code": fiber.StatusInternalServerError,
"msg": err.Error()},
)
},
})

//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

// 设置全局错误
app.Use(recover.New(recover.Config{
//Next: func(c *fiber.Ctx) bool {
// return c.Query("refresh") == "true"
//},
EnableStackTrace: true,
StackTraceHandler: func(e interface{}) {
//堆栈信息

},
}))
// This panic will be catch by the middleware
app.Get("/", func(c *fiber.Ctx) error {
panic("大爷!")
})

//放到最后来啊!
app.Use(func(c *fiber.Ctx) error {
fmt.Println(c)
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"code": "404",
"msg": "找不到这个地址啊!你估计是搞错地址了!",
})
})

app.Listen(":3000")
}

执行访问:

image.png

6、 Fiber 中间件篇

来自官网的文档的搬砖系列!

6.1 前言:

Fiberv1和Fiberv2的中间件有区别,

Fiberv2中的中间件必须有return c.next()才可以!

其实这个中间件和我们的所知的fastapi和gin的大致的功能是一样的,都是类似的钩子函数一样的。

在FiberV2中它自己内置了很多的中间件:

  • 用于基础使用用户名和密码认证的中间件 - BasicAuth
  • 缓存作用的中间件 - Cache
  • 压缩使用的中间件件 - Compress
  • 跨域中间件- CORS
  • 过期设置的中间件- ETag
  • 设置访问的Favicon图标的中间件
  • 配置静态文件服务的一些配置信息中间件-# FileSystem
  • 限流中间件 -# Limiter
  • 性能分析中间件 -# Pprof
  • 日志记录中间件 -# Logger
  • 全局异常捕获Recover中间件
  • 代理请求访问中间件
  • 全局链路追踪RequestID中间件
  • Session处理中间件
  • 接口请求超时限制的Timeout中间件

下面我的开始搬砖了!!!我就挑几个聊聊!一一的实践一下看看具体的中间件玩法!

6.2 BasicAuth用户名密码基础认证中间件

首先BasicAuth其实就是请求的时候,你需要提供用户名和账号,来验证!

示例代码:

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

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
)

func main() {
app := fiber.New()

// 配置我们基础的认证的需要的用户和密码信息-----简单的配置
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
"name": "xiaozhong",
"password": "123456",
},
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("basicauth 认证的处理成功!!")

})

app.Listen(":3000")
}

浏览器访问吧!这样明细:然后访问地址:

image.png

此时需要你提供用户名和密码:

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

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
)

func main() {
app := fiber.New()

// 配置我们基础的认证的需要的用户和密码信息-----简单的配置
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
"xiaozhong": "123456",
"tongxue": "123456",
},
Realm: "Forbidden",
Authorizer: func(user, pass string) bool {
if user == "xiaozhong" && pass == "123456" {
return true
}
if user == "tongxue" && pass == "123456" {
return true
}
return false
},
//认证失败,机型让它弹出窗口!!!!!
//Unauthorized: func(c *fiber.Ctx) error {
//
// return c.Status(200).SendString("basicauth 认证的失败!!!!非法访问!")
//},
ContextUsername: "_user",
ContextPassword: "_pass",
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("basicauth 认证的处理成功!!")
})

app.Listen(":3000")
}

输入用户名和密码:

image.png

image.png

image.png

6.3 跨域中间件

  • 跨域默认配置
1
2
3
4
5
6
7
8
9
yaml复制代码var ConfigDefault = Config{
Next: nil,
AllowOrigins: "*",
AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH",
AllowHeaders: "",
AllowCredentials: false,
ExposeHeaders: "",
MaxAge: 0,
}
  • 跨域中间件使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)

func main() {
app := fiber.New()

app.Use(cors.New())
// Or extend your config for customization
app.Use(cors.New(cors.Config{
AllowOrigins: "https://gofiber.io, https://gofiber.net",
AllowHeaders: "Origin, Content-Type, Accept",
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("basicauth 认证的处理成功!!")
})

app.Listen(":3000")
}

6.4 限流中间件

  • 限流中间件件默认配置
1
2
3
4
5
6
7
8
9
10
go复制代码var ConfigDefault = Config{
Max: 5,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusTooManyRequests)
},
}
  • 限流中间件使用:

示例:5秒内最多给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
go复制代码package main

import (
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
)

func main() {
app := fiber.New()

app.Use(limiter.New(limiter.Config{
Next: nil,
Max: 2,
// 5秒内最多给访问2个
Expiration: 5 * time.Second,
KeyGenerator: func(c *fiber.Ctx) string {
return c.Get("x-forwarded-for")
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).SendString("你丫的访问这么快!!!")
},
//Store: myCustomStore{}
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("basicauth 认证的处理成功!!")
})

app.Listen(":3000")
}

超过的时候:
image.png
正常的时候:

image.png

6.5 日志中间件

官网文档中提示提供的很多的参数:

甚至它还已经包括了响应体了内容了!!!不需要我fastapi那样还需要自己去写一次了!!!好像可以哟!哈哈

如,日志中可以记录的常量信息如下:

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复制代码// Logger variables
const (
TagPid = "pid"
TagTime = "time"
TagReferer = "referer"
TagProtocol = "protocol"
TagIP = "ip"
TagIPs = "ips"
TagHost = "host"
TagMethod = "method"
TagPath = "path"
TagURL = "url"
TagUA = "ua"
TagLatency = "latency"
TagStatus = "status" // response status
TagResBody = "resBody" // response body
TagQueryStringParams = "queryParams" // request query parameters
TagBody = "body" // request body
TagBytesSent = "bytesSent"
TagBytesReceived = "bytesReceived"
TagRoute = "route"
TagError = "error"
TagHeader = "header:" // request header
TagQuery = "query:" // request query
TagForm = "form:" // request form
TagCookie = "cookie:" // request cookie
TagLocals = "locals:"
// colors
TagBlack = "black"
TagRed = "red"
TagGreen = "green"
TagYellow = "yellow"
TagBlue = "blue"
TagMagenta = "magenta"
TagCyan = "cyan"
TagWhite = "white"
TagReset = "reset"
)

默认的日志中间件的配置:

1
2
3
4
5
6
7
8
kotlin复制代码var ConfigDefault = Config{
Next: nil,
Format: "[${time}] ${status} - ${latency} ${method} ${path}\n",
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stderr,
}
  • Next:是处理是否传递到下一个的,可以自定义
  • Format:定义日志记录的格式
  • TimeFormat:日志的记录的时间格式
  • TimeZone:日志记录的时间时区
  • TimeInterval:时间即那个
  • Output:输出到哪里,默认的是控制台

默认的日志配置

1
less复制代码app.Use(logger.New())

然后请求接口控制台会输出日志信息:

image.png

添加全局追踪ID日志配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/requestid"
)

func main() {
app := fiber.New()
app.Use(requestid.New())
app.Use(logger.New(logger.Config{
Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}\n",
TimeFormat: "02-Jan-2006",
TimeZone: "Asia/Shanghai",
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

app.Listen(":3000")
}

上面的示例,执行后,我完全没看到有requestid

所以继续往下试一试下入文件看看:

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

import (
"log"
"os"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/requestid"
)

func main() {
app := fiber.New()
app.Use(requestid.New())
file, err := os.OpenFile("./req.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer file.Close()
app.Use(logger.New(logger.Config{
Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}\n",
TimeFormat: "02-Jan-2006",
TimeZone: "Asia/Shanghai",
Output: file,
}))
//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

app.Listen(":3000")
}

查看我们的日志文件/req.log:

1
2
3
bash复制代码15784 9e7bd80c-4198-4d3e-800a-7fef42810a55 200 - GET /ok1
15784 9f7bd80c-4198-4d3e-800a-7fef42810a55 200 - GET /ok1
15784 a07bd80c-4198-4d3e-800a-7fef42810a55 200 - GET /ok1

嗯嗯,算是看了!!!!

6.6 全局异常中间件

  • 默认的配置信息:
1
2
3
4
5
yaml复制代码var ConfigDefault = Config{
Next: nil,
EnableStackTrace: false,
StackTraceHandler: defaultStackTraceHandler,
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"github.com/gofiber/fiber/v2/middleware/recover"

"github.com/gofiber/fiber/v2"
)

func main() {
app := fiber.New()

//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

app.Use(recover.New())
// This panic will be catch by the middleware
app.Get("/", func(c *fiber.Ctx) error {
panic("I'm an error")
})
app.Listen(":3000")
}

执行结果:

image.png

这里有疑问,我想捕获我自己定义现实怎么显示呢?
一个自定义的示例:

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

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
app := fiber.New()

//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

// 设置全局错误
app.Use(recover.New(recover.Config{
//Next: func(c *fiber.Ctx) bool {
// return c.Query("refresh") == "true"
//},
EnableStackTrace: true,
StackTraceHandler: func(e interface{}) {
//堆栈信息

},
}))
// This panic will be catch by the middleware
app.Get("/", func(c *fiber.Ctx) error {
panic("I'm an error")
})
app.Listen(":3000")
}

好像还是有点迷糊?如果全局的处理错误呐?
会看我们的fiber.New配置项中其实有一个ErrorHandler,我们只需要在这个地方定义我们的自己的处理器就可以了!!!

自定义全局异常处理函数:

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

import (
"fmt"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
app := fiber.New(fiber.Config{
// Override default error handler
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
//获取当前的异常响应码
code := fiber.StatusInternalServerError
// 如果有自定义的响应码的话,那么就返回自己的自定义的响应码信息
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
fmt.Println(code)
fmt.Println(err)
// 发送自定义的异常错误的响应页面
//err = ctx.Status(code).SendFile(fmt.Sprintf("./%d.html", code))
err = ctx.JSON(fiber.Map{
"code": code,
"msg": err.Error(),
})
// 如果找不到这个页面,那么就直接其他其他的异常
if err != nil {
// In case the SendFile fails
return ctx.Status(fiber.StatusInternalServerError).SendString("程序员哥哥睡眠不足,系统崩溃了!")
}
// 返回这个函数
return nil
},
})

//仅仅是设置响应码信息
app.Get("/ok1", func(c *fiber.Ctx) error {
return c.Status(200).SendString("日志记录!!!")
})

// 设置全局错误
app.Use(recover.New(recover.Config{
//Next: func(c *fiber.Ctx) bool {
// return c.Query("refresh") == "true"
//},
EnableStackTrace: true,
StackTraceHandler: func(e interface{}) {
//堆栈信息

},
}))
// This panic will be catch by the middleware
app.Get("/", func(c *fiber.Ctx) error {
panic("我是故意的!")
})
app.Listen(":3000")
}

然后访问验证:

image.png

6.7 全局异常中间件-提取出全局错误处理问题

1
2
3
4
5
6
7
8
css复制代码app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"code": 500,
"msg": err.Error()},
)
},
})

3.总结


以上仅仅是个人结合官网做的一系列的实践总结梳理,如有笔误!欢迎批评指正!感谢各位大佬!

码字不易,要不你点个赞呗 哈哈!

结尾

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822

本文转载自: 掘金

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

springboot利用AbstractRoutingDat

发表于 2021-11-02

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

动态切换数据源:

springboot提供了一个AbstractRoutingDataSource类。我们可以实现一个类继承AbstractRoutingDataSource并且determineCurrentLookUpKey()方法。

具体步骤:

数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/graduate?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
#MySQL配置
dbmanager:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/hfb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
db37:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/sys?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
dbmanager37:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/srb_core?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root

编写 DataSourceContextHolder设置和保存当前线程使用的数据源

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

//默认数据源
private static final String DEFAULT_DATASOURCE = "pq";

//保存线程连接的数据源
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

public static String getDataSource() {
return CONTEXT_HOLDER.get();
}

public static void setDataSource(String key) {
CONTEXT_HOLDER.set(key);
}

public static void cleanDataSource() {
CONTEXT_HOLDER.remove();
}
}

编写DynamicDataSource 

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public class DynamicDataSource extends AbstractRoutingDataSource {

/**
* @return 切换数据源的时候该方法会被调用
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}


}

配置类

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
less复制代码/**
* 多数据源配置类
*/
@Configuration
public class DataSourceConfig {

//@Primary
@Bean(name = "db")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSourcePq() {
return new DruidDataSource();
}

@Bean("db_37")
@ConfigurationProperties("spring.datasource.pq37")
public DataSource dataSourcePq37() {
return new DruidDataSource();
}

@Bean("db_manager_37")
@ConfigurationProperties("spring.datasource.pqmanager37")
public DataSource dataSourcePqManager37() {
return new DruidDataSource();
}

@Primary //必须有一个数据源标记为Primary
@Bean("db_manager")
@ConfigurationProperties("spring.datasource.pqmanager")
public DataSource dataSourcePqManager() {
return new DruidDataSource();
}

/**
* 数据源选择器 如果此处标记@Primary会导致循环依赖问题
*
* @return
*/
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("db") DataSource PqSource,
@Qualifier("db_manager") DataSource PqManagerSource,
@Qualifier("db_37") DataSource PqSource37,
@Qualifier("db_manager_37") DataSource PqManagerSource37) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//配置默认数据源
dynamicDataSource.setDefaultTargetDataSource(PqSource);
//保存所有可切换的数据源
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("db", PqSource);
dataSourceMap.put("db_manager", PqManagerSource);
dataSourceMap.put("db_37", PqSource37);
dataSourceMap.put("db_manager_37", PqManagerSource37);
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}


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

@Resource(name = "dynamicDataSource")
private DataSource dynamicDataSource;

@Autowired
private MybatisProperties mybatisProperties;

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
sqlSessionFactoryBean.setConfiguration(mybatisProperties.getConfiguration());
return sqlSessionFactoryBean.getObject();
}

@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(dynamicDataSource);
}

}

利用拦截器拦截请求,查看请求参数是否存在某种参数(代表需要切换数据源,不存在则为默认数据源)。也可以使用AOP的方法作用于某个方法,

利用自定义注解配置需要切换的数据源,在切面那里只需利用反射得到对应的数据源在进行切换。

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
java复制代码/**
* 拦截请求切换数据源
*/
public class DataSourceInterceptor extends HandlerInterceptorAdapter {

/**
* 拦截请求
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String dataSourceKey = request.getParameter("dataSourceKey");
System.out.println(dataSourceKey);
if (StringUtils.isNotEmpty(dataSourceKey)) {
DataSourceContextHolder.setDataSource(dataSourceKey);//重点
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {


// System.out.println(DataSourceContextHolder.getDataSource());
DataSourceContextHolder.cleanDataSource();
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
}
}

配置拦截器

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class InterceptorsConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(new DataSourceInterceptor());
//拦截所有路径
registration.addPathPatterns("/sys/**");
}
}

本文转载自: 掘金

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

SpringBoot MDC

发表于 2021-11-02

在程序中,日志一直是一个至关重要的部分,排查问题、统计数据…..

解决问题

SpringBoot添加全局自定义日志链路信息。

1.过滤器,拦截指定请求,可取出参数、请求头等信息,可根据业务自定义添加进日志进程(MDC)。

2.时间拦截器,根据注解进行拦截,向进程日志(MDC)中,注入方法执行时间。

3.具体业务,可自行向进程日志(MDC)中注入所需信息。

1.MDCFilter

请求过滤器,用于向MDC中添加请求头等信息。

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复制代码
/**
* 拦截请求信息,添加到日志
*
* @author litong
* @date 2020/7/23 10:46
*/
@Component
@Log4j2
@Order(2)
@AllArgsConstructor
public class MDCFilter extends OncePerRequestFilter {

private MDCLogProperties mdcLogProperties;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
MDC.put(mdcLogProperties.getHeaderSessionKey(),
request.getHeader(mdcLogProperties.getHeaderSessionKey())
);
MDC.put("url", request.getRequestURI());
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}

2.ApiTimerLog

注解,用于标注需要打印方法执行时间的标识。

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 时间记录annotation
* 标注需要记录时间消耗的方法
*
* @author litong
* @date 2020/7/23 15:30
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiTimerLog {
}

3.TimeAspect

时间切片

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
java复制代码/**
* @author litong
* @date 2020/7/23 15:31
*/
@Aspect
@Component
@Slf4j
public class TimeAspect {

// 修正Timer注解的全局唯一限定符
@Pointcut("@annotation(com.ltz.ltzg.common.log.annotation.ApiTimerLog)")
private void pointcut() {
}

// 按包注入
// @Around("execution(* com.ltz.ltzg.auth.controller.*.*(..))" +
// "|| execution(* com.ltz.ltzg.api.controller.*.*(..))")

// 按注解注入
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

// 开始时间
long start = System.currentTimeMillis();

// 调用目标方法
Object result = R.err(ErrorEnums.SHOW_FAIL);
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throw throwable;
} finally {
// 获取执行时间
long end = System.currentTimeMillis();
long time = end - start;
MDC.put("executionTime", time + "");

R result1 = new R();
try {
result1 = (R) result;
} catch (Exception e) {
}
MDC.put("errcode", result1.getCode() + "");
log.info("Api-Link");
}

return result;
}
}

4.logback-local.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--该日志将日志级别不同的log信息保存到不同的文件中 -->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<include resource="org/springframework/boot/logging/logback/file-appender.xml"/>

<springProperty scope="context" name="springAppName"
source="spring.application.name" />

<!-- 日志在工程中的输出位置 -->
<property name="LOG_FILE" value="C:/data/logs/${springAppName}" />
<!--<property name="LOG_FILE" value="/data/ltz/logs/${springAppName}" />-->

<!-- 控制台的日志输出样式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- })[%thread] [url=%X{url};ltz-token=%X{ltz-token};executionTime=%X{executionTime};errcode=%X{errcode}] {magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!-- 日志输出编码 -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>

<!-- 文件输出 -->
<appender name="localfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<maxFileSize>${LOG_FILE_MAX_SIZE:-1000MB}</maxFileSize>
<maxHistory>${LOG_FILE_MAX_HISTORY:-7}</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [url=%X{url};ltz-token=%X{ltz-token};executionTime=%X{executionTime};errcode=%X{errcode}] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender>
<!-- sql文件输出 -->
<appender name="sqlfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}.sqllog</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.sqlLog</fileNamePattern>
      <maxHistory>3</maxHistory>
</rollingPolicy>
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender>

<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="localfile"/>
</root>

<logger name="dao" level="debug" additivity="false">
<appender-ref ref="sqlfile" />
</logger>
</configuration>

5.日志模板说明

MDC中添加的key,需要在日志模板中,用%X{url}的方式,打印。

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* @author litong
* @date 2020/6/1 17:10
*/
@RequestMapping("/test")
@RestController
public class TestController {


@ApiTimerLog
@GetMapping("/a")
public R a() {
return R.ok("a");
}
}

日志输出

1
ini复制代码2020-07-27 13:41:16.960  INFO 24052[http-nio-18989-exec-5] [url=/test/a;ltz-token=123;executionTime=0;errcode=1000]{magenta} --- [io-18989-exec-5] Api-Link

本文转载自: 掘金

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

【收藏篇】Zookeeper单机部署、必备命令、场景实战

发表于 2021-11-02

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

前言

昨天花哥在第一篇文章中介绍了Zookeeper是什么,大家对它应该也有了一个初步的概念,今天呢,我们就实际操作一波,看它是如何部署到我们的机器上,在代码中又是如何应用的。

单机版安装(windows)

  1. 打开Zookeeper官网 首页,点击Download,就可以跳转到下载页,根据需要选择对应的版本。

image-20211102094012711.png

  1. 如果官网访问失败,花哥也将【3.6.3】、【3.7.0】两个版本上传到百度网盘,有需要的小伙伴可以直接下载

链接:pan.baidu.com/s/19wHXUKTf… 提取码:chwm

  1. 将压缩包解压,打开conf目录,将zoo_sample.cfg拷贝一份并命名为zoo.cfg

image.png

  1. 上述配置完毕后,打开bin目录,双击zkServer.cmd启动服务端
  2. 服务启动完成,双击zkCli.cmd即可以进行测试

注意:zookeeper依赖java环境,因此在安装前,保证操作系统中java环境正常。

单机版安装(linux)

  1. 下载解压
1
2
3
4
shell复制代码# cd /usr/local
# wget https://dlcdn.apache.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz
# tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz
# cd apache-zookeeper-3.7.0-bin
  1. 修改配置
1
shell复制代码# cp conf/zoo_sample.cfg conf/zoo.cfg
  1. 启动服务
1
shell复制代码# bin/zkServer.sh start
  1. 启动客户端
1
shell复制代码# bin/zkCli.sh

注意:如果wget无法使用,可以使用yum -y install wget先将其安装,或者直接将zookeeper包下载到本地,然后上传至服务器。

配置文件说明

在单机版中,我们只需要配置以下三个参数就可以正常启动

1
2
3
4
5
6
ini复制代码#心跳时间,单位毫秒
tickTime = 2000
#数据存放目录
dataDir = /usr/local/zookeeper
#客户端连接端口
clientPort = 2181

当然,在实际应用中,上面三个参数可能是不够的,这时我们可以在官网指南中查看每一个配置项的作用,根据实际需求选择。
\

image.png

命令使用说明

  • 使用ls可以查看zookeeper当前包含的节点

image.png

  • 使用create创建一个新的子节点

image.png

  • 使用ls再来看下根目录下包含的子节点,testData已经被创建

image.png

  • 使用get查看节点内容

image.png

  • 使用set设置节点内容

image.png

  • 使用delete删除节点

image.png

常用命令介绍

  • 客户端命令

命令 说明 示例
ls 获取节点 ls /
create 创建子节点 create /testData 100
delete 删除节点 delete /testData
get 从指定节点读取数据 get -s /testData
set 设置数据到指定节点 set /testData 200help
help 查看帮助
quit 退出客户端
  • 服务命令

命令 说明
sh bin/zkServer.sh start 启动zk服务
sh bin/zkServer.sh stop 停止zk服务
sh bin/zkServer.sh restart 重启zk服务
sh bin/zkServer.sh status 查看服务状态

代码测试

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复制代码package com.basic.business.demo;
​
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
​
/**
* zk测试demo
*
*/
public class ZookeeperDemo implements Watcher {
​
   private static ZooKeeper zk = null;
   private static Stat stat = new Stat();
​
   public static void main(String[] args) throws Exception {
       //被监听的节点
       String path = "/testData";
       //连接zookeeper并注册监听器
       zk = new ZooKeeper("127.0.0.1:2181", 5000, new ZookeeperDemo());
       //注册监听器,监听节点/testData值的变化
       zk.getData(path,true,stat);
       Thread.sleep(Integer.MAX_VALUE);
  }
​
   public void process(WatchedEvent event) {
       //zk连接成功事件
       if (KeeperState.SyncConnected == event.getState()) {
           //zk节点数据变化时通知事件
           if (event.getType() == EventType.NodeDataChanged) {
               try {
                   System.out.println("修改后的值:" + new String(zk.getData(event.getPath(), true, stat)));
              } catch (Exception e) {
              }
          }
      }
  }
}

然后连接zk客户端,修改子节点/testData的内容,就能观察到idea控制台能够打印出修改后的内容。

执行结果8.gif

官网实例

有兴趣的小伙伴,可以跟着官网的示例走一下,共计有两个类,代码贴出来,花哥就不再演示了。地址:zookeeper.apache.org/doc/current…

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
java复制代码//Executor.java

/**
* A simple example program to use DataMonitor to start and
* stop executables based on a znode. The program watches the
* specified znode and saves the data that corresponds to the
* znode in the filesystem. It also starts the specified program
* with the specified arguments when the znode exists and kills
* the program if the znode goes away.
*/
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class Executor
implements Watcher, Runnable, DataMonitor.DataMonitorListener
{
String znode;
DataMonitor dm;
ZooKeeper zk;
String filename;
String exec[];
Process child;

public Executor(String hostPort, String znode, String filename,
String exec[]) throws KeeperException, IOException {
this.filename = filename;
this.exec = exec;
zk = new ZooKeeper(hostPort, 3000, this);
dm = new DataMonitor(zk, znode, null, this);
}

/**
* @param args
*/
public static void main(String[] args) {
if (args.length < 4) {
System.err
.println("USAGE: Executor hostPort znode filename program [args ...]");
System.exit(2);
}
String hostPort = args[0];
String znode = args[1];
String filename = args[2];
String exec[] = new String[args.length - 3];
System.arraycopy(args, 3, exec, 0, exec.length);
try {
new Executor(hostPort, znode, filename, exec).run();
} catch (Exception e) {
e.printStackTrace();
}
}

/***************************************************************************
* We do process any events ourselves, we just need to forward them on.
*
* @see org.apache.zookeeper.Watcher#process(org.apache.zookeeper.proto.WatcherEvent)
*/
public void process(WatchedEvent event) {
dm.process(event);
}

public void run() {
try {
synchronized (this) {
while (!dm.dead) {
wait();
}
}
} catch (InterruptedException e) {
}
}

public void closing(int rc) {
synchronized (this) {
notifyAll();
}
}

static class StreamWriter extends Thread {
OutputStream os;

InputStream is;

StreamWriter(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
start();
}

public void run() {
byte b[] = new byte[80];
int rc;
try {
while ((rc = is.read(b)) > 0) {
os.write(b, 0, rc);
}
} catch (IOException e) {
}

}
}

public void exists(byte[] data) {
if (data == null) {
if (child != null) {
System.out.println("Killing process");
child.destroy();
try {
child.waitFor();
} catch (InterruptedException e) {
}
}
child = null;
} else {
if (child != null) {
System.out.println("Stopping child");
child.destroy();
try {
child.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
FileOutputStream fos = new FileOutputStream(filename);
fos.write(data);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
System.out.println("Starting child");
child = Runtime.getRuntime().exec(exec);
new StreamWriter(child.getInputStream(), System.out);
new StreamWriter(child.getErrorStream(), System.err);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
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
java复制代码//DataMonitor.java

/**
* A simple class that monitors the data and existence of a ZooKeeper
* node. It uses asynchronous ZooKeeper APIs.
*/
import java.util.Arrays;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.AsyncCallback.StatCallback;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.data.Stat;

public class DataMonitor implements Watcher, StatCallback {

ZooKeeper zk;
String znode;
Watcher chainedWatcher;
boolean dead;
DataMonitorListener listener;
byte prevData[];

public DataMonitor(ZooKeeper zk, String znode, Watcher chainedWatcher,
DataMonitorListener listener) {
this.zk = zk;
this.znode = znode;
this.chainedWatcher = chainedWatcher;
this.listener = listener;
// Get things started by checking if the node exists. We are going
// to be completely event driven
zk.exists(znode, true, this, null);
}

/**
* Other classes use the DataMonitor by implementing this method
*/
public interface DataMonitorListener {
/**
* The existence status of the node has changed.
*/
void exists(byte data[]);

/**
* The ZooKeeper session is no longer valid.
*
* @param rc
* the ZooKeeper reason code
*/
void closing(int rc);
}

public void process(WatchedEvent event) {
String path = event.getPath();
if (event.getType() == Event.EventType.None) {
// We are are being told that the state of the
// connection has changed
switch (event.getState()) {
case SyncConnected:
// In this particular example we don't need to do anything
// here - watches are automatically re-registered with
// server and any watches triggered while the client was
// disconnected will be delivered (in order of course)
break;
case Expired:
// It's all over
dead = true;
listener.closing(KeeperException.Code.SessionExpired);
break;
}
} else {
if (path != null && path.equals(znode)) {
// Something has changed on the node, let's find out
zk.exists(znode, true, this, null);
}
}
if (chainedWatcher != null) {
chainedWatcher.process(event);
}
}

public void processResult(int rc, String path, Object ctx, Stat stat) {
boolean exists;
switch (rc) {
case Code.Ok:
exists = true;
break;
case Code.NoNode:
exists = false;
break;
case Code.SessionExpired:
case Code.NoAuth:
dead = true;
listener.closing(rc);
return;
default:
// Retry errors
zk.exists(znode, true, this, null);
return;
}

byte b[] = null;
if (exists) {
try {
b = zk.getData(znode, false, null);
} catch (KeeperException e) {
// We don't need to worry about recovering now. The watch
// callbacks will kick off any exception handling
e.printStackTrace();
} catch (InterruptedException e) {
return;
}
}
if ((b == null && b != prevData)
|| (b != null && !Arrays.equals(prevData, b))) {
listener.exists(b);
prevData = b;
}
}
}

写在最后

今天介绍了zookeeper的单机部署和一些常用命令,已经在java中如何使用,下一章花哥对Zookeeper的核心内容进行讲解,如果觉得文章有一些不妥,小伙伴们可以大胆提出来,一起进步,共同学习。

本文转载自: 掘金

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

1…435436437…956

开发者博客

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