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

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


  • 首页

  • 归档

  • 搜索

从零开始学java - 第十三天 枚举 包 Java 数据结

发表于 2021-11-13

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

今天继续~

枚举

  • 一般用来表示一组常量,比如1年4个季节
  • 使用关键字enum定义
1
2
3
java复制代码enum Game{
CSGO,LOL
}

内部类中使用枚举

1
2
3
4
5
6
7
8
9
java复制代码public class Test{
enum Game{
CSGO,LOL
}
public static void main(String[] args){
Game g1 = Game.CSGO;
System.out.println(g1);// 输出CSGO
}
}

ps:可以把枚举相应的理解为键值对相同的列表

迭代枚举元素

1
2
3
4
5
6
7
8
9
10
java复制代码enum Game{
CSGO,LOL
}
public class Test{
public static void main(String[] args){
for(Game games : Game.values()){
System.out.println(games);
}
}
}

ps:values()可以获取所有值,通过java加强for循环来循环获取的值并把他们输出

在 switch 中使用枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码enum Game{
CSGO,LOL
}
public class Test{
public static void main(String[] args){
Game g1 = Game.CSGO;
switch(g1){
case CSGO:
System.out.println("反恐精英");
break;
case LOL:
System.out.println("英雄联盟");
break;
}
}
}

ps:枚举实例化所获取的值即单个值本身,可直接用在switch里进行匹配

values(), ordinal() 和 valueOf() 方法

values() - 返回枚举中的所有值

ordinal() - 返回枚举中的所有索引

valueOf() - 返回指定字符串的枚举常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码enum Game{
CSGO,LOL
}
public class Test{
public static void main(String[] args){
Game[] arr = Game.values();

for(Game gow : arr){
System.out.println(gow);// 值
System.out.println(gow.ordinal());// 索引
}
System.out.println(Game.valueOf("LOL"));
}
}

枚举类

  • 枚举可以拥有自己的变量,方法,构造函数
  • 枚举的构造函数必须是private
  • 如果枚举类里面有抽象方法的话,它的实例必须实现它
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码enum Game{
CSGO,LOL;

private Game()
{
System.out.println("the game is start");
}
public void GameMode()
{
System.out.println("wa ha ha");
}
}

包

  • 用于区别命名空间
  • 把功能相关的类或接口放在一个包里方便应用
  • 通过package关键字定义,要写在最前面
    比如一个路径为code/java/hello.java的包定义应该为:
1
2
3
java复制代码package code.java
public class hello{
}

java自带的包

java.lang - 打包基础的类

java.io - 包含输入输出功能的函数

创建包

写出两个相同包的文件:

1
2
3
4
5
6
java复制代码package game;

interface Game{
public void watch();
public void play();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package game;
public class Computer implements Game{
public void watch(){
System.out.println("i watching you");
}
public void play(){
System.out.println("i want play");
}
public static void main(String[] args){
Computer cm = new Computer();
cm.watch();
cm.play();
}

}

然后去编译他们,通过mkdir来创建包:

1
2
3
4
5
powershell复制代码mkdir game
cp Game.class Computer.class game
java game/Computer
Computer watch
Computer play

import 关键字

  • 使用import关键字可以导入想要用的包
1
java复制代码import 包名.*

ps:*代表导入这个包中的所有东西,可以把*替换成相应的类名来引入相应的类

package 的目录结构

类放在包中会有两种情况:

  • 包名成为类名的一部分
  • 包名必须与目录结构吻合

Java 数据结构

枚举

  • 枚举接口定义了一种从数据结构中取回连续元素的方式

位集合

  • 位集合类实现了一组可以单独设置和清除的位或标志
  • 在设置和清除布尔值的时候非常有用

向量

  • 向量和传统数组十分类似,但是向量可以动态变化

栈

  • 可以想象成一个开口容器,先进后出

字典

  • 它是一个键值对的集合
  • 它是一个抽象类

哈希表

  • 是一种用户定义键结构的基础上来组织数据的手段

属性

  • 属性列表中每一个键对应的值都是一个字符串
    今天就学到这里,晚安~

本文转载自: 掘金

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

5分钟 了解二叉树所有基础概念!

发表于 2021-11-13

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

想必很多初学数据结构的同学对二叉树的各种概念,可能被弄的一头雾水,所以这篇文章就帮你整理各种关于二叉树的基本概念,和一些算法方面的技巧。🌞

  1. 树和二叉树的定义

树

树🌲:树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。from《维基百科》

image.png
其中有一下几个特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;
  • 树里面没有环路(cycle)
    示例图片:

image.png
一些关于树的术语

  1. 节点的度:一个节点含有的子树的个数称为该节点的度;
  2. 树的度:一棵树中,最大的节点度称为树的度;
  3. 叶节点或终端节点:度为零的节点;
  4. 非终端节点或分支节点:度不为零的节点;
  5. 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
  6. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
  7. 兄弟节点:具有相同父节点的节点互称为兄弟节点;
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  9. 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
  10. 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
  11. 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
  12. 节点的祖先:从根到该节点所经分支上的所有节点;
  13. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
  14. 森林:由m(m>=0)棵互不相交的树的集合称为森林;

二叉树

每个节点最多含有两个子树的树称为二叉树;

完全二叉树

对于一棵二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;

image.png

满二叉树

所有叶节点都在最底层的完全二叉树;

image.png

搜索二叉树 也称二叉搜索树、有序二叉树;

左节点 < 根结点 < 右节点

image.png

平衡二叉树

当且仅当任何节点的两棵子树的高度差不大于1的二叉树;

image.png

  1. 二叉树的性质

  1. 在二叉树的第 i 层至多有 2^(i -1)个结点。(i>=1) (每一层节点数量的特点)
  2. 深度为 k 的二叉树至多有 2^(k-1)个结点(k >=1)。(结点总个数的特点)
  3. 对任何一棵二叉树T, 如果其叶度为0结点数为n0,度为1的结点数为n1, 度为2的结点数为 n2,结点总数为n,则n0=n2+1。(由n = n0 + n1 + n2 和 n = n1 + 2*n2 +1 可推导)
  4. 具有 n (n>=0) 个结点的完全二叉树的深度为+1
  1. 二叉树的存储结构

顺序存储结构:如果二叉树是满二叉树或者完全二叉树推荐因为空间利用率比较高,也可以快速定位到元素
链式存储结构:如果是别的推荐使用 链式存储结构

  1. 二叉树的遍历

遍历中的前,中,后 都是针对根结点而言的。

a. 前序遍历

输出结点顺序为:根节点 左结点 右结点

b. 中序遍历

输出结点顺序为:左结点 根结点 右结点

c. 后续遍历

输出结点顺序为:左节点 右结点 根结点

举个🌰吧:

image.png
前序遍历:1 2 4 5 7 8 3 6

中序遍历:4 2 7 5 8 1 3 6

后序遍历:4 7 8 5 2 6 3 1

本文转载自: 掘金

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

undo log多版本链实现ReadView机制 undo

发表于 2021-11-13

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

undo log版本链

undo log是用来记录事务回滚之前的操作数据,在每条数据都有两个隐藏字段,trx_id、roll_pointer。

  • trx_id:最近一次更新这条数据的事务ID
  • roll_pointer:指向这个事务生成之前的undo log

有了这个字段,这样的话undo log 对于同一条数据的日志记录,并不是只有一条,而是不同的事务操作都进行了记录,这样的话就用链表链接起来。下面通过一个具体的案例来了解下undo log版本链。

在最初的时候,有事务A(事务id为:10)插入一条数据值为A,那么这条数据的隐藏字段以及指向的undo log如下图所示:

image.png

插入的数据值为A,事务di为10,由于是第一次插入,则版本链roll_pointer指向一个空的undo log。
接着事务B对这条数据进行了修改,将值更为是B, 事务ID为20,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示:

image.png

这个是时候事务C又对值进行了修改,将值更改为C,事务ID是30,那么也会生成一个记录,如下图所示:

image.png

从上面的示意图可以看出,多个事务串行执行的时候,每个事务修改数据后,都会更新隐藏字段txr_id和roll_pointer,同时将之前修改的undo log日志通过roll_pinter指针串联起来,形成一个重要的版本链。

多版本链路ReadView机制

在执行一个事务的时候,会生成一个ReadView,有四个比较重要的东西:

