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

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


  • 首页

  • 归档

  • 搜索

力扣第一百零一题-对称二叉树 前言 一、思路 二、实现 三、

发表于 2021-11-20

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

前言

力扣第一百零一题 对称二叉树 如下所示:

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

1
2
3
4
5
markdown复制代码    1
/ \
2 2
/ \ / \
3 4 4 3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

1
2
3
4
5
markdown复制代码    1
/ \
2 2
\ \
3 3

一、思路

题目中有一个关键信息:镜像对称。

那什么是镜像对称呢?就是指 根节点的左孩子 left 与右孩子 right相同,且 left.left == right.right 相同,left.right == right.left。如下图所示:

image.png

所以我们可以从根节点出发,到达根节点的左孩子 left 和右孩子 right,如果值相同则继续比对 left.left 是否与 right.right 相等,再比对 left.right 和 right.left 即可。

因为每一个节点都有可能是一个子树,所以我们这里使用递归是一个不错的选择,就不用手动去维护栈了。

在递归中我们要处理一些特殊的边界情况,如下所示:

  1. 如果根节点为空 null,则直接返回 true 即可。(空节点我们认为也是镜像对称的)
  2. 如果 left == right == null,也可以返回 true(两个空节点是相等的)
  3. 如果 left 和 right 中只有一个为空,此时返回 false(很显然空节点不等于有值的节点)

举个例子

我们以下方的树作为例子:

image.png

  1. 对比根节点的左孩子,相等则继续向下

image.png

  1. 继续对比子树 left 的左孩子和子树 right 的右孩子,发现 5 != 3,则表示这个树不为镜像对称

image.png

  1. 找到结果后,直接返回 false 即可

二、实现

实现代码

实现代码与思路中保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    public boolean isSymmetric(TreeNode root) {
if(root == null)
return true;
return dfs(root.left, root.right);
}

public boolean dfs(TreeNode left, TreeNode right) {
if (left == null && right == null)
return true;
else if (left == null || right == null)
return false;
else if (left.val != right.val)
return false;
return dfs(left.left, right.right) && dfs(left.right, right.left);
}

测试代码

1
2
3
4
5
6
7
java复制代码    public static void main(String[] args) {
TreeNode treeNode = new TreeNode(1,
new TreeNode(2, new TreeNode(3), new TreeNode(4)),
new TreeNode(2, new TreeNode(4), new TreeNode(3)));
boolean flag = new Number101().isSymmetric(treeNode);
System.out.println(flag);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

数据分析-数据预处理 数据分析-数据预处理

发表于 2021-11-20

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

数据分析-数据预处理

处理重复值

duplicated( )查找重复值

1
2
3
4
5
6
7
css复制代码 import pandas as pd
 a=pd.DataFrame(data=[['A',19],['B',19],['C',20],['A',19],['C',20]],
                columns=['name','age'])
 print(a)
 print('--------------------------')
 a=a.duplicated()
 print(a)

image-20211119093216546

只判断全局不判断每个

any()

1
2
3
4
5
6
7
css复制代码 import pandas as pd
 a=pd.DataFrame(data=[['A',19],['B',19],['C',20],['A',19],['C',20]],
                columns=['name','age'])
 print(a)
 print('--------------------------')
 a=any(a.duplicated())
 print(a)

image-20211119093406143

drop_duplicates( )删除重复值

参数inplace 是否在原数据上修改

1
2
3
4
5
6
7
8
9
10
css复制代码 import pandas as pd
 a=pd.DataFrame(data=[['A',19],['B',19],['C',20],['A',19],['C',20]],
                columns=['name','age'])
 print(a)
 print('--------------------------')
 b=a.drop_duplicates(inplace=False)
 a.drop_duplicates(inplace=True)
 print(a)
 print('--------------------------')
 print(b)

image-20211119093806010

处理缺失值

NaN表示缺失值

1
2
3
python复制代码 import pandas as pd
 a=pd.read_csv(r'text.csv')
 print(a)

image-20211119094341814

isnull( )判断所有位置元素是否缺失

1
2
3
python复制代码 import pandas as pd
 a=pd.read_csv(r'text.csv')
 print(a.isnull())

image-20211119094701761

any( )判断行列元素是否缺失

1
2
3
4
python复制代码 import pandas as pd
 a=pd.read_csv(r'text.csv')
 print(a.isnull().any())
 print(a.isnull().any(axis=1))

image-20211119094939603

del( )dropna( )删除

1
2
3
4
python复制代码 import pandas as pd
 a=pd.read_csv(r'text.csv')
 del a['name']
 print(a)

image-20211119095458462

1
2
3
4
5
6
python复制代码 import pandas as pd
 a=pd.read_csv(r'text.csv')
 b=a.dropna(axis=0)
 print(b)
 c=a.dropna(axis=1)
 print(c)

image-20211119095640211

del( )删除指定列,dropna( )删除含有缺失值的列(行)

fillna( )缺失值填补

1
2
3
4
python复制代码import pandas as pd
a=pd.read_csv(r'text.csv')
a=a.fillna('wu')
print(a)

image-20211119100057705

根据上(下)数据填充

pad / ffill: 按照上一行进行填充 backfill / bfill: 按照下一行进行填充

1
2
3
4
5
6
7
8
9
python复制代码import pandas as pd
a=pd.read_csv(r'text.csv')
print(a)
print('---------------------')
b=a.fillna(method='pad')
print(b)
print('---------------------')
c=a.fillna(method='bfill')
print(c)

image-20211119105520231

数值型数据填充

平均值mean()

每列的平均值填充

1
2
3
4
5
6
python复制代码import pandas as pd
a=pd.read_csv(r'text.csv')
print(a)
print('---------------------')
a=a.fillna(a.mean())
print(a)

image-20211119103513133

中位数median( )

1
2
3
4
5
6
python复制代码import pandas as pd
a=pd.read_csv(r'text.csv')
print(a)
print('---------------------')
a=a.fillna(a.median( ))
print(a)

image-20211119103627377

字符型数据填充

众数mode( )

1
2
3
4
5
6
7
python复制代码import pandas as pd
a=pd.read_csv(r'text.csv')
print(a)
print('---------------------')
for i in a.columns:
a[i] = a[i].fillna(a[i].mode()[0])
print(a)

image-20211119104421611

数据变换

map( )数据转换

1
2
3
4
5
css复制代码import pandas as pd
data={'sex':[1,0,1,1,0]}
a=pd.DataFrame(data)
a['sex-T']=a['sex'].map({1:'男',0:'女'})
print(a)

image-20211119111114072

哑变量

1
2
3
4
5
ini复制代码import pandas as pd
data={'sex':['男','女','男','女','保密']}
a=pd.DataFrame(data)
a=pd.get_dummies(a)
print(a)

image-20211119113232240

\

本文转载自: 掘金

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

bisect 库

发表于 2021-11-20

本来是想自己实现但是复杂度太高,偶然搜到 python 中有一个库 bisect 可以实现真的是太妙了 。

bisect

基本用法,使用起来很方便,其实就是在找可以插入的位置,但是不会真的插入:

1
2
3
4
ini复制代码import bisect 

l = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
bisect.bisect(l, 55) # returns 7

耗时测试,从结果来看 bisect 的速度最快:

1
2
3
4
5
6
7
8
9
python复制代码import timeit 
import bisect

print(timeit.timeit('bisect.bisect([1, 4, 9, 16, 25, 36, 49, 64, 81, 100], 55)','import bisect'))
# 0.2991909169941209
print(timeit.timeit('next(i for i,n in enumerate([1, 4, 9, 16, 25, 36, 49, 64, 81, 100]) if n > 55)'))
# 0.7511563330190256
print(timeit.timeit('next(([1, 4, 9, 16, 25, 36, 49, 64, 81, 100].index(n) for n in [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] if n > 55))'))
# 0.6965440830099396

bisect_left 和 bisect_right

另外还有两种常用方法,bisect_left 和 bisect_right 函数,用入处理将会插入重复数值的情况,返回将会插入的位置。

  • bisect_left(seq, x) x 存在时返回 x 左侧的位置
  • bisect_right(seq, x) x 存在时返回 x 右侧的位置

举例:

1
2
3
4
5
6
7
8
bash复制代码print(bisect.bisect_left([2,4,7,9],2)) 
# 0
print(bisect.bisect_left([2,4,7,9],3))
# 1
print(bisect.bisect_right([2,4,7,9],2))
# 1
print(bisect.bisect_right([2,4,7,9],3))
# 1

insort

把目标值插入到有序序列中,并能保持有序顺序

1
2
3
ini复制代码arr=[2,4,7,9]
bisect.insort(arr,6)
arr # [2, 4, 6, 7, 9]

本文转载自: 掘金

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

Go语言搬砖 collections队列

发表于 2021-11-20

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

简介

collections是无意在github上发现的一款小巧好用的队列包,还是国人写的,不错不错

目前已有的功能包括:

  • 先进先出队列
  • 后进先出队列
  • 优先队列
  • 双端队列
  • 有序Map
  • 计数器
  • 排序

缺点就是已经好几年没更新了,但是了功能还能用😅

官网传送门: github.com/chenjiandon…

安装

先使用go将包下载到本地

1
js复制代码go get github.com/chenjiandongx/collections

在代码编辑器里面引入既可使用

1
js复制代码import "github.com/chenjiandongx/collections"

使用

所以的功能方法都在collections里面,不同功能使用不同方法

image.png

先进先出

先进先出的优点在一些需要按顺序执行的场景,确保流程可以按照预先 设定好的顺序执行,这在做运维自动化时特别有用(一个脚本执行完后再执行另一个脚本)

运行以下代码会 在控制台输出从0到9的数字(从小到大)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码var q  = Q()

func Q() *collections.Queue {
q := collections.NewQueue()
return q
}

func main() {
for i :=0; i < 10;i++{
q.Put(i)
}
for i:=0;i <10 ;i++ {
if item,ok:=q.Get(); ok{
fmt.Println(item)
}
}
}

后进先出

运行以下代码,会发现 数字是从大到小输出的

1
2
3
4
5
6
7
8
9
10
11
js复制代码func main() {
q := collections.NewLifoQueue()
for i := 0; i < 10; i++ {
q.Put(i)
}
for i := 0; i < 10; i++ {
if item, ok := q.Get(); ok {
fmt.Println(item)
}
}
}

实战demo

上面的小demo运行起来好像也没太大问题,但是如果加上了goroutine,在实际运行中可能达不到想要的效果,所以这里需要结合channe来实现一个,可以不断提交任务,但每次只执行一个任务,且只有这个任务执行完了,再执行下一个任务

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
js复制代码var (
q = collections.NewQueue()
)

func main() {
go QueueApi()
Web()
}

func Web() {
r := gin.Default()
r.GET("/api/devops", func(c *gin.Context) {

args1 := c.Query("args1")
args2 := c.Query("args2")
data := map[string]string{
"parameters1": args1,
"parameters2": args2,
}
q.Put(data)

c.JSON(http.StatusOK, gin.H{
"message": "commit success",
"data": "ok",
"code": 200,
})
})

r.Run(":8080")
}

func QueueApi() {
fmt.Println("开始监听队列....")
for {
select {
case <-time.After(time.Second * 5):
if item, ok := q.Get(); ok {
result := item.(map[string]string)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
//TODO 具体执行的逻辑
time.Sleep(10 * time.Second)
fmt.Println(result)
}()
wg.Wait()
}
}
}
}

该demo使用提顺序执行队列,GET接口/api/devops接收提交参数,可以不停的提交,但是方法QueueApi每5秒循环查一次,当查询到队列里有数据,就开始执行,每次只执行一个,执行成功后。继续循环以上逻辑

1
2
js复制代码#测试
curl --location --request GET 'localhost:8080?args1=hello&args2=world'

总结

code的力量还是很强大的,虽然这个包并不出名,作者也不是鼎鼎大名大佬,但是写出来的包依然非常棒(实用)

至于该包的其它的功能,有需要的同学可以自行探索哦

本文转载自: 掘金

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

一文带你分析Mybatis一级缓存源码 Mybatis一级缓

发表于 2021-11-20

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

Mybatis一级缓存

前提

Mybatis的一级缓存一般SqlSession级别的,是默认开启的

在将源码解析之前,需要带着以下几个疑问进行阅读

  • 一级缓存到底是什么?
  • 一级缓存什么时候被创建?
  • 一级缓存的工作流程是什么?

一级缓存是什么?

image.png

打开SqlSession发现,目前跟只有clearCache()方法跟缓存有关系
点开clearCache()按照以下顺序依次点开

image.png

再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那么这个cache是什么东⻄呢?点进去发现,cache其实就是private Map<Object, Object> cache = new HashMap<>();

image.png

HashMap();也就是⼀个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是本地存放的⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤。那么第一个疑问就已经能够解答,一级缓存本质上是一个HashMap对象。

一级缓存什么时候被创建的?

一般来讲,是在Executor中创建的,因为Executor是执行器,用来执行sql请求,⽽且清除缓存的⽅法也在Executor中执⾏,所以很可能缓存的创建也很有可能在Executor中。我们可以看看Executor方法:

image.png

Executor中有⼀个createCacheKey()⽅法,这个⽅法是创建缓存的⽅法啊,跟进去看看,你发现createCacheKey()⽅法是由BaseExecutor执⾏的,createCacgeKey()代码如下:

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
java复制代码CacheKey cacheKey = new CacheKey();
/MappedStatement的id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset就是0
cacheKey.update(rowBounds.getOffset());
// limit就是Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
...
//后⾯是update了sql中带的参数
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;

创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个update⽅法最终由updateList的list来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能理解这五个值都是什么了

image.png

这⾥需要注意⼀下最后⼀个值,configuration.getEnvironment().getId()这是什么,这其实就是定义在mybatis-config.xml中的标签,⻅如下。

image.png

所以一级缓存是在执行器的子类BaseExecutor的createCacgeKey()方法中创建的。

一级缓存的工作流程?

一级缓存工作流程图

image.png

通过BaseExecutor的query()方法,代码如下

1
2
3
4
5
6
7
8
java复制代码public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/拿到转换后的sql
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//调用具体实现方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@SuppressWarnings("unchecked")
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,CacheKey key,BoundSql boundSql) throws SQLException {
...
//判断缓存的map集合中是否存有插入的sql数据,如果有就直接去除,如果没有就查询数据库
list=resultHandler==null? (List<E>) localCache.getObject(key) : null;
if (list!=null) {
//这个主要是处理存储过程⽤的。
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list=queryFromDatabase(ms, parameter, rowBounds, resultHandler, key,boundSql);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {

localCache.removeObject(key);
}
//把数据库查询结果存到一级缓存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {

localOutputParameterCache.putObject(key, parameter);
}
return list;
}

通过上述代码,客户端调用query()方法,query()方法进过一系列的转换后拿到转换后的boundSql,并且创建Cache缓存,然后一起传入下一个query()方法中,query()方法通过一个三元表示式表示如果查不到的话,就从数据库查,在queryFromDatabase()方法中,会对localcache进⾏写⼊。localcache对象的put⽅法最终交给Map进⾏存放。这个就是基本一级缓存工作流程

1
2
3
4
5
java复制代码private Map<Object, Object> cache = new HashMap<Object, Object>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

本文转载自: 掘金

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

java注解

发表于 2021-11-20

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

简单入门

现在大家开发过程中,经常会用到注解。
比如@Controller 等等,但是有时候也会碰到自定义注解,在开发中公司的记录日志就用到了自定义注解。身为渣渣猿还是有必要学习下自定义注解的。

这篇我们先写一个简单的注解列子,不会立马介绍各种什么元注解。从例子中感受下注解的作用

定义个注解

1
2
3
4
5
6
7
8
9
java复制代码package com.kevin.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Kevin {
String name() default "kevin";
}

解析并测试这个注解

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

import com.kevin.annotation.Kevin;

@Kevin
public class Test {

public static void showKevin(Class c) {
System.out.println(c.getName());
boolean isExist = c.isAnnotationPresent(Kevin.class);

if (isExist) {
Kevin kevin = (Kevin) c.getAnnotation(Kevin.class);
System.out.println(kevin.name());
}
}

public static void main(String[] args) {
Test.showKevin(Test.class);
}
}

运行结果

1
2
3
4
5
java复制代码
com.kevin.Test
kevin

Process finished with exit code 0

总结

上面几句代码,我们已经实现了一个简单的自定义注解,是不是很简单。
大家不要吧注解想想的太复杂,其实任何东西大规模的应用肯定是易用易懂的。

简单介绍

整体图示

image.png

内置注解

@Override 重写覆盖

这个注解大家应该经常用到,主要在子类重写父类的方法,比如toString()方法

1
2
3
4
5
6
7
8
9
kotlin复制代码package com.kevin.demo;

public class Demo1 {

@Override
public String toString(){
return "demo1";
}
}

@Deprecated 过时

@Deprecated可以修饰的范围很广,包括类、方法、字段、参数等,它表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的。

1
2
3
4
5
6
7
8
9
csharp复制代码package com.kevin.demo;

public class Demo1 {

@Deprecated
public void goHome(){
System.out.println("过时的方法");
}
}

idea中调用这些方法,编译器也会显示删除线并警告

@SuppressWarning 压制Java的编译警告

@SuppressWarnings表示压制Java的编译警告,它有一个必填参数,表示压制哪种类型的警告.

关键字 用途
all to suppress all warnings
boxing to suppress warnings relative to boxing/unboxing operations
cast to suppress warnings relative to cast operations
dep-ann to suppress warnings relative to deprecated annotation
deprecation to suppress warnings relative to deprecation
fallthrough to suppress warnings relative to missing breaks in switch statements
finally to suppress warnings relative to finally block that don¡¯t return
hiding to suppress warnings relative to locals that hide variable
incomplete-switch to suppress warnings relative to missing entries in a switch statement (enum case)
nls to suppress warnings relative to non-nls string literals
null to suppress warnings relative to null analysis
rawtypes to suppress warnings relative to un-specific types when using generics on class params
restriction to suppress warnings relative to usage of discouraged or forbidden references
serial to suppress warnings relative to missing serialVersionUID field for a serializable class
static-access to suppress warnings relative to incorrect static access
synthetic-access to suppress warnings relative to unoptimized access from inner classes
unchecked to suppress warnings relative to unchecked operations
unqualified-field-access to suppress warnings relative to field access unqualified
unused to suppress warnings relative to unused code

上面的方法,我们就可以增加

1
2
3
4
5
typescript复制代码    @SuppressWarnings("deprecation")
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
demo1.goHome();
}

元注解

元注解:注解的注解,即java为注解开发特准备的注解。

我们以上面讲到的java内置注解@Override为例,学习下java元注解

1
2
3
4
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Target

@Target表示注解的目标,@Override的目标是方法(ElementType.METHOD),ElementType是一个枚举,其他可选值有:

  • TYPE:表示类、接口(包括注解),或者枚举声明
  • FIELD:字段,包括枚举常量
  • METHOD:方法
  • PARAMETER:方法中的参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:本地变量
  • ANNOTATION_TYPE:注解类型
  • PACKAGE:包

目标可以有多个,用{}表示,比如@SuppressWarnings的@Target就有多个,定义为:

1
2
3
4
5
less复制代码@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}

如果没有声明@Target,默认为适用于所有类型。我们上篇文章的demo就没有声明@Target

@Retention

@Retention表示注解信息保留到什么时候,取值只能有一个,类型为RetentionPolicy,它是一个枚举,有三个取值:

  • SOURCE:只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉
  • CLASS:保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留
  • RUNTIME:一直保留到运行时

如果没有声明@Retention,默认为CLASS。

@Override和@SuppressWarnings都是给编译器用的,所以@Retention都是RetentionPolicy.SOURCE。

@Documented

用于指定javadoc生成API文档时显示该注解信息。Documented是一个标记注解,没有成员。

1
2
3
4
5
less复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Inherited

@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。

看个栗子

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

@Inherited
@Retention(RetentionPolicy.RUNTIME)
static @interface Test {
}

@Test
static class Base {
}

static class Child extends Base {
}

public static void main(String[] args) {
System.out.println(Child.class.isAnnotationPresent(Test.class));
}

}

main方法检查Child类是否有Test注解,输出为true,这是因为Test有注解@Inherited,如果去掉,输出就变成false了

本文转载自: 掘金

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

数据库版本控制中间件flyway企业落地 二原理 三整合

发表于 2021-11-20

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

flyway为数据库控制插件,使所有的数据库脚本都在ide(idea,eclipse等)中控制,这样能做到版本有迹可循。

这里需要注意如果使用flyway 就要禁止在数据库管理软件中更改数据库表结构

一.springboot整合flyway

1.pom.xml

1
2
3
4
5
xml复制代码        <dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.4.4</version>
</dependency>

2.application.yml

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码spring:
flyway: # flyway 数据库 DDL 版本控制
enabled: true # 正式环境才开启
clean-disabled: true # 禁用数据库清理
encoding: UTF-8
locations: classpath:/db #脚本存放地址
table: flyway_schema_history_systemportal #flyway记录表,记录了当前执行到了那个脚本
baseline-version: 1 # 基线版本默认开始序号 默认为 1
baseline-on-migrate: true # 针对非空数据库是否默认调用基线版本,为空的话默认会调用基线版本
placeholder-replacement: false
placeholders: # 定义 afterMigrateError.sql 要清理的元数据表表名
flyway-table: ${spring.flyway.table}

在第一次初始化时有可能出现以下错误,需要加上 placeholder-replacement: false。

3.脚本结构

在定义初始化脚本时,版本号建议为V+(编号)+__(为双下划线)+业务名称+init,例如楼主的 V1__systemportal_init.sql。

在定义变更版本时,版本号为 V+(编号)+__(为双下划线)+操作+表名称,例如楼主的为V2__update_table.sql。

其中V1__systemportal_init.sql(初始化脚本)为项目所有的表结构与数据 以下方式导出。

V2__update_table.sql为变更版本(表结构变化的脚本,如果没有变更,只存在init脚本即可)其中内容如下 。

1
2
sql复制代码  ALTER TABLE "systemportal"."task_plan"
ALTER COLUMN "update_user" TYPE varchar(255) USING "update_user"::varchar(255);

执行以上操作后,将原数据库表数据全部删除(记得做好备份) 然后启动项目,即可在数据库中生成表,说白了,就是按照db中的脚本依次执行一遍。

二.原理

当使用flyway时,会生成一个记录表,记录当前已经执行的脚本名称。(script字段就是已经执行了的db中的脚本)
根据上文,在初始化时会产生以下记录。

如果再次启动时,会查询该表是否有大于V2版本的脚本,如果有执行这些脚本 如果没有什么也不执行。

三.整合quartz

当整合quartz时,需要查询表,但是因为使用了flyway此时并没有表结构,所以需要处理。

1.注释掉@PostConstruct中内容

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
java复制代码@Component
public class RuntimeConfig implements ApplicationListener<ContextRefreshedEvent> {

@Autowired
private Scheduler scheduler;

@Autowired
private QrtzJobDao qrtzJobDao;

@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
this.initJob();
}

public void initJob() {
QrtzJobVO qrtzJobVO = new QrtzJobVO();
List<QrtzJobVO> scheduleJobList = qrtzJobDao.queryQrtzJobAll(qrtzJobVO);
scheduleJobList.forEach(scheduleJob -> {
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(this.scheduler, scheduleJob.getJobId());
if (cronTrigger == null) {
ScheduleUtils.createScheduleJob(this.scheduler, scheduleJob);
} else {
ScheduleUtils.updateScheduleJob(this.scheduler, scheduleJob);
}
});
}
}

本文转载自: 掘金

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

leetcode 889Construct Binary

发表于 2021-11-20

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

描述

Given two integer arrays, preorder and postorder where preorder is the preorder traversal of a binary tree of distinct values and postorder is the postorder traversal of the same tree, reconstruct and return the binary tree.

If there exist multiple answers, you can return any of them.

Example 1:

1
2
ini复制代码Input: preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1]
Output: [1,2,3,4,5,6,7]

Example 2:

1
2
ini复制代码Input: preorder = [1], postorder = [1]
Output: [1]

Note:

1
2
3
4
5
6
7
sql复制代码1 <= preorder.length <= 30
1 <= preorder[i] <= preorder.length
All the values of preorder are unique.
postorder.length == preorder.length
1 <= postorder[i] <= postorder.length
All the values of postorder are unique.
It is guaranteed that preorder and postorder are the preorder traversal and postorder traversal of the same binary tree.

解析

根据题意,给定两个整数数组,preorder 和 postorder ,其中 preorder 是不同值的二叉树的前序遍历, postorder 是同一棵树的后序遍历,重构并返回二叉树。如果存在多个答案,要求可以返回其中任何一个。遇到树的问题直接深度优先遍历 DFS 即可解决各种疑难杂症。

使用一个 preorder 和一个 postorder 重构还原一个二叉树要找到其中的规律,从例子一种我们可以看出来:

  • 根结点是 postorder 的最后一个元素 1 ,将其从 postorder 弹出
  • 右子树的根节点为 3 ,其在 preorder 索引为 i ,右子树的节点包含了 preorder[i:]
  • 左子树的节点包含了 preorder[1:i]

不断递归进行上述过程,即可得到重构的二叉树,这个题还是相对来说比较简单。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码class TreeNode(object):
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution(object):
def constructFromPrePost(self, preorder, postorder):
"""
:type preorder: List[int]
:type postorder: List[int]
:rtype: TreeNode
"""
def dfs(pre, post):
if not pre:
return None
if len(pre)==1:
return TreeNode(post.pop())
node = TreeNode(post.pop())
idx = pre.index(post[-1])
node.right = dfs(pre[idx:], post)
node.left = dfs(pre[1:idx], post)
return node
return dfs(preorder, postorder)

运行结果

1
2
erlang复制代码Runtime: 40 ms, faster than 75.96% of Python online submissions for Construct Binary Tree from Preorder and Postorder Traversal.
Memory Usage: 13.8 MB, less than 5.77% of Python online submissions for Construct Binary Tree from Preorder and Postorder Traversal.

原题链接:leetcode.com/problems/co…

您的支持是我最大的动力

本文转载自: 掘金

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

学不懂Netty?看不懂源码?不存在的,这篇文章手把手带你阅

发表于 2021-11-20

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

提前准备好如下代码, 从服务端构建着手,深入分析Netty服务端的启动过程。

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
java复制代码public class NettyBasicServerExample {

public void bind(int port){
//netty的服务端编程要从EventLoopGroup开始,
// 我们要创建两个EventLoopGroup,
// 一个是boss专门用来接收连接,可以理解为处理accept事件,
// 另一个是worker,可以关注除了accept之外的其它事件,处理子任务。
//上面注意,boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景,
// worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。
EventLoopGroup bossGroup=new NioEventLoopGroup();

EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//服务端要启动,需要创建ServerBootStrap,
// 在这里面netty把nio的模板式的代码都给封装好了
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
//配置Server的通道,相当于NIO中的ServerSocketChannel
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) //设置ServerSocketChannel对应的Handler
//childHandler表示给worker那些线程配置了一个处理器,
// 这个就是上面NIO中说的,把处理业务的具体逻辑抽象出来,放到Handler里面
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new NormalInBoundHandler("NormalInBoundA",false))
.addLast(new NormalInBoundHandler("NormalInBoundB",false))
.addLast(new NormalInBoundHandler("NormalInBoundC",true));
socketChannel.pipeline()
.addLast(new NormalOutBoundHandler("NormalOutBoundA"))
.addLast(new NormalOutBoundHandler("NormalOutBoundB"))
.addLast(new NormalOutBoundHandler("NormalOutBoundC"))
.addLast(new ExceptionHandler());
}
});
//绑定端口并同步等待客户端连接
ChannelFuture channelFuture=bootstrap.bind(port).sync();
System.out.println("Netty Server Started,Listening on :"+port);
//等待服务端监听端口关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放线程资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

public static void main(String[] args) {
new NettyBasicServerExample().bind(8080);
}
}
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复制代码public class NormalInBoundHandler extends ChannelInboundHandlerAdapter {
private final String name;
private final boolean flush;

public NormalInBoundHandler(String name, boolean flush) {
this.name = name;
this.flush = flush;
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("InboundHandler:"+name);
if(flush){
ctx.channel().writeAndFlush(msg);
}else {
throw new RuntimeException("InBoundHandler:"+name);
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("InboundHandlerException:"+name);
super.exceptionCaught(ctx, cause);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class NormalOutBoundHandler extends ChannelOutboundHandlerAdapter {
private final String name;

public NormalOutBoundHandler(String name) {
this.name = name;
}

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("OutBoundHandler:"+name);
super.write(ctx, msg, promise);
}
}

在服务端启动之前,需要配置ServerBootstrap的相关参数,这一步大概分为以下几个步骤

  • 配置EventLoopGroup线程组
  • 配置Channel类型
  • 设置ServerSocketChannel对应的Handler
  • 设置网络监听的端口
  • 设置SocketChannel对应的Handler
  • 配置Channel参数

Netty会把我们配置的这些信息组装,发布服务监听。

ServerBootstrap参数配置过程

下面这段代码是我们配置ServerBootStrap相关参数,这个过程比较简单,就是把配置的参数值保存到ServerBootstrap定义的成员变量中就可以了。

1
2
3
4
5
6
7
8
java复制代码bootstrap.group(bossGroup, workerGroup)
//配置Server的通道,相当于NIO中的ServerSocketChannel
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) //设置ServerSocketChannel对应的Handler
//childHandler表示给worker那些线程配置了一个处理器,
// 这个就是上面NIO中说的,把处理业务的具体逻辑抽象出来,放到Handler里面
.childHandler(new ChannelInitializer<SocketChannel>() {
});

我们来看一下ServerBootstrap的类关系图以及属性定义

ServerBootstrap类关系图

如图8-1所示,表示ServerBootstrap的类关系图。

  • AbstractBootstrap,定义了一个抽象类,作为抽象类,一定是抽离了Bootstrap相关的抽象逻辑,所以很显然可以推断出Bootstrap应该也继承了AbstractBootstrap
  • ServerBootstrap,服务端的启动类,
  • ServerBootstrapAcceptor,继承了ChannelInboundHandlerAdapter,所以本身就是一个Handler,当服务端启动后,客户端连接上来时,会先进入到ServerBootstrapAccepter。

image-20210910154646643
图8-1 ServerBootstrap类关系图
AbstractBootstrap属性定义


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
java复制代码public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
@SuppressWarnings("unchecked")
private static final Map.Entry<ChannelOption<?>, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0];
@SuppressWarnings("unchecked")
private static final Map.Entry<AttributeKey<?>, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0];
/**
* 这里的EventLoopGroup 作为服务端 Acceptor 线程,负责处理客户端的请求接入
* 作为客户端 Connector 线程,负责注册监听连接操作位,用于判断异步连接结果。
*/
volatile EventLoopGroup group; //
@SuppressWarnings("deprecation")
private volatile ChannelFactory<? extends C> channelFactory; //channel工厂,很明显应该是用来制造对应Channel的
private volatile SocketAddress localAddress; //SocketAddress用来绑定一个服务端地址

// The order in which ChannelOptions are applied is important they may depend on each other for validation
// purposes.
/**
* ChannelOption 可以添加Channer 添加一些配置信息
*/
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
/**
* ChannelHandler 是具体怎么处理Channer 的IO事件。
*/
private volatile ChannelHandler handler;
}

对于上述属性定义,整体总结如下:

  1. 提供了一个ChannelFactory对象用来创建Channel,一个Channel会对应一个EventLoop用于IO的事件处理,在一个Channel的整个生命周期中 只会绑定一个EventLoop,这里可理解给Channel分配一个线程进行IO事件处理,结束后回收该线程。
  2. AbstractBootstrap没有提供EventLoop而是提供了一个EventLoopGroup,其实我认为这里只用一个EventLoop就行了。
  3. 不管是服务器还是客户端的Channel都需要绑定一个本地端口这就有了SocketAddress类的对象localAddress。
  4. Channel有很多选项所有有了options对象LinkedHashMap<channeloption<?>, Object>
  5. 怎么处理Channel的IO事件呢,我们添加一个事件处理器ChannelHandler对象。

ServerBootstrap属性定义

ServerBootstrap可以理解为服务器启动的工厂类,我们可以通过它来完成服务器端的 Netty 初始化。主要职责:|

  • EventLoop初始化
  • channel的注册
  • pipeline的初始化
  • handler的添加过程
  • 服务端连接处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {

private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);

// The order in which child ChannelOptions are applied is important they may depend on each other for validation
// purposes.
//SocketChannel相关的属性配置
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
private final ServerBootstrapConfig config = new ServerBootstrapConfig(this); //配置类
private volatile EventLoopGroup childGroup; //工作线程组
private volatile ChannelHandler childHandler; //负责SocketChannel的IO处理相关的Handler

public ServerBootstrap() { }
}

服务端启动过程分析

了解了ServerBootstrap相关属性的配置之后,我们继续来看服务的启动过程,在开始往下分析的时候,先不妨来思考以下这些问题

  • Netty自己实现的Channel与底层JDK提供的Channel是如何联系并且构建实现的
  • ChannelInitializer这个特殊的Handler处理器的作用以及实现原理
  • Pipeline是如何初始化以的

ServerBootstrap.bind

先来看ServerBootstrap.bind()方法的定义,这里主要用来绑定一个端口并且发布服务端监听。

根据我们使用NIO相关API的理解,无非就是使用JDK底层的API来打开一个服务端监听并绑定一个端口。

1
java复制代码 ChannelFuture channelFuture=bootstrap.bind(port).sync();
1
2
3
4
java复制代码public ChannelFuture bind(SocketAddress localAddress) {
validate();
return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
}
  • validate(), 验证ServerBootstrap核心成员属性的配置是否正确,比如group、channelFactory、childHandler、childGroup等,这些属性如果没配置,那么服务端启动会报错
  • localAddress,绑定一个本地端口地址

doBind

doBind方法比较长,从大的代码结构,可以分为三个部分

  • initAndRegister 初始化并注册Channel,并返回一个ChannelFuture,说明初始化注册Channel是异步实现
  • regFuture.cause() 用来判断initAndRegister()是否发生异常,如果发生异常,则直接返回
  • regFuture.isDone(), 判断initAndRegister()方法是否执行完成。
    • 如果执行完成,则调用doBind0()方法。
    • 如果未执行完成,regFuture添加一个监听回调,在监听回调中再次判断执行结果进行相关处理。
    • PendingRegistrationPromise 用来保存异步执行结果的状态

从整体代码逻辑来看,逻辑结构还是非常清晰的, initAndRegister()方法负责Channel的初始化和注册、doBind0()方法用来绑定端口。这个无非就是我们使用NIO相关API发布服务所做的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}

if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();

doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

initAndRegister

这个方法顾名思义,就是初始化和注册,基于我们整个流程的分析可以猜测到

  • 初始化,应该就是构建服务端的Handler处理链
  • register,应该就是把当前服务端的连接注册到selector上

下面我们通过源码印证我们的猜想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//通过ChannelFactory创建一个具体的Channel实现
channel = channelFactory.newChannel();
init(channel); //初始化
} catch (Throwable t) {
//省略....
}
//这个代码应该和我们猜想是一致的,就是将当前初始化的channel注册到selector上,这个过程同样也是异步的
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) { //获取regFuture的执行结果
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}

channelFactory.newChannel()

这个方法在分析之前,我们可以继续推测它的逻辑。

在最开始构建服务端的代码中,我们通过channel设置了一个NioServerSocketChannel.class类对象,这个对象表示当前channel的构建使用哪种具体的API

1
2
3
java复制代码bootstrap.group(bossGroup, workerGroup)
//配置Server的通道,相当于NIO中的ServerSocketChannel
.channel(NioServerSocketChannel.class)

而在initAndRegister方法中,又用到了channelFactory.newChannel()来生成一个具体的Channel实例,因此不难想到,这两者必然有一定的联系,我们也可以武断的认为,这个工厂会根据我们配置的channel来动态构建一个指定的channel实例。

channelFactory有多个实现类,所以我们可以从配置方法中找到channelFactory的具体定义,代码如下。

1
2
3
4
5
java复制代码public B channel(Class<? extends C> channelClass) {
return channelFactory(new ReflectiveChannelFactory<C>(
ObjectUtil.checkNotNull(channelClass, "channelClass")
));
}

channelFactory对应的具体实现是:ReflectiveChannelFactory,因此我们定位到newChannel()方法的实现。

ReflectiveChannelFactory.newChannel

在该方法中,使用constructor构建了一个实例。

1
2
3
4
5
6
7
8
java复制代码@Override
public T newChannel() {
try {
return constructor.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
}
}

construtor的初始化代码如下, 用到了传递进来的clazz类,获得该类的构造器,该构造器后续可以通过newInstance创建一个实例对象

而此时的clazz其实就是:NioServerSocketChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {

private final Constructor<? extends T> constructor;

public ReflectiveChannelFactory(Class<? extends T> clazz) {
ObjectUtil.checkNotNull(clazz, "clazz");
try {
this.constructor = clazz.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
" does not have a public non-arg constructor", e);
}
}
}

NioServerSocketChannel

NioServerSocketChannel的构造方法定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class NioServerSocketChannel extends AbstractNioMessageChannel
implements io.netty.channel.socket.ServerSocketChannel {
private static ServerSocketChannel newSocket(SelectorProvider provider) {
try {
return provider.openServerSocketChannel();
} catch (IOException e) {
throw new ChannelException(
"Failed to open a server socket.", e);
}
}
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
}

当NioServerSocketChannel实例化后,调用newSocket方法创建了一个服务端实例。

newSocket方法中调用了provider.openServerSocketChannel(),来完成ServerSocketChannel的创建,ServerSocketChannel就是Java中NIO中的服务端API。

1
2
3
java复制代码public ServerSocketChannel openServerSocketChannel() throws IOException {
return new ServerSocketChannelImpl(this);
}

通过层层推演,最终看到了Netty是如何一步步封装,完成ServerSocketChannel的创建。

设置非阻塞

在NioServerSocketChannel中的构造方法中,先通过super调用父类做一些配置操作

1
2
3
4
java复制代码public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

最终,super会调用AbstractNioChannel中的构造方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp; //设置关心事件,此时是一个连接事件,所以是OP_ACCEPT
try {
ch.configureBlocking(false); //设置非阻塞
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}

throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}

继续分析initAndRegister

分析完成channel的初始化后,接下来就是要将当前channel注册到Selector上,所以继续回到initAndRegister方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码final ChannelFuture initAndRegister() {
//省略....
//这个代码应该和我们猜想是一致的,就是将当前初始化的channel注册到selector上,这个过程同样也是异步的
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) { //获取regFuture的执行结果
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}

注册到某个Selector上,其实就是注册到某个EventLoopGroup中,如果大家能有这个猜想,说明前面的内容是听懂了的。

config().group().register(channel)这段代码,其实就是获取在ServerBootstrap中配置的bossEventLoopGroup,然后把当前的服务端channel注册到该group中。

此时,我们通过快捷键想去看一下register的实现时,发现EventLoopGroup又有多个实现,我们来看一下类关系图如图8-2所示。

image-20210910170502364
图8-3 EventLoopGroup类关系图
而我们在前面配置的EventLoopGroup的实现类是NioEventLoopGroup,而NioEventLoopGroup继承自MultithreadEventLoopGroup,所以在register()方法中,我们直接找到父类的实现方法即可。

MultithreadEventLoopGroup.register

这段代码大家都熟了,从NioEventLoopGroup中选择一个NioEventLoop,将当前channel注册上去

1
2
3
4
java复制代码@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}

next()方法返回的是NioEventLoop,而NioEventLoop又有多个实现类,我们来看图8-4所示的类关系图。

image-20210910171415854
图8-4 NioEventLoop类关系图
从类关系图中发现,发现NioEventLoop派生自SingleThreadEventLoop,所以next().register(channel);方法,执行的是SingleThreadEventLoop中的register

SingleThreadEventLoop.register

1
2
3
4
java复制代码@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
1
2
3
4
5
6
java复制代码@Override
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}

ChannelPromise, 派生自Future,用来实现异步任务处理回调功能。简单来说就是把注册的动作异步化,当异步执行结束后会把执行结果回填到ChannelPromise中

AbstractChannel.register

抽象类一般就是公共逻辑的处理,而这里的处理主要就是针对一些参数的判断,判断完了之后再调用register0()方法。

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
java复制代码@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
ObjectUtil.checkNotNull(eventLoop, "eventLoop");
if (isRegistered()) { //判断是否已经注册过
promise.setFailure(new IllegalStateException("registered to an event loop already"));
return;
}
if (!isCompatible(eventLoop)) { //判断eventLoop类型是否是EventLoop对象类型,如果不是则抛出异常
promise.setFailure(
new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
return;
}

AbstractChannel.this.eventLoop = eventLoop;
//Reactor内部线程调用,也就是说当前register方法是EventLoop线程触发的,则执行下面流程
if (eventLoop.inEventLoop()) {
register0(promise);
} else { //如果是外部线程
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}

AbstractChannel.register0

Netty从EventLoopGroup线程组中选择一个EventLoop和当前的Channel绑定,之后该Channel生命周期中的所有I/O事件都由这个EventLoop负责。

register0方法主要做四件事:

  • 调用JDK层面的API对当前Channel进行注册
  • 触发HandlerAdded事件
  • 触发channelRegistered事件
  • Channel状态为活跃时,触发channelActive事件

在当前的ServerSocketChannel连接注册的逻辑中,我们只需要关注下面的doRegister方法即可。

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
java复制代码private void register0(ChannelPromise promise) {
try {
// check if the channel is still open as it could be closed in the mean time when the register
// call was outside of the eventLoop
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
doRegister(); //调用JDK层面的register()方法进行注册
neverRegistered = false;
registered = true;

// Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
pipeline.invokeHandlerAddedIfNeeded(); //触发Handler,如果有必要的情况下

safeSetSuccess(promise);
pipeline.fireChannelRegistered();
// Only fire a channelActive if the channel has never been registered. This prevents firing
// multiple channel actives if the channel is deregistered and re-registered.
if (isActive()) { //此时是ServerSocketChannel的注册,所以连接还处于非活跃状态
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
beginRead();
}
}
} catch (Throwable t) {
// Close the channel directly to avoid FD leak.
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}

AbstractNioChannel.doRegister

进入到AbstractNioChannel.doRegister方法。

javaChannel().register()负责调用JDK层面的方法,把channel注册到eventLoop().unwrappedSelector()上,其中第三个参数传入的是Netty自己实现的Channel对象,也就是把该对象绑定到attachment中。

这样做的目的是,后续每次调Selector对象进行事件轮询时,当触发事件时,Netty都可以获取自己的Channe对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
// Force the Selector to select now as the "canceled" SelectionKey may still be
// cached and not removed because no Select.select(..) operation was called yet.
eventLoop().selectNow();
selected = true;
} else {
// We forced a select operation on the selector before but the SelectionKey is still cached
// for whatever reason. JDK bug ?
throw e;
}
}
}
}

服务注册总结

上述代码比较绕,但是整体总结下来并不难理解

  • 初始化指定的Channel实例
  • 把该Channel分配给某一个EventLoop
  • 然后把Channel注册到该EventLoop的Selector中

AbstractBootstrap.doBind0

分析完了注册的逻辑后,再回到AbstractBootstrap类中的doBind0方法,这个方法不用看也能知道,ServerSocketChannel初始化了之后,接下来要做的就是绑定一个ip和端口地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {

//获取当前channel中的eventLoop实例,执行一个异步任务。
//需要注意,以前我们在课程中讲过,eventLoop在轮询中一方面要执行select遍历,另一方面要执行阻塞队列中的任务,而这里就是把任务添加到队列中异步执行。
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
//如果ServerSocketChannel注册成功,则调用该channel的bind方法
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}

channel.bind方法,会根据ServerSocketChannel中的handler链配置,逐个进行调用,由于在本次案例中,我们给ServerSocketChannel配置了一个 LoggingHandler的处理器,所以bind方法会先调用LoggingHandler,然后再调用DefaultChannelPipeline中的bind方法,调用链路

-> DefaultChannelPipeline.ind

​ -> AbstractChannel.bind

​ -> NioServerSocketChannel.doBind

最终就是调用前面初始化好的ServerSocketChannel中的bind方法绑定本地地址和端口。

1
2
3
4
5
6
7
java复制代码protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}

构建SocketChannel的Pipeline

在ServerBootstrap的配置中,我们针对SocketChannel,配置了入站和出站的Handler,也就是当某个SocketChannel的IO事件就绪时,就会按照我们配置的处理器链表进行逐一处理,那么这个链表是什么时候构建的,又是什么样的结构呢?下面我们来分析这部分的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new NormalInBoundHandler("NormalInBoundA",false))
.addLast(new NormalInBoundHandler("NormalInBoundB",false))
.addLast(new NormalInBoundHandler("NormalInBoundC",true));
socketChannel.pipeline()
.addLast(new NormalOutBoundHandler("NormalOutBoundA"))
.addLast(new NormalOutBoundHandler("NormalOutBoundB"))
.addLast(new NormalOutBoundHandler("NormalOutBoundC"))
.addLast(new ExceptionHandler());
}
});

childHandler的构建

childHandler的构建过程,在AbstractChannel.register0方法中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码final ChannelFuture initAndRegister() {
Channel channel = null;
try {
channel = channelFactory.newChannel(); //这是是创建channel
init(channel); //这里是初始化
} catch (Throwable t) {
//省略....
}
ChannelFuture regFuture = config().group().register(channel); //这是是注册
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}

return regFuture;
}

ServerBootstrap.init

init方法,调用的是ServerBootstrap中的init(),代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Override
void init(Channel channel) {
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, newAttributesArray());

ChannelPipeline p = channel.pipeline();

final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler; //childHandler就是在服务端配置时添加的ChannelInitializer
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
// 此时的Channel是NioServerSocketChannel,这里是为NioServerSocketChannel添加处理器链。
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler(); //如果在ServerBootstrap构建时,通过.handler添加了处理器,则会把相关处理器添加到NioServerSocketChannel中的pipeline中。
if (handler != null) {
pipeline.addLast(handler);
}

ch.eventLoop().execute(new Runnable() { //异步天剑一个ServerBootstrapAcceptor处理器,从名字来看,
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
//currentChildHandler,表示SocketChannel的pipeline,当收到客户端连接时,就会把该handler添加到当前SocketChannel的pipeline中
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

其中,对于上述代码的核心部分说明如下

  • ChannelPipeline 是在AbstractChannel中的构造方法中初始化的一个DefaultChannelPipeline
1
2
3
4
5
6
java复制代码protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
  • p.addLast是为NioServerSocketChannel添加handler处理器链,这里添加了一个ChannelInitializer回调函数,该回调是异步触发的,在回调方法中做了两件事
+ 如果ServerBootstrap.handler添加了处理器,则会把相关处理器添加到该pipeline中,在本次演示的案例中,我们添加了LoggerHandler
+ 异步执行添加了ServerBootstrapAcceptor,从名字来看,它是专门用来接收新的连接处理的。

我们在这里思考一个问题,为什么NioServerSocketChannel需要通过ChannelInitializer回调处理器呢? ServerBootstrapAcceptor为什么通过异步任务添加到pipeline中呢?

原因是,NioServerSocketChannel在初始化的时候,还没有开始将该Channel注册到Selector对象上,也就是没办法把ACCEPT事件注册到Selector上,所以事先添加了ChannelInitializer处理器,等待Channel注册完成后,再向Pipeline中添加ServerBootstrapAcceptor。

ServerBootstrapAcceptor

按照下面的方法演示一下SocketChannel中的Pipeline的构建过程

  1. 启动服务端监听
  2. 在ServerBootstrapAcceptor的channelRead方法中打上断点
  3. 通过telnet 连接,此时会触发debug。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;

child.pipeline().addLast(childHandler); //在这里,将handler添加到SocketChannel的pipeline中

setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);

try {
//把当前客户端的链接SocketChannel注册到某个EventLoop中。
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}

ServerBootstrapAcceptor是服务端NioServerSocketChannel中的一个特殊处理器,该处理器的channelRead事件只会在新连接产生时触发,所以这里通过 final Channel child = (Channel) msg;可以直接拿到客户端的链接SocketChannel。

ServerBootstrapAcceptor接着通过childGroup.register()方法,把当前NioSocketChannel注册到工作线程中。

事件触发机制的流程

在ServerBootstrapAcceptor中,收到客户端连接时,会调用childGroup.register(child)把当前客户端连接注册到指定NioEventLoop的Selector中。

这个注册流程和前面讲解的NioServerSocketChannel注册流程完全一样,最终都会进入到AbstractChannel.register0方法。

AbstractChannel.register0

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
java复制代码private void register0(ChannelPromise promise) {
try {
// check if the channel is still open as it could be closed in the mean time when the register
// call was outside of the eventLoop
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;

// Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
pipeline.invokeHandlerAddedIfNeeded();

safeSetSuccess(promise);
pipeline.fireChannelRegistered(); //执行pipeline中的ChannelRegistered()事件。
// Only fire a channelActive if the channel has never been registered. This prevents firing
// multiple channel actives if the channel is deregistered and re-registered.
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
beginRead();
}
}
} catch (Throwable t) {
// Close the channel directly to avoid FD leak.
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}

pipeline.fireChannelRegistered()

1
2
3
4
5
java复制代码@Override
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(head);
return this;
}

下面的事件触发,分为两个逻辑

  • 如果当前的任务是在eventLoop中触发的,则直接调用invokeChannelRegistered
  • 否则,异步执行invokeChannelRegistered。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRegistered();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRegistered();
}
});
}
}

invokeChannelRegistered

触发下一个handler的channelRegistered方法。

1
2
3
4
5
6
7
8
9
10
11
java复制代码private void invokeChannelRegistered() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRegistered(this);
} catch (Throwable t) {
invokeExceptionCaught(t);
}
} else {
fireChannelRegistered();
}
}

Netty服务端启动总结

到此为止,整个服务端启动的过程,我们就已经分析完成了,主要的逻辑如下

  • 创建服务端Channel,本质上是根据用户配置的实现,调用JDK原生的Channel
  • 初始化Channel的核心属性,unsafe、pipeline
  • 初始化Channel的Pipeline,主要是添加两个特殊的处理器,ChannelInitializer和ServerBootstrapAcceptor
  • 注册服务端的Channel,添加OP_ACCEPT事件,这里底层调用的是JDK层面的实现,讲Channel注册到BossEventLoop中的Selector上
  • 绑定端口,调用JDK层面的API,绑定端口。

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

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

Ribbon 拦截请求的原理

发表于 2021-11-20

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

你好,我是悟空呀。

那么如果让你设计一个负载均衡组件,你会怎么设计?

我们需要考虑这几个因素:

  • 如何获取及同步服务器列表?涉及到与注册中心的交互。
  • 如何将负载进行分摊?涉及到分摊策略。
  • 如何将客户端请求进行拦截然后选择服务器进行转发?涉及到请求拦截。

抱着这几个问题,我们从负载均衡的原理 + Ribbon 的架构来学习如何设计一个负载均衡器,相信会带给你一些启发。

Ribbon 拦截请求的原理

本文最开始提出了一个问题:负载均衡器如何将客户端请求进行拦截然后选择服务器进行转发?

结合上面介绍的 Ribbon 核心组件,我们可以画一张原理图来梳理下 Ribbon 拦截请求的原理:

图片

第一步:Ribbon 拦截所有标注@loadBalance注解的 RestTemplate。RestTemplate 是用来发送 HTTP 请求的。

第二步:将 Ribbon 默认的拦截器 LoadBalancerInterceptor 添加到 RestTemplate 的执行逻辑中,当 RestTemplate 每次发送 HTTP 请求时,都会被 Ribbon 拦截。

第三步:拦截后,Ribbon 会创建一个 ILoadBalancer 实例。

第四步:ILoadBalancer 实例会使用 RibbonClientConfiguration 完成自动配置。就会配置好 IRule,IPing,ServerList。

第五步:Ribbon 会从服务列表中选择一个服务,将请求转发给这个服务。

作者简介:悟空,8年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构。本文已收录至 www.passjava.cn

本文转载自: 掘金

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

1…269270271…956

开发者博客

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