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

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


  • 首页

  • 归档

  • 搜索

《跟二师兄学Nacos》02篇 Nacos的临时与持久化实例

发表于 2021-08-05

学习不用那么功利,二师兄带你从更高维度轻松阅读源码~

本篇文章Nacos核心逻辑篇,给大家讲解一下「临时实例」与「持久化实例」的区别及运用场景。

Nacos的临时实例与持久化实例

在Nacos Client进行实例注册时,我们知道是通过Instance对象来携带实例的基本信息的。在Instance中有一个ephemeral字段,用来表示该实例是临时实例,还是持久化实例。

1
2
3
4
5
6
7
8
9
10
php复制代码public class Instance implements Serializable {

/**
* If instance is ephemeral.
*
* @since 1.0.0
*/
private boolean ephemeral = true;
// 省略其他
}

从源码可以看出ephemeral字段是1.0.0版本新增的,用来表示注册的实例是否是临时实例还是持久化实例。

目前,无论是Nacos 1.x版本,还是2.x版本,ephemeral的默认值都是true。在1.x版本中服务注册默认采用http协议,2.x版本默认采用grpc协议,但这都未影响到ephemeral字段的默认值。

也就是说,一直以来,Nacos实例默认都是以临时实例的形式进行注册的。

当然,也是可以通过application的配置来改变这里默认值的。比如:

1
2
ini复制代码# false为永久实例,true表示临时实例
spring.cloud.nacos.discovery.ephemeral=false

上面是基于Spring Cloud进行配置,false为永久实例,true表示临时实例,默认为true。

临时实例与持久化实例的区别

临时实例与持久化实例的区别主要体现在服务器对该实例的处理上。

临时实例向Nacos注册,Nacos不会对其进行持久化存储,只能通过心跳方式保活。默认模式是:客户端心跳上报Nacos实例健康状态,默认间隔5秒,Nacos在15秒内未收到该实例的心跳,则会设置为不健康状态,超过30秒则将实例删除。

持久化实例向Nacos注册,Nacos会对其进行持久化处理。当该实例不存在时,Nacos只会将其健康状态设置为不健康,但并不会对将其从服务端删除。

另外,可以使用实例的ephemeral来判断健康检查模式,ephemeral为true对应的是client模式(客户端心跳),为false对应的是server模式(服务端检查)。

为什么要设计两种模式?

上面说了两种模式的不同和处理上的区别,那么Nacos为什么设计两种模式,它们是为了应对什么样的场景而存在呢?

对于临时实例,健康检查失败,则直接可以从列表中删除。这种特性就比较适合那些需要应对流量突增的场景,服务可以进行弹性扩容。当流量过去之后,服务停掉即可自动注销了。

对于持久化实例,健康检查失败,会被标记成不健康状态。它的好处是运维可以实时看到实例的健康状态,便于后续的警告、扩容等一些列措施。

除了上述场景之外,持久化实例还有另外一个场景用的到,那就是保护阈值。

Nacos的保护阈值

关于保护阈值,在前面的文章中专门写到过。

Nacos中可以针对具体的实例设置一个保护阈值,值为0-1之间的浮点类型。本质上,保护阈值是⼀个⽐例值(当前服务健康实例数/当前服务总实例数)。

⼀般情况下,服务消费者要从Nacos获取可⽤实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。

但在⾼并发、⼤流量场景会存在⼀定的问题。比如,服务A有100个实例,98个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。

保护阈值存在的意义在于当服务A健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。

Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,但这样也⽐造成雪崩要好。牺牲了⼀些请求,保证了整个系统的可⽤。

这里我们看到了不健康实例的另外一个作用:防止产生雪崩。

那么,如果所有的实例都是临时实例,当雪崩场景发生时,Nacos的阈值保护机制是不是就没有足够的(包含不健康实例)实例返回了?如果有一部分实例是持久化实例,即便它们已经挂掉,状态为不健康的,但当触发阈值保护时,还是可以起到分流的作用。

小结

关于Nacos临时实例与持久化实例就聊这么多了。如果想更深入了解,其实可以读一下源码。由于基于gRPC的实现过于复杂,可读性不够强,如果想阅读,建议阅读基于Http的实现。

如果文章内容有问题或想技术讨论请联系我(微信:zhuan2quan,备注Nacos),如果觉得写的还不错,值得一起学习,那就关注一下吧。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

Kotlin 协程实战进阶(二、进阶篇:协程的取消、异常处理

发表于 2021-08-05

src=http___pic2.zhimg.com_50_v2-34618557fb1a4a3e93fd66597b562464_hd.jpg&refer=http___pic2.zhimg.jpg

前言:学习这件事不在乎有没有人教你,最重要的是在于你自己有没有觉悟和恒心。 —— 法布尔

前言

上一篇文章对协程的概念和原理、协程框架的基础使用、挂起函数以及挂起与恢复等做了详细的分析,如果您对协程有一定的理解,可以阅读《Kotlin 协程实战进阶(一、筑基篇)》我们来对协程整体认识来做一个整体的交流。由于篇幅原因还有一部分重要的知识点没有讲解到,接下来继续分析 Kotlin 协程的重要要素和使用,首先来回顾一下上篇文章的整体内容:

  • 1、Coroutine:协程的概念和原理:协程是什么以及它的作用和特点,图解分析协程的工作原理。
  • 2、Coroutine builders:协程的构建,协程构建器创建协程的三种方式。
  • 3、CoroutineScope:协程作用域,协程运行的上下文环境,用来提供函数支持,也是用来增加限制。常见的7种作用域(包含Lifecycle支持的协程)以及作用域的分类和行为规则。
  • 4、Job & Deferred:协程的句柄,实现对协程的控制和管理,Deferred有返回值。
  • 5、CoroutineDispatcher:协程调度器,确定相应的协程在那个线程上执行,调度器的四种模式以及withContext主要是为了切换协程上下文环境。
  • 6、CoroutineContext:协程上下文,表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID以及组合上下文的使用。
  • 7、CoroutineStart:一个枚举类,为协程构建器定义四中启动模式。
  • 8、suspend:挂起函数,Kotlin 协程最核心的关键字。一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。

本文大纲

Kotlin协程.png

三、协程取消

在日常的开发中,我们都知道应该避免不必要的任务,需要控制好协程的生命周期,在不需要使用的时候将它取消。

1.调用 cancel 方法

协程通过抛出一个特殊的异常CancellationException来处理取消操作。一旦抛出了CancellationException异常,便可以使用这一机制来处理协程的取消。在调用job.cancel时你可以传入一个 CancellationException实例来提供指定错误信息:

1
kotlin复制代码public fun cancel(cause: CancellationException? = null)

该参数是可空的,如果不传参数则会使用默认的defaultCancellationException()作为参数。

子协程会通过抛出异常的方式将取消的情况通知到它的父协程。父协程通过传入的取消原因来决定是否来处理该异常。如果子协程因为CancellationException而被取消的,那么对于父协程来说不需要进行额外操作。

我们可以通过直接取消协程启动所涉及的整个作用域 (scope) 来取消所有已创建的子协程:

1
2
3
4
5
6
7
8
9
kotlin复制代码//创建作用域
val scope = CoroutineScope(Dispatchers.Main)
//启动一个协程
val job = scope.launch {
//TODO
}

//作用域取消
scope.cancel()

取消作用域会取消它的所有子协程。注意:已取消的作用域无法再创建协程。可以使用try…catch捕获到CancellationException。

如果仅仅是因为要取消某个进行中的任务而取消其中某一个协程,那么调用该协程的job.cancel()方法确保只会取消跟job相关的特定协程,而不会影响其它兄弟协程。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码//创建作用域
val scope = CoroutineScope(Dispatchers.Main)

//协程job1将会被取消,而另一个job2则不受任何影响
val job1 = scope.launch {
//TODO
}
val job2 = scope.launch {
//TODO
}

//取消单个协程
job1.cancel()

被取消的子协程并不会影响其余兄弟协程。

如果使用的是androidx KTX库的话,在大部分情况下都不需要创建自己的作用域,所以也就不需要负责取消它们。
viewModelScope和lifecycleScope都是 CoroutineScope对象,它们都会在适当的时间点被取消。当ViewModel被清除时,在其作用域内启动的协程也会被一起取消。lifecycleScope会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏。

2.协程的状态检查

如果仅仅是调用了cancel方法,并不意味着协程所处理的任务也会停止。在使用协程处理了一些相对较为繁重的工作,比如读取多个文件,不会立即停止此任务的进行。

举个栗子,我们使用协程来500毫秒打印一次数据,先让协程运行1.2秒,然后将其取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码    fun jobTest() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0

while (i < 5) {//打印前五条消息
if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
print("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}

delay(1200)//延迟1.2s
print("等待1.2秒后")

job.cancel()
print("协程被取消")
}

打印数据如下:

取消后的协程状态.gif

当job.cancel方法被调用后,我们的协程转变为取消中 (cancelling) 的状态。但是紧接着我们发现第3和第4条数据打印到了命令行中。当协程处理的任务结束后,协程又转变为了已取消 (cancelled) 状态。

重新来看看Job的生命周期:

一个Job可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问Job的属性: isActive、isCancelled 和 isCompleted。Job的生命周期如下图(来自官网):

image.png

如果协程处于活跃状态,协程运行出错或者调用job.cancel()都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true)。当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true。

协程所处理的任务不会仅仅在调用cancel方法时就停止,相反,我们需要修改代码来定期检查协程是否处于活跃状态。在处理任务之前添加对协程状态的检查:

1
kotlin复制代码    while (i < 5 && isActive) //当job是活跃状态继续执行

那么我们的任务只会在协程处于活跃的状态下执行。在协程取消后,第3和第4条数据不会被打印出来。

3.join() & await() 的取消

等待协程处理结果有两种方法: 来自launch的Job.join()方法,由async返回的Deferred.await()方法。

Job.join()会挂起协程,直到任务处理完成。

1
2
3
4
5
6
7
kotlin复制代码val job = launch {
//TODO
}

job.cancel()//取消协程
job.join()//挂起并调用协程,直到job完成
//job.cancelAndJoin()//挂起并调用协程,直到被取消的job完成

与job.cancel()一起使用时,会按照以下方式进行:

  • 如果在job.cancel()之后再调用job.join(),那么协程会一直处于挂起状态直到任务处理完成;
  • 在job.join()之后调用job.cancel()没有什么影响,因为job已经完成了。

如果需要获取协程处理结果,那么应该使用Deferred。当协程完成后,结果会由Deferred.await()返回。Deferred继续自Job,它同样可以被取消。

1
2
3
4
5
6
7
dart复制代码val deferred = async {
delay(1000)
print("asyncTest")
}

deferred.cancel()//取消
deferred.await()//会抛出JobCancellationException
  • delay():在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程。你可以认为它实际上就是触发了一个延时任务,告诉协程调度系统多久之后再来执行后面的代码。

在已取消的deferred上调用await()会抛出JobCancellationException异常。因为await()是负责在协程处理结果出来之前一直将协程挂起,如果协程被取消了那么协程就不会继续进行计算也不会有结果产生。因此,在协程取消后调用await()会抛出JobCancellationException异常: 因为Job已经被取消。

另外,如果在deferred.await()之后调用deferred.cancel()不会有任何情况发生,因为协程已经处理结束。

4.finally释放资源

如果要在协程取消后执行某个特定的操作,比如关闭可能正在使用的资源,或者是针对取消需要进行日志打印,又或者是执行其余的一些清理代码。那该怎么样做?

当协程被取消后会抛出CancellationException异常,我们可以将挂起的任务放置于try…catch…finally代码块中,catch中捕获取消后抛出的异常,在finally代码块中执行需要做的清理任务。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码val job = GlobalScope.launch {
try {
//TODO
delay(500L)
} catch (e: CancellationException) {
print("协程取消抛出异常:$e")
} finally {
print("协程清理工作")
}
}

job.cancel()//取消协程

打印数据如下:

1
2
kotlin复制代码[DefaultDispatcher-worker-1] 协程取消抛出异常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@bb81f53
[DefaultDispatcher-worker-1] 协程清理工作

但是,如果需要执行的清理工作也需要挂起,那么上面就行不通了,因为一旦协程处于取消中状态,它将不能再转为挂起 (suspend) 状态。

5.NonCancellable

如果协程被取消后需要调用挂起函数进行清理任务,可使用NonCancellable单例对象用于withContext函数创建一个无法被取消的协程作用域中执行。这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码val job = launch {
try {
//TODO
} catch (e: CancellationException) {
print("协程取消抛出异常")
} finally {
withContext(NonCancellable) {
delay(100)//或者其他挂起函数
print("协程清理工作")
}
}
}

delay(500L)
job.cancel()//取消协程

但是这个方法需要慎用,这样做风险很高,因为可能会无法控制协程的执行。

6.withTimeout

withTimeout 函数用于指定协程的运行超时时间,如果超时则会抛出TimeoutCancellationException,从而令协程结束运行。

1
2
3
4
5
6
kotlin复制代码withTimeout(1300) {//1300毫秒后超时抛出TimeoutCancellationException异常
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500)
}
}

打印结果如下:

1
2
3
4
css复制代码I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

如果你不想抛出异常,可以使用withTimeoutOrNull(),在超时时返回null而不是异常。

四、异常处理

1.try…catch

协程使用一般的 Kotlin 语法处理异常: try…catch或内建的工具方法,比如runCatching(其内部还是使用了 try…catch),所有未捕获的异常一定会被抛出。但是,不同的协程 Builder 对异常有不同的处理方式。

使用launch时,异常会在它发生的第一时间被抛出,就可以将抛出异常的代码包裹到try…catch中:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
try {
print("模拟抛出一个数组越界异常")
throw IndexOutOfBoundsException() //launch 抛出异常
} catch (e: Exception) {
//处理异常
print("这里处理抛出的异常")
}
}

打印数据如下:

1
2
kotlin复制代码 [main] 模拟抛出一个数组越界异常
[main] 这里处理抛出的异常

当async被用作根协程 (CoroutineScope实例或supervisorScope的直接子协程) 时不会自动抛出异常,而是在调用await()时才会抛出异常。为了捕获其中抛出的异常,可以用try…catch包裹调用await()的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码supervisorScope {
val deferred = async {
print("模拟抛出一个算术运算异常")
throw ArithmeticException()
}

try {
deferred.await()
} catch (e: Exception) {
//处理异常
print("这里处理抛出的异常")
}
}

打印数据如下:

1
2
kotlin复制代码 [main] 模拟抛出一个算术运算异常
[main] 这里处理抛出的异常

注意:async在 coroutineScope 构建器或在其他协程创建的协程中抛出的异常不会被 try/catch 捕获!

相对来来说,Job是会在层级间自动传播异常,除了当async被用作根协程时不会自动抛出异常外,async中其他协程所创建的协程中产生的异常总是会被传播,无论协程的 Builder 是什么。这样一来catch部分的代码块就不会被调用,异常会被传播和传递到scope:

1
2
3
4
5
6
7
8
9
10
11
12
13
Kotlin复制代码scope.launch {
try {
val deferred = async {
print("模拟抛出一个空指针异常")
throw NullPointerException()
}
deferred.await()
} catch (e: Exception) {
// async 中抛出的异常将不会在这里被捕获
// 但是异常会被传播和传递到 scope
print("这里不能不会异常,异常向上传播")
}
}

由于scope的直接子协程是launch,如果async中产生了一个异常,这个异常将就会被立即抛出。原因是async (包含一个Job在它的CoroutineContext中) 会自动传播异常到它的父级 (launch),这会让异常被立即抛出。打印数据如下:

image.png

2.异常在作用域内的传播

协程的异常是会分发传播的,牵连到其他兄弟协程以及父协程。

当协程因出现异常失败时,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。最后将异常再传播给它的父级。当异常到达当前层次结构的根,在当前协程作用域启动的所有协程都将被取消。

image.png

一般情况下这样的异常传播是合理的,但是在应用中处理与用户的交互,当其中一个子协程出现异常,那就可能导致所在作用域被取消,也就无法开启新的协程,最后整个UI组件都无法响应。

可以使用SupervisorJob来解决这个问题,下面会讲解到。这里先给出异常在作用域内的传播的结论。当协程出现异常时,会根据当前作用域触发异常传递:

  • coroutineScope 一般情况下,协程的取消操作会通过协程的层次结构来进行传播。如果取消父协程或者父协程抛出异常,那么子协程都会被取消。而如果子协程被取消,则不会影响同级协程和父协程,但如果子协程抛出异常则也会导致同级协程被取消和将异常传递给父协程,进而导致整个协程作用域失败。
  • supervisorScope 它的取消操作只会向下传播,一个子协程的运行失败不会影响到其他子协程,内部的异常不会向上传播,不会影响父协程和兄弟协程的运行。

3.SupervisorJob

它类似于常规的Job,唯一不同的是SupervisorJob的取消只会向下传播,一个子协程的运行失败不会影响到其他协程。

  • SupervisorJob:    它的一个子协程的运行失败或取消不会导致自己失败,也不会影响到其他子协程。SupervisorJob不会取消它自己和它的子协程,也不会传播异常并传递给它的父级,它会让子协程自己处理异常。
  • supervisorScope:   监督作用域,使用SupervisorJob创建一个作用域。一个子域的失败不会导致这个范围失败,也不会影响它的其他子域,可以实现一个自定义的策略来处理其子域的失败。作用域本身的失败(在[block]或取消中抛出异常)会导致作用域及其所有子Job失败,但不会取消父Job。

为了更好理解下面的例子,你需要知道的两个知识点:

1.协程通过抛出一个特殊的异常CancellationException来处理取消操作。(上面协程取消讲到)

2.未被捕获的异常一定会被抛出,无论您使用的是哪种 Job,如果异常没有被捕获处理,而且 CoroutineContext没有一个CoroutineExceptionHandler(稍后讲到) 时,异常会到达默认线程的 ExceptionHandler。在JVM中,异常会被打印在控制台;在Android中,无论异常在那个Dispatcher中发生,都会导致应用崩溃。

下面使用suspend main函数举个栗子,方便在控制台打印数据:

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
kotlin复制代码suspend fun main() {
try {
coroutineScope {
print("1")
val job1 = launch {//第一个子协程
print("2")
throw NullPointerException()//抛出空指针异常
}
val job2 = launch {//第二个子协程
delay(1000)
print("3")
}
try {//这里try…catch捕获CancellationException
job2.join()
println("4")//等待第二个子协程完成:
} catch (e: Exception) {
print("5. $e")//捕获第二个协程的取消异常
}
}
} catch (e: Exception) {//捕获父协程的取消异常
print("6. $e")
}

Thread.sleep(3000)//阻塞主线程3秒,以保持JVM存活,等待上面执行完成
}

image.png

上面代码稍微有点儿复杂,但也不难理解。我们在一个 coroutineScope 当中启动了两个同级的子协程,在job1中抛出了未捕获的异常,那么job2也抛出取消异常,接着job1中的异常向上传递给父协程,在最外层捕获到了传递给父级的NullPointerException。

如果把 coroutineScope 换成 supervisorScope,其他不变,运行结果会是怎样呢?

image.png

我们发现job1抛出的异常并没有影响父级作用域以及作用域内的其他子协程job2的执行。注意:将SupervisorJob作为参数传入一个协程的构造参数里面不能带来上面这样的效果。

那么应该在什么时候去使用Job或SupervisorJob呢?如果您想要在出现错误时不会退出父级和其他平级的协程,那就使用SupervisorJob或supervisorScope。比如一个网络请求失败了,所有其他的请求都将被立即取消,这种需求选择coroutineScope。相反,如果即使一个请求失败了其他的请求也要继续,则可以使用 supervisorScope,当一个协程失败了,supervisorScope是不会取消剩余子协程的。

建议大家尽量不要直接使用标准库API,除非对协程的机制非常熟悉。对于可能出异常的情况,尽量做好异常处理,不要将问题复杂化。

4.CoroutineExceptionHandler

CoroutineExceptionHandler异常处理器属于协程上下文的一种,需要将其添加到协程上下文中。可以处理未捕获的异常。在这里进行自定义日志记录或异常处理,它类似于对线程使用Thread.uncaughtExceptionHandler。

异常如果需要被捕获,则需要满足下面两个条件:

  • 这个异常是被自动抛出异常的协程所抛出的 (是launch,而不是async);
  • CoroutineExceptionHandler设置在CoroutineScope的上下文中或者在一个根协程 (CoroutineScope或者supervisorScope的直接子协程) 中。

定义一个异常处理器:

1
2
3
kotlin复制代码val handler = CoroutineExceptionHandler { context, exception ->
print("捕获到的异常: $exception")
}

举个例子,在下面的代码中,launch产生的异常会被handler捕获,而async的不会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码runBlocking {
val scope = CoroutineScope(Job())

val job = scope.launch(handler) {//父协程中设置异常处理器
launch {//子协程抛出异常
throw NullPointerException()
}

async {//没有任何效果,用户调用await()会异常崩溃
throw IllegalArgumentException()
}
}

job.join()//暂停协程,直到任务完成
}

CoroutineExceptionHandler只会在预计由用户不处理的异常上调用,它可以捕获CoroutineExceptionHandler设置在父协程上下文中并且launch抛出的异常。在async中使用它没有任何效果,因为async构建器始终会捕获所有异常并将其表示在结果Deferred对象中,因此它的CoroutineExceptionHandler也无效。当async内部发生了异常且没有捕获时,那么调用async.await()依然会导致应用崩溃。打印数据如下:

1
kotlin复制代码[DefaultDispatcher-worker-1] 捕获到的异常: java.lang.NullPointerException

handler被设置给了一个内部协程,那么它将不会捕获异常,SupervisorJob直接子协程例外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Kotlin复制代码runBlocking {
val scope = CoroutineScope(Job())
scope.launch {
launch(handler) {//子协程设置没有意义,不会打印数据,因为异常向上传递,而父协程中没有handler则无法捕获
throw NullPointerException()//抛出空指针异常
}
}

supervisorScope {
launch(handler) {//SupervisorJob不会让异常向上传递,会使用子协程内部的异常处理器来处理
throw IllegalArgumentException()//抛出非法参数异常
}
}
}

子协程设置异常处理器是无效的,因为子协程出了异常依然会抛到父协程,而父协程中没有handler则无法捕获,所以在子协程中捕获异常没有意义。在监督作业(SupervisorJob)直接子协程设置异常处理器,不会让异常向上传递,从而被其内部的异常处理器来处理。打印数据如下:

1
kotlin复制代码 [main] 捕获到的异常: java.lang.IllegalArgumentException

没有被捕获的异常会被传播,想要避免取消操作在异常发生时被传播,记得使用SupervisorJob;反之则使用Job。

注意:

1.协程内部使用CancellationException来进行取消,这个异常会被所有的处理者忽略,所以那些可以被catch代码块捕获的异常仅仅应该被用来作为额外调试使用的资源。

2.当协程的多个子协程因异常失败时,一般规则是”取第一个异常”,因此将处理第一个异常。在第一个异常之后发生的所有其他异常读作为被抑制的异常绑定至第一个异常。

五、Channel

Channel是非阻塞的通信基础设施,它实际上就是一个队列,而且是并发安全的,可以用来连接协程,实现不同协程的通信。可以在两个或多个协程之间完成消息传递,多个作用域可以通过一个Channel对象来进行数据的发送和接收。类似于BlockingQueue+挂起函数,称为热数据流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码GlobalScope.launch {
// 1. 创建 Channel
val channel = Channel<Int>()

// 2. Channel 发送数据
launch {
for (i in 1..3) {
delay(100)
channel.send(i)//发送
}
channel.close()//关闭Channel,发送结束
}

// 3. Channel 接收数据
launch {
repeat(3) {
val receive = channel.receive()//接收
print("接收 $receive")
}
}
}

三个步骤使用Channel在实现协程之间数据传输。在一个协程中每隔100毫秒发送一条数据,在另一个协程中接收数据。打印如下:

1
2
3
kotlin复制代码[main] 接收 1
[main] 接收 2
[main] 接收 3

1.创建 Channel

创建Channel的方式有两种:

  • 直接使用Channel对象创建,如上
  • 拓展函数produce:启动一个生产者协程,返回一个ReceiveChannel。它启动的协程在结束后会自动关闭对应的Channel。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码GlobalScope.launch {
// 1. produce创建一个 Channel
val channel = produce<Int> {
for (i in 1..3) {
delay(100)
send(i)//发送数据
}
}

// 2. 接收数据
launch {
for (value in channel) {//for 循环打印接收到的值(直到渠道关闭)
print("接收 $value")
}
}
}

拓展函数produce直接将创建Channel和发送数据合为一步了。

2.发送数据

  • channel.send():发送数据。
  • channel.close():关闭Channel,数据发送完毕。

当我们数据发送完毕的时候,可以使用Channel.close()关闭通道,数据发送结束。

3.接收数据

  • channel.receive():接收数据。

一般调用 Channel#receive() 获取数据,但是这个方法只能获取一次传递的数据,如果我们知道获取数据的次数:

1
2
3
4
kotlin复制代码repeat(3) {//重复3次接收数据
val receive = channel.receive()//接收数据
print("接收 $receive")
}

如果我们不知道接收的数据有多少,则使用迭代Channel来接收数据:

1
2
3
kotlin复制代码for (value in channel) {// for 循环打印接收到的值(直到渠道关闭)
print("接收 $value")
}

Channel 的可以说为协程注入了灵魂。每一个独立的协程不再是孤独的个体, Channel 可以让他们更加方便的协作起来。但是在Flow出来之后,就很少使用到Channel了,接下来我们看看冷数据流Flow。

热数据流与冷数据流:

  • 热数据流:无观察者时,也会生产数据。你不订阅,它也会发送数据。比如某场影片在电影院播放,你要去电影院看才能看到,你不去这场电影也是会正常放的;
  • 冷数据流:无消费者时,则不会生产数据。你触发了,它才有数据发送过来。比如这场电影在网络上公开了,你不去播放他就不会播放,你主动播放了他才会播放。RxJava相对应的是协程的冷数据流Flow。

六、Flow

Flow是一种异步数据流,它按顺序发出值并正常或异常完成。是 Kotlin 协程的响应式API,类似于 RxJava 的存在。

每一个Flow其内部是按照顺序执行的,这一点跟Sequences很类似。Flow跟Sequences之间的区别是Flow不会阻塞主线程的运行,而Sequences会阻塞主线程的运行。

1.基础

创建 Flow 对象

Flow也为我们提供了快速创建操作:

  • Flow:    创建Flow的普通方法,从给定的一个挂起函数创建一个冷数据流。
  • channelFlow:支持缓冲通道,线程安全,允许不同的CorotineContext发送事件。
  • .asFlow(): 将其他数据转换成普通的flow,一般是集合向Flow的转换。
  • flowof(vararg elements: T):使用可变数组快速创建flow,类似于listOf()。

比如可以使用 (1..3).asFlow()或者flowof(1..3)创建Flow对象。

消费数据

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码lifecycleScope.launch {
//1.创建一个Flow
flow<Int> {
for (i in 1..3) {
delay(200)
//2.发出数据
emit(i)
}
}.collect {//3.从流中收集值
print("收集:$it")
}
}

打印数据如下:

1
2
3
kotlin复制代码[main] 收集:1
[main] 收集:2
[main] 收集:3

和 RxJava 一样,在创建 Flow 对象的时候我们也需要调用 emit 方法发射数据,collect 方法用来消费收集数据。

  • emit(value): 收集上游的值并发出。不是线程安全,不应该并发调用。线程安全请使用channelFlow而不是flow。
  • collect():  接收给定收集器emit()发出的值。它是一个挂起函数,在所在作用域的线程上执行。

flow的代码块只有调用collected()才开始运行,正如 RxJava 创建的 Observables只有调用subscribe()才开始运行一样。如果熟悉 RxJava 的话,则可以理解为collect()对应subscribe(),而emit()对应onNext()。

对比类型 Flow RxJava
数据源 Flow<T> Observable<T>
订阅 collect subscribe
发射 emit() onNext()

冷数据流

Flow是一种冷数据流,流生成器中的代码直到流被收集起来才会运行。一个Flow创建出来之后,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。

所谓冷数据流,就是只有消费时才会生产的数据流,这一点与 Channel 正对应:Channel 的发送端并不依赖于接收端。

1
2
3
4
5
6
7
8
9
10
11
scss复制代码val flow = flow {
for (i in 1..3) {
delay(200)
emit(i)//从流中发出值
}
}

lifecycleScope.launch {
flow.collect { print("$it") }
flow.collect { print("$it") }
}

消费它会输出 1,2,3,重复消费它会重复输出 1,2,3。RxJava 的 Observable 也是如此,每次调用它的 subscribe 都会重新消费一次。

2.线程切换

RxJava 也是一个基于响应式编程模型的异步框架,牛逼的地方就是切换线程。提供了两个切换调度器的 API 分别是 subscribeOn 和 observeOn,Flow也可以设定它运行时所使用的调度器,它更加简单,只需使用flowOn就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码lifecycleScope.launch {
//创建一个Flow<T>
flow {
for (i in 1..3) {
delay(200)
emit(i)//从流中发出值
}
}.flowOn(Dispatchers.IO)//将上面的数据发射操作放到 IO 线程中的协程
.collect { value ->
// 具体数据的消费处理
}
}

通过flowOn()改变的是Flow函数内部发射数据时的线程,而在collect收集数据时会自动切回创建Flow时的线程。

Flow的调度器 API 中看似只有flowOn与subscribeOn对应,其实不然,collect所在协程的调度器则与observeOn指定的调度器对应。

对比类型 Flow RxJava
改变数据发送的线程 flowOn subscribeOn
改变消费数据的线程 它自动切回所在协程的调度器 observeOn

注意:不允许在内部使用withContext()来切换flow的线程。因为flow不是线程安全的,如果一定要这么做,请使用channelFlow。

3.异常处理

Flow的异常处理也比较直接,直接调用 catch 函数即可:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码lifecycleScope.launch {
flow {
emit(10)//从流中发出值
throw NullPointerException()//抛出空指针异常
}.catch { e ->//捕获上游抛出的异常
print("caught error: $e")
}.collect {
print("收集:$it")
}
}

打印数据如下:

1
2
kotlin复制代码[main] 收集:10
[main] caught error: java.lang.NullPointerException

在Flow的参数中抛了一个空指针异常,在catch函数中就可以直接捕获到这个异常。如果没有调用catch函数,未捕获异常会在消费时抛出。catch 函数只能捕获它的上游的异常。

Flow中的catch对应着 RxJava 中的 onError:

对比 Flow RxJava
异常 catch onError

注意:流收集还可以使用try{}catch{}块来捕获异常。

4.完成和取消

onCompletion

如果我们想要在流完成时执行逻辑,可以使用 onCompletion:

1
2
3
4
5
6
7
8
9
kotln复制代码lifecycleScope.launch {
flow {
emit(10)
}.onCompletion {//流操作完成回调
print("Flow 操作完成")
}.collect {
print("收集:$it")
}
}

打印数据如下:

1
2
kotlin复制代码[main] 收集:10
[main] Flow 操作完成
对比 Flow RxJava
完成 onCompletion onComplete

注意:流还可以使用try{}finally{}块在收集完成时执行一个动作。

取消

Flow没有提供取消操作,Flow的消费依赖于collect末端操作符,而它们又必须在协程当中调用,因此Flow的取消主要依赖于末端操作符所在的协程的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码lifecycleScope.launch {
//1.创建一个子协程
val job = launch {
//2.创建flow
val intFlow = flow {
(1..5).forEach {
delay(1000)
//3.发送数据
emit(it)
}
}

//4.收集数据
intFlow.collect {//收集
print(it)
}
}

//5.在3.5秒后取消协程
delay(3500)
job.cancelAndJoin()
}

1000毫秒发送一次数据,3500毫秒后取消协程,因此flow收集到1,2,3后被取消。想要取消Flow只需要取消它所在的协程。

5.背压

什么是背压?就是在生产者的生产速率高于消费者的处理速率的情况下出现,发射的量大于消费的量,造成了阻塞,就相当于压力往回走,这就是背压。只要是响应式编程,就一定会有背压问题。处理背压问题有以下三种方式:

  • buffer:   指定固定容量的缓存;
  • conflate:  保留最新值;
  • collectLatest:新值发送时,取消之前的。

添加缓冲

可以为buffer指定一个容量。不需要等待收集执行就立即执行发射数据,只是数据暂时被缓存而已,提高性能,如果我们只是单纯地添加缓存,而不是从根本上解决问题就始终会造成数据积压。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码lifecycleScope.launch {
val time = measureTimeMillis {//计算耗时
flow {
for (i in 1..3) {
delay(100)//假设我们正在异步等待100毫秒
emit(i)//发出下一个值
}
}.buffer()//缓存排放,不要等待
.collect { value ->
delay(300)//假设我们处理了300毫秒
print(value)
}
}

print("收集耗时:$time ms")
}

需要100毫秒才能发射一个元素;收集器处理一个元素需要300毫秒。那么顺序执行发送接收三个数据的话大概需要1200毫秒,但是使用buffer()创建缓存,不要等待,运行更快。打印数据如下:

1
2
3
4
kotlin复制代码[main] 1
[main] 2
[main] 3
[main] 收集耗时:1110 ms

conflate

当flow表示操作的部分结果或操作状态更新时,可能不需要处理每个值,而是只处理最近的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码lifecycleScope.launch {
val time = measureTimeMillis {//计算耗时
flow {
for (i in 1..3) {
delay(100)//假设我们正在异步等待100毫秒
emit(i)//发出下一个值
}
}.conflate()//合并排放,而不是逐个处理
.collect { value ->
delay(300)//假设我们处理了300毫秒
print(value)
}
}

print("收集耗时:$time ms")
}

当数字1扔在处理时,数字2和数字3已经产生了,所以数字2被合并,只有最近的数字1(数字3)被交付给收集器。打印数据如下:

1
2
3
kotlin复制代码[main] 1
[main] 3
[main] 收集耗时:802 ms

collectLatest

另一种方法是取消慢速收集器,并在每次发出新值时重新启动它。collectLatest在它们执行和 conflate操作符相同的基本逻辑,但是在新值上取消其块中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码lifecycleScope.launch {
val time = measureTimeMillis {
flow {
for (i in 1..3) {
delay(100)//假设我们正在异步等待100毫秒
emit(i)//发出下一个值
}
}.collectLatest { value ->//取消并重新启动最新的值
print("收集的值:$value")
delay(300)//假设我们处理了300毫秒
print("完成:$value")
}
}

print("收集耗时:$time ms")
}

由于collectLatest的代码需要300毫秒的时间,但是每100毫秒就会发出一个新值,所以我们看到代码块在每个值上运行,但只在最后一个值上完成。打印数据如下:

1
2
3
4
5
kotlin复制代码[main] 收集的值:1
[main] 收集的值:2
[main] 收集的值:3
[main] 完成:3
[main] 收集耗时:698 ms

6.操作符

Kotlin 协程的flow提供了许多操作符来处理数据,下面整理了一些比较常用的操作符:

基本操作符

Flow 操作符 作用
map 转换操作符,将值转换为另一种形式输出
take 接收指定个数发出的值
filter 过滤操作符,返回只包含与给定规则匹配的原始值的流。

末端操作符

做 collect 处理,collect 是最基础的末端操作符。

末端流操作符 作用
collect 最基础的收集数据,触发flow的运行
toCollection 将结果添加到集合
launchIn 在指定作用域直接触发流的执行
toList 给定的流收集到 List 集合
toSet 给定的流收集到 Set 集合
reduce 规约,从第一个元素开始累加值,并将参数应用到当前累加器的值和每个元素。
fold 规约,从[初始]值开始累加值,并应用[操作]当前累加器值和每个元素

功能性操作符

功能性操作符 作用
retry 重试机制 ,当流发生异常时可以重新执行
cancellable 接收的的时候判断 协程是否被取消 ,如果已取消,则抛出异常
debounce 防抖节流 ,指定时间内的值只接收最新的一个,其他的过滤掉。搜索联想场景适用

回调操作符

回调流操作符 作用
onStart 在上游流开始之前被调用。 可以发出额外元素,也可以处理其他事情,比如发埋点
onEach 在上游向下游发出元素之前调用
onEmpty 当流完成却没有发出任何元素时回调,可以用来兜底。

组合操作符

组合流操作符 作用
zip 组合两个流,分别从二者取值,一旦一个流结束了,那整个过程就结束了
combine 组合两个流,在经过第一次发射以后,任意方有新数据来的时候就可以发射,另一方有可能是已经发射过的数据

展平流操作符

展平流有点类似于 RxJava 中的 flatmap,将你发射出去的数据源转变为另一种数据源。

展平流操作符 作用
flatMapConcat 串行处理数据,展开合并成一个流
flatMapMerge 并发地收集所有流,并将它们的值合并到单个流中,以便尽快发出值
flatMapLatest 一旦发出新流,就取消前一个流的集合

其他还有一些操作符,我这里就不一一介绍了,感兴趣可以查看 API。在实际场景中按需使用,比如搜索场景使用debounce防抖,网络请求使用retry重试,数据合并使用combine等操作符。

学习协程和Kotlin还是很有必要的,简化代码的逻辑,写出优雅的代码,提升开发效率。

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!

本人水平有限,文章难免会有错误,请批评指正,不胜感激 !

Kotlin协程学习三部曲:

  • 《Kotlin 协程实战进阶(一、筑基篇)》
  • 《Kotlin 协程实战进阶(二、进阶篇)》
  • 《Kotlin 协程实战进阶(三、原理篇)》

Kotlin协程实战项目

参考链接:

  • Kotlin官网
  • 《深入理解Kotlin协程》
  • 慕课网之《新版Kotlin从入门到精通》
  • 最全面的Kotlin协程: Coroutine/Channel/Flow 以及实际应用

希望我们能成为朋友,在 Github、掘金 上一起分享知识,一起共勉!Keep Moving!

本文转载自: 掘金

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

操作系统之文件系统| 8月更文挑战 机械硬盘存储数据原理 设

发表于 2021-08-05

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战


现在目录和文件的抽象结构组成的文件系统已经是操作系统必备的功能。而文件存储离不开存储介质,让我们以机械硬盘为例探索文件系统的实现。

机械硬盘存储数据原理

image.png
计算机硬件的总线结构分为不同层级,磁盘的位置在最下层的外设IO设备。分层的图中,距离CPU越近速度越快,但对应的成本越高,外设IO总线成本低,用于外接IO设备,如磁盘、鼠标键盘等。

此时此刻我有一块机械硬盘,硬盘连接到外设IO总线后,操作系统要如何使用它呢?对于机械硬盘存储来说,存,存在机械硬盘哪个地方,读取,从机械硬盘哪个地方读取?解决这个问题,需要了解机械硬盘的物理结构以及读写数据的方式。
首先机械硬盘有几个结构:磁头、柱面、盘片、磁道、扇区。(就不贴图了,自行看机械硬盘拆机视频)
其材质是磁性材质,磁性有个特点,就是可以频繁修改正负极,磁头带电,与磁性材质触碰可以修改磁性材质正负极。基于这个特点,可以将正极视为0,负极视为1,以此存储二进制数据。比如我们需要存储一个十进制数字8,可以将磁头移动到第1一个柱面,第一个盘片,第一个扇区上第N个字节(可能这个扇区前N个字节长度已经被写入数据了,在磁盘中有些地方是存储了这些元信息可以直接读取,当数据删除只会调整被占用字节的长度大小,所以误删除文件可以进行恢复,然如果被覆盖写入了,就再也无法恢复)。十进制8的二进制为1000,存储到磁性材质中就是:-+++,读取时当然别忘了记录存储的数据长度,不然读取时不知道数据何时结束。

那么磁头又是如何移动的呢?比如如何让磁头移动到第1柱面第一个盘片第一个磁道的第一个扇区?此处只说下移动过程,不探讨如何控制磁头的移动,因为我感觉这部分又是另外一个领域了,需要设计电路。

接着说磁头是如何移动的,每个盘片的上下都有一个磁头,所以盘片选择不需要移动,只需要控制对应的盘片移动。然后磁头会根据指定的磁道进行移动。磁头一开始是静止不动,然后进行加速,在进行减速到达对应磁道,然后在等待盘片的旋转使目标扇区移动到磁头下方。所以机械硬盘的确定读写位置耗时有如下几部分:

  1. 寻道耗时,寻找对应磁道耗时,会进行加速减速过程
  2. 旋转耗时,等待盘片旋转到对应扇区

知道这两个耗时后,对于机械硬盘时3600转/s,7200转/s相信你也理解了,转速越快,对应的旋转耗时越短。

由于每次读写需要经过两个耗时,比如我在第一秒需要写入数据到磁盘第一个盘片第一磁道第一扇区,在第三秒也需要写入数据到到同个位置。正常来说两次写入需要两次定位耗时,一次读写进行一次定位耗时被称为随机读写。一种优化方式是将第一次写入和第二次写入合并在一起在进行写入,只需进行一次定位耗时,这种针对机械硬盘的优化被称为顺序读写。那么固态硬盘具备顺序读写的特性吗?它又是如何优化?

设备交互

当有了机械硬盘,并且与IO总线连接,那么操作系统又是如何与机械硬盘交互呢?其实类似于Java中使用第三方中间件提供的Jar包,包含了一系列的API接口,只需当一个掉包侠即可。硬盘的制造商也会提供一系列的API给到开发者进行调用。当然也有另外一种方式,内存映射(MMP)。区别会在虚拟化文章探讨。后面在进行交互都以API接口进行。

文件系统

目录和文件抽象的文件系统已经是所有操作系统都具备的功能。下面来探讨如何实现一个文件系统。
文件本质就是一些在机械硬盘上联系存储的二进制数据,文件系统仅关心如何读写文件不必关心文件的格式是图片还是文本。同时一些元信息也需要得到存储,如创建时间,文件大小等。
目录本质与文件没有区别,也可以看做一个文件,但目录存储的信息只能是包含一些文件。

以Linux中的文件系统为例,文件系统可以简化为如下几个功能呢个:

  1. 创建、修改、删除文件
  2. 创建、修改、删除目录

现有一块机械硬盘64kb,基于此实现简单文件系统。

文件存储实现

既然机械硬盘用于存储文件,那么64kb全用于存储文件,当写入一个文件后,第二个写入的文件紧紧跟在第一个文件后面是否可行?当然不行,有如下几个问题需要解决。

  1. 第一个问题是没有考虑文件如果要增加大小如何处理。
  2. 第二个是无法实现”显示所有文件列表”的需求,因为没有记录文件写入文件到磁盘的位置,以及文件的名称。
  3. 如果我要保存文件到磁盘,我需要找到磁盘哪些位置是空闲的。

对于第二个问题,需要有一个类似索引的东西来标明每个文件的名称以及存储在磁盘的哪个位置,大小是多少。这个类似索引的数据也被叫做文件的元数据,名词叫做inode。其数据结构为

  • 文件名称
  • 文件大小
  • 文件存储在磁盘的起始地址
    划分数据结构之后,我们的数据存储就不仅仅是用户数据了,还需要存储文件元信息inode。所以需要将64kb的空间分为两部分,一部分存储inode信息,另外一部分存用户数据。inode存储的位置称为inode表。并且每个inode都有一个唯一的inodeId,通过inodeId可以在inode表中很容易找到对应的indoe信息。

对于第一个问题,解决方法有很多,比如在inode中存储的磁盘起始地址指向的文件末尾指向下一块文件地址。另外一个方法是使用多个存储存储的磁盘地址进行表示。

对于第三个问题。由于将磁盘空间划分2部分,所以除了判断存储用户数据是否有空闲空间还需要判断inode是否有空闲空间。如果按照链表方式存储文件,那么可以用两个指针的方式存储空闲空间的起始地址。所以还需要对整个磁盘划分出来一部分存储空闲空间信息,这部分就叫它空闲表。

经过以上设计,最终我们的文件系统被划分为三部分,空闲表区、Inode区、用户数据区

  • 空闲表区:存储Innode表以及用户数据区空闲的区域地址。
  • Inode区:存储文件以及目录的Inode信息
  • 用户数据区:存储用户的文件数据和目录数据

目录存储实现

目录其实相当于一个特殊的文件:存储的不是用户多样的数据,比如txt文本,或者是jpg图片。而目录存储数据的是子目录的名称以及inode编号。并且目录的inode信息中会额外有一个字段来标示其为目录类型。与文件区分开来。

文件读取

假设现在有一个/foo/1.txt文件,其大小为1kb,存储在这个简单的文件系统中,下面尝试读取一下。

  1. 从inode表中读取’/‘目录的inode信息,找到目录存储的地址信息
  2. 通过地址直接访问磁盘目录数据,找到下一个目录’foo’的inode号
  3. 通过’foo’的inode号找到’foo’目录的inode信息
  4. 通过’foo’inode信息中的数据地址信息,找到’1.txt’文件的inode号
  5. 访问文件,通过’1.txt’inode中存储的数据地址,访问1.txt的数据

文件写入

假设现在有一个/foo/1.txt文件,其大小为1kb,需要向该文件末尾写入数据’Hello disk!’。

  1. 从inode表中读取’/‘目录的inode信息,找到目录存储的地址信息
  2. 通过地址直接访问磁盘目录数据,找到下一个目录’foo’的inode号
  3. 通过’foo’的inode号找到’foo’目录的inode信息
  4. 通过’foo’inode信息中的数据地址信息,找到’1.txt’文件的inode号
  5. 访问文件,通过’1.txt’inode中存储的数据地址,并且根据当前文件长度计算出文件末尾的地址。
  6. 向空闲列表区查询空闲的用户数据区地址,比如找到地址0xAA地址处是空闲,则向文件末尾写入0xAA地址标示下一个存储数据的节点,然后将文件末尾处被覆盖的数据写入到新地址,然后写入’Hello disk!’,最后更新空闲列表(或者在查询时候指明写入数据的长度可以直接更新)。

通过以上理解,感觉echo >> 的追加写入也不再神秘。

并发读/写

当然在实际使用时候,一个文件不一定只是被一个进程读写。在并发读的情况没有意外发生。但当并发写入时,意外就来临了。所以通常会对写入进行加锁,加锁的思路也很简单可以在inode中存储一个字段用于标识当前文件是否被写入,如果被写入则不能再被访问了。当然加锁后需要解决锁带来的问题,比如死锁。

总结

通过常规思路思考了一下文件系统的实现,当然这才是开始,后续的工作就是对性能进行优化。在学习新技术时,我会去了解这个技术解决的核心问题是什么(说白了就就是去了解需求是什么),然后设计一个常规的方案出来(说白了就是第一步干啥第二部干啥)。常规方案通常是非常容易都能想出来的。然后下一步去思考常规方案的问题所在。比如性能、实现复杂度等问题。然后对这些问题逐个进行优化。然后在去看新技术是如何实现,与我“设想”的方案有何区别。取其精华,去其糟粕。

本文转载自: 掘金

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

Elasticsearch 对文本实现模糊、精确、分词搜

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎,它具有强大的搜索功能。

对文本搜索一般可以分为三种类型:模糊搜索、精确搜索、分词搜索。

  • 模糊搜索:如sql中的like查询语句,匹配包含搜索关键字的内容。
  • 精确搜索:文本内容与搜索关键字一致。
  • 分词搜索:将文本先进行分词,包括搜索关键字分词和搜索内容进行分词,再匹配相关内容。

用一个例子说明:

我们有一串文本「我正在学习数据结构和算法」,

如果是模糊搜索,用「学习」、「数据结构」、「算法」等关键词就能搜索出结果

如果是精确搜索,搜索关键词一定为「我正在学习数据结构和算法」才能搜出结果

如果是分词搜索,「算法之美」关键词就能搜索出结果,因为分词搜索,只需要将关键字的分词匹配上就可以了。而使用模糊搜索,是不能正确搜索出结果的。

在Elasticsearch中,使用“term”,“match”,“match_phrase”,“keyword”进行相关搜索。接下来我们用实验演示不同的搜索效果。

相关准备:

索引base-product-spu-info中有一条数据:

1
2
3
json复制代码{
"spuName" : "【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付"
}

对「【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付」分词(默认分词器,单个字分词)

1
2
3
4
5
6
csharp复制代码GET base-product-spu-info/_analyze
{
"analyzer": "standard",
"text": "【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付"
}
// 分词结果:市|场|价|2532|huawei|watch|2|pro|4g|智|能|手|表|移|动|支|付

term 搜索

term搜索是对搜索词不进行分词搜索,但对搜索的字段还是会分词,而加keyword属性,则是不分词的精准搜索

  1. 关键字「智」搜索
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName": {
      "value": "智"
    }
  }
}
}
// 结果:搜索出数据
  1. 关键字「智能」搜索
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName": {
      "value": "智能"
    }
  }
}
}
// 结果:不能搜索出数据
  1. 关键字「Pro」搜索
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName": {
      "value": "Pro"
    }
  }
}
}
// 结果:不能搜索出数据
  1. 关键字「pro」搜索(小写)
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName": {
      "value": "pro"
    }
  }
}
}
// 结果:搜索出数据
  1. 关键字「【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付」搜索
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName": {
      "value": "【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付"
    }
  }
}
}
// 结果:不能搜索出数据
  1. 关键字「【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付」搜索(增加keyword关键字)
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "term": {
    "spuName.keyword": {
      "value": "【市场价2532】HUAWEI WATCH 2 Pro 4G智能手表 移动支付"
    }
  }
}
}
// 结果:搜索出数据

match搜索 (分词搜索)

先对搜索词进行分词,再进行分词搜索

1
2
3
4
5
6
7
8
9
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "match": {
    "spuName": "手机"
  }
}
}
// 结果:搜索出数据

match_phrase 搜索(模糊搜索)

短语搜索, 要求所有的分词必须同时出现在文档中,同时位置必须紧邻一致**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码GET base-product-spu-info/_search
{
"query": {
  "match_phrase": {
    "spuName": "智能手表"
  }
}
}
// 结果:搜索出数据

GET base-product-spu-info/_search
{
"query": {
  "match_phrase": {
    "spuName": "智能手表1"
  }
}
}
// 结果:不能搜索出数据

本文转载自: 掘金

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

JMH微基准测试入门案例

发表于 2021-08-04

JMH - java Microbenchmark Harness

微基准测试,他是测试某个方法的性能到底是好还是不好。
这个测试框架是2013年发出来的,有JLT开发人员开发,后来归到OpenJDK下面。

  • 官网:openjdk.java.net/projects/co…

下面介绍什么是JMH,他是用来干什么的,怎么使用?基于idea中使用。

创建JMH测试

1.创建maven项目,添加依赖。

1.1 jmh-core (jmh的核心)
1.2 mh-generator-annprocess(注解处理包)

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码 <!--jmh依赖-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.21</version>
<scope>test</scope>
</dependency>

2.idea安装JMH插件JMH plugin

File->Settings->Plugins->JMH plugin

在这里插入图片描述

3. 打开运行程序注解配置

因为JMH在运行的时候他用到了注解,注解这个东西你自己得写一个程序得解释他,所以你要把这
个给设置上允许JMH能够对注解进行处理:
Compiler -> Annotation Processors -> Enable Annotation Processing(打钩)

在这里插入图片描述

4. 定义需要测试类

看这里,写了一个类,并行处理流的一个程序,定义了一个list集合,然后往这个集合里扔了1000个数。
写了一个方法来判断这个数到底是不是一个质数。
写了两个方法,第一个是用forEach来判断我们这1000个数里到底有谁是质数;第二个是使用了并行处理流。
这个forEach的方法就只有单线程里面执行,挨着从头拿到尾,从0拿到1000,但是并行处理的时候会有多个线程采用ForkJoin的方式来把里面的数分成好几份并行的尽兴处理。一种是串行处理,一种是并行处理,都可以对他们进行测试,但需要注意这个基准测试并不是对比测试的,你只是侧试一下你这方法写出这样的情况下他的吞吐量到底是多少,这是一个非常专业的测试的工具。严格的来讲这部分是测试开发专业的。

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
java复制代码import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Jmh {

static List<Integer> nums = new ArrayList<>();

static {
Random r = new Random();
for (int i = 0; i < 10000; i++) {
nums.add(1000000 + r.nextInt(1000000));
}
}

public static void foreach() {
nums.forEach(v -> isPrime(v));
}

static void parallel() {
nums.parallelStream().forEach(Jmh::isPrime);
}

static boolean isPrime(int num) {
for (int i = 2; i <= num / 2; i++) {
if (num % i == 0) return false;
}
return true;
}
}

5. 写单元测试

这个测试类一定要在test package下面
我对这个方法进行测试testForEach,很简单我就调用Jmh这个类的foreach就行了,对它测试
最关键的是我加了这个注解@Benchmark,这个是JMH的注解,是要被JMH来解析处理的,
这也是我们为什么要把那个Annotation Processing给设置上的原因,非常简单,
你只要加上注解就可以对这个方法进行微基准测试了,点击右键直接run

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码import org.openjdk.jmh.annotations.*;

public class JmhTest {
@Benchmark
@Warmup(iterations = 1, time = 3)//在专业测试里面首先要进行预热,预热多少次,预热多少时间
@Fork(5)//意思是用多少个线程去执行我们的程序
@BenchmarkMode(Mode.Throughput)//是对基准测试的一个模式,这个模式用的最多的是Throughput吞吐量
@Measurement(iterations = 1, time = 3)//是整个测试要测试多少遍,调用这个方法要调用多少次
public void testForEach() {
Jmh.foreach();
}
}

在这里插入图片描述

6. 运行测试类,如果遇到下面的错误:

ERROR: org.openjdk.jmh.runner.RunnerException: ERROR: Exception while trying to acquire the JMH lock (C:\WINDOWS/jmh.lock): 拒绝访问。, exiting. Use -Djmh.ignoreLock=true to forcefully continue.
at org.openjdk.jmh.runner.Runner.run(Runner.java:216)
at org.openjdk.jmh.Main.main(Main.java:71)

这个错误是因为JMH运行需要访问系统的TMP目录,解决办法是:
打开Run Configuration -> Environment Variables -> include system environment viables(勾选)
在这里插入图片描述
最后结果:
在这里插入图片描述

JMH中的基本概念

  1. Warmup
    预热,由于JVM中对于特定代码会存在优化(本地化),预热对于测试结果很重要
  2. Mesurement
    总共执行多少次测试
  3. Timeout
  4. Threads
    线程数,由fork指定
  5. Benchmark mode
    基准测试的模式
  6. Benchmark
    测试哪一段代码

这个是JMH的一个入门,严格来讲这个和我们的关系其实并不大,这个是测试部门干的事儿,但是你了
解一下没有特别多的坏处,你也知道你的方法最后效率高或者底,可以通过一个简单的JMH插件来帮你
完成,你不要在手动的去写这件事儿了。
如果说大家对JMH有兴趣,你们在工作中可能会有用的上大家去读一下官方的例子,官方大概有好几十
个例子程序,你可以自己一个一个的去研究。
官方样例:
hg.openjdk.java.net/code-tools/…

本文转载自: 掘金

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

Java使用Aviator表达式 记录(二)

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

AviatorScript 支持常见的类型,如数字、布尔值、字符串等等,同时将大整数、BigDecimal、正则表达式也作为一种基本类型来支持。

数字

数字包括整数和浮点数,AviatorScript 对 java 的类型做了缩减和扩展,同时保持了一致的运算符规则。

整数和算术运算

整数例如 -99、0、1、2、100……等等,对应的类型是 java 中的 long 类型。AviatorScript 中并没有 byte/short/int 等类型,统一整数类型都为 long,支持的范围也跟 java 语言一样:-9223372036854774808~9223372036854774807。

整数也可以用十六进制表示,以 0x 或者 0X 开头的数字,比如 0xFF(255)、0xAB(171) 等等。

整数可以参与所有的算术运算,比如加减乘除和取模等等。

1
2
3
4
5
6
7
8
9
10
css复制代码let a = 99;
let b = 0xFF;
let c = -99;

println(a + b);
println(a / b);
println(a- b + c);
println(a + b * c);
println(a- (b - c));
println(a/b * b + a % b);

加减乘除对应的运算符就是 +,-,*,/ 这都比较好理解,取模运算符就是 % ,规则和语法和 java 是一样的。

需要注意,整数相除的结果仍然是整数,比如例子中的 a/b 结果就是 0,遵循 java 的整数运算规则。

运算符之间的优先级如下:

  • 单目运算符 - 取负数
  • *, /
  • +,-

整个规则也跟 java 的运算符优先级保持一致。你可以通过括号来强制指定优先级,比如例子中的 a-(b-c) 就是通过括号,强制先执行 b-c ,再后再被 a 减。

通常来说,复杂的算术表达式,从代码可读性和稳健角度,都推荐使用括号来强制指定优先级。

大整数(BigInt)

对于超过 long 的整数, AviatorScript 还特别提供了大整数类型的支持,对应 java.math.BigInteger 类。任何超过 long 范围的整数字面量,会自动提升为 BigInteger 对象(以下简称 BigInt),任何数字以 N 字母结尾就自动变 BigInt:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码## examples/bigint.av

let a = 10223372036854774807; ## Literal BigInteger
let b = 1000N; ## BigInteger
let c = 1000; ## long type

println(a);

println(a + a);

println(a * a);

println(a + b + c);

10223372036854774807 是一个远远超过 long 返回的数字,b 也是 BigInt 类型,因为它以 N 结尾,BigInt 的算术运算和一般整数没有什么两样,采用同样的算术运算符和规则,执行这段脚本将打印

1
2
3
4
复制代码10223372036854774807
20446744073709549614
104517335803944147014652834074681887249
10223372036854776807

请注意,默认的 long 类型在计算后如果超过范围溢出后,不会自动提升为 BigInt,但是 BigInt 和 long 一起参与算术运算的时候,结果为 BigInt 类型。关于类型转换的规则,我们后面再详细介绍。

浮点数

数字除了整数之外,AviatorScript 同样支持浮点数,但是仅支持 double 类型,也就是双精度 64 位,符合 IEEE754 规范的浮点数。传入的 java float 也将转换为 double 类型。所有的浮点数都被认为是 double 类型。浮点数的表示形式有两种:

  1. 十进制的带小数点的数字,比如 1.34159265 , 0.33333 等等。
  2. 科学计数法表示,如 1e-2 , 2E3 等等,大小写字母 e 皆可。

看一个简单例子,牛顿法求平方根

1
2
3
4
5
6
7
8
9
10
11
ini复制代码## examples/square_root.av

let a = 2;
let err = 1e-15;
let root = a;

while math.abs(a - root * root) > err {
root = (a/root + root) / 2.0;
}

println("square root of 2 is: " + root);

这个例子稍微复杂了一点,因为我们用了后面才会讲到的 while 循环语句(参见条件语句),不过整体逻辑还是比较简单的,求 2 的平方根,我们通过不停计算 (a/root + root)/2.0 的值,看看是否在误差范围( err 指定)内,不在就继续迭代计算,否则就跳出循环打印结果:

1
csharp复制代码square root of 2 is: 1.414213562373095

浮点数的运算符跟整数一样,同样支持加减乘除,优先级也是一样。浮点数和浮点数的算术运算结果为浮点数,浮点数和整数的运算结果仍然为浮点数。

高精度计算(Decimal)

浮点数是无法用于需要精确运算的场景,比如货币运算或者物理公式运算等,这种情况下如果在 Java 里一般推荐使用 BigDecimal 类型,调用它的 add/sub 等方法来做算术运算。

AviatorScript 将 BigDecimal 作为基本类型来支持(下文简称 decimal 类型),只要浮点数以 M 结尾就会识别类型为 deicmal,例如 1.34M 、 0.333M 或者科学计数法 2e-3M 等等。

decimal 同样使用 +,-,*,/ 来做算术运算, AviatorScript 重载了这些运算符的方法,自动转成 BigDecimal 类的各种运算方法。我们把求平方根的例子改成 decimal 运算

1
2
3
4
5
6
7
8
9
10
11
ini复制代码## examples/bigdecimal.av

let a = 2M;
let err = 1e-15M;
let root = a;

while math.abs(a - root * root) > err {
root = (a/root + root) / 2.0M;
}

println("square root of 2M is: " + root);

运算结果 root 的类型也是 decimal 。除了 double 以外的数字类型和 decimal 一起运算,结果为 decimal。任何有 double 参与的运算,结果都为 double。

默认运算精度是 MathContext.DECIMAL128 ,你可以通过修改引擎配置项 Options.MATH_CONTEXT 来改变。

如果你觉的为浮点数添加 M 后缀比较麻烦,希望所有浮点数都解析为 decimal ,可以开启 Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL 选项。

数字类型转换

数字类型在运算的时候,会遵循一定的类型转换规则:

  • 单一类型参与的运算,结果仍然为该类型,比如整数和整数相除仍然是整数,double 和 double 运算结果还是 double。
  • 多种类型参与的运算,按照下列顺序: long -> bigint -> decimal -> double 自动提升,比如 long 和 bigint 运算结果为 bigint, long 和 decimal 运算结果为 decimal,任何类型和 double 一起运算结果为 double

你可以通过 long(x) 将一个数字强制转化为 long,这个过程中可能丢失精度,也可以用 double(x) 将一个数字强转为 double 类型。

1
2
3
4
5
6
7
css复制代码## examples/double.av

let a = 1;
let b = 2;

println("a/b is " + a/b);
println("a/double(b) is " + a/double(b));

a 和 b 都是 long 类型,他们相除的结果仍然是整数, 1/2 结果为 0,但是当使用 double 函数将 b 强制转为 double 类型,两者的结果就是浮点数了:

1
2
css复制代码a/b is 0
a/double(b) is 0.5

字符串

在任何语言中,字符串都是最基本的类型,比如 java 里就是 String 类型。AviatorScript 中同样支持字符串,只要以单引号或者双引号括起来的连续字符就是一个完整的字符串对象,例如:

  • "hello world"
  • 'hello world'
  • "a" 或者 'a'

字符串可以直接通过 println 函数打印。

字符串的长度可以通过 string.length 函数获取:

1
2
3
4
5
6
7
8
9
ini复制代码## examples/string.av

let a = "hello world";

println(a);
println(string.length(a));
``

打印:

hello world
11

1
2
3
go复制代码

字符串拼接可以用 `+` 号(这又是一个运算符重载):

examples/string.av

let a = “hello world”;
let b = ‘AviatorScript’;

println(a);
println(string.length(a));
println(a + ‘,’ + b + 5);

1
2
3
4
5
6
7
8
9
10
11
markdown复制代码字符串拼接 `a + ',' + b + 5`  包括了数字 5 和字符串 `','` , 任何类型和字符串相加,都将拼接为字符串,这跟 java 的规则一致。因此上面最后一行代码将打印 `hello world,AviatorScript5` 。


字符串还包括其他函数,如截取字符串 `substring`,都在 `string` 这个 namespace 下,具体见[函数库列表](https://www.yuque.com/boyan-avfmj/aviatorscript/ashevw)。


### 转义



同样,和其他语言类似,遇到特殊字符,AviatorScript 中的字符串也支持转义字符,和 java 语言一样,通过 `` 来转义一个字符,比如我们想表示的字符串中有单引号,如果我们继续使用单引号来表示字符串,这时候就需要用到转义符:

examples/escape_string.av

println(‘Dennis’s car’);
println(‘AviatorScript is great.\r\nLet’s try it!’);

1
2
3
r复制代码

特殊字符,比如 `\r` 、 `\n` 、 `\t` 等也是同样支持。上述例子我们使用了换行 `\r\n` ,将打印:

Dennis’s car
AviatorScript is great.
Let’s try it!

1
2
3
复制代码

当然,针对引号这个情况,这里你可以简单用双引号来表示字符串,就可以避免转义:

println(“Dennis ‘s car”);

1
2
3
4
5
shell复制代码

### 字符串插值(String Interpolation)

字符串拼接可以用加法,比如

let name = “aviator”;
let s = “hello,” + name;

1
2
go复制代码
拼接后的字符串 s 就是 `hello,aviator` 。 `+` 加法对字符串拼接做了特别优化,内部会自动转化成 `StringBuilder` 来做拼接。但是对于更复杂的场景,字符串拼接的语法仍然显得过于丑陋和繁琐,因此 5.1.0 开始, AviatorScript 支持了字符串插值,一个例子:

examples/string_interpolation.av

let name = “aviator”;
let a = 1;
let b = 2;
let s = “hello, #{name}, #{a} + #{b} = #{a + b}”;
p(s);

1
2
ruby复制代码
字符串中 `#{}` 括起来的表达式都将在当前上下文里自动执行求值,然后插入到最终的结果字符串,上面的例子将输出:

hello, aviator, 1 + 2 = 3

1
2
3
4
5
6
7
8
9
bash复制代码
AviatorScript 内部做了大量优化,在编译模式复用 Expression 的情况下性能比使用加法拼接字符串更快。

## 布尔类型和逻辑运算

布尔类型用于表示真和假,它只有两个值 `true` 和 `false`  分别表示真值和假值。


比较运算如大于、小于可以产生布尔值:

examples/boolean.av

println(“3 > 1 is “ + (3 > 1));
println(“3 >= 1 is “ + (3 >= 1));
println(“3 >= 3 is “ + (3 >= 3));
println(“3 < 1 is “ + (3 < 1));
println(“3 <= 1 is “ + (3 <= 1));
println(“3 <= 3 is “ + (3 <= 3));
println(“3 == 1 is “ + (3 == 1));
println(“3 != 1 is “ + (3 != 1));

1
2
复制代码
输出:

3 > 1 is true
3 >= 1 is true
3 >= 3 is true
3 < 1 is false
3 <= 1 is false
3 <= 3 is true
3 == 1 is false
3 != 1 is true

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

上面演示了所有的逻辑运算符:

- `>`  大于
- `>=` 大于等于

<!---->

- `<` 小于
- `<=` 小于等于

<!---->

- `==` 等于
- `!=`  不等于

本文转载自: 掘金

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

资深开发竟然不清楚int(1)和int(10)的区别 困惑

发表于 2021-08-04

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

困惑

最近遇到个问题,有个表的要加个user_id字段,user_id字段可能很大,于是我提mysql工单alter table xxx ADD user_id int(1)。领导看到我的sql工单,于是说:这int(1)怕是不够用吧,接下来是一通解释。

其实这不是我第一次遇到这样的问题了,其中不乏有工作5年以上的老司机。包括我经常在也看到同事也一直使用int(10),感觉用了int(1),字段的上限就被限制,真实情况肯定不是这样。

数据说话

我们知道在mysql中 int占4个字节,那么对于无符号的int,最大值是2^32-1 = 4294967295,将近40亿,难道用了int(1),就不能达到这个最大值吗?

1
2
3
4
sql复制代码CREATE TABLE `user` (
`id` int(1) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

id字段为无符号的int(1),我来插入一个最大值看看。

1
2
sql复制代码mysql> INSERT INTO `user` (`id`) VALUES (4294967295);
Query OK, 1 row affected (0.00 sec)

可以看到成功了,说明int后面的数字,不影响int本身支持的大小,int(1)、int(2)…int(10)没什么区别。

零填充

一般int后面的数字,配合zerofill一起使用才有效。先看个例子:

1
2
3
4
sql复制代码CREATE TABLE `user` (
`id` int(4) unsigned zerofill NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

注意int(4)后面加了个zerofill,我们先来插入4条数据。

1
2
3
scss复制代码mysql> INSERT INTO `user` (`id`) VALUES (1),(10),(100),(1000);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

分别插入1、10、100、1000 4条数据,然后我们来查询下:

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> select * from user;
+------+
| id |
+------+
| 0001 |
| 0010 |
| 0100 |
| 1000 |
+------+
4 rows in set (0.00 sec)

通过数据可以发现 int(4) + zerofill实现了不足4位补0的现象,单单int(4)是没有用的。
而且对于0001这种,底层存储的还是1,只是在展示的会补0。

总结

int后面的数字不能表示字段的长度,int(num)一般加上zerofill,才有效果。zerofill的作用一般可以用在一些编号相关的数字中,比如学生的编号 001 002 … 999这种,如果mysql没有零填充的功能,但是你又要格式化输出等长的数字编号时,那么你只能自己处理了。

image.png

本文转载自: 掘金

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

Spring Boot 回顾(四):深入理解SpringFa

发表于 2021-08-04

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

前言

上一篇我们讲到Spring Boot自动装配的原理,而且通过阅读源码,我们看到其实自动装配的奥义其实就是SpringFactoriesLoader,今天我们再来揭秘下SpringFactoriesLoader的神秘面纱。

介绍

SpringFactoriesLoader工厂的加载机制类似java提供的SPI机制一样,是Spring提供的一种加载方式。只需要在classpath路径下新建一个文件META-INF/spring.factories,并在里面按照Properties格式填写好接口和实现类即可通过SpringFactoriesLoader来实例化相应的Bean。其中key可以是接口、注解、或者抽象类的全名。value为相应的实现类,当存在多个实现类时,用“,”进行分割。其实关于spring.factories的实现也是Spring Boot中约定优于配置的体现,可以说,整个Spring Boot是遵循约定优于配置这个理念产生的,因此它才能如此的快而且简单。

原理

先放一张SpringFactoriesLoader工作的流程图

不存在存在读取指定路径下的资源文件依次实例化对象对结果进行排序返回结果EndStart查找缓存构建Properties对象获取指定key对应的value值逗号分割value值保存结果到缓存
接下来我们进入重点,打开SpringFactoriesLoader的源码,里面比较简单,有2个成员变量和4个方法,其中方法分别是loadFactories、loadFactoryNames、loadSpringFactories和instantiateFactory。我们先看下成员变量

1
2
3
java复制代码
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

其中第一个是寻找工厂的位置,工厂可以存放在多个jar文件中,第二个则是自定义用于存储工厂的缓存。然后我们了解下其中的方法,首先是loadFactories这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
// 如果未指定类加载器,则使用默认的
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 获取指定工厂名称列表
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
// 如果记录器Trace跟踪激活的话,将工厂名称列表输出
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
// 创建结果集
List<T> result = new ArrayList<>(factoryImplementationNames.size());
for (String factoryImplementationName : factoryImplementationNames) {
// 实例化工厂类,并添加到结果集中
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
// 对结果集列表进行排序
AnnotationAwareOrderComparator.sort(result);
return result;
}

可以看到大致的工作流程如下:

  1. 通过classLoader去加载工厂获取其对应类名称,如果未指定类加载器,则使用默认的;
  2. 通过instantiateFactory方法实例化工厂类,并添加到结果集中;
  3. 通过AnnotationAwareOrderComparator#sort方法对工厂进行排序;
    接着我们看下loadFactoryNames,这个比较简单,主要逻辑都在loadSpringFactories,源码就不放上来了,其逻辑大致是读取 classpath上 所有的 jar 包中的所有 META-INF/spring.factories属 性文件,找出其中定义的匹配类型 factoryClass 的工厂类,然后并返回这些工厂类的名字列表,注意是包含包名的全限定名。最后是instantiateFactory方法,它的主要作用就是实例化Bean对象

总结

以上我们通过阅读了SpringFactoriesLoader的相关代码加深了对其理解,总结起来就是SpringFactoriesLoader是用于Spring框架内部的通用工厂加载机制。SpringFactoriesLoader通过loadFactories方法来加载并实例化来自FACTORIES_RESOUCE_LOCATION路径中的文件给定的工厂类型,而这些文件可能包含在类路径的jar包中。这些文件通常都命名为spring.factories,并且都是以properties属性作为格式,文件中key表示的是接口或者抽象类的全限定名称,而值是以逗号分隔的实现类的名称列表。

本文转载自: 掘金

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

spring cloud gateway 一个请求到转发走过

发表于 2021-08-04

spring cloud gateway 一个请求到转发走过的路途-源码解析

简介

Spring Cloud Gateway是基于Spring Boot 2.x,Spring WebFlux构建的,是新一代的网关解决方案。目前在打算用gateway网关替换掉原有的zuul网关,利用gateway提供的特性来提升原有网关性能。所以借此机会分析了下网关源码。

工作原理

这里贴一张官网的图
image.png
客户端向Gateway网关发出请求。如果网关处理映射请求与路由匹配,则将其发送到网关处理请求。请求经过网关多个过滤器链(这是涉及一个设计模式:责任链模式)。过滤器由虚线分隔的原因是,过滤器可以在发送请求之前和之后运行逻辑。所有“前置”过滤器逻辑均被执行。然后发出代理请求。发出代理请求后,将运行“后置”过滤器逻辑。

源码解析

首先从GatewayAutoConfiguration看起

负载均衡

GatewayAutoConfiguration注解中的GatewayLoadBalancerClientAutoConfiguration注解中有个LoadBalancerClientFilter是处理负载均衡的关键代码。

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
java复制代码public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
// preserve the original url
addOriginalRequestUrl(exchange, url);

if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url before: " + url);
}

final ServiceInstance instance = choose(exchange);

if (instance == null) {
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}

URI uri = exchange.getRequest().getURI();

// if the `lb:&lt;scheme&gt;` mechanism was used, use `&lt;scheme&gt;` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}

URI requestUrl = loadBalancer.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);

if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}

exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}

我们在配置路由转发的时候在配置服务时会写lb://xxx,源码中看到了熟悉的lb。

1
2
3
4
java复制代码if (url == null 
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}

url为null或不是lb则跳过此过滤器,否则进行负载均衡处理。
负载关键代码是choose方法,返回ServiceInstance对象(serviceId,host,port等信息)作为负载均衡后的结果。

1
2
3
4
java复制代码protected ServiceInstance choose(ServerWebExchange exchange) {
return loadBalancer.choose(
((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}

choose方法内部通过serviceId,通过ribbon去nacos里找到服务名对应的实例,并负载均衡选出一台实例服务ip和端口返回。将lb://xxx那部分替换成具体ip+端口后接请求路径,放入到上下文中key为gatewayRequestUrl。后通过NettyRoutingFilter过滤器(可以看到这个过滤器的order顺序是Integer.MAX_VALUE,目的就是为了处在最后的位置发送请求)使用httpclient发送请求到下游服务。

负载均衡流程图

image.png

请求转发

核心代码:DispatcherHandler.handle(ServcerWebExchange exchange),它是org.springframework.web.reactive包下的,所有的请求都会经过这里。webflux暂时没有研究,不过大体能看出关键代码和逻辑。

1
2
3
4
5
6
7
8
9
java复制代码if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -&gt; mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -&gt; invokeHandler(exchange, handler))
.flatMap(result -&gt; handleResult(exchange, result));

我们可以看到mapping -> mapping.getHandler(exchange) debug进去发现getHandlerInternal()方法里面。

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复制代码protected Mono&lt;?&gt; getHandlerInternal(ServerWebExchange exchange) {
// don't handle requests on management port if set and different than server port
if (this.managementPortType == DIFFERENT && this.managementPort != null
&& exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
}
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());

return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
.flatMap((Function&lt;Route, Mono&lt;?&gt;&gt;) r -&gt; {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isDebugEnabled()) {
logger.debug(
"Mapping [" + getExchangeDesc(exchange) + "] to " + r);
}

exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -&gt; {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for ["
+ getExchangeDesc(exchange) + "]");
}
})));
}

它的实现方法在RoutePredicateHandlerMapping类中。开头if判断不用看,核心方法是lookupRoute(exchange)

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
java复制代码protected Mono&lt;Route&gt; lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -&gt; Mono.just(route).filterWhen(r -&gt; {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.doOnError(e -&gt; logger.error(
"Error applying predicate for route: " + route.getId(),
e))
.onErrorResume(e -&gt; Mono.empty()))
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.&lt;Route&gt;empty().log("noroute"))
.next()
// TODO: error handling
.map(route -&gt; {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});

/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
}

首先outeLocator.getRoutes()的实现方法RouteDefinitionRouteLocator.getRoutes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码Flux&lt;Route&gt; routes = this.routeDefinitionLocator.getRouteDefinitions()
.map(this::convertToRoute);

if (!gatewayProperties.isFailOnRouteDefinitionError()) {
// instead of letting error bubble up, continue
routes = routes.onErrorContinue((error, obj) -&gt; {
if (logger.isWarnEnabled()) {
logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
+ " will be ignored. Definition has invalid configs, "
+ error.getMessage());
}
});
}

return routes.map(route -&gt; {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});

routeDefinitionLocator.getRouteDefinitions()又有很多实现方法。。。分别都是从缓存中获取路由信息,从注册中心获取路由信息,从配置文件中获取路由信息等,总而言之就是获取到路由RouteDefinition对象,通过convertToRoute方法将RouteDefinition对象转换成Route对象(咱们网关配置的谓词和过滤器都放入了Route对象,构建Route对象的时候又涉及一个设计模式:建造者模式)。
再往上看,获取到路由信息后Mono.just(route).filterWhen()大概就是我们请求过来的url对某个路由信息做匹配过滤。将我们在路由里配置的id放入上下文中,key为GATEWAY_PREDICATE_ROUTE_ATTR(id如不指定,则为UUID)
image.png
我这里配置的路由有两个,从图中断点可以看出,第一个路由信息和当前访问的url不匹配,返回为false,第二个路由信息匹配上了,返回为true。
这样我们再一路返回到lookupRoute方法,经过上面的一顿操作,又将route路由放入上下文中key为GATEWAY_ROUTE_ATTR。
再一路往上返回,回到最初的handle
image.png
经过对mapping和路由的一系列前置处理,我们是不是就应该执行真正的过滤等处理逻辑了,下面就是执行处理的关键代码。
关键代码:invokeHandler(exchange, handler)

1
2
3
4
5
6
7
8
9
10
java复制代码private Mono&lt;HandlerResult&gt; invokeHandler(ServerWebExchange exchange, Object handler) {
if (this.handlerAdapters != null) {
for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
if (handlerAdapter.supports(handler)) {
return handlerAdapter.handle(exchange, handler);
}
}
}
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}

我们进入handlerAdapter.handle(exchange, handler)方法,再经过多个实现
SimpleHandlerAdapter -> FilteringWebHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public Mono&lt;Void&gt; handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List&lt;GatewayFilter&gt; gatewayFilters = route.getFilters();

List&lt;GatewayFilter&gt; combined = new ArrayList&lt;&gt;(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);

if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}

return new DefaultGatewayFilterChain(combined).filter(exchange);
}

我们看到之前放入上下文的key为GATEWAY_ROUTE_ATTR的路由现在可以用到了!我们取出Route路由对象,记得之前RouteDefinition对象转Route的时候做了什么吗?是不是放入了过滤器?这里取出之前set进的过滤器集合,然后new DefaultGatewayFilterChain(combined).filter(exchange),执行过滤!(又是个设计模式:责任链模式,设计模式写法不固定,主要是思想哈,它这里的写法是通过index标记改执行哪一个过滤器,然后通过index游标移动来经过过滤器链条)
还记得上面的负载均衡过滤器吗,他的order为int最大值,所以肯定最后要走到LoadBalancerClientFilter.filter,然后执行choose方法通过负载均衡算法选举出服务器实例,再通过httpClient调用下游服务。是不是又和前面负载均衡源码解析的步骤连起来了!

ps

里面有很多细节其实还没有写进去,比如路由信息会放入本地缓存中,路由信息获取也可自定义获取方式,比如从数据库中,从redis中等。大体上的流程就是这样了,有不对的地方欢迎指正!

本文转载自: 掘金

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

SpringBoot 默认json解析器详解和自定义字段序列

发表于 2021-08-04

这是我参与8月更文挑战的第3天,活动详情查看: 8月更文挑战

前言

在我们开发项目API接口的时候,一些没有数据的字段会默认返回NULL,数字类型也会是NULL,这个时候前端希望字符串能够统一返回空字符,数字默认返回0,那我们就需要自定义json序列化处理

SpringBoot默认的json解析方案

我们知道在springboot中有默认的json解析器,Spring Boot 中默认使用的 Json 解析技术框架是 jackson。我们点开 pom.xml 中的 spring-boot-starter-web 依赖,可以看到一个 spring-boot-starter-json 依赖:

1
2
3
4
5
6
xml复制代码 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.7</version>
<scope>compile</scope>
</dependency>

Spring Boot 中对依赖都做了很好的封装,可以看到很多 spring-boot-starter-xxx 系列的依赖,这是 Spring Boot 的特点之一,不需要人为去引入很多相关的依赖了,starter-xxx 系列直接都包含了所必要的依赖,所以我们再次点进去上面这个 spring-boot-starter-json 依赖,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码 <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.4</version>
<scope>compile</scope>
</dependency>

我们在controller中返回json时候通过注解@ResponseBody就可以自动帮我们将服务端返回的对象序列化成json字符串,在传递json body参数时候 通过在对象参数上@RequestBody注解就可以自动帮我们将前端传过来的json字符串反序列化成java对象

这些功能都是通过HttpMessageConverter这个消息转换工具类来实现的

SpringMVC自动配置了Jackson和Gson的HttpMessageConverter,SpringBoot对此做了自动化配置

JacksonHttpMessageConvertersConfiguration

org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码	@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnBean(ObjectMapper.class)
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jackson", matchIfMissing = true)
static class MappingJackson2HttpMessageConverterConfiguration {

@Bean
@ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class,
ignoredType = {
"org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter",
"org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);
}

}

JacksonAutoConfiguration

org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {

@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}

}

Gson的自动化配置类

org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码	@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Gson.class)
@Conditional(PreferGsonOrJacksonAndJsonbUnavailableCondition.class)
static class GsonHttpMessageConverterConfiguration {

@Bean
@ConditionalOnMissingBean
GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson);
return converter;
}

}

自定义SprinBoot的JSON解析

日期格式解析

默认返回的是时间戳类型格式,但是时间戳会少一天需要在数据库连接url上加上时区如:

1
ini复制代码spring.datasource.url=jdbc:p6spy:mysql://47.100.78.146:3306/mall?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&autoReconnect=true
  1. 使用@JsonFormat注解自定义格式
1
2
java复制代码	@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;

但是这种要对每个实体类中的日期字段都需要添加此注解不够灵活

  1. 全局添加
    在配置文件中直接添加spring.jackson.date-format=yyyy-MM-dd

NULL字段不返回

  1. 在接口中如果不需要返回null字段可以使用@JsonInclude注解
1
2
java复制代码    @JsonInclude(JsonInclude.Include.NON_NULL)
private String title;

但是这种要对每个实体类中的字段都需要添加此注解不够灵活

  1. 全局添加 在配置文件中直接添加spring.jackson.default-property-inclusion=non_null

自定义字段序列化

自定义null字符串类型字段返回空字符NullStringJsonSerializer序列化

1
2
3
4
5
6
7
8
java复制代码public class NullStringJsonSerializer extends JsonSerializer {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (o == null) {
jsonGenerator.writeString("");
}
}
}

自定义null数字类型字段返回0默认值NullIntegerJsonSerializer序列化

1
2
3
4
5
6
7
8
java复制代码public class NullIntegerJsonSerializer extends JsonSerializer {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (o == null) {
jsonGenerator.writeNumber(0);
}
}
}

自定义浮点小数类型4舍5入保留2位小数DoubleJsonSerialize序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class DoubleJsonSerialize extends JsonSerializer {
private DecimalFormat df = new DecimalFormat("##.00");

@Override
public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (value != null) {
jsonGenerator.writeString(NumberUtil.roundStr(value.toString(), 2));
}else{
jsonGenerator.writeString("0.00");
}

}
}

自定义NullArrayJsonSerializer序列化

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class NullArrayJsonSerializer extends JsonSerializer {


@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if(o==null){
jsonGenerator.writeStartArray();
}else {
jsonGenerator.writeObject(o);
}
}
}

自定义BeanSerializerModifier使用我们自己的序列化器进行bean序列化

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

private JsonSerializer _nullArrayJsonSerializer = new NullArrayJsonSerializer();

private JsonSerializer _nullStringJsonSerializer = new NullStringJsonSerializer();

private JsonSerializer _nullIntegerJsonSerializer = new NullIntegerJsonSerializer();

private JsonSerializer _doubleJsonSerializer = new DoubleJsonSerialize();

@Override
public List changeProperties(SerializationConfig config, BeanDescription beanDesc,
List beanProperties) { // 循环所有的beanPropertyWriter
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter writer = (BeanPropertyWriter) beanProperties.get(i);
// 判断字段的类型,如果是array,list,set则注册nullSerializer
if (isArrayType(writer)) { //给writer注册一个自己的nullSerializer
writer.assignNullSerializer(this.defaultNullArrayJsonSerializer());
}
if (isStringType(writer)) {
writer.assignNullSerializer(this.defaultNullStringJsonSerializer());
}
if (isIntegerType(writer)) {
writer.assignNullSerializer(this.defaultNullIntegerJsonSerializer());
}
if (isDoubleType(writer)) {
writer.assignSerializer(this.defaultDoubleJsonSerializer());
}
}
return beanProperties;
} // 判断是什么类型

protected boolean isArrayType(BeanPropertyWriter writer) {
Class clazz = writer.getPropertyType();
return clazz.isArray() || clazz.equals(List.class) || clazz.equals(Set.class);
}

protected boolean isStringType(BeanPropertyWriter writer) {
Class clazz = writer.getPropertyType();
return clazz.equals(String.class);
}

protected boolean isIntegerType(BeanPropertyWriter writer) {
Class clazz = writer.getPropertyType();
return clazz.equals(Integer.class) || clazz.equals(int.class) || clazz.equals(Long.class);
}

protected boolean isDoubleType(BeanPropertyWriter writer) {
Class clazz = writer.getPropertyType();
return clazz.equals(Double.class) || clazz.equals(BigDecimal.class);
}


protected JsonSerializer defaultNullArrayJsonSerializer() {
return _nullArrayJsonSerializer;
}

protected JsonSerializer defaultNullStringJsonSerializer() {
return _nullStringJsonSerializer;
}

protected JsonSerializer defaultNullIntegerJsonSerializer() {
return _nullIntegerJsonSerializer;
}

protected JsonSerializer defaultDoubleJsonSerializer() {
return _doubleJsonSerializer;
}
}

应用我们自己bean序列化使其生效 提供MappingJackson2HttpMessageConverter类
在配置类中提供MappingJackson2HttpMessageConverter类,使用ObjectMapper 做全局的序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class ClassJsonConfiguration {
@Bean
public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

ObjectMapper mapper = converter.getObjectMapper();

// 为mapper注册一个带有SerializerModifier的Factory,此modifier主要做的事情为:判断序列化类型,根据类型指定为null时的值

mapper.setSerializerFactory(mapper.getSerializerFactory().withSerializerModifier(new MyBeanSerializerModifier()));

return converter;
}
}

此类会代替SpringBoot默认的json解析方案。事实上,此类中起作用的是ObjectMapper 类,因此也可直接配置此类。

1
2
3
4
5
6
7
8
java复制代码 @Bean
public ObjectMapper om() {
ObjectMapper mapper = new ObjectMapper();
// 为mapper注册一个带有SerializerModifier的Factory,此modifier主要做的事情为:判断序列化类型,根据类型指定为null时的值

mapper.setSerializerFactory(mapper.getSerializerFactory().withSerializerModifier(new MyBeanSerializerModifier()));
return mapper;
}

通过上面方式自定义序列化,还可以通过注解 @JsonSerialize序列化自定义如:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Component
public class DoubleSerialize extends JsonSerializer<Double> {

private DecimalFormat df = new DecimalFormat("##.00");

@Override
public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
if(value != null) {
gen.writeString(df.format(value));
}
}
}

然后再需要使用字段上面加上

1
2
java复制代码 @JsonSerialize(using = DoubleJsonSerialize.class)
private BigDecimal price;

配置文件jackson详细配置

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
yml复制代码  spring:
jackson:
# 设置属性命名策略,对应jackson下PropertyNamingStrategy中的常量值,SNAKE_CASE-返回的json驼峰式转下划线,json body下划线传到后端自动转驼峰式
property-naming-strategy: SNAKE_CASE
# 全局设置@JsonFormat的格式pattern
date-format: yyyy-MM-dd HH:mm:ss
# 当地时区
locale: zh
# 设置全局时区
time-zone: GMT+8
# 常用,全局设置pojo或被@JsonInclude注解的属性的序列化方式
default-property-inclusion: NON_NULL #不为空的属性才会序列化,具体属性可看JsonInclude.Include
# 常规默认,枚举类SerializationFeature中的枚举属性为key,值为boolean设置jackson序列化特性,具体key请看SerializationFeature源码
serialization:
WRITE_DATES_AS_TIMESTAMPS: true # 返回的java.util.date转换成timestamp
FAIL_ON_EMPTY_BEANS: true # 对象为空时是否报错,默认true
# 枚举类DeserializationFeature中的枚举属性为key,值为boolean设置jackson反序列化特性,具体key请看DeserializationFeature源码
deserialization:
# 常用,json中含pojo不存在属性时是否失败报错,默认true
FAIL_ON_UNKNOWN_PROPERTIES: false
# 枚举类MapperFeature中的枚举属性为key,值为boolean设置jackson ObjectMapper特性
# ObjectMapper在jackson中负责json的读写、json与pojo的互转、json tree的互转,具体特性请看MapperFeature,常规默认即可
mapper:
# 使用getter取代setter探测属性,如类中含getName()但不包含name属性与setName(),传输的vo json格式模板中依旧含name属性
USE_GETTERS_AS_SETTERS: true #默认false
# 枚举类JsonParser.Feature枚举类中的枚举属性为key,值为boolean设置jackson JsonParser特性
# JsonParser在jackson中负责json内容的读取,具体特性请看JsonParser.Feature,一般无需设置默认即可
parser:
ALLOW_SINGLE_QUOTES: true # 是否允许出现单引号,默认false
# 枚举类JsonGenerator.Feature枚举类中的枚举属性为key,值为boolean设置jackson JsonGenerator特性,一般无需设置默认即可
# JsonGenerator在jackson中负责编写json内容,具体特性请看JsonGenerator.Feature

本文转载自: 掘金

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

1…579580581…956

开发者博客

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