  • m_ids:此时有哪些事务在MySQL里执行还没提交
  • min_trx_id:m_ids里最小的值;
  • max_trx_id:mysql下一个要生成的事务id,就是最大事务id
  • creator_trx_id:当前这个事务的id

接着上面的例子,在数据库已经有一行数据,事务id是30,初始情况如下图:
image.png

此时有两个事务并发执行,事务A(id=40),事务B(id=50),事务B更新数据,事务A查询数据,如下图所示:

image.png

此时事务A开启一个ReadView,这个ReadView里的m_id包含事务A和事务B的两个id(40,50),min_trx_id=40,max_trx_id=60(假设步长10),creator_trx_id=45(事务A自己)
这时候事务A查询数据的时候,会判断一下当前这行数据的txr_id是否小于ReadView中的min_trx_id,此时发现txr_Id=30,是小于ReadView里的min_trx_id,那么认为在事务开启之前,修改这行数据的数据早就提交,此时可以查询到这行数据。如下如所示:

image.png

此时事务B也开启执行,把这行数据修改为值B,然后这行数据txr_id设置为自己的id(50),同时roll_pointer指向修改之前的undo log,事务B提交事务执行成功如下图所示:

image.png

这个时候事务A再次查询,就会发现此时txt_id=50,此时的txr_id是大于ReadView里的min_txr_id=40,同时小于ReadView里的max_txr_id= 60,说明这条数据在事务A开启的时候发生的修改,此时的txr_id=50 也在m_ids(40,50)中,那么说你这条数据的事务是事务A同一时段并发执行然后提交的,所以对这样数据是不能查询的,如下如图所示:

image.png

这时候事务A在查询的时候会根据数据roll_pointer顺着undo log日志链表往下找,会找到最近一条undo log,trx_id=30,此时发现小于ReadView中的min_txr_id=40,说明这条数据的版本是在事务A开启之前执行提交的。
在接着假设事务A查询完数据后又对这行数据的值修改,改成了值A,此时的trx_id=40,同时保存修改之前值的快照链,如下图所示:

image.png

此时事务A再次查询的时候会发现最新的数据是trx_id=45,是跟自己的trx_id一样,那说嘛这行数据是自己修改的,是可以查询的。

image.png

再接着在事务A的期间,又来个是事务D,此时的事务id是60,然后将数据更新值为D,并且成功提交,此时的undo log版本链如下图所示:

image.png

此时事务A再次查询的时候,发现大于自己的事务ID,那么说明这数据是事务A开启之后,然后又有事务来更新了数据,是不允许查询的,此时会顺着undo log版本链往下找,就会找到值A自己更新的那条数据。

本文转载自: 掘金

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

如何实现单链表 如何实现单链表?

发表于 2021-11-13

如何实现单链表?

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

笔者最近在学习《数据结构与算法之美》,正好借着这个机会边练习边记录一下自己学习的知识点。不啰嗦,直接开始。

一、什么是链表

链表和数组一样都是线性的数据结构。

链表由一个一个的结点连接组成,结点又由数据域和指针域两部分组成。数据域存储数据,指针域存储指向下一个结点的指针(或者叫引用)。

正是因为使用指针指向下一个结点,所以链表可以将零散的内存空间串联起来。

常见的链表有:单链表、循环链表、双链表和双向循环链表等等。
链表及常见类型 - 副本.png

二、链表的特点

  • 链表查询时间复杂度 O(n),删除插入时间复杂度 O(1) :因为链表使用指针指向下一个结点,所以当想要查询某个结点时,只能从第一个结点依次进行遍历,直到目标结点。但删除和插入只需要将指针指向新结点即可。
  • 链表与数组相比,需要额外的内存空间:因为除了数据存储的空间之外,还存储了指向下一结点指针。

三、单链表实现

单链表的实现有两个难点:一个是插入结点操作,另一个是删除结点操作。

3.1 插入

单链表的插入有三种方式,一个是插入到链表头部、二是插入到链表尾部、三是在链表两个结点直接插入新结点。

插入到链表头部只需要将新结点的指针指向头结点,再将新结点设置为头结点即可。

头插法.png

插入到链表尾部,需要先遍历到链表的尾结点,在将尾结点的指针指向新结点即可。

尾插法.png

链表两个结点直接插入新结点,例如将结点 p 插入到结点 a 和 结点 b之间,先将结点 p.next 指向结点 b,在将结点 a.next 结点 p。

插入两个结点之间.png

3.2 删除

删除链表中的某个结点,例如删除结点 c 。先找到结点 c 的前驱结点 b,将 b.next = b.next.next即可。

删除结点.png

3.3 具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
java复制代码/**
* @author xuls
* @date 2021/11/9 20:49
* 单链表
*/
public class SingleLinkedList {
/**
* 存储字符串数据的结点类
*/
private static class Node{
private String data;
private Node next;//指向下一个结点

public Node(String data, Node next) {
this.data = data;
this.next = next;
}

@Override
public String toString() {
return "Node{" +
"data='" + data + '\'' +
'}';
}
}

private Node head;//头结点

//将数据插入到单链表头部
public void insertHead(String data){
Node node = new Node(data, null);
//如果头结点为 null 则直接插入
if (head == null){
head = node;
}else {
//将结点的 next 指向 head,再将 node 设置为 head
node.next = head;
head = node;
}
}

//将数据插入到单链表尾部
public void insertTail(String data){
Node node = new Node(data, null);
//如果头结点为 null 直接插入
if (head == null){
head = node;
}else {
Node temp = head;
//找到指向下一个结点为 null 的结点,这个结点即最后一个结点
while ( temp.next != null){
temp = temp.next;
}
//将最后一个结点的下一个结点指向新结点
temp.next = node;
}
}

// 将 data 插入到 node 结点之前
public void insertBefore(String data,Node node){
if (node == null){
return;
}
//插入头结点之前
if (node == head){
insertHead(data);
return;
}

//找到插入结点的前结点
Node before = head;
while (before != null && before.next != node){
before = before.next;
}

if (before == null){
//说明 node 结点不在单链表中直接返回
return;
}

Node newNode = new Node(data, null);
newNode.next = node;
before.next = newNode;
}

// 将 data 插入到 node 结点之后
public void insertAfter(String data,Node node){
if (node == null){
return;
}
Node newNode = new Node(data, null);
newNode.next = node.next;
node.next = newNode;
}

//删除结点
public void delete(Node node){
if (node == null || head == null){
return;
}
if (node == head){
head = head.next;
return;
}
//找到删除结点的前结点
Node before = head;
while (before != null && before.next != node){
before = before.next;
}

if (before == null){
//删除结点不在链表之中
return;
}

before.next = before.next.next;
}

//找到 index 位置的结点
public Node find(int index){
if (head == null || index < 0){
return null;
}

Node node = head;
int position = 0;
while (node != null && position != index){
node = node.next;
position++;
}

return node;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("SingleLinkedList:");
if (head == null){
builder.append("null");
}else {
Node node = head;
while (node != null){
builder.append(node.data).append("-->");
node = node.next;
}
builder.append("null");
}
return builder.toString();
}
}

四、数组 or 链表

数组和链表是常见的基本数据结构,两者经常放在一起进行比较,同样是线性数据结构我们如何进行选择?

4.1 随机访问、插入、删除时间复杂度

随机访问 删除、插入
数组 O(1) O(n)
链表 O(n) O(1)

4.2 数组存储 vs 链表存储

数组需要连续的内存空间,比如你需要申请 99 M 大小的内存空间,但没有 99 M 的连续内存空间了,数组创建就会失败。

数组连续的内存空间可以利用 CPU 的缓存机制,使得数据访问更快。

数组的大小固定,虽然我们可以实现动态扩容的数组,但是频繁的数据搬移也是非常耗时的。

链表使用指针将不连续的内存空间连接在一起,但这就需要使用额外的内存空间存储指向下一结点的指针。链表频繁的创建对象也可能产生内存碎片。

综上根据你的需要,具体问题具体分析。

传送门:如何实现动态扩容的数组

XDM,动动小手点赞评论,求求了。

catdan.gif

本文转载自: 掘金

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

👩🏻‍💻算法系统学习-贪心策略(可绝对贪婪问题详解) 前言

发表于 2021-11-13

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

前言

👩🏻‍💻 该系列是基于有一定语言基础(C,C++,Java等等)和基本的数据结构基础进行的算法学习专栏,如果觉得有点吃力 😥 ,建议先了解前提知识再学习喔!本个专栏会将用更容易理解的表达去学习算法,如果在一些表述上存在问题还请各位多多指点 🧑🏻‍🚀 ,要是觉得还不错记得点个 👍本专栏快捷门:juejin.cn/column/7024…

可绝对贪婪问题

Case1:键盘输入一个高精度的正整数n,去掉其中任意s个数字后剩下的数字按原左右次将组成一个新的正整数。对于给定的n和s,使得剩下的数字组成新数最小并输出。

问题分析:

在位数固定的前提下,让高位的数字尽量小,其值就越小,依据贪心策略就可以解决这个问题。但是数据的不同也会造成不同种情况的出现,例如 假设1,假设2,假设3

假设1:

n=“12435863” s=2(去掉两个数)

开始

第一次比较 “1 2 4 3 5 8 6”

1相对于2 在高位,但是数字小于低位的2, 所以不变

第二次比较“1 2 4 3 5 8 6”

2相对于4 在高位,但是数字小于低位的4 ,所以不变

第三次比较“1 2 4 3 5 8 6”

4相对于3 在高位,但是数字大于低位的4 ,所以4被移除。

第四次比较“1 2 3 5 8 6”

3相对于5 在高位,但是数字小于低位的5, 所以不变

第五次比较“1 2 3 5 8 6”

5相对于8 在高位,但是数字小于低位的8, 所以不变

第五次比较“1 2 3 5 8 6”

8相对于6 在高位,但是数字大于低位的8,所以8被移除。

最后输出 12356 为最小数

结束

因此,可得相邻数字只需要从前向后比较;

假设2:

n=“231183” s=3(去掉三个数)

开始

第一次比较 “2 3 1 1 8 3”

2相对于3 在高位,但是数字小于低位的3,所以不变

第二次比较“2 3 1 1 8 3”

即得“2 1 1 8 3”

3相对于1 在高位,但是数字大于低位的1,所以3被移除。

第三次比较“2 1 1 8 3”

2相对于1 在高位,但是数字大于低位的1,所以2被移除。

即得“ 1 1 8 3”

第四次比较“ 1 1 8 3”

1相对于1在高位,两者相等, 所以不变

第五次比较“ 1 1 8 3”

1相对于8在高位,但是数字小于低位的8, 所以不变

第六次比较“ 1 1 8 3”

8相对于1在高位,但是数字大于低位的1, 所以8被移除

即得“ 1 1 3”

最后输出 113 为最小数

结束

因此,可得当第i位与第i+1位比较,若删除第i位后,必须向前考虑第i-1位与第i+1位进行比较,才能保证结果的正确性。


假设3:

n=“123456” s=3(去掉三个数)

可以清楚的看到,相邻的数字比较都不用删除,这时要考虑将后三位删除,即为最小值为123

当然也还有另外一种可能

假设3-1

n=”120083” s=3(去掉三个数)

3 比 0 大删除 即“1 0 0 8 3”

2比 0 大删除 即“ 0 0 8 3”

8比 3 大删除 即“ 0 0 3”

最小值3

由此,在n含有0时,当删除掉一些数字后,结果高位可能会出现数字0,直接输出这个数据不合理。应该将所有高位的0去除再输出。特别地还要考虑若结果串是0000时,不能将0都删除,而要保留一个“0”最后输出

因此,从以上假设来看,进行算法设计时,从具体到抽象的归纳一定要选取大量不同的实例充分了解和体会解决问题的过程,规律和各种不同情况,才能设计出正确的算法。

算法设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Java复制代码delete(char [],int b,int k){
int i;
for(i=b;i<length(n)-k;i++){
n[i]=n[i+k];
}
main(){

char n[100];
int s,i,j,j1,c,data [100],len;
cin>>n>>s;//n 为正整数,去除s个数字
len=length(n);
}
if(s>len){
cout<<"数据错误!";
return;
}
j1=0;
for(i=1;i<=s;i++){
{for(j=1;j<length(n);j=j+1)
{
if(n[j]>n[j+1]){ //贪心选择
delete(n,j,1);

}
if(j>j1){
data[i]=j+i; //记录删除数字的位置
}else{ //假设2向前删除
data[i]=data[i-1]-1;

}
j1=j;
break;
}
if(j>length(n)){
break;
}
for(i=i;i<=s;i++){
j=len-i+1;
delete(n,j,1);
data[i]=j;

} while(n[1]='0' && length(n)>1){
delete(n,1,1); //将字符串首的若干“0”去掉
cout<<n;


for(i=1;i<=s;i++){
cout<<data[i]<<'';
}
}
}
}
}

综上所述:算法主要由四部分组成:初始化,相邻数字比较(必要时删除),处理比较过程中删除不够s位的情况和结果输出。

跋尾

本篇内容就到这里了~ 我是Zeus👩🏻‍🚀来自一个互联网底层组装员,下一篇再见! 📖

本文转载自: 掘金

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

《DUBBO系列》源码解析-dubbo协议

发表于 2021-11-13

网络协议

网络协议(应用层)是计算机网络中双方共同确定的交流语义,因为在通信双方都是以字节流的形式进行交换信息,所以需要一种约定对信息进行编解码操作。

常见的协议模式

既然协议是一种双方的约定,那如何制定这种通信的规范呢?在TCP协议中是以字节流的形式进行传输数据,并且为了提高数据传输效率,会对消息进行缓冲再一起发送。这就导致一个问题,每次发送的内容不一定是一个具有完整意义的报文。基于TCP协议之上的应用层协议需要知道一个完整的数据从何开始,从何结束,这就是所谓粘包和拆包。因此有以下常见处理方式:

  • 定长
    固定长度的协议报文,每次读取固定长度的字节进行解析。缺点是真实场景中报文长度都是不固定的,灵活性太差。
  • 使用特殊字符
    使用某个特殊字符作为结束标志,如果需要传的内容和特殊字符一样就很蛋疼。
  • 协议头+payload
    使用固定长度的消息头加可变长度的payload,在消息头中标识payload的长度,这种方式就比较好一些,也是常用的方式。

dubbo协议

dubbo协议是开源RPC框架DUBBO中自定义的私有化协议,使用协议头+payload的方式。

image.png
duboo协议大概就长这样,接下来我们来看一下DUBBO框架在消息的编解码这块是怎么处理的。DUBBO的模块划分很清晰,因此可以先从这个入口开始看Netty服务的启动。
org.apache.dubbo.remoting.transport.netty.NettyServer#doOpen

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
ini复制代码@Override
protected void doOpen() throws Throwable {
NettyHelper.setNettyLoggerFactory();
ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));
ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));
ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));
bootstrap = new ServerBootstrap(channelFactory);
final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
channels = nettyHandler.getChannels();
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("backlog", getUrl().getPositiveParameter(BACKLOG_KEY, Constants.DEFAULT_BACKLOG));
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this); // 适配器设计模式
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", adapter.getDecoder()); // 解码器
pipeline.addLast("encoder", adapter.getEncoder()); // 编码器
pipeline.addLast("handler", nettyHandler);
return pipeline;
}
});
// bind
channel = bootstrap.bind(getBindAddress());
}

可以看出这里对Netty的编解码接口进行了适配,NettyCodecAdapter 类中组合了一个重要的接口 Codec2,这是DUBBO中编解码抽象的一个接口。还有两个内部类InternalEncoder 和 InternalDecoder 实现了Netty中的编解码的接口,接口方法中会调用Codec2的方法从而实现接口适配。

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
ini复制代码private class InternalEncoder extends OneToOneEncoder {

@Override
protected Object encode(ChannelHandlerContext ctx, Channel ch, Object msg) throws Exception {
org.apache.dubbo.remoting.buffer.ChannelBuffer buffer =
org.apache.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer(1024);
NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler);
try {
codec.encode(channel, buffer, msg); // 真正进行编码
} finally {
NettyChannel.removeChannelIfDisconnected(ch);
}
return ChannelBuffers.wrappedBuffer(buffer.toByteBuffer());
}
}

private class InternalDecoder extends SimpleChannelUpstreamHandler {

private org.apache.dubbo.remoting.buffer.ChannelBuffer buffer =
org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception {
Object o = event.getMessage();
if (!(o instanceof ChannelBuffer)) {
ctx.sendUpstream(event);
return;
}

ChannelBuffer input = (ChannelBuffer) o;
int readable = input.readableBytes();
if (readable <= 0) {
return;
}

org.apache.dubbo.remoting.buffer.ChannelBuffer message;
if (buffer.readable()) {
if (buffer instanceof DynamicChannelBuffer) {
buffer.writeBytes(input.toByteBuffer());
message = buffer;
} else {
int size = buffer.readableBytes() + input.readableBytes();
message = org.apache.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer(
size > bufferSize ? size : bufferSize);
message.writeBytes(buffer, buffer.readableBytes());
message.writeBytes(input.toByteBuffer());
}
} else {
message = org.apache.dubbo.remoting.buffer.ChannelBuffers.wrappedBuffer(
input.toByteBuffer());
}

NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
Object msg;
int saveReaderIndex;

try {
// decode object.
do {
saveReaderIndex = message.readerIndex();
try {
msg = codec.decode(channel, message); // 真正进行解码
} catch (IOException e) {
buffer = org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
throw e;
}
if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
message.readerIndex(saveReaderIndex);
break;
} else {
if (saveReaderIndex == message.readerIndex()) {
buffer = org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
throw new IOException("Decode without read data.");
}
if (msg != null) {
Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress());
}
}
} while (message.readable());
...// 以下省略

了解了接口的适配,接下来看看Codec2 接口

image.png
可以看到Codec2使用了SPI注解是一个扩展点,但是框架本身只写了dubbo协议一种编解码的实现,和dubbo协议相关的类有

  • AbstractCodec // 抽象类,定义了一些静态方法
  • ExchangeCodec // duboo协议编解码主要逻辑
  • DubboCodec // 解析payload主要逻辑
  • DubboCountCodec // 包装类,添加额外功能
    接下详细看 org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(),解码过程
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
ini复制代码protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// check magic number. 这里需要找到魔数的位置进行保存然后继续读取,逻辑对应org.apache.dubbo.remoting.transport.netty.NettyCodecAdapter.InternalDecoder#messageReceived 中
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
return super.decode(channel, buffer, readable, header);
}
// check length. HEADER_LENGTH即消息头的长度16个字节
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}

// get data length. 16-12=4个字节记录payload的大小
int len = Bytes.bytes2int(header, 12);
checkPayload(channel, len);

int tt = len + HEADER_LENGTH;
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}

// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

try {
return decodeBody(channel, is, header); // 解析payload
} finally {
if (is.available() > 0) {
try {
if (logger.isWarnEnabled()) {
logger.warn("Skip input stream " + is.available());
}
StreamUtils.skipUnusedStream(is);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}

接下来开始解析payload,会根据header中的信息使用不同的对象序列化等策略,此时应该对应默认dubbo协议
org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody

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
scss复制代码protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK); // 由此可知header中第三个字节保存的是序列化方式
// get request id.
long id = Bytes.bytes2long(header, 4); // 由此可知从第5个到12个共8个字节记录 请求Id(异步转同步很重要的参数,下回分析)
if ((flag & FLAG_REQUEST) == 0) { // 这里在第3个字节中同时记录了是请求还是响应,是twoway类型(双方有来有回)还是event类型(不需要回应),没理解为啥要这样,这应该是后面加的功能
// decode response.
Response res = new Response(id);
if ((flag & FLAG_EVENT) != 0) {
res.setEvent(true);
}
// get status.
byte status = header[3]; // 第4个字节记录响应的状态
res.setStatus(status);
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto); // 对象序列化
if (status == Response.OK) {
Object data;
if (res.isHeartbeat()) {
data = decodeHeartbeatData(channel, in);
} else if (res.isEvent()) {
data = decodeEventData(channel, in);
} else {
data = decodeResponseData(channel, in, getRequestData(id));
}
res.setResult(data);
} else {
res.setErrorMessage(in.readUTF());
}
} catch (Throwable t) {
res.setStatus(Response.CLIENT_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
return res;
}
...省略
}

通过源码的追踪,我们了解了dubboo 协议大致如下

1
2
3
4
5
6
7
lua复制代码header--16个字节
--1,2 魔数dabb
--3 序列化标记
--4 status
--5~12 requestId
--12~16 date length
payload-- Bytes.bytes2int(header, 12);

dubbo 协议看起来比较紧凑,在header中没有预留多余的字段,然后其实RPC 调用中还有很多参数是放在payload里的需要进行反序列化才能解析得到。通过阅读源码我们发现了一个问题,ExchangeCodec类从名字来看可以支持不同协议的转换,但是魔数和请求头的校验确固定了,并且其中的 decodeBody方法和子类DubboCodec中是一样的…总之能根据自己的业务场景设计出一个自定义协议还是挺有挑战性的。关于DUBBO3 中的主推的Triple 协议请听下回分解。

Triple 协议是 Dubbo3 推出的主力协议。Triple 意为第三代,通过 Dubbo1.0/ Dubbo2.0 两代协议的演进,以及云原生带来的技术标准化浪潮,Dubbo3 新协议 Triple 应运而生。

本文转载自: 掘金

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

软件架构-缓存技术

发表于 2021-11-13

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

你好,我是看山。

本文源自并发编程网的翻译邀请,翻译的是 Jakob Jenkov 的 《软件架构》中关于缓存技术的内容,虽然是 2014 年的文章,但是从软件架构层面上,并不过时。

缓存

缓存是一种加速数据查找(数据读取)的技术,直接读取本地缓存的数据,而不是从数据源读取数据,数据源包括数据库、其他远程系统。

图片

缓存是比源数据更靠近使用方的一块存储空间,可以更快的读取操作。缓存的存储介质一般是内存或磁盘,很多时候会选择内存作为缓存介质,但是内存缓存会在系统重启时丢失数据。

在软件系统中,数据缓存存在多层缓存级别或多层缓存系统。在 web 应用中,缓存至少有 3 种存储位置,如下图所示:

图片

在 web 应用中,我们会使用各种各样的数据库存储数据,这些数据库可以将数据存放在内存中,以便我们直接读取,而不需要从磁盘中读取数据。web 服务器可以在内存中缓存图片、css 文件、js 文件等,不需要每次需要的时候从硬盘中访问文件。web 应用可以将从数据库读取的数据缓存起来,这样就不需要每次使用的时候都通过网络从数据库中读取数据了。最后,浏览器也可能存储静态文件和数据。在支持 HTML5 的浏览器中,有 localstorage 存储空间、应用数据缓存、本地 sql 存储等技术支持缓存。

当我们提到缓存的时候,有下面几项内容需要考虑:

•写缓存•保持缓存和远程系统数据同步•管理缓存大小

我会在接下来的内容中讨论这几项内容。

写缓存

第一项挑战是从远程系统中读取数据写到缓存中,一般有两种方式:

•提前写缓存•用时写缓存

提前写缓存是在系统启动的时候,就将需要的数据缓存起来。要做到这一点,需要提前知道哪些数据需要缓存。但是我们有时候并不知道哪些数据需要在系统启动时候就缓存起来。

用时写缓存是说,在第一次使用数据的时候,将数据缓存起来,之后就可以使用缓存中的数据了。这种操作的方式是,首先检查缓存中是否有数据,有就直接使用,如果没有,就从远程系统读取数据,然后写入缓存中。

下表中我列出了提前写入和用时写入的优缺点:

|

| 优点 | 缺点 |
| — | — | — |
| 提前写缓存 | 比用时写入减少了第一次缓存数据的延迟 | 系统启动初始化缓存数据的时候,需要比较长的时间。而且,有可能缓存的数据永远不会被用到。 |
| 用时写缓存 | 缓存的数据都是需要被用到的数据,而且没有启动延迟 | 在第一次缓存数据的时候,用的时间比较长,可能导致用户体验不一致 |

当然,在真正实践过程中,我们可能两种方式并用:我们可以对热点数据使用提前缓存的方式,对其他数据使用用时缓存的方式。

保持缓存和远程系统数据同步

缓存数据的一个巨大挑战是保持缓存数据与远程系统数据保持同步,也就是数据一致。根据系统结构的不同,一般有不同的方式实现这个,我们来聊聊这几种方式。

直接式缓存

直写式缓存是允许读写缓存的一种方式,这种方式是,保存缓存数据的计算机,在将数据写入缓存的同时,将数据写到远程系统中。简单说就是,写入操作被写到远程系统中。

只有远程系统的数据只能被直写式缓存修改时,这种方式才起作用。如果所有的数据读写都要经过直写式缓存系统,那就很容易将写入的数据更新到远程系统中,保持缓存与远程系统数据的一致性。

基于过期时间

如果远程系统可以不依赖远程系统进行数据更新,那缓存和远程系统之间数据同步就很难通过直写式缓存方式保证了。

保持缓存数据同步的一种方法是,为数据设置一个缓存时间。当数据过期时,就把这些数据从缓存中清除。如果再次需要读取这些数据,可以从远程系统中读取最新的数据缓存起来。

数据过期时间取决于系统需要,有些类型的数据(比如文章),可能不需要随时的完全更新,可以设置 1 小时的过期时间。对于某些文章,你甚至可以忍受 24 小时的过期时间。

需要注意的是,如果过期时间比较短,可能会频繁读取远程系统,降低缓存的作用。

主动过期

还有一种方式是主动过期,是指主动更新缓存数据。比如,远程系统数据更新时,发送一条消息到缓存系统中,指示系统数据已被更新,可以将数据设置为过期。

主动过期的优点是,可能保证远程系统数据更新后,缓存数据被尽快的更新。还有一个附加好处是“基于过期时间”方式没有办法是实现的,就是不会频繁更新没有修改的数据。

主动过期的缺点是,需要能够检测远程系统数据的变化。如果远程系统是一个关系型数据库,可以被不同的机制更新数据,那每种更新机制都需要报告他们更新了哪些数据,否则,就没有办法向缓存数据的系统通知过期消息了。

管理缓存大小

管理缓存大小,是一个重要的方面。许多系统存储了大量数据,以至于不可能将所有数据都存储在缓存中。因此,需要一种机制来管理缓存的数据量。管理缓存大小通常是将不需要的缓存数据清除,来腾出足够的空间。一般有下面几种方式:

•基于时间清理•先进先出(FIFO)•先进后出(FILO)•最少被使用•最小访问间隔

基于时间清理方式是类似于前面提到的基于时间过期。除了可以保持数据与远程系统同步,还能够减少缓存数据的大小。可以开启一个单独的监听线程,也可以在读写新值的时候清理数据。

先进先出清理方式意味着,当写入一个新的缓存的时候,就需要删除最早插入的缓存值。如果空间足够,也是可以不删除任何数据的。

先进后出的方式正好和先进先出相反,这种方式对于先存储的数据时热点数据的情况比较有用。

最少被使用清理方式是首先清理访问次数最少的缓存数据。这种方式的目的是避免清理热点数据,为了实现这种方式,需要记录缓存数据被访问的次数。需要注意一个问题,缓存中的旧值可能有较高的访问次数,这样就意味着这些旧值不会被清理。比如一篇旧文章的缓存,以前被访问过很多次,但是最近很少访问了,但是因为原来的访问量很高,尽管目前访问量较低,也不会被清理。为了避免这种情况,访问次数可以是针对 N 个小时统计。

最小访问间隔清理方式是将访问时间间隔考虑在内。访问某个缓存数据时,就需要标记访问该数据的时间并增加访问次数。第二次访问这个缓存数据时,就增加访问次数,并计算平均访问时间。那些曾经是热点数据,被频繁访问,但是最近访问时间间隔变长,访问频率下降的数据,其平均访问时间会降低,当降到足够低的时候,就会被清理。

有一种变化方式是,只计算最后 N 次访问的时间。N 可以是 100、1 或者其他任何有意义的数。每当访问计数到 N 时,访问计数被重置为 0,记录下来访问时间。这种方式可以更快的清理热度下降的数据。

还有一种变化方式是,定期重置访问计数,并且只使用最小访问的清理方式。比如,每缓存一个小时的数据,前一个小时的访问计数会存储在另一个变量中,以便决策清理时使用。下一个小时访问计数重置为 0。这种机制具有上次变化相同的效果。

最后两个变体之间的差异总结起来就是在每次缓存检查时,访问计数是否已达到 N,或者时间间隔是否已超过 Y。第一种方式是每隔 N 次访问一次系统时钟,而第二种方式在每次访问时都读取一次系统时钟(查看时间间隔是否已过期)。因为检查一个整数通常比读取系统时钟快,所以我会选择第一种方式。

请记住,即使使用缓存大小管理系统,也需要清理、读取和存储数据,以保证他们能够与远程系统保持一致。尽管缓存的数据被大量访问而驻留在系统中,有时候也需要与远程系统同步。

服务器集群中的缓存

单一服务中的缓存设计更加简单,因为你能够保证,所有写入操作都通过一个服务器,可以使用直写式缓存方式。但是在分布式集群中,情况会比较复杂,下图说明了这种情况:

图片

简单的使用直写式缓存只会更新写操作的服务器上的缓存,集群中其他服务器对此完全不知情,也就不会更新数据。

在服务器集群中,可以使用基于时间的过期策略或者主动过期策略,来保证缓存数据与远程系统的同步。

缓存产品

实现自己的缓存系统并不难弄,取决于是否需要深度定制。如果没有必要自己实现缓存系统,可以用已经现成的缓存产品。比如:

  • Memcached
  • Ehcache
  • Redis【译者加】

我不知道这些产品是否能够满足需要,但是我知道他们用的比较广泛。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Java 日志记录最佳实践,写得太好了吧! 一、日志简介 二

发表于 2021-11-13

作者:GeekerLou

来源:jianshu.com/p/546e9aace657

一、日志简介

1.1 日志是什么(WHAT)

日志:记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。

通常,Java程序员在开发项目时都是依赖Eclipse/IDEA等集成开发工具的Debug 调试功能来跟踪解决Bug,但项目发布到了测试、生产环境怎么办?你有可能会说可以使用远程调试,但实际并不能允许让你这么做。

所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。

很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其不科学的!对于日志来说,只是在方法开始、结束、异常时输出一些什么,那是绝对不够的,这样的日志对于日志分析没有任何意义。如果在方法的开始和结束整个日志,那方法中呢?如果方法中没有日志的话,那就完全失去了日志的意义!如果应用出现问题要查找由什么原因造成的,也没有什么作用。这样的日志还不如不用!

1.2 日志有什么用(WHY)

不管是使用何种编程语言,日志输出几乎无处不再。总结起来,日志大致有以下几种用途:

  • 「问题追踪」:辅助排查和定位线上问题,优化程序运行性能。
  • 「状态监控」:通过日志分析,可以监控系统的运行状态。
  • 「安全审计」:审计主要体现在安全上,可以发现非授权的操作。

1.3 总结

日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现 BUG 时能快速进行定位,并能找出其中的原因。

作为一个有修养的程序猿,对日志这个东西应当引起足够的重视。

二、日志框架(HOW)

2.1 常用的日志框架

log4j、Logging、commons-logging、slf4j、logback,开发的同学对这几个日志相关的技术不陌生吧,为什么有这么多日志技术,它们都是什么区别和联系呢?且看下文分解:

2.1.1 Logging

这是 Java 自带的日志工具类,在 JDK 1.5 开始就已经有了,在 java.util.logging 包下。通常情况下,这个基本没什么人用了,了解一下就行。

2.1.2 commons-logging

commons-logging 是日志的门面接口,它也是Apache 最早提供的日志门面接口,用户可以根据喜好选择不同的日志实现框架,而不必改动日志定义,这就是日志门面的好处,符合面对接口抽象编程。现在已经不太流行了,了解一下就行。

2.1.3 Slf4j

slf4j,英文全称为“Simple Logging Facade for Java”,为java提供的简单日志Facade。Facade门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过slf4j接入不同的日志系统。

因此slf4j入口就是众多接口的集合,它不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。具体有哪些接口,全部都定义在slf4j-api中。查看slf4j-api源码就可以发现,里面除了public final class LoggerFactory类之外,都是接口定义。因此slf4j-api本质就是一个接口定义。

2.1.4 Log4j

Log4j 是 Apache 的一个开源日志框架,也是市场占有率最多的一个框架。

注意:log4j 在 2015.08.05 这一天被 Apache 宣布停止维护了,用户需要切换到 Log4j2上面去。

下面是官宣原文:

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life. For complete text of the announcement please see the Apache Blog. Users of Log4j 1 are recommended to upgrade to Apache Log4j 2.

2.1.5 Log4j2

Log4j 2 Apache Log4j 2是apache开发的一款Log4j的升级产品。

Log4j2与Log4j1发生了很大的变化,log4j2不兼容log4j1。

2.1.6 Logback

Logback 是 Slf4j 的原生实现框架,同样也是出自 Log4j 一个人之手,但拥有比 log4j 更多的优点、特性和更做强的性能,现在基本都用来代替 log4j 成为主流。

Logback相对于log4j拥有更快的执行速度。基于我们先前在log4j上的工作,logback 重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上10倍。在保证logback的组件更加快速的同时,同时所需的内存更加少。

2.2 日志框架怎么选

选项太多了的后果就是选择困难症,我的看法是没有最好的,只有最合适的:

  • commons-loggin、slf4j 只是一种日志抽象门面,不是具体的日志框架。log4j、logback 是具体的日志实现框架。
  • 在比较关注性能的地方,选择Logback或自己实现高性能Logging API可能更合适。推荐:slf4j + logback.
  • 在已经使用了Log4j的项目中,如果没有发现问题,继续使用可能是更合适的方式:推荐组合为:slf4j + log4j2.
  • 如果不想有依赖则使用java.util.logging或框架容器已经提供的日志接口。

三、记录日志的时机

在看线上日志的时候,我们可曾陷入到日志泥潭?该出现的日志没有,无用的日志一大堆,或者需要的信息分散在各个角落,特别是遇到紧急的在线bug时,有效的日志被大量无意义的日志信息淹没,焦急且无奈地浪费大量精力查询日志。那什么是记录日志的合适时机呢?

总结几个需要写日志的点:

  • 「编程语言提示异常」:如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用warn或者error级别。
  • 「业务流程预期不符」:除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。
  • 「系统核心角色,组件关键动作」:系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录INFO级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点INFO记录,其余酌情考虑DEBUG级别。
  • 「系统初始化」:系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录INFO日志,打印出参数以及启动完成态服务表述。

四、日志打印最佳实践

4.1 日志变量定义

日志变量往往不变,最好定义成final static,变量名用大写。

1
java复制代码private static final Logger log = LoggerFactory.getLogger({SimpleClassName}.getClass());

通常一个类只有一个 log 对象,如果有父类可以将 log 定义在父类中。

日志变量类型定义为门面接口(如 slf4j 的 Logger),实现类可以是 Log4j、Logback 等日志实现框架,不要把实现类定义为变量类型,否则日志切换不方便,也不符合抽象编程思想。

另外,推荐引入lombok的依赖,在类的头部加上@Slf4j的注解,之后便可以在程序的任意位置使用log变量打印日志信息了,使用起来更加简洁一点,在重构代码尤其是修改类名的时候无需改动原有代码。

4.2 参数占位格式

使用参数化形式{}占位,[]进行参数隔离

1
2
css复制代码log.debug("Save order with order no:[{}], and order amount:[{}]");
log.debug("Save order with order no:[{}], and order amount:[{}]");

这种可读性好,这样一看就知道[]里面是输出的动态参数,{}用来占位类似绑定变量,而且只有真正准备打印的时候才会处理参数,方便定位问题。

如果日志框架不支持参数化形式,且日志输出时不支持该日志级别时会导致对象冗余创建,浪费内存,此时就需要使用 isXXEnabled 判断,如:

1
2
3
4
lua复制代码if(log.isDebugEnabled()){
// 如果日志不支持参数化形式,debug又没开启,那字符串拼接就是无用的代码拼接,影响系统性能
log.debug("Save order with order no:" + orderNo + ", and order amount:" + orderAmount);
}

至少 debug 级别是需要开启判断的,线上日志级别至少应该是 info 以上的。

这里推荐大家用 SLF4J 的门面接口,可以用参数化形式输出日志,debug 级别也不必用 if 判断,简化代码。

4.3 日志的基本格式

日志输出主要在文件中,应包括以下内容:

  • 日志时间
  • 日志级别主要使用
  • 调用链标识(可选)
  • 线程名称
  • 日志记录器名称
  • 日志内容
  • 异常堆栈(不一定有)
1
ini复制代码11:44:44.827 WARN [93ef3E0120160803114444] [main] [ClassPathXmlApplicationContext] Exception encountered during context initialization - cancelling refresh attempt

4.3.1 日志时间

作为日志产生的日期和时间,这个数据非常重要,一般精确到毫秒。由于线上一般配置为按天滚动日志文件,日期标识在文件名上,所以可以不放在这个时间中,使用 HH:mm:ss.SSS 格式即可。非要加上也未尝不可,格式推荐:yyyy-MM-dd HH:mm:ss.SSS。

4.3.2 日志级别

日志的输出都是分级别的,不同的设置不同的场合打印不同的日志。下面拿最普遍用的 Log4j 日志框架来做个日志级别的说明,这个也比较齐全,其他的日志框架也都大同小异。

主要使用如下的四个级别:

  • DEBUG:DEUBG 级别的主要输出调试性质的内容,该级别日志主要用于在开发、测试阶段输出。该级别的日志应尽可能地详尽,开发人员可以将各类详细信息记录到DEBUG里,起到调试的作用,包括参数信息,调试细节信息,返回值信息等等,便于在开发、测试阶段出现问题或者异常时,对其进行分析。
  • INFO:INFO日志主要记录系统关键信息,旨在保留系统正常工作期间关键运行指标,开发人员可以将初始化系统配置、业务状态变化信息,或者用户业务流程中的核心处理记录到INFO日志中,方便日常运维工作以及错误回溯时上下文场景复现。建议在项目完成后,在测试环境将日志级别调成 INFO,然后通过 INFO 级别的信息看看是否能了解这个应用的运用情况,如果出现问题后是否这些日志能否提供有用的排查问题的信息。
  • WARN:WARN 级别的主要输出警告性质的内容,这些内容是可以预知且是有规划的,比如,某个方法入参为空或者该参数的值不满足运行该方法的条件时。在 WARN 级别的时应输出较为详尽的信息,以便于事后对日志进行分析
  • ERROR:ERROR 级别主要针对于一些不可预知的信息,诸如:错误、异常等,比如,在 catch 块中抓获的网络通信、数据库连接等异常,若异常对系统的整个流程影响不大,可以使用 WARN 级别日志输出。在输出 ERROR 级别的日志时,尽量多地输出方法入参数、方法执行过程中产生的对象等数据,在带有错误、异常对象的数据时,需要将该对象一并输出

4.3.2.1 INFO和DEBUG的选择

DEBUG级别比INFO低,包含调试时更详细的了解系统运行状态的东西,比如变量的值等等,都可以输出到DEBUG日志里。INFO是在线日志默认的输出级别,反馈系统的当前状态给最终用户看的。输出的信息,应该对最终用户具有实际意义的。从功能角度上说,Info输出的信息可以看作是软件产品的一部分,所以需要谨慎对待,不可随便输出。尝试记录INFO日志时不妨在头脑中模拟线上运行,如果这条日志会被频繁打印或者大部分时间对于纠错起不到作用,就应当考虑下调为DEBUG级别。

  • 由于info及debug日志打印量远大于ERROR,出于前文日志性能的考虑,如果代码为核心代码,执行频率非常高,务必推敲日志设计是否合理,是否需要下调为DEBUG级别日志。
  • 注意日志的可读性,不妨在写完代码review这条日志是否通顺,能否提供真正有意义的信息。
  • 日志输出是多线程公用的,如果有另外一个线程正在输出日志,上面的记录就会被打断,最终显示输出和预想的就会不一致。

4.3.2.2 WARN,ERROR的选择

当方法或者功能处理过程中产生不符合预期结果或者有框架报错时可以考虑使用,常见问题处理方法包括:

  • 增加判断处理逻辑,尝试本地解决:增加逻辑判断吞掉报警永远是最优选择。
  • 抛出异常,交给上层逻辑解决
  • 记录日志,报警提醒
  • 使用返回码包装错误做返回

一般来说,WARN级别不会短信报警,ERROR级别则会短信报警甚至电话报警,ERROR级别的日志意味着系统中发生了非常严重的问题,必须有人马上处理,比如数据库不可用,系统的关键业务流程走不下去等等。错误的使用反而带来严重的后果,不区分问题的重要程度,只要有问题就error记录下来,其实这样是非常不负责任的,因为对于成熟的系统,都会有一套完整的报错机制,那这个错误信息什么时候需要发出来,很多都是依据单位时间内ERROR日志的数量来确定的。因此如果我们不分轻重缓急,一律ERROR对待,就会徒增报错的频率,久而久之,我们的救火队员对错误警报就不会那么在意,这个警报也就失去了原始的意义。

WARN代表可恢复的异常,此次失败不影响下次业务的执行,开发人员会苦恼某些场景下几次失败可容忍,频率高的时候需要提醒,记录ERROR的结果是线上时不时出现容忍范围内的报警,这时报警是无意义的。但反之不记录ERROR日志,真正出现问题则不会有实时报警,错过最佳处理时机。

强调ERROR报警

  • ERROR级别的日志打印通常伴随报警通知。ERROR的报出应该伴随着业务功能受损,即上面提到的系统中发生了非常严重的问题,必须有人马上处理。

ERROR日志目标

  • 给处理者直接准确的信息:error信息形成自身闭环。

问题定位:

  • 发生了什么问题,哪些功能受到影响
  • 获取帮助信息:直接帮助信息或帮助信息的存储位置
  • 通过报警知道解决方案或者找何人解决

日志模板

1
2
ini复制代码log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [Probably need to do] [params] .”);
log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [please contact xxx@xxx] [params] .”);

4.3.3 调用链标识

在分布式应用中,用户的一个请求会调用若干个服务完成,这些服务可能还是嵌套调用的,因此完成一个请求的日志并不在一个应用的日志文件,而是分散在不同服务器上不同应用节点的日志文件中。该标识是为了串联一个请求在整个系统中的调用日志。

调用链标识格式:

  • 唯一字符串(trace ID)
  • 调用层级(span ID)

调用链标识作为可选项,无该数据时只输出 [] 即可。

4.3.4 线程名称

输出该日志的线程名称,一般在一个应用中一个同步请求由同一线程完成,输出线程名称可以在各个请求产生的日志中进行分类,便于分清当前请求上下文的日志。

4.3.5 日志记录器名称

日志记录器名称一般使用类名,日志文件中可以输出简单的类名即可,看实际情况是否需要使用包名和行号等信息。主要用于看到日志后到哪个类中去找这个日志输出,便于定位问题所在。

4.3.6 日志内容

  • 禁用 System.out.println和System.err.println
  • 变参替换日志拼接
  • 输出日志的对象,应在其类中实现快速的 toString 方法,以便于在日志输出时仅输出这个对象类名和 hashCode
  • 预防空指针:不要在日志中调用对象的方法获取值,除非确保该对象肯定不为 null,否则很有可能会因为日志的问题而导致应用产生空指针异常。
1
2
3
4
5
c复制代码// 不推荐
log.debug( "Load student(id={}), name: {}" , id , student.getName() );

// 推荐
log.debug( "Load student(id={}), student: {}" , id , student );

对于一些一定需要进行拼接字符串,或者需要耗费时间、浪费内存才能产生的日志内容作为日志输出时,应使用 log.isXxxxxEnable() 进行判断后再进行拼接处理,比如:

1
2
3
4
5
6
7
8
erlang复制代码if (log.isDebugEnable()) {
StringBuilder builder = new StringBuilder();
for (Student student : students) {
builder.append("student: ").append(student);
}
builder.append("value: ").append(JSON.toJSONString(object));
log.debug( "debug log example, detail: {}" , builder );
}

4.3.7 异常堆栈

异常堆栈一般会出现在 ERROR 或者 WARN 级别的日志中,异常堆栈含有方法调用链的系统,以及异常产生的根源。异常堆栈的日志属于上一行日志的,在日志收集时需要将其划至上一行中。

4.4 日志文件

日志文件放置于固定的目录中,按照一定的模板进行命名,推荐的日志文件名称:

1
2
xml复制代码当前正在写入的日志文件名:<应用名>[-<功能名>].log
已经滚入历史的日志文件名:<应用名>[-<功能名>].log.<yyyy-MM-dd>

4.5 日志配置

根据不同的环境配置不同的日志输出方式:

  • 本地调试可以将日志输出到控制台上
  • 测试环境或者生产环境输出到文件中,每天产生一个文件,如果日志量庞大可以每个小时产生一个日志文件
  • 生产环境中的文件输出,可以考虑使用异步文件输出,该种方式日志并不会马上刷新到文件中去,会产生日志延时,在停止应用时可能会导致一些还在内存中的日志未能及时刷新到文件中去而产生丢失,如果对于应用的要求并不是非常高的话,可暂不考虑异步日志

logback 日志工具可以在日志文件滚动后将前一文件进行压缩,以减少磁盘空间占用,若使用 logback 对于日志量庞大的应用建议开启该功能。

4.6 日志使用规范

  1. 在一个对象中通常只使用一个Logger对象,Logger应该是static final的,只有在少数需要在构造函数中传递logger的情况下才使用private final。
1
arduino复制代码private static final Logger log = LoggerFactory.getLogger(Main.class);
  1. 不要使用具体的日志实现类
1
ini复制代码InterfaceImpl interface = new InterfaceImpl();

这段代码大家都看得懂吧?应该面向接口的对象编程,而不是面向实现,这也是软件设计模式的原则,正确的做法应该是。

1
ini复制代码Interface interface = new InterfaceImpl();

日志框架里面也是如此,上面也说了,日志有门面接口,有具体实现的实现框架,所以大家不要面向实现编程。

  1. 输出Exceptions的全部Throwable信息。因为log.error(msg)和log.error(msg,e.getMessage())这样的日志输出方法会丢失掉最重要的StackTrace信息。
1
2
3
4
5
6
7
8
9
c复制代码void foo(){
try{
//do somehing
}catch(Exception e){
log.error(e.getMessage());//错误示范
log.erroe("Bad Things",e.getMessage());//错误示范
log.error("Bad Things",e);//正确演示
}
}
  1. 不允许记录日志后又抛出异常。如捕获异常后又抛出了自定义业务异常,此时无需记录错误日志,由最终捕获方进行异常处理。不能又抛出异常,又打印错误日志,不然会造成重复输出日志。
1
2
3
4
5
6
7
8
csharp复制代码void foo() throws LogException{
try{
//do somehing
}catch(Exception e){
log.error("Bad Things",e);//正确
throw new LogException("Bad Things",e);
}
}
  1. 不允许使用标准输出

包括System.out.println()和System.error.println()语句。因为这个只会打印到控制台,而不会记录到日志文件中,不方便管理日志。此外,标准输出不会显示类名和行号信息,一旦代码中大量出现标准输出的代码,且日志中打印有标准输出的内容,很难定位日志内容和日志打印的位置,根本无法排查问题,想删除无用日志输出也改不动,这个是笔者在重构古董代码的时候亲自踩过的一个坑。

1
2
3
4
5
6
7
8
9
csharp复制代码void foo(){
try{
//do somehing
}catch(Exception e){
Syste.out.println(e.getMessage());//错误
System.error.println(e.getMessage());//错误
log.error("Bad Things",e);//正确
}
}
  1. 不允许出现printStackTrace
1
2
3
4
5
6
7
8
csharp复制代码void foo(){
try{
//do somehing
}catch(Exception e){
e.printStacktrace();//错误
log.error("Bad Things",e);//正确
}
}

来看一下它的源码:

1
2
3
csharp复制代码public void printStackTrace() {
printStackTrace(System.err);
}

它其实也是利用 System.err 输出到了Tomcat控制台。

  1. 禁止在线上环境开启debug级别日志输出

出于日志性能的考虑,如果代码为核心代码,执行频率非常高,则输出日志建议增加判断,尤其是低级别的输出<debug、info、warn>。

一是因为项目本身 debug 日志太多,二是各种框架中也大量使用 debug 的日志,线上开启 debug 不久就会打满磁盘,影响业务系统的正常运行。

  1. 不要在大循环中打印日志

如果你的框架使用了性能不高的 Log4j 框架,那就不要在上千个 for 循环中打印日志,这样可能会拖垮你的应用程序,如果你的程序响应时间变慢,那要考虑是不是日志打印的过多了。

1
2
3
ini复制代码for(int i=0; i<2000; i++){
log.info("XX");
}

最好的办法是在循环中记录要点,在循环外面总结打印出来。

  1. 打印有意义的日志

通常情况下在程序日志里记录一些比较有意义的状态数据:程序启动,退出的时间点;程序运行消耗时间;耗时程序的执行进度;重要变量的状态变化。

五、参考资料

  1. Java 程序如何正确地打日志
  2. Java 应用中的日志
  3. 优秀日志实践准则
  4. Java常用日志框架介绍

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅发布,黑暗模式太炸了!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

本文转载自: 掘金

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

NET 5/6 配置自动注册 AutoConfigure

发表于 2021-11-13

功能打散揉碎成模块之后, 最麻烦的莫过于各个模块的配置如何加载.

.NET4.8 之前, 可以用自定义的 JsonConfig (读取 .config 文件太麻烦) 来加载配置,

.NET Core 之后提供了强大的配置系统, 如果在使用那个 JsonConfig 就显的太潦草了.

但是配置分布于各个模块, 模块和模块之间只是通过接口约束, 在这种情况下又如何使用配置呢?

在启动项目里注册 ?

一个两个也就算了, 百八十个的子模块, 按这样搞法, 岂不是一团乱麻?


搞过 IoC 自动注册的, 都知道扫描目录下的 DLL, 然后 AddSingleton, AddScoped, AddTransient, 这个不成功问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c#复制代码[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegistAttribute : Attribute
{
public RegistMode Mode { get; }
public Type ForType { get; }

...
...

var ts = asm.GetExportedTypes();
var tmps = ts.SelectMany(t => t.etCustomAttributes<RegistAttribute>().Select(a => new { t, attr = a }));
foreach (var t in tmps)
{
Regist(sc, t.attr.ForType ?? t.t, t.t, t.attr.Mode);
}

...
...
case RegistMode.Singleton:
sc.AddSingleton(forType, type);
...
...

不便之处

麻烦的是, IServiceCollection.Configure<T>(IConfiguration) 方法需要泛型参数 T。

基于现有知识,要想用上面注册 IoC 的方式来注册配置,那基本是不现实的:

因为 Attribute 目前还没有正式支持泛型

如果不使用泛型 Attribute, 只能想办法变通变通了:

通过反射来实现

扫描 DLL 里实现了 ICfg 接口的类型, 通过 Activator 创建一个实例, 然后调用 AutoConfigure

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
c#复制代码public interface ICfg
{
string Section { get; }

public void AutoConfigure(IServiceCollection sc, IConfiguration configuration);
}
...
...
public abstract class CfgBase<T> : ICfg where T : class
{
public abstract string Section { get; }

public void AutoConfigure(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}

...
...
public class ServiceCfg : CfgBase<ServiceCfg>
{
public override string Section => "Service";
...
...
var ts = asm.ExportedTypes;
var cfgTypes = ts.Where(t => !t.IsAbstract && !t.IsInterface && t.sAssignableTo(typeof(ICfg)));
foreach (var ct in cfgTypes)
{
var o = (ICfg)Activator.CreateInstance(ct, true);
o.AutoConfigure(sc, configuration);
...
...

这种方法其实还好, 唯一不爽的是, 必须通过 Activator 来创建一个对象, 然后在进行配置注册。

通过泛型特性的实现方法

上面说 Attribute 还未正式支持泛型,意思是说已经可以这样写了:

1
2
3
4
5
6
7
8
c#复制代码public class RegistCfgAttribute<T> : RegistCfgAttribute where T : class
...
...
[RegistCfg<PriceChangeJobCfg>("PriceChange")]
public class PriceChangeJobCfg : BasePriceStockChangeJobCfg
{
...
...

前提是,要启用 preview 语法支持,修改项目文件, 加入 LangVersion

1
2
3
4
xml复制代码<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>

如果项目比较多, 一个一个加比较麻烦,也可以通过修改:Directory.Build.props 文件 (放到解决方案根目录下) :

1
2
3
4
5
xml复制代码<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>

这个方法看起来比较清爽, 但是是 preview 的, 能不能成为正式的, 还不好说。


完整示例

Program.cs

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
c#复制代码public static IHostBuilder CreateHostBuilder(string[] args) =>
Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, configuration) =>
{
//以 windows service 运行时, TopShelf 会将 c:\windows\system32 做为 baseDir, 会从这个目录里加载配置,
//所以, 用 Topshelf + CreateHostBuilder 这种方法的, 需要手动指定 basePath.
//直接 new ConfigurationBuilder() 的貌似没有这个问题.
var dir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
configuration.SetBasePath(dir);

//加载各个模块输出的配置
var dir2 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cfgs");
var fs = Directory.GetFiles(dir2, "*.json");
foreach (var f in fs)
configuration.AddJsonFile(f, true, true);
})
.ConfigureServices((hostContext, services) =>
{
#region 自动配置, 自动注册IoC
//通过 ICfg 实现的配置自动注册
services.AutoConfigure(hostContext.Configuration, Assembly.GetExecutingAssembly());
services.AutoConfigure(hostContext.Configuration);

// 通过泛型 Attribute 实现的配置自动注册, 需开启 preview 语法支持。
services.AutoConfigureByPreview(hostContext.Configuration, Assembly.GetExecutingAssembly());
services.AutoConfigureByPreview(hostContext.Configuration);

//从当前运行的 Assembly 里注册
services.AutoRegist(Assembly.GetExecutingAssembly());
services.AutoRegist();
#endregion
})
.ConfigureLogging((context, b) => b.AddLog4Net("log4net.config", true));

ICfg 配置类 (通过反射来实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码public interface ICfg
{
string Section { get; }
public void AutoConfigure(IServiceCollection sc, IConfiguration configuration);
}

public abstract class CfgBase<T> : ICfg where T : class
{
public abstract string Section { get; }

public void AutoConfigure(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}

public class ProducerCfg : CfgBase<ProducerCfg>
{
public override string Section => "Producer";
public string BrokerServerAddress { get; set; }
}

泛型特性配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c#复制代码public abstract class RegistCfgAttribute : Attribute
{
public abstract void Regist(IServiceCollection sc, IConfiguration configuration);
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class RegistCfgAttribute<T> : RegistCfgAttribute where T : class
{
public string Section { get; }
public RegistCfgAttribute(string section)
{
this.Section = section;
}
public override void Regist(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}

[RegistCfg<PriceChangeJobCfg>("PriceChange")]
public class PriceChangeJobCfg : BasePriceStockChangeJobCfg
{
public int TaskCount { get; set; } = 5;
}

扩展:

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
c#复制代码public static class RegistExtensions
{
public static void AutoRegist(this IServiceCollection sc, Assembly asm)
{
try
{
var ts = asm.GetExportedTypes();
var tmps = ts.SelectMany(t => t.GetCustomAttributes<RegistAttribute>().Select(a => new { t, attr = a }));

foreach (var t in tmps)
{
Regist(sc, t.attr.ForType ?? t.t, t.t, t.attr.Mode);
}
}
catch (Exception e)
{
}
}

private static void Regist(IServiceCollection sc, Type forType, Type type, RegistMode mode)
{

switch (mode)
{
case RegistMode.Singleton:
sc.AddSingleton(forType, type);
break;
case RegistMode.Scoped:
sc.AddScoped(forType, type);
break;
case RegistMode.Transient:
sc.AddTransient(forType, type);
break;
}
}


public static void AutoRegist(this IServiceCollection sc, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoRegist(sc, asm);
}

public static void AutoConfigure(this IServiceCollection sc, IConfiguration configuration, Assembly asm)
{
try
{
var ts = asm.ExportedTypes;
var cfgTypes = ts.Where(t => !t.IsAbstract && !t.IsInterface && t.IsAssignableTo(typeof(ICfg)));
foreach (var ct in cfgTypes)
{
var o = (ICfg)Activator.CreateInstance(ct, true);
o.AutoConfigure(sc, configuration);
}
}
catch
{
}
}

public static void AutoConfigure(this IServiceCollection sc, IConfiguration configuration, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoConfigure(sc, configuration, asm);
}

public static void AutoConfigureByPreview(this IServiceCollection sc, IConfiguration configuration, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoConfigureByPreview(sc, configuration, asm);
}

public static void AutoConfigureByPreview(this IServiceCollection sc, IConfiguration configuration, Assembly asm)
{
try
{
var ts = asm.GetExportedTypes();
var tmps = ts.Select(t => t.GetCustomAttribute<RegistCfgAttribute>())
.Where(t => t != null);

foreach (var t in tmps)
{
t.Regist(sc, configuration);
}
}
catch (Exception e)
{
}
}

private static IEnumerable<Assembly> DetectAssemblys(string searchPattern = "CNB.Job.*.dll")
{
var dlls = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, searchPattern);

foreach (var dll in dlls)
{
var asm = Assembly.LoadFrom(dll);
yield return asm;
}
}

}

本文转载自: 掘金

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

TCP重置攻击演示 前言

发表于 2021-11-13

前言

在TCP中,有一个标志是RST标志,它表示立即终止连接,也就是如果连接上的一方收到另一方的发送的RST数据包,它将立即关闭连接。

那是不是可以伪造一个数据包,让两台设备之间断开连接?

理论是可以的,但是需要几个很重要的参数,根据TCP规范,为了能正确响应RST数据包,RST数据包中必须具有与接受者期望的相同序列号,这个序列号如果不对,那还是无法断开连接。

假设A向B发送一个数据包,序列号为10,其中包含5个字节的数据,它将期望收到来自B的确认数据包,其确认号为15 (10 + 5)。

这需要知道TCP的三次握手过程。

三次握手分析

理解这一点的最好方法是使用Wireshark工具实际查看TCP数据包。

假设本机电脑ip是192.168.43.157,手机的ip是192.168.43.226。

image.png

由电脑监听7070端口,手机连接之后首先会发出三个包,被称为TCP的三次握手,其实就是互相交换一下控制信息。

第一次握手是由手机发出,包中的Flag为SYN,只有来自发送方的第一个数据包才应该设置此标志,它还包含自己的SEQ号,如果对方回应了,则对方的ACK确认号就是自己的SEQ+1,这里的SEQ是1952416868.

image.png

第二次握手是由电脑回应手机所发出的,包中的Flag为SYN、ACK,还有确认号是1952416869,用来告诉对方自己收到了你的请求,当然还要告诉对方自己的SEQ号,同样要等待对方回应,如果回应中的ACK值等于自己的SEQ+1,那么连接就建立完毕,这里的SEQ是1982386088。

image.png

第三次还是手机发出,用来回应电脑,告诉自己也能收到你的数据包,包中的Flag为ACK,ACK确认号为1982386089,SEQ是1952416869。

image.png

现在双方都互相持有对方的SEQ序列号,自身的SEQ值都被+1。

好了,现在由电脑先发出2字节的数据,Wireshark会捕获到4个包,原因是每发送一个数据包,都需要会有回应,电脑发送后,收到手机的回应,而手机也发送了2个字节的数据,需要电脑回应,所以有4个包。

image.png

这里最重要的还是SEQ的变化。

下面是电脑发送的数据包,SEQ还是自己的序列号,但是Next sequence number变成了他自身的SEQ+数据包大小,而他希望收到手机回应的数据包中ACK必须是这个Next sequence number值(1982386091),

image.png

所以来看下手机回应的。

可以看到ACK确认号就是1982386091,而其他都没有变。

image.png

接着手机又发出两个字节的数据,同样的流程,同样的校验方式。

netwox伪造数据包

现在来伪造一个数据包演示RST攻击。

而这个数据包中要包含原地址/端口,目标地址/端口,还要最重要的SEQ号。

我找了很多向客户端发送RST数据包的方法,最后发现netwox这个工具很方便,可用于伪造数据包。在www.cse.iitm.ac.in/~chester/co… 的第32页有一个很好的TCP攻击示例。

命令如下。

1
java复制代码netwox 40 -l 原ip -m 目标ip -o 监听的端口号 -p 客户端端口后 -B -q 原的SEQ号

从Wireshark中就可以看到客户端的端口号是42560,而原的SEQ号由最初的1982386088变成了1982386091,所以最终执行以下命令。

1
java复制代码netwox 40 -l 192.168.43.157 -m 192.168.43.226 -o 7070 -p 42560 -B -q 1982386091

执行后就能看到Android中抛出SocketException异常。

录屏_选择区域_20211113131342.gif

断开SSH连接

现在来尝试一下断开一个已经与远程服务取得连接的ssh。

还是来拿Wireshark筛选一下,条件是tcp.port==22 and tcp

1
java复制代码netwox 40 -m 116.62.xx.xx -l 192.168.43.157 -o 22 -p 37156 -B -q 1328649361

最后在终端会输出以下内容

1
java复制代码root@meet:~# packet_write_wait: Connection to 116.62.xx.xx port 22: Broken pipe

本文转载自: 掘金

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

1…354355356…956

开发者博客

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