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

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


  • 首页

  • 归档

  • 搜索

在 Go 语言项目中使用 Docker

发表于 2020-05-31

原文链接:在 Go 语言项目中使用 Docker

容器(Container) 将程序及其所需的任何内容捆绑在一起,包括依赖项、工具和配置文件等等。这样使得程序不受环境的干扰,真正意义上做到开发环境和生成环境完全一致。

而 Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。与传统虚拟机相比,Docker 也有许多优点,如:更高效的系统资源利用和更快速的启动时间。

在本文中,通过一个简单的 Go 语言项目,您将学习如何在 Go 语言项目中使用 Docker。

Golang and Docker logo

Golang and Docker logo

创建一个简单的 Go 语言项目

让我们来创建一个作为示例的 Go 语言项目。在命令行下输入以下命令以创建文件夹:

1
复制代码mkdir gdp

我们将使用 Go Module 进行依赖性管理。转到项目的根目录,然后初始化 Go Module:

1
2
复制代码cd gdp
go mod init github.com/linehk/gdp

我们将创建一个简单的 hello 服务器。在项目根目录中创建一个名为 hello_server.go 的新文件:

1
复制代码touch hello_server.go

文件内容如下:

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

import (
"fmt"
"log"
"net/http"
"time"

"github.com/gorilla/mux"
)

func handler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name := query.Get("name")
if name == "" {
name = "Guest"
}
log.Printf("Received request for %s.\n", name)
w.Write([]byte(fmt.Sprintf("Hello, %s!\n", name)))
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/", handler)
server := &http.Server{
Handler: r,
Addr: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Println("Starting Server.")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

该项目使用 gorilla/mux 包来创建 HTTP 路由(导入包是为了展示 Docker 容器捆绑依赖项的作用),地址是 localhost:8080。

尝试在本地编译并运行项目

让我们先尝试在本地编译和运行项目。可以在项目根目录输入以下命令来编译项目:

1
复制代码go build

go build 命令将会生成一个名为 gdp 的可执行文件。可以像这样运行该文件:

1
2
复制代码./gdp
2020/08/19 21:33:49 Starting Server.

我们的 hello 服务器现在正在运行,可以尝试使用 curl 或其它工具与其交互:

1
2
复制代码curl http://localhost:8080
Hello, Guest!
1
2
复制代码curl http://localhost:8080?name=sulinehk
Hello, sulinehk!

编写 Dockerfile 定义 Docker 镜像

让我们来为这个项目编写 Dockerfile,在根目录创建文件名为 Dockerfile 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码# 拉取 Go 语言最新的基础镜像
FROM golang:latest

# 在容器内设置 /app 为当前工作目录
WORKDIR /app

# 把文件复制到当前工作目录
COPY . .

# 设置 GOPROXY 环境变量
ENV GOPROXY="https://goproxy.cn"

# 下载全部依赖项
RUN go mod download

# 编译项目
RUN go build -o gdp .

# 暴露 8080 端口
EXPOSE 8080

# 执行可执行文件
CMD ["./gdp"]

构建镜像和运行容器

镜像(Image) 是实际的软件分发包,其中包含运行应用程序所需的所有内容。

而容器根据镜像构建,是镜像的运行示例,类似 Go 语言中结构体定义和结构体变量之间的关系。

  • 构建镜像:
1
复制代码docker build -t gdp .
  • 运行容器:
1
2
复制代码docker run -d -p 8080:8080 gdp
aa6a1afbe1b13ad0b0d1d656e157f762c5fe2229a8e0d95a025df26396ffc08f
  • 与容器内运行的服务器交互:
1
2
复制代码curl http://localhost:8080
Hello, Guest!
1
2
复制代码curl http://localhost:8080?name=sulinehk
Hello, sulinehk!

下面是一些其它的 Docker 命令:

Docker 命令

Docker 命令

总结

可以看出,一个定义良好的 Dockfile 文件在整个流程中起到承上启下的作用。

Dockerfile

Dockerfile

参考链接

Docker — 从入门到实践

Docker Documentation

本文转载自: 掘金

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

大厂必知必会--G1收集器详解及调优

发表于 2020-05-31
前言

每周一次的例行发版,照常来临。我熟悉的打开发版系统,同时也打开了服务监控系统,好让别人看的出来我是一个对业务负责的小伙子。吨吨吨,吨吨吨,一顿操作…妈耶,这是咋回事,下游服务dubbo请求超时这么多,看了看我的劳力土,再看了看监控,一分钟400+的请求超时,请求超时数是原来的10倍,上升到了百的量级…

故事发生在这周二的深夜。当时准备上一个需求,会导致服务中单个线程的IO变长,这个是预知的。在灰度中,发现了下游的服务dubbo请求超时数增加,脑海中立马想到了去查了底层存储的监控,发现底层存储服务的999线都正常,凭借着多年的经验,脑海中一个声音告诉我一定是服务GC出问题了。

右边的请求超时个数是之前的10倍(涉及到公司业务保密,相关监控数据打码处理)
然后看了服务的GC,果然单次gc的时间比之前增加了不少。

当时立马找了下游的服务,告诉我这部分的超时是在范围允许之内的,心里终于松了一口气。于是继续搞其他的业务去了,虽然我故作淡定,其实当时心里已经慌的不行了。

这时候机智的你是不是说不行就扩容呗,但我是那么容易妥协的人吗?这不是考验我内功的时候吗?虽然我平时干着CRUD的活,但哪一个程序员不喜欢研究各种技术呢?这是所有程序员的浪漫!于是忙完了手头上的活之后开始了gc调优。

处理过程
  1. 将gc日志打印出来。可以在虚拟机的启动gc参数中增加-XX:+PrintGCDetails参数,将每次的GC的耗时信息打印出来。
  2. 分析日志,找到到底是哪些GC阶段导致了GC时间的上涨。

当时的GC日志:

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
复制代码%xwEx[GC pause (G1 Evacuation Pause) (young), 0.0962103 secs]
[Parallel Time: 23.3 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 146441.2, Avg: 146441.3, Max: 146441.3, Diff: 0.1]
[Ext Root Scanning (ms): Min: 1.5, Avg: 1.8, Max: 2.4, Diff: 1.0, Sum: 7.2]
[Update RS (ms): Min: 1.0, Avg: 1.5, Max: 1.7, Diff: 0.6, Sum: 5.9]
[Processed Buffers: Min: 27, Avg: 34.8, Max: 41, Diff: 14, Sum: 139]
[Scan RS (ms): Min: 0.3, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 1.3]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.5, Max: 1.1, Diff: 1.1, Sum: 1.9]
[Object Copy (ms): Min: 18.3, Avg: 19.0, Max: 19.6, Diff: 1.2, Sum: 76.1]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[GC Worker Total (ms): Min: 23.1, Avg: 23.2, Max: 23.2, Diff: 0.1, Sum: 92.7]
[GC Worker End (ms): Min: 146464.4, Avg: 146464.4, Max: 146464.5, Diff: 0.1]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.7 ms]
[Other: 72.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 69.9 ms]
[Ref Enq: 0.6 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.8 ms]
[Eden: 4824.0M(4824.0M)->0.0B(4820.0M) Survivors: 88.0M->92.0M Heap: 5044.1M(8192.0M)->224.1M(8192.0M)]
[Times: user=0.17 sys=0.00, real=0.09 secs]

当时GC中基本都是young GC类型的日志,因此不存在Mixed GC中对于old genaration回收所造成的gc时间增加。然后发现了Ref Proc这一过程消耗的比较久,然后迅速的想到了并行开启-XX:+ParallelRefProcEnabled,并且调大了并行标记的线程-XX:ConcGCThreads,以及增加了-XX:G1HeapRegionSize,一套下来行云流水,开始了枯燥的发版过程。果然一切都在我的预料之中:

可以看到,单次gc的时间比之前少了一半还不止,然后看下游服务的超时率:

搞定,回去睡觉!欢迎各位点个赞或者评论留言!
等等,我这就完了?当然不是,我的目的是想让大家看了这篇之后就知道以后如何去优化高并发服务的gc问题,我敢说即使很多在一线大厂中打拼了很多年的老开发,对gc参数调优这块也是一知半解,因此这部分的知识会了,可以很容易在面试以及在工作中脱颖而出!我会在下面的篇幅中循序渐进,由浅入深的讲解关于虚拟机的GC。

常见的收集器对比

目前大多数的公司开发都是基于jdk8,主流使用的GC收集器主要是G1,这里拿CMS和G1进行简单的对比,来突出G1这款收集器是多么的优秀!可能有的小公司使用的Parallel收集器,但是Parallel真的是一款毫无特色的收集器,用它跟G1对比简直差距太大了。这里只先做对比,相关细节后续在详解,这里只是突出为什么大多数公司选择G1的原因。

G1回收阶段
  1. 初始标记
  2. 并发标记
  3. 最终标记(并发)
  4. 筛选回收(并发)
CMS回收阶段
  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除
CMS和G1的相同部分
  1. 初始标记都需要停顿,即不能与用户线程并行,需要Stop The Wrold。都是标记能与GC Roots直接关联的对象(GC Roots的概念会在后续的虚拟机系列中详解,本篇中与GC无关的概念先不介绍)
  2. 并发标记可以与用户线程一起,即不需要停顿。并发标记主要是沿着GC Reference Chain进行对象可达性分析的过程。
CMS和G1的不同点
  1. CMS的并发回收是与用户线程一起进行的,不需要停顿。如果有垃圾是在标记过程后产生的,那么就会产生一个回收不彻底的问题。设想一下一边打扫房间,一边有人在那扔垃圾,那么最后的打扫效果是不是不好?而G1的筛选回收是需要暂停用户线程,但是是并发回收的,所以速度快,而且回收效率高。
  2. CMS的回收器不可以对young generation回收,只能对old generation回收。因此需要与其他收集器配合,比如ParNew或者Serial。而G1无论是young还是old都可以回收,人家可以独立管理整个java堆。而且回收算法是采用的标记整理(局部是复制清除算法,因为young generation中采用复制清除效率更高),不会产生内存碎片的问题。对比CMS只是可怜的标记清除,在一些高qps场景的服务中,内存碎片化很严重,很容易造成Full Gc!
  3. java堆在使用这两款回收器的时候,内存布局不同。在使用CMS收集器时,新生代和老年代是物理隔离的(为两个不同的连续的内存区域)。而用G1是对整个Heap划分成若干个不同的Region,新生代和老年代是相互交叉的内存区域,是逻辑上的分类。这样划分的目的是规避每次在对象进行可达性分析都要在堆的全区域中进行。比如一个Collection Set中的每个region都对应着一个Remembered set,G1可以维护这个Remembered Set,从中挑选出最具回收性价比的region进行回收,G1嘛,就是Grabage First,提高回收效率。

因此通过对比发现,从业务的角度考虑,G1优点不言而喻,分代收集,不容易产生内存碎片化,一次回收比较彻底,不容易Full gc,并发标记,停顿时间相对可控(基于停顿时间和region占用大小判断是否有价值回收)。

G1相关概念介绍

  • Region,可以理解为G1所管理的堆中的一个最小单位内存。具体堆中的Region个数是按照RegionSize进行划分,堆中的objects就是分布在一个个Region上,如果对象大小超过了RegionSize,那么该对象就分布在内存区域连续的不同Region上。
  • Young region和Old region,顾名思义,young generation的object都是分配在young region中,old generation的大部分object都是分配在old region中(当对象的大小超过了RegionSize的一半时,这些humongous objects就会被分配在humongous region中,这部分region也会被划分为old generation)。young generation包括Eden-region和Survivor-region,Eden-region就是所有新生成的对象分配的region;而Survivor-region就是标记过程中young generation存活的对象所存储的地方,Survivor-region中某些object可能会在后续的阶段被提升为old generation。

【图片引用自Oracle官网】
比如红色块的region表示Eden-region,带有S标记的红色块表示Survivor-region,二者都是属于young generation。浅蓝色的纯色块为old region,带有H标记的浅蓝色块表示humongous region,二者都是属于old generation。
那么可以这样来理解整个GC过程,对于young gc而言,将整个young geenration的对象进行可达性分析,如果发现对象可达,则标记该对象是存活,如果不可达,则标记对象为dead,然后将标记为存活的对象拷贝到survivor regions或者old regions中,将dead objects所处的regions回收。

对于Mixed Gc而言,除了会进行上述的young GC中的对于整个young generation进行标记以及拷贝之外,还会对部分old generation中的old regions进行回收,回收的算法采用的是标记–整理,即尽可能的释放出连续的内存空间范围。

G1回收阶段分类
  1. youg-only phase:将Young regions中的对象提升到Old,存到到Old regions中,然后回收young regions。
  2. space-reclamation phase:常说的Mixed GC中的一个阶段。不仅对young regions进行回收,也会筛选有价值的Old regions进行回收。

这两个阶段之间是可以转换的,当老年代的内存使用空间与总老年代空间的百分比超过了一定的阈值(后面篇幅中会讲解),不仅会对young generation进行回收,也会触发对老年代的回收。

说白了就是超过这个阈值之后,会先触发young-only phase阶段,然后young GC完了之后在进行space-reclamation phase阶段,对老年代进行回收;如果老年代的使用空间占总得老年代空间没有到达阈值,则不会触发space-reclamation phase,只会继续开启下一个youg-only phase,进行循环。

G1回收期间的停顿阶段
  • youg-only phase的停顿
1. initial Mark:标记GC Roots能直接关联到的对象,停顿时间很短。标记之后会进入Concurrent Marking(不需要停顿),并发标记作用是确定Old regions中的所有存活对象是否需要在接下来的`space-reclamation phase`保留。初始标记这一过程可能还没有跑完,另一个不包含初始标记的young gc过程可能就已经开始干活了。初始标记并不一定发生在所有的young gc中,只有老年代的使用空间占总老年代空间大小超过了InitiatingHeapOccupancyPercent,才会触发初始标记。
2. Remark:这一个停顿主要是为了完成标记的过程。因为上一个过程是并发标记,是跟随用户线程一起跑的,因此可能就会并发标记过后,一些对象随着用户线程的执行,可达性发生了变化,因此需要用户线程停下来,去清理一些浮动垃圾。这一过程主要处理`reference processing`和一些class的卸载,虽然停顿,但是可以有多个标记线程。
3. CleanUp:也需要停顿。主要进行region的回收,且对空的region也会进行回收。并且确定是否要接着进行`space-reclamation phase`阶段。如果需要进行`space-recalmation phase`,则继续进行,然后cleanUp部分young regions和old regions(有回收价值的old region)。如果确定不需要接着进行`space-reclamation phase`,则当前的cleanup阶段就相当于是`youg-only phase`的cleanUp,只是单纯的表示对young generation的cleanUp已经完成了。
  • space-reclamation phase的停顿

比如Colletions(可以理解可能需要被回收的region的集合,并且一个GC周期中可能有多个这样的集合)中的老年代object引用了新生代的object,那么新生代对象所处的region就会把老年代的region记录进新生代region维护的Remembered set中。因此space-reclamation phase阶段不仅会对新生代object的region进行GC,也会分析新生代region背后的Remembered set,挑选出回收性价比高的一些old regions进行回收。

【图片来自于oracle官网】
到此为止,我已经把我知道的G1的回收机制都告诉你了,如果你对关于G1回收的算法感兴趣,比如G1标记算法Snapshot-At-The-Beginning等想进一步了解,可以去oracle官网中查看,后续有时间,我也会出一期这样的文章。接下来我会针对不同的GC场景进行分析,以及给出相关的建议。坐稳了,发车!

GC实战优化环节

Full GC

这是G1(大多数收集器也同样)的一个兜底GC的阶段。原因主要是太多old generation的内存占用导致,比如程序中使用了某一个模板解析引擎,但是没有提取变量,导致每一次执行都是编译一个新的class,这种情况就很容易导致Full GC,或者有很多humongous objects的存在。如果程序中发生了Full gc,除了GC相关的调优,也需要多花时间去优化你的业务代码。

直白一点就是因为创建了太多的object,导致g1不能及时的去回收。常见的场景是Concurrent Marking没有及时complete。

因此Full GC的优化思路主要分为两个方面:

  1. 缩小Concurrent Marking的时间;
  2. 或者调大old gen区域。

Full GC调优Tips:

  1. 如果可能是大对象太多造成的,可以gc+heap=info查看humongous regions个数。可以增加通过-XX:G1HeapRegionSize增加Region Size,避免老年代中的大对象占用过多的内存。
  2. 增加heap大小,对应的效果G1可以有更多的时间去完成Concurrent Marking。
  3. 增加Concurrent Marking的线程,通过-XX:ConcGCThreads设置。
  4. 强制mark阶段提早进行。因为在Mark阶段之前,G1会根据应用程序之前的行为,去确定the Initiating Heap Occupancy Percent(IHOP)阈值大小,比如是否需要执行initial Mark,以及后续CleanUp阶段的space-reclamation phase;如果服务流量突然增加或者其他行为改变的话,那么基于之前的预测的阈值就会不准确,可以采取下面的思路:
1. 可以增加G1在`IHOP`分析过程中的所需要的内存空间,通过`-XX:G1ReservePercent`来设置,提高预测的效率。
2. 关闭G1的自动`IHOP`分析机制,`-XX:-G1UseAdaptiveIHOP`,然后手动的指定这个阈值大小,`-XX:InitiatingHeapOccupancyPercent`。这样就省去了每次预测的一个时间消耗。
  1. Full gc可能是系统中的humongous object比较多,系统找不到一块连续的regions区域来分配。可以通过-XX:G1HeapRegionSize增加region size,或者将整个heap调大。

Mixed GC或者Young GC调优

Reference Object Processing时间消耗比较久

gc日志中可以看Ref Proc和Ref Enq,Ref ProcG1根据不同引用类型对象的要求去更新对应的referents;Ref EnqG1如果实际引用对象已经不可达了,那么就会将这些引用对象加入对应的引用队列中。如果这一过程比较长,可以考虑将这个过程开启并行,通过-XX:+ParallelRefProcEnabled。

young-only回收较久

主要原因是Collection Set中有太多的存活对象需要拷贝。可以通过gc日志中的Evacuate Collection Set看到对应的时间,可以增加young geenration的最小大小,通过-XX:G1NewSizePercent。
也可能是某一个瞬间,幸存下来的对象一下子有很多,这种情况会造成gc停顿时间猛涨,一般应对这种情况通过-XX:G1MaxNewSizePercent这个参数,增加young generation最大空间。

Mixed回收时间较久

通过开启gc+ergo+cset=trace,如果是predicated young regions花费比较长,可以针对上文中的方法。如果是predicated old regions比较长,则可以通过以下方法:

  • 增加-XX:G1MixedGCCountTarget这个参数,将old generation的regions分散到较多的Collection(上文有解释)中,增加-XX:G1MixedGCCountTarget参数值。避免单次处理较大块的Collection。

那么现在回过头去看我之前调整的参数,是不是明白了我调整了之后,服务的GC效率立马提升了呢?其实调优的过程不是一蹴而就的,需要持续打磨,有了经验之后,你看到之后想到的东西永远比别人多!

手写辛苦,欢迎各位点个赞留言,也顺带恭喜FPX和TES携手共进决赛哈哈。

本文转载自: 掘金

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

突发!HashiCorp禁止在中国使用企业版VAULT软件

发表于 2020-05-30

前言

昨天HashiCorp突然发布一则消息,禁止在中国使用Vault软件的企业版本,官方申明是这样的:

HashiCorp的解释是因为中国的出口管制的原因导致无法出售HASHICORP软件或者使用企业版的Vault。所以在没有取得HashiCorp书面协议的前提下,不得在中国境内使用,部署和安装HashiCorp的Vault企业版本软件。

注意,这里只是禁止使用企业版本的Vault软件,个人版本和HashiCorp公司的其他软件并不在此限制之内。大家不要被网络上面的谣言所迷惑,一定要勇于探索真理。

HashiCorp公司介绍

那么这个影响到底对我们有多大呢?我们先看下HashiCorp公司的成长史。

更多精彩内容且看:

  • 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新
  • Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新
  • Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新
  • java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程

HashiCorp于2012年成立,由Mitchell Hashimoto和Armon Dadgar创办,并陆续推出了Vagrant、Packer 、 Terraform、Consul , Vault 和 Nomad以满足不同的需求。

HashiCorp专注于提供DevOps基础设施自动化工具,集开发、运营和安全性于一体,可以帮助开发者编写和部署应用程序,加速应用程序分发,助力企业提升开发效率。公司还推出了一个商业平台Atlas,为公共云服务供应商和私人云技术公司等提供支持。

HashiCorp于2014年获得了1000万美元A轮融资。并在最近,也就是2020-03-18月E轮融资获得了1.75亿美元。主要投资方包括:GGV纪源资本,红点投资,Mayfield Fund,IVP (Institutional Venture Partners)等知名机构。

HashiCorp采用开源的方式和云厂商合作,为云的使用提供了一套通用的工作流程。合作方包括2000多家上市公司。

在2019 胡润研究院发布《2019胡润全球独角兽榜》,HashiCorp排名第138位。

HashiCorp旗下的软件

HashiCor提供了一整套的技术服务,涵盖了云服务的每一层,帮助企业轻松在云环境中操作,每个产品都是为特定的云基础设置自动化来服务的。

区分下来,可以分为Provision,Secure,Connect和Run四个部分。

Provision

Provision的意思就是安装。

Terraform可以实现用代码的形式来安装cloud或者infrastructure。基础结构即代码,使用 Terraform 配置语言可以轻松跨整个工作流实现资源管理自动化。

基本上大部分的公有云都支持使用Terraform。

Secure

安装好基础组件之后,那么就需要保证他们使用的安全性。那么就需要用到Vault。也就是今天被禁止使用的Vault。

Vault是一款企业级私密信息管理工具。

在企业级应用开发过程中,我们每时每刻都在使用到私密信息,包括密码,密钥,token等等。那么如果在公司内部的开发者之间共享这些密码,密钥,token就是一个很实在的问题。

而Vault就是这样的一套统一的管理私密信息的接口。

难道被禁的原因是Vault的安全性协议?

Connect

安全性也保证了,那么接下来就是连接服务了。

Consul是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件。在国内有大量的使用案例。

Run

最后就是运行了,Nomad可以用来对容器进行管理和调度。从而更加快捷的部署和更加方便的管理线上资源。

总结

虽然目前被禁用的只是Vault的企业版本,但是还是让人感到深深的危机感,中国的企业什么时候能够做出世界级的软件平台,让我们拭目以待!

本文作者:flydean程序那些事

本文链接:www.flydean.com/hashicorp-t…

本文来源:flydean的博客

欢迎关注我的公众号:程序那些事,更多精彩等着您!

本文转载自: 掘金

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

阿里面试官:小伙子,给我说一下Spring 和 Spring

发表于 2020-05-29

前言

对于 Spring和 SpringBoot到底有什么区别,我听到了很多答案,刚开始迈入学习 SpringBoot的我当时也是一头雾水,随着经验的积累、我慢慢理解了这两个框架到底有什么区别,相信对于用了 SpringBoot很久的同学来说,还不是很理解 SpringBoot到底和 Spring有什么区别,看完文章中的比较,或许你有了不同的答案和看法!

什么是Spring

作为 Java开发人员,大家都 Spring都不陌生,简而言之, Spring框架为开发 Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:
SpringJDBC、SpringMVC、SpringSecurity、SpringAOP、SpringORM、SpringTest,这些模块缩短应用程序的开发时间,提高了应用开发的效率例如,在 JavaWeb开发的早期阶段,我们需要编写大量的代码来将记录插入到数据库中。但是通过使用 SpringJDBC模块的 JDBCTemplate,我们可以将操作简化为几行代码。

什么是Spring Boot

SpringBoot基本上是 Spring框架的扩展,它消除了设置 Spring应用程序所需的 XML配置,为更快,更高效的开发生态系统铺平了道路。

SpringBoot中的一些特征:

1、创建独立的 Spring应用。
2、嵌入式 Tomcat、 Jetty、 Undertow容器(无需部署war文件)。
3、提供的 starters 简化构建配置
4、尽可能自动配置 spring应用。
5、提供生产指标,例如指标、健壮检查和外部化配置
6、完全没有代码生成和 XML配置要求

从配置分析

Maven依赖

首先,让我们看一下使用Spring创建Web应用程序所需的最小依赖项

1
2
3
4
5
6
7
8
9
10
复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.0.RELEASE</version>
</dependency>

与Spring不同,Spring Boot只需要一个依赖项来启动和运行Web应用程序:

1
2
3
4
5
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId> spring-boot-starter-web</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>

在进行构建期间,所有其他依赖项将自动添加到项目中。

另一个很好的例子就是测试库。我们通常使用 SpringTest, JUnit, Hamcrest和 Mockito库。在 Spring项目中,我们应该将所有这些库添加为依赖项。但是在 SpringBoot中,我们只需要添加 spring-boot-starter-test依赖项来自动包含这些库。

Spring Boot为不同的Spring模块提供了许多依赖项。一些最常用的是:

spring-boot-starter-data-jpa spring-boot-starter-security spring-boot-starter-test spring-boot-starter-web spring-boot-starter-thymeleaf

有关 starter的完整列表,请查看Spring文档。

MVC配置

让我们来看一下 Spring和 SpringBoot创建 JSPWeb应用程序所需的配置。

Spring需要定义调度程序 servlet,映射和其他支持配置。我们可以使用 web.xml 文件或 Initializer类来完成此操作:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setConfigLocation("com.pingfangushi");
container.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic dispatcher = container.addServlet(
"dispatcher", new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}

还需要将 @EnableWebMvc注释添加到 @Configuration类,并定义一个视图解析器来解析从控制器返回的视图:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@EnableWebMvc
@Configuration
public class ClientWebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver bean = new InternalResourceViewResolver();
bean.setViewClass(JstlView.class);
bean.setPrefix("/WEB-INF/view/");
bean.setSuffix(".jsp");
return bean;
}
}

再来看 SpringBoot一旦我们添加了 Web启动程序, SpringBoot只需要在 application配置文件中配置几个属性来完成如上操作:

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
上面的所有Spring配置都是通过一个名为auto-configuration的过程添加 Bootweb starter来自动包含的。

这意味着 SpringBoot将查看应用程序中存在的依赖项,属性和 bean,并根据这些依赖项,对属性和 bean进行配置。当然,如果我们想要添加自己的自定义配置,那么 SpringBoot自动配置将会退回。

配置模板引擎

现在我们来看下如何在Spring和Spring Boot中配置Thymeleaf模板引擎。

在 Spring中,我们需要为视图解析器添加 thymeleaf-spring5依赖项和一些配置:

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
复制代码@Configuration
@EnableWebMvc
public class MvcWebConfig implements WebMvcConfigurer {
@Autowired
private ApplicationContext applicationContext;

@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(applicationContext);
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
return templateResolver;
}

@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
registry.viewResolver(resolver);
}
}

SpringBoot1X只需要 spring-boot-starter-thymeleaf的依赖项来启用 Web应用程序中的 Thymeleaf支持。   但是由于 Thymeleaf3.0中的新功能,我们必须将 thymeleaf-layout-dialect 添加为 SpringBoot2XWeb应用程序中的依赖项。配置好依赖,我们就可以将模板添加到 src/main/resources/templates文件夹中, SpringBoot将自动显示它们。

Spring Security 配置

为简单起见,我们使用框架默认的 HTTPBasic身份验证。让我们首先看一下使用 Spring启用 Security所需的依赖关系和配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfigurerAdapter extends
WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("admin")
.password(passwordEncoder().encode("password"))
.authorities("ROLE_ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

这里我们使用 inMemoryAuthentication来设置身份验证。同样, SpringBoot也需要这些依赖项才能使其工作。但是我们只需要定义 spring-boot-starter-security的依赖关系,因为这会自动将所有相关的依赖项添加到类路径中。

SpringBoot中的安全配置与上面的相同 。

应用程序启动引导配置

Spring和 SpringBoot中应用程序引导的基本区别在于 servlet。Spring使用 web.xml 或 SpringServletContainerInitializer作为其引导入口点。SpringBoot仅使用 Servlet3功能来引导应用程序,下面让我们详细来了解下

Spring 引导配置
Spring支持传统的 web.xml引导方式以及最新的 Servlet3+方法。

配置 web.xml方法启动的步骤

Servlet容器(服务器)读取 web.xml

web.xml中定义的 DispatcherServlet由容器实例化

DispatcherServlet通过读取 WEB-INF/{servletName}-servlet.xml来创建 WebApplicationContext。最后, DispatcherServlet注册在应用程序上下文中定义的 bean

使用 Servlet3+方法的 Spring启动步骤

容器搜索实现 ServletContainerInitializer的类并执行 SpringServletContainerInitializer找到实现所有类 WebApplicationInitializerWebApplicationInitializer创建具有XML或上下文 @Configuration类 WebApplicationInitializer创建 DispatcherServlet与先前创建的上下文。

SpringBoot 引导配置

Spring Boot应用程序的入口点是使用@SpringBootApplication注释的类

1
2
3
4
5
6
复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

默认情况下, SpringBoot使用嵌入式容器来运行应用程序。在这种情况下, SpringBoot使用 publicstaticvoidmain入口点来启动嵌入式 Web服务器。此外,它还负责将 Servlet, Filter和 ServletContextInitializerbean从应用程序上下文绑定到嵌入式 servlet容器。SpringBoot的另一个特性是它会自动扫描同一个包中的所有类或 Main类的子包中的组件。

SpringBoot提供了将其部署到外部容器的方式。我们只需要扩展 SpringBootServletInitializer即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* War部署
*/
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(Application.class);
}

@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
super.onStartup(servletContext);
servletContext.addListener(new HttpSessionEventPublisher());
}
}

这里外部 servlet容器查找在war包下的 META-INF文件夹下MANIFEST.MF文件中定义的 Main-class, SpringBootServletInitializer将负责绑定 Servlet, Filter和 ServletContextInitializer。

打包和部署

最后,让我们看看如何打包和部署应用程序。这两个框架都支持 Maven和 Gradle等通用包管理技术。但是在部署方面,这些框架差异很大。例如,Spring Boot Maven插件在 Maven中提供 SpringBoot支持。它还允许打包可执行 jar或 war包并 就地运行应用程序。

在部署环境中 SpringBoot 对比 Spring的一些优点包括:

1、提供嵌入式容器支持
2、使用命令_java -jar_独立运行jar
3、在外部容器中部署时,可以选择排除依赖关系以避免潜在的jar冲突
4、部署时灵活指定配置文件的选项
5、用于集成测试的随机端口生成

结论

简而言之,我们可以说 SpringBoot只是 Spring本身的扩展,这次的文章就到这里了,看完希望你们都能有收获!

本文转载自: 掘金

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

带你深入理解 Flutter 中的字体“冷”知识

发表于 2020-05-29

本篇将带你深入理解 Flutter 开发过程中关于字体和文本渲染的“冷”知识,帮助你理解和增加关于 Flutter 中字体绘制的“无用”知识点。

毕竟此类相关的内容太少了

首先从一个简单的文本显示开始,如下代码所示,运行后可以看到界面内出现了一个 H 字母,它的 fontSize 是 100,Text 被放在一个高度为 200 的 Container 中,然后如果这时候有人问你:Text 显示 **H 字母需要占据多大的高度,你知道吗?**

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
复制代码
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
alignment: Alignment.center,
child: new Row(
children: <Widget>[
Container(
child: new Text(
"H",
style: TextStyle(
fontSize: 100,
),
),
),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

一、TextStyle

如下代码所示,为了解答这个问题,首先我们给 Text 所在的 Container 增加了一个蓝色背景,并增加一个 100 * 100 大小的红色小方块做对比。

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
复制代码@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Colors.blue,
child: new Text(
"H",
style: TextStyle(
fontSize: 100,
),
),

),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

结果如下图所示,可以看到 H 字母的上下有着一定的 padding 区域,蓝色Container 的大小明显超过了 100 ,但是黑色的 H 字母本身并没有超过红色小方块,那蓝色区域的高度是不是 Text 的高度,它的大小又是如何组成的呢?

事实上,前面的蓝色区域是字体的行高,也就是 line height ,关于这个行高,首先需要解释的就是 TextStyle 中的 height 参数。

默认情况下 height 参数是 null,当我们把它设置为 1 之后,如下图所示,可以看到蓝色区域的高度和红色小方块对齐,变成了 100 的高度,也就是行高变成了 100 ,而 H 字母完整的显示在蓝色区域内。

那 height 是什么呢?根据文档可知,首先 TextStyle 中的 height 参数值在设置后,其效果值是 fontSize 的倍数:

  • 当 height 为空时,行高默认是使用字体的量度(这个量度后面会有解释);
  • 当 height 不是空时,行高为 height * fontSize 的大小;

如下图所示,蓝色区域和红色区域的对比就是 height 为 null 和 1 的对比高度。

另外上图的 BaseLine 也解释了:为什么 fontSize 为 100 的 H 字母,不是充满高度为 100 的蓝色区域。

根据上图的示意效果,在 height 为 1 的红色区域内,H 字母也应该是显示在基线之上,而基线的底部区域是为了如 g 和 j 等字母预留,所以如下图所示,在 Text 内加入 g 字母并打开 Flutter 调试的文本基线显示,由 Flutter 渲染的绿色基线也可以看到符合我们预期的效果。

忘记截图由 g 的了,脑补吧。

接着如下代码所示,当我们把 height 设置为 2 ,并且把上层的高度为 200 的 Container 添加一个紫色背景,结果如下图所示,可以看到蓝色块刚好充满紫色方块,因为 fontSize 为 100 的文本在 x2 之后恰好高度就是 200。

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
复制代码@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
color: Colors.purple,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Colors.blue,
child: new Text(
"Hg",
style: TextStyle(
fontSize: 100,
height: 2,
),
),

),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

不过这里的 Hg 是往下偏移的,为什么这样偏移在后面会介绍,还会有新的对比。

最后如下图所示,是官方提供的在不同 TextStyle 的 height 参数下, Text 所占高度的对比情况。

二、StrutStyle

那再回顾下前面所说的默认字体的量度,这个默认字体的量度又是如何组成的呢?这就不得不说到 StrutStyle 。

如下代码所示,在之前的代码中添加 StrutStyle :

  • 设置了 forceStrutHeight 为 true ,这是因为只有 forceStrutHeight 才能强制重置 Text 的 height 属性;
  • 设置了StrutStyle 的 height 设置为 1 ,这样 TextStyle 中的 height 等于 2 就没有了效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码  @override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
color: Colors.purple,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Colors.blue,
child: new Text(
"Hg",
style: TextStyle(
fontSize: 100,
height: 2,
),
strutStyle: StrutStyle(
forceStrutHeight: true,
fontSize: 100,
height: 1
),

),

),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

效果如下图所示,虽然 TextStyle 的 height 是 2 ,但是显示出现是以 StrutStyle 中 height 为 1 的效果为准。

然后查看文档对于 StrutStyle 中 height 的描述,可以看到:height 的效果依然是 fontSize 的倍数,但是不同的是这里的对 fontSize 进行了补充说明 : ascent + descent = fontSize,其中:

  • ascent 代表的是基线上方部分;
  • descent 代表的是基线的半部分
  • 其组合效果如下图所示:

Flutter 中 ascent 和 descent 是不能用代码单独设置。

除此之外,StrutStyle 的 fontSize 和 TextStyle 的 fontSize 作用并不一样:当我们把 StrutStyle 的 fontSize 设置为 50 ,而 TextStyle 的 fontSize 依然是 100 时,如下图所示,可以看到黑色的字体大小没有发生变化,而蓝色部分的大小变为了 50 的大小。

有人就要说那 StrutStyle 这样的 fontSize 有什么用?

这时候,如果在上面条件不变的情况下,把 Text 中的文本变成 "Hg\nHg" 这样的两行文本,可以看到换行后的文本重叠在了一起,所以 StrutStyle的 fontSize 也是会影响行高。

另外,在 StrutStyle 中还有另外一个参数也会影响行高,那就是 leading 。

如下图所示,加上了 leading 后才是 Flutter 中对字体行高完全的控制组合,leading 默认为 null ,同时它的效果也是 fontSize 的倍数,并且分布是上下均分。

所以如下代码所示,当 StrutStyle 的 fontSize 为 100 ,height 为 1,leading 为 1 时,可以看到 leading 的大小让蓝色区域变为了 200,从而 和紫色区域高度又重叠了,不同的对比之前的 Hg 在这次充满显示是居中。

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
复制代码
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
color: Colors.purple,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Colors.blue,
child: new Text(
"Hg",
style: TextStyle(
fontSize: 100,
height: 2,
),
strutStyle: StrutStyle(
forceStrutHeight: true,
fontSize: 100,
height: 1,
leading: 1
),

),

),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

因为 leading 是上下均分的,而 height 是根据 ascent 和 descent 的部分放大,明显 ascent 比 descent 大得多,所以前面的 TextStyle 的 height 为 2 时,充满后整体往下偏移。

三、backgroundColor

那么到这里应该对于 Flutter 中关于文本大小、度量和行高等有了基本的认知,接着再介绍一个属性:TextStyle 的 backgroundColor 。

介绍这个属性是为了和前面的内容产生一个对比,并且解除一些误解。

如下代码所示,可以看到 StrutStyle 的 fontSize 为 100 ,height 为 1,按照前面的介绍,蓝色的区域大小应该是和红色小方块一样大。

然后我们设置了 TextStyle 的 backgroundColor 为具有透明度的绿色,结果如下图所示,可以看到 backgroundColor 的区域超过了 StrutStyle,显示为默认情况下字体的度量。

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
复制代码
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
color: Colors.purple,
alignment: Alignment.center,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Colors.blue,
child: new Text(
"Hg",
style: TextStyle(
fontSize: 100,
backgroundColor: Colors.green.withAlpha(180)
),
strutStyle: StrutStyle(
forceStrutHeight: true,
fontSize: 100,
height: 1,
),

),

),
Container(
height: 100,
width: 100,
color: Colors.red,
)
],
),
)

),
),
);
}

这是不是很有意思,事实上也可以反应出,字体的度量其实一直都是默认的 ascent + descent = fontSize,我们可以改变 TextStyle 的 height 或者 StrutStyle 来改变行高效果,但是本质上的 fontSize 其实并没有变。

如果把输入内容换成 "H\ng" ,如下图所示可以看到更有意思的效果。

四、TextBaseline

最后再介绍一个属性 :TextStyle 的 TextBaseline,因为这个属性一直让人产生“误解”。

关于 TextBaseline 有两个属性,分别是 alphabetic 和 ideographic ,为了更方便解释他们的效果,如下代码所示,我们通过 CustomPaint 把不同的基线位置绘制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
复制代码
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Container(
height: 200,
width: 400,
color: Colors.purple,
child: CustomPaint(
painter: Text2Painter(),
),
)

),
),
);
}

class Text2Painter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
var baseLine = TextBaseline.alphabetic;
//var baseLine = TextBaseline.ideographic;

final textStyle =
TextStyle(color: Colors.white, fontSize: 100, textBaseline: baseLine);
final textSpan = TextSpan(
text: 'My文字',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);

final left = 0.0;
final top = 0.0;
final right = textPainter.width;
final bottom = textPainter.height;
final rect = Rect.fromLTRB(left, top, right, bottom);
final paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawRect(rect, paint);

// draw the baseline
final distanceToBaseline =
textPainter.computeDistanceToActualBaseline(baseLine);

canvas.drawLine(
Offset(0, distanceToBaseline),
Offset(textPainter.width, distanceToBaseline),
paint..color = Colors.blue..strokeWidth = 5,
);

// draw the text
final offset = Offset(0, 0);
textPainter.paint(canvas, offset);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

如下图所示,蓝色的线就是 baseLine,从效果可以直观看到不同 baseLine 下对齐的位置应该在哪里。

但是事实上 baseLine 的作用并不会直接影响 TextStyle 中文本的对齐方式,Flutter 中默认显示的文本只会通过 TextBaseline.alphabetic 对齐的,如下图所示官方人员也对这个问题有过描述 #47512。

这也是为什么要用 CustomPaint 展示的原因,因为用默认 Text 展示不出来。

举个典型的例子,如下代码所示,虽然在 Row 和 Text 上都是用了 ideographic ,但是其实并没有达到我们想要的效果。

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
复制代码 @override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
color: Colors.lime,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
mainAxisSize: MainAxisSize.max,
children: [
Text(
'我是中文',
style: TextStyle(
fontSize: 55,
textBaseline: TextBaseline.ideographic,
),
),
Spacer(),
Text('123y56',
style: TextStyle(
fontSize: 55,
textBaseline: TextBaseline.ideographic,
)),
])),
),
);
}

关键就算 Row 设置了 center ,这段文本看起来还是不是特别“对齐”。

自从,关于 Flutter 中的字体相关的“冷”知识介绍完了,不知道你“无用”的知识有没有增多呢?

本文转载自: 掘金

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

Docker 入门:镜像 image

发表于 2020-05-28

主要内容:

  • 什么是镜像
  • 下载镜像 pull
  • 设置下载加速源
  • 查看镜像
  • 上传镜像 push

📀 什么是镜像(image)

镜像是一个文件系统,提供了容器运行时需要用到的文件和参数配置。相当于平时在使用某个软件时需要下载的安装包,也相当于安装操作系统时需要用到 ISO 文件。

我们可以基于某一个镜像创建多个容器。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcde5ef3a1~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcde5ef3a1~tplv-t2oaga2asx-image.image

📥下载镜像

如果想运行某个 Docker 容器,可以直接从 Docker Hub 中下载对应的镜像,然后通过镜像创建容器就可以了。Docker Hub 类似于 GitHub,你可以把自己写好的镜像放到上面托管,需要用的时候再下载下来。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcf1682cfb~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcf1682cfb~tplv-t2oaga2asx-image.image

下载镜像需要执行 docker image pull 命令,是不是和 git 指令很像? 在 DockerHub 上,点击进入详情页,可以看到很多下载标签,可以根据标签下载指定的版本。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4646375~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4646375~tplv-t2oaga2asx-image.image

下载 utuntu 14.04, 也可以点击标签进入 Dockerfile 查看具体信息。

1
复制代码docker image pull utuntu:14.04

utuntu 的镜像就会下载下来

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4ab9d8a~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4ab9d8a~tplv-t2oaga2asx-image.image

下载一个镜像以后,可以通过 docker image ls 命令查看有哪些镜像。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4a5872d~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbce4a5872d~tplv-t2oaga2asx-image.image

然后通过镜像就可以创建容器运行了:

1
2
3
复制代码docker container run ubuntu:14.04  
# 或者通过 image id
docker container run 6e4f1fe62ff1

🚀设置国内源

国内下载官方源下载速度会比较慢,有时候等得让人想哭,所以最好设置一个国内源地址。

镜像加速地址提供几个参考,现在用的是 163 的:

  • registry.docker-cn.com
  • hub-mirror.c.163.com
  • 3laho3y3.mirror.aliyuncs.com
  • f1361db2.m.daocloud.io
  • mirror.ccs.tencentyun.com

在 docker toolbox 中配置国内源:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码# 登录 host    
$ docker-machine ssh default 

# 添加国内源地址
$ sudo sed -i "s|EXTRA_ARGS='|EXTRA_ARGS='--registry-mirror=[http://hub-mirror.c.163.com](http://hub-mirror.c.163.com/) |g" \
/var/lib/boot2docker/profile 

# 退出机器
$ exit 

# 重启 host
$ docker-machine restart default

或者修改 /etc/docker/daemon.json 文件(没验证):

1
2
3
4
复制代码# vi /etc/docker/daemon.json  
{
    "registry-mirrors": ["http://hub-mirror.c.163.com"]
}

🔍查看 image 详情

可以通过 inspect 指令查看 image 的详细信息。比如开放了哪些端口,设置了哪些环境变量,最终运行的是什么命令。

1
复制代码docker container inspect image_id

查看 image 的构建历史:

1
2
复制代码docker image history utuntu:14.04  
# 或者 docker image history image_id

每一行代表的是一个层级 layer, 可以看到 image 是如何构建的, missing 表示中间层,也就是在构建最终的 image 时产生的临时 image。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcf17f1fd8~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbcf17f1fd8~tplv-t2oaga2asx-image.image

也就是说,在制作成一个 image 之前,会有很多的步骤,而每做一个步骤,都会产生一层。最终会合并所有的中间步骤,得到会使用的 image。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbd0867f2b4~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbd0867f2b4~tplv-t2oaga2asx-image.image

📤上传 image

上传 image 首先需要给本地的 image 制作远程标签

1
2
3
复制代码docker image tag python:3.7-alpine looker53/python:3.7-alpine  
# 也可以用 image id
docker image tag e854017db514 looker53/python:3.7-alpine

这里表示把本地的 python 镜像,标签为 3.7-alpine 设置成 looker53 这个用户的 python 镜像,标签也叫 3.7-alpine,也可以设成其他名字。

登陆 looker53 这个账号:

1
复制代码docker login

然后通过 push 命令推送到远程仓库:

1
复制代码docker image push looker53/python:37alpine

接着,在 DockerHub 当中就可以看到推送的仓库了:

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbd0a154426~tplv-t2oaga2asx-image.image

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/28/1725bbbd0a154426~tplv-t2oaga2asx-image.image

本文转载自: 掘金

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

SpringBoot前后端分离项目,集成Spring Sec

发表于 2020-05-28

本文讲解使用SpringBoot版本:2.2.6.RELEASE,Spring Security版本:5.2.2.RELEASE

Java流行的安全框架有两种Apache Shiro和Spring Security,其中Shiro对于前后端分离项目不是很友好,最终选用了Spring Security。SpringBoot提供了官方的spring-boot-starter-security,能够方便的集成到SpringBoot项目中,但是企业级的使用上,还是需要稍微改造下,本文实现了如下功能:

  • 匿名用户访问无权限资源时的异常处理
  • 登录用户是否有权限访问资源
  • 基于redis的分布式session共享
  • session超时的处理
  • 限制同一账号同时登录最大用户数(顶号)
  • 登录成功和失败后返回json
  • 同时支持3种token存放位置:cookie,http header,request parameter

快速使用,引入依赖

1
2
3
4
5
6
7
8
9
10
复制代码<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring session redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

spring-boot-starter-security用于集成spring security,spring-session-data-redis集成了redis和spring-session。

定制化接入Spring Security

使用Spring Security为的就是写最少的代码,实现更多的功能,在定制化Spring Security,核心思路就是:重写某个功能,然后配置。

  • 比如你要查自己的用户表做登录,那就实现UserDetailsService接口;
  • 比如前后端分离项目,登录成功和失败后返回json,那就实现AuthenticationFailureHandler/AuthenticationSuccessHandler接口;
  • 比如扩展token存放位置,那就实现HttpSessionIdResolver接口;
  • 等等…

最后,将上述做的更改配置到security里。套路就是这个套路,下边咱们实战一下。

Don’t bb, show me code.

1. 处理匿名用户无权访问

实现AuthenticationEntryPoint接口,可以处理匿名用户访问无权限资源时的异常,如下:

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
复制代码@Slf4j
@Component
public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.warn("用户需要登录,访问[{}]失败,AuthenticationException={}", request.getRequestURI(), e);

ServletUtils.render(request, response, RestResponse.fail(ResponseCode.USER_NEED_LOGIN));
}
}

public class ServletUtils {

/**
* 渲染到客户端
*
* @param object 待渲染的实体类,会自动转为json
*/
public static void render(HttpServletRequest request, HttpServletResponse response, Object object) throws IOException {
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
response.setHeader("Content-type", "application/json;charset=UTF-8");

response.getWriter().print(JSONUtil.toJsonStr(object));
}
}

需要注意的是,当程序出现异常错误时(比如500),也会进入到commence方法中。

2. 基于数据库的用户登录认证逻辑

从数据库中查出登录用户的信息(如密码)、角色、权限等,然后返回一个UserDetails类型的实体,security会自动根据密码和用户相关状态(是否锁定、是否启停、是否过期等)判断用户登录成功或者失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
复制代码@Slf4j
@Component
public class DefaultUserDetailsService implements UserDetailsService {

@Autowired
private SystemService systemService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StrUtil.isBlank(username)) {
log.info("登录用户:{} 不存在", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}

// 查出密码
UserVO userVO = systemService.loadUserByUsername(username);
if (ObjectUtil.isNull(userVO) || StrUtil.isBlank(userVO.getUserId())) {
log.info("登录用户:{} 不存在", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
return new LoginUser(userVO, IpUtils.getIpAddr(ServletUtils.getRequest()), LocalDateTime.now(), LoginType.PASSWORD);
}

}

/**
* 扩展用户信息
*
* @author songyinyin
* @date 2020/3/14 下午 05:29
*/
@Data
public class LoginUser implements UserDetails, CredentialsContainer {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

/**
* 用户
*/
private UserVO user;

/**
* 登录ip
*/
private String loginIp;

/**
* 登录时间
*/
private LocalDateTime loginTime;

/**
* 登陆类型
*/
private LoginType loginType;

public LoginUser() {
}

public LoginUser(UserVO user, String loginIp, LocalDateTime loginTime, LoginType loginType) {
this.user = user;
this.loginIp = loginIp;
this.loginTime = loginTime;
this.loginType = loginType;
}

public LoginUser(UserVO user, String loginIp, LocalDateTime loginTime, String loginType) {
this.user = user;
this.loginIp = loginIp;
this.loginTime = loginTime;
this.loginType = LoginType.valueOf(loginType);
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getUserName();
}

/**
* 账户是否未过期,过期无法验证
*/
@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
* <p>
* 密码锁定
* </p>
*/
@Override
public boolean isAccountNonLocked() {
return ObjectUtil.equal(user.getPwdLockFlag(), LockFlag.UN_LOCKED);
}

/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 用户是否被启用或禁用。禁用的用户无法进行身份验证。
*/
@Override
public boolean isEnabled() {
return ObjectUtil.equal(user.getStopFlag(), StopFlag.ENABLE);
}

/**
* 认证完成后,擦除密码
*/
@Override
public void eraseCredentials() {
user.setPassword(null);
}
}

同时LoginUser还实现了CredentialsContainer接口,用户认证成功后,擦除密码,然后返给前端。

3. 登录成功的处理

登录成功后,一般要记录登录日志,然后把认证之后的用户authentication返给前端

1
2
3
4
5
6
7
8
9
10
11
复制代码@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

// TODO 登录成功 记录日志
ServletUtils.render(request, response, RestResponse.success(authentication));
}
}

4. 登录失败的处理

登录失败后,可以根据不同的AuthenticationException,来区分是为什么登录失败,这里需要有日志打印,然后根据业务需求,返回信息给前端。比如要求是无论什么错误,都返回登录失败,这里的示例是进行了登录失败的区分。

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
复制代码@Slf4j
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
RestResponse result;
String username = UserUtil.loginUsername(request);
if (e instanceof AccountExpiredException) {
// 账号过期
log.info("[登录失败] - 用户[{}]账号过期", username);
result = RestResponse.build(ResponseCode.USER_ACCOUNT_EXPIRED);

} else if (e instanceof BadCredentialsException) {
// 密码错误
log.info("[登录失败] - 用户[{}]密码错误", username);
result = RestResponse.build(ResponseCode.USER_PASSWORD_ERROR);

} else if (e instanceof CredentialsExpiredException) {
// 密码过期
log.info("[登录失败] - 用户[{}]密码过期", username);
result = RestResponse.build(ResponseCode.USER_PASSWORD_EXPIRED);

} else if (e instanceof DisabledException) {
// 用户被禁用
log.info("[登录失败] - 用户[{}]被禁用", username);
result = RestResponse.build(ResponseCode.USER_DISABLED);

} else if (e instanceof LockedException) {
// 用户被锁定
log.info("[登录失败] - 用户[{}]被锁定", username);
result = RestResponse.build(ResponseCode.USER_LOCKED);

} else if (e instanceof InternalAuthenticationServiceException) {
// 内部错误
log.error(String.format("[登录失败] - [%s]内部错误", username), e);
result = RestResponse.fail(ResponseCode.USER_LOGIN_FAIL);

} else {
// 其他错误
log.error(String.format("[登录失败] - [%s]其他错误", username), e);
result = RestResponse.fail(ResponseCode.USER_LOGIN_FAIL);
}
// TODO 登录失败 记录日志
ServletUtils.render(request, response, result);
}
}

5. 退出登录的回调

和登录成功、失败类似,记录日志,然后返回前端json。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Slf4j
@Component
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

// TODO 登出成功 记录登出日志
ServletUtils.render(request, response, RestResponse.success());
}
}

6. 登录超时的处理

用户登录后,当达到超时时间后(session过期),自动将用户退出登录

1
2
3
4
5
6
7
8
9
10
11
复制代码@Slf4j
@Component
public class InvalidSessionHandler implements InvalidSessionStrategy {

@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
log.info("用户登录超时,访问[{}]失败", request.getRequestURI());

ServletUtils.render(request, response, RestResponse.fail(ResponseCode.USER_LOGIN_TIMEOUT));
}
}

7. 同一账号同时登录的用户数受限的处理

比如某用户同时登陆的会话数,超过了系统的设置,大白话就是被顶号了,这时会由SessionInformationExpiredStrategy处理。
还有,在线用户被管理员提出后,也会触发。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Slf4j
@Component
public class SessionInformationExpiredHandler implements SessionInformationExpiredStrategy {

@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {

ServletUtils.render(sessionInformationExpiredEvent.getRequest(),
sessionInformationExpiredEvent.getResponse(), RestResponse.fail(ResponseCode.USER_MAX_LOGIN));
}
}

8. 自定义鉴权的实现

当用户登录后,怎么能判定用户是否有权限访问该资源呢?还记得咱们在**【2. 基于数据库的用户登录认证逻辑】**,从数据库中会把用户的权限角色查出来了,为咱们现在的鉴权提供的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码@Slf4j
@Service("ps")
public class PermissionService {

public boolean permission(String permission) {
LoginUser loginUser = UserUtil.loginUser();
for (String userPermission : loginUser.getUser().getPermissions()) {
if (permission.matches(userPermission)) {
return true;
}
}
if (log.isDebugEnabled()) {
log.debug("用户userId={}, userName={} 权限不足以访问[{}], 用户具有权限:{}, 访问", loginUser.getUser().getUserId(),
loginUser.getUsername(), permission, loginUser.getUser().getPermissions());
} else {
log.info("用户userId={}, userName={} 权限不足以访问[{}]", loginUser.getUser().getUserId(), loginUser.getUsername(), permission);
}
return false;
}
}

@RestController
public class UserController {

@Autowired
protected IUserService userService;

@GetMapping("/user/page")
@ApiOperation(value = "分页查询用户")
@PreAuthorize("@ps.permission('system:user:page')")
public TableResponse<UserVO> page() {
IPage<User> page = userService.getPage();

List<UserVO> userVOList = page.getRecords().stream().map(e -> {
UserVO userVO = new UserVO();
BeanUtils.copyPropertiesIgnoreNull(e, userVO);
return userVO;
}).collect(Collectors.toList());

return TableResponse.success(page.getTotal(), userVOList);
}
}

使用@PreAuthorize注解,即可保护应用的资源。不过,需要配置 @EnableGlobalMethodSecurity(prePostEnabled = true) 才能使@PreAuthorize生效

9. 登录用户没有权限访问的处理

用户虽然登录了,但是权限不够访问某些资源,这时候就需要AccessDeniedHandler来处理了

1
2
3
4
5
6
7
8
9
复制代码@Slf4j
@Component
public class LoginUserAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ServletUtils.render(request, response, RestResponse.build(ResponseCode.NO_AUTHENTICATION));
}
}

10. 自定义Session解析器

官方实现了Cookie和 Session的解析,在实际的项目中,还会遇到token拼接到URL上的情况,这时候可以HttpSessionIdResolver接口

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
复制代码/**
* 同时支持 sessionId 存到 cookie,header 和 request parameter
*
* @author songyinyin
* @date 2020/3/18 下午 05:53
*/
@Slf4j
@Service("httpSessionIdResolver")
public class RestHttpSessionIdResolver implements HttpSessionIdResolver {

public static final String AUTH_TOKEN = "GitsSessionID";

private String sessionIdName = AUTH_TOKEN;

private CookieHttpSessionIdResolver cookieHttpSessionIdResolver;

public RestHttpSessionIdResolver() {
initCookieHttpSessionIdResolver();
}

public RestHttpSessionIdResolver(String sessionIdName) {
this.sessionIdName = sessionIdName;
initCookieHttpSessionIdResolver();
}

public void initCookieHttpSessionIdResolver() {
this.cookieHttpSessionIdResolver = new CookieHttpSessionIdResolver();
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookieName(this.sessionIdName);
this.cookieHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}


@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
// cookie
List<String> cookies = cookieHttpSessionIdResolver.resolveSessionIds(request);
if (CollUtil.isNotEmpty(cookies)) {
return cookies;
}
// header
String headerValue = request.getHeader(this.sessionIdName);
if (StrUtil.isNotBlank(headerValue)) {
return Collections.singletonList(headerValue);
}
// request parameter
String sessionId = request.getParameter(this.sessionIdName);
return (sessionId != null) ? Collections.singletonList(sessionId) : Collections.emptyList();
}

@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
log.info(AUTH_TOKEN + "={}", sessionId);
response.setHeader(this.sessionIdName, sessionId);
this.cookieHttpSessionIdResolver.setSessionId(request, response, sessionId);
}

@Override
public void expireSession(HttpServletRequest request, HttpServletResponse response) {
response.setHeader(this.sessionIdName, "");
this.cookieHttpSessionIdResolver.setSessionId(request, response, "");
}
}

配置Spring Security

做了这么多的准备工作后,终于到了配置的时候了,Spring Security通过建造者模式,使得配置变得简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
复制代码@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private DefaultUserDetailsService userDetailsService;
/**
* 登出成功的处理
*/
@Autowired
private LoginFailureHandler loginFailureHandler;
/**
* 登录成功的处理
*/
@Autowired
private LoginSuccessHandler loginSuccessHandler;
/**
* 登出成功的处理
*/
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
/**
* 未登录的处理
*/
@Autowired
private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint;
/**
* 超时处理
*/
@Autowired
private InvalidSessionHandler invalidSessionHandler;
/**
* 顶号处理
*/
@Autowired
private SessionInformationExpiredHandler sessionInformationExpiredHandler;
/**
* 登录用户没有权限访问资源
*/
@Autowired
private LoginUserAccessDeniedHandler accessDeniedHandler;

/**
* 配置认证方式等
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}

/**
* http相关的配置,包括登入登出、异常处理、会话管理等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests()
// 放行接口
.antMatchers(GitsResourceServerConfiguration.AUTH_WHITELIST).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 异常处理(权限拒绝、登录失效等)
.and().exceptionHandling()
.authenticationEntryPoint(anonymousAuthenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
.accessDeniedHandler(accessDeniedHandler)//登录用户没有权限访问资源
// 登入
.and().formLogin().permitAll()//允许所有用户
.successHandler(loginSuccessHandler)//登录成功处理逻辑
.failureHandler(loginFailureHandler)//登录失败处理逻辑
// 登出
.and().logout().permitAll()//允许所有用户
.logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑
.deleteCookies(RestHttpSessionIdResolver.AUTH_TOKEN)
// 会话管理
.and().sessionManagement().invalidSessionStrategy(invalidSessionHandler) // 超时处理
.maximumSessions(1)//同一账号同时登录最大用户数
.expiredSessionStrategy(sessionInformationExpiredHandler) // 顶号处理
;

}

}

@EnableWebSecurity注解用来启用Spring Security,@EnableGlobalMethodSecurity(prePostEnabled = true)用来使@PreAuthorize生效。还有一部分细节写在代码的注释里了,这样看起来更方便直观点。

配置完成后,post请求ip:port/login,就可以看到登录的结果了,如下:

image.png

后记

到此,你应该能配置出较为完善的安全框架了,本文的所有代码都已经开源,并且经过了测试。

地址:gitee.com/songyinyin/…

按照本文的思路和步骤,你已经迈过了SpringSecurity最初的一步,它让你对整个Security框架有个大概的了解,当然,肯定会有一些疑问,比如为什么从头到尾没有看到登录的接口?登录的时候,怎么就跳到了UserDetailsService#loadUserByUsername()方法中的?

不妨留言说说你刚接触SpringSecurity时的疑惑


微信搜索「 读钓的YY 」,第一时间阅读优质原创好文。

原创不易,读到最后,请为本文点个赞吧,感谢万分。

本文转载自: 掘金

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

个人吐血系列-总结RocketMQ 大纲

发表于 2020-05-28

一般面试问消息队列,都是结合自己的项目进行回答的…最好有个项目有消息队列的中间件.本项目使用了RocketMQ

大纲

大纲

大纲

什么是消息队列?消息队列的主要作用是什么?

我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。

  • 异步处理:非核心流程异步化,提高系统响应性能
  • 应用解耦:
    • 系统不是强耦合,消息接受者可以随意增加,而不需要修改消息发送者的代码。消息发送者的成功不依赖消息接受者(比如有些银行接口不稳定,但调用方并不需要依赖这些接口)
    • 消息发送者的成功不依赖消息接受者(比如有些银行接口不稳定,但调用方并不需要依赖这些接口)
  • 最终一致性:最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。
    • 先写消息再操作,确保操作完成后再修改消息状态。定时任务补偿机制实现消息可靠发送接收、业务操作的可靠执行,要注意消息重复与幂等设计
    • 所有不保证100%不丢消息的消息队列,理论上无法实现最终一致性。
  • 广播:只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情
  • 流量削峰与监控:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。
  • 日志处理:将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题
  • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用于单纯的消息通讯,如实现点对点消息队列或者聊天室等。

推荐浅显易懂的讲解:

  • 消息队列基础
  • 面试官问你什么是消息队列?把这篇甩给他!

kafka、activemq、rabbitmq、rocketmq都有什么区别?

  • ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。
  • RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
  • RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的
  • kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。

MQ在高并发情况下,假设队列满了如何防止消息丢失?

  1. 生产者可以采用重试机制。因为消费者会不停的消费消息,可以重试将消息放入队列。
  2. 死信队列,可以理解为备胎(推荐)
    • 即在消息过期,队列满了,消息被拒绝的时候,都可以扔给死信队列。
    • 如果出现死信队列和普通队列都满的情况,此时考虑消费者消费能力不足,可以对消费者开多线程进行处理。

谈谈死信队列

死信队列用于处理无法被正常消费的消息,即死信消息。

当一条消息初次消费失败,消息队列 RocketMQ 版会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 版不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中,该特殊队列称为死信队列。

死信消息的特点:

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。

死信队列的特点:

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 版不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。

消息队列 RocketMQ 版控制台提供对死信消息的查询、导出和重发的功能。

消费者消费消息,如何保证MQ幂等性?

幂等性

消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息;

解决方案

  • MQ消费者的幂等行的解决一般使用全局ID 或者写个唯一标识比如时间戳 或者UUID 或者订单
  • 也可利用mq的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。
  • 给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

使用异步消息时如何保证数据的一致性

  1. 借助数据库的事务:使用异步消息怎么还能借助到数据库事务?这需要在数据库中创建一个本地消息表,这样可以通过一个事务来控制本地业务逻辑更新和本地消息表的写入在同一个事务中,一旦消息落库失败,则直接全部回滚。如果消息落库成功,后续就可以根据情况基于本地数据库中的消息数据对消息进行重投了。关于本地消息表和消息队列中状态如何保持一致,可以采用 2PC 的方式。在发消息之前落库,然后发消息,在得到同步结果或者消息回调的时候更新本地数据库表中消息状态。然后只需要通过定时轮询的方式对状态未已记录但是未发送的消息重新投递就行了。但是这种方案有个前提,就是要求消息的消费者做好幂等控制,这个其实异步消息的消费者一般都需要考虑的。
  2. 除了使用数据库以外,还可以使用 Redis 等缓存。这样就是无法利用关系型数据库自带的事务回滚了。

RockMQ不适用Zookeeper作为注册中心的原因,以及自制的NameServer优缺点?

  1. ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致。
  2. 另外,当有新的服务器加入时,NameServer 并不会立马通知到 Produer,而是由 Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决)
  3. 包括组件通信间使用 Netty 的自定义协议
  4. 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略)
  5. 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤)
  6. Broker 同步双写和异步双写中 Master 和 Slave 的交互

创作不易哇,觉得有帮助的话,给个小小的star呗。
github.com/DreamCats/J…

小Demo项目结合了RocketMQ消息队列:github.com/DreamCats/s…

本文转载自: 掘金

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

【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本

发表于 2020-05-27

buildSrc 的缺陷

Android 开发中统一不同 module 的依赖版本十分重要,传统的方式是使用 ext 的方式

ext

ext

之前我发过关于使用 buildSrc 简化项目中 gradle 代码的译文:什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin

该种方式可以很好的管理 gradle 的公共配置,这其中当然包括依赖版本

配置依赖

配置依赖

如图,在使用依赖时有代码提示,而且可以点击进入查看

但是由于 buildSrc 是对全局的所有 module 的配置,因此在构建速度上会慢一些。那么有没有一个更纯净的方式来配置依赖版本呢?

今天我们来介绍一种新的方式

自定义 plugin + includeBuild

使用 Gradle Composite builds 可以很容易解决这一问题

我们新建一个 module,命名为 version ,并将原来的 buildSrc 的代码转移过来

1
2
3
4
5
复制代码class DependencyVersionPlugin : Plugin<Project> {
override fun apply(project: Project) {

}
}

在 version 的 build.gradle 文件加入

1
2
3
4
5
6
7
8
复制代码gradlePlugin {
plugins {
version {
id = 'com.flywith24.version'
implementationClass = 'com.flywith24.version.DependencyVersionPlugin'
}
}
}

在 settings.gradle 加入 includeBuild("version") (重点)

1
2
3
4
5
复制代码includeBuild("version")

rootProject.name='VersionControlDemo'
include ':app'
include ':lib'

接下来在需要引用的 module 中引入该插件

1
2
3
复制代码plugins {
id "com.flywith24.version"
}

之后我们就可以使用了

Demo

demo代码截图

demo代码截图

demo代码截图

demo代码截图

demo 在这

往期文章

该系列主要介绍一些「骚操作」,它未必适合生产环境使用,但是是一些比较新颖的思路

  • 【奇技淫巧】AndroidStudio Nexus3.x搭建Maven私服遇到问题及解决方案
  • 【奇技淫巧】什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin
  • 【奇技淫巧】gradle依赖查找太麻烦?这个插件可能帮到你
  • 【奇技淫巧】Android组件化不使用 Router 如何实现组件间 activity 跳转
  • 【奇技淫巧】新的图片加载库?基于Kotlin协程的图片加载库——Coil
  • 【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化

我的其他系列文章 在这里

关于我

我是 Fly_with24

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

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

【译】 解密 RxJava 的异常处理机制

发表于 2020-05-26

前言

  • 原标题: Beyond Basic RxJava Error Handling
  • 原文地址: proandroiddev.com/beyond-basi…
  • 原文作者:Elye

今天看到一篇大神 Elye 关于 RxJava 异常的处理的文章,让我对 RxJava 异常的处理有了一个清晰的了解,用了 RxJava 很久了对里面的异常处理机制一直很懵懂。

通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案

  • just 和 fromCallable 区别?
  • 什么是 RxJavaPlugins.setErrorHandler?
  • Crashes 发生在 just() 中的处理方案?
  • Crashes 发生在 subscribe success 中的处理方案?
  • Crashes 发生在 subscribe error 中的处理方案?
  • Crashes 发生在 complete 中的处理方案?
  • Crashes 发生在 complete 之前的处理方案?

这篇文章涉及很多重要的知识点,请耐心读下去,应该可以从中学到很多技巧。

译文

大部分了解 RxJava 的人都会喜欢它,因为它能够封装 onError 回调上的错误处理,如下所示:

1
2
3
4
5
6
复制代码Single.just(getSomeData())
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) } // Expect all error capture
)

你可能会以为所有的 Crashes 都将调用 handleError 来处理,但其实并全都是这样的

Crashes 发生在 just() 中

我先来看一个简单的例子,假设 crashes 发生在 getSomeData() 方法内部

1
2
3
4
5
复制代码Single.just(getSomeData() /**🔥Crash🔥**/ )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) } // Crash NOT caught ⛔️

这个错误将不会在 handleError 中捕获,因为 just() 不是 RxJava 调用链的一部分,如果你想捕获它,你可能需要在最外层添加 try-catch 来处理,如下所示:

1
2
3
4
5
6
7
8
9
10
复制代码try { 
Single.just(getSomeData() /**🔥Crash🔥**/ )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) } // Crash NOT caught ⛔️
)
} catch (exception: Exception) {
handleError(exception) // Crash caught ✅
}

如果你不使用 just ,而是使用 RxJava 内部的一些东西,例如 fromCallable,错误将会被捕获

1
2
3
4
5
6
复制代码Single.fromCallable{ getSomeData() /**🔥Crash🔥**/ }
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) } // Crash caught ✅
)

Crashes 发生在 subscribe success 中

让我们来假设一下 Crashes 出现在 subscribe success 中,如下所示

1
2
3
4
5
6
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) /**🔥Crash🔥**/ },
{ error -> handleError(error) } // Crash NOT caught ⛔️
)

这个错误将不会被 handleError 捕获,奇怪的是,如果我们将 Single 换成 Observable,异常就会被捕获,如下所示:

1
2
3
4
5
6
7
复制代码Observable.just(getSomeData() )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) /**🔥Crash🔥**/ },
{ error -> handleError(error) }, // Crash caught ✅
{ handleCompletion() }
)

原因是在 Single 中成功的订阅被认为是一个完整的流。因此,错误不再能被捕获。而在 Observable 中,它认为 onNext 需要处理,因此 crash 仍然可以被捕获,那么我们应该如何解决这个问题呢

错误的处理方式,像之前一样,在最外层使用 try-catch 进行异常捕获

1
2
3
4
5
6
7
8
9
10
复制代码try { 
Single.just(getSomeData())
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result)/**🔥Crash🔥**/ },
{ error -> handleError(error) } // Crash NOT caught ⛔️
)
} catch (exception: Exception) {
handleError(exception) // Crash NOT caught ⛔️
}

但是这样做其实异常并没有被捕获,crash 依然在传递,因为 RxJava 在内部处理了 crash,并没有传递到外部

一种很奇怪的方式,在 subscribe successful 中,执行 try-catch

1
2
3
4
5
6
7
8
9
10
11
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.subscribe(
{ result -> try {
handleResult(result) /**🔥Crash🔥**/
} catch (exception: Exception) {
handleError(exception) // Crash caught ✅
}
},
{ error -> handleError(error) }, // Crash NOT caught ⛔️
)

这种方式虽然捕获住了这个异常,但是 RxJava 并不知道如何处理

一种比较好的方式

上文提到了使用 Single 在 subscribe successful 中不能捕获异常,因为被认为是一个完整的流,处理这个情况比较好的方式,可以使用 doOnSuccess 方法

1
2
3
4
5
6
7
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnSuccess { result -> handleResult(result) /*🔥Crash🔥*/ }, }
.subscribe(
{ /** REMOVE CODE **/ },
{ error -> handleError(error) } // Crash caught ✅
)

当我们按照上面方式处理的时候,错误将会被 onError 捕获,如果想让代码更好看,可以使用 doOnError 方法,如下所示:

1
2
3
4
5
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnSuccess { result -> handleResult(result) /*🔥Crash🔥*/ }, }
.doOnError { error -> handleError(error) } // Crash NOT stop ⛔️
.subscribe()

但是这并没有完全解决 crash 问题,虽然已经捕获了但并没有停止,因此 crash 仍然发生。

更准确的解释,它实际上确实捕获了 crash,但是 doOnError 不是完整状态,因此错误仍应该在 onError 中处理,否则它会在里面 crash,所以我们至少应该提供一个空的 onError

1
2
3
4
5
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnSuccess { result -> handleResult(result) /*🔥Crash🔥*/ }, }
.doOnError { error -> handleError(error) } // Crash NOT stop ⛔️
.subscribe({} {}) // But crash stop here ✅

Crashes 发生在 subscribe error 中

我们来思考一下如果 Crashes 发生在 subscribe error 中怎么处理,如下所示:

1
2
3
4
5
6
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) /**🔥Crash🔥**/ }
)

我们可以想到使用上文提到的方法来解决这个问题

1
2
3
4
5
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnSuccess { result -> handleResult(result) }, }
.doOnError { error -> handleError(error) /*🔥Crash🔥*/ }
.subscribe({} {}) // Crash stop here ✅

尽管这样可以避免 crash ,但是仍然很奇怪,因为没有在 crash 时做任何事情,我们可以按照下面的方式,在 onError 中捕获异常,这是一种非常有趣的编程方式。

1
2
3
4
5
复制代码Single.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnSuccess { result -> handleResult(result) }, }
.doOnError { error -> handleError(error) /*🔥Crash🔥*/ }
.subscribe({} { error -> handleError(error) }) // Crash caught ✅

不管怎么样这个方案是可行的,在这里只是展示如何处理,后面还会有很好的方式

Crashes 发生在 complete 中

例如 Observable 除了 onError 和 onNext(还有类似于 Single 的 onSuccess)之外,还有onComplete 状态。

如果 crashes 发生在如下所示的 onComplete 中,它将不会被捕获。

1
2
3
4
5
6
7
复制代码Observable.just(getSomeData() )
.map { item -> handleMap(item) }
.subscribe(
{ result -> handleResult(result) },
{ error -> handleError(error) }, // Crash NOT caught ⛔️
{ handleCompletion()/**🔥Crash🔥**/ }
)

我们可以按照之前的方法在 doOnComplete 方法中进行处理,如下所示:

1
2
3
4
5
6
复制代码Observable.just(getSomeData() )
.map { item -> handleMap(item) }
.doOnNext{ result -> handleResult(result) }
.doOnError{ error -> handleError(error) } Crash NOT stop ⛔️
.doOnComplete { handleCompletion()/**🔥Crash🔥**/ }
.subscribe({ }, { }, { }) // Crash STOP here ✅

最终 crash 能够在 doOnError 中捕获,并在我们提供的最后一个空 onError 函数处停止,但是我们通过这种解决方法逃避了问题。

Crashes 发生在 complete 之前

让我们看另一种有趣的情况,我们模拟一种情况,我们订阅的操作非常慢,无法轻易终止(如果终止,它将crash)

1
2
3
4
复制代码val disposeMe  = Observable.fromCallable { Thread.sleep(1000) }
.doOnError{ error -> handleError(error) } // Crash NOT caught ⛔️
.subscribe({}, {}, {}) // Crash NOT caught or stop ⛔️
Handler().postDelayed({ disposeMe.dispose() }, 100)

我们在 fromCallable 中等待 1000 才能完成,但是在100毫秒,我们通过调用 disposeMe.dispose() 终止操作。

这将迫使 Thread.sleep(1000)在结束之前终止,从而使其崩溃,无法通过 doOnError 或者提供的 onError 函数捕获崩溃

即使我们在最外面使用 try-catch,也是没用的,也无法像其他所有 RxJava 内部 crash 一样起作用。

1
2
3
4
5
6
7
8
复制代码try {
val disposeMe = Observable.fromCallable { Thread.sleep(1000) }
.doOnError{} // Crash NOT caught ⛔️
.subscribe({}, {}, {}) // Crash NOT caught or stop ⛔️
Handler().postDelayed({ disposeMe.dispose() }, 100)
} catch (exception: Exception) {
handleError(exception) // Crash NOT caught too ⛔️
}

RxJava Crash 终极解决方案

对于 RxJava 如果确实发生了 crash,但 crash 不在您的控制范围内,并且您希望采用一种全局的方式捕获它,可以用下面是解决方案。

1
复制代码RxJavaPlugins.setErrorHandler { e -> handleError(e) }

注册 ErrorHandler 它将捕获上述任何情况下的所有 RxJava 未捕获的错误( just() 除外,因为它不属于RxJava 调用链的一部分)

但是要注意用于调用错误处理的线程在 crash 发生的地方挂起,如果你想确保它总是发生在主UI线程上,用 runOnUiThread{ } 包括起来

1
2
3
复制代码RxJavaPlugins.setErrorHandler { e -> 
runOnUiThread { handleError(e))}
}

因此,对于上面的情况,由于在完成之前终止而导致 Crash,下面将对此进行处理。

1
2
3
4
5
复制代码RxJavaPlugins.setErrorHandler { e -> handle(e) } // Crash caught ✅
val disposeMe = Observable.fromCallable { Thread.sleep(1000) }
.doOnError{ error -> handleError(error) } // Crash NOT caught ⛔️
.subscribe({}, {}, {}) // Crash NOT caught or stop ⛔️
Handler().postDelayed({ disposeMe.dispose() }, 100)

有了这个解决方案,并不意味着注册 ErrorHandler 就是正确的方式

1
复制代码RxJavaPlugins.setErrorHandler { e -> handle(e) }

通过了解上面发生 Crash 处理方案,您就可以选择最有效的解决方案,多个方案配合一起使用,以更健壮地处理程序所发生的的 Crash。

译者思考

作者大概总了 5 种 RxJava 可能出现的异常的位置

  • Crashes 发生在 just() 中
  • Crashes 发生在 subscribe success 中
  • Crashes 发生在 subscribe error 中
  • Crashes 发生在 complete 中
  • Crashes 发生在 complete 之前

总的来说 RxJava 无法判断这些超出生命周期的、不可交付的异常中哪些应该或不应该导致应用程序崩溃,最后作者给出了,RxJava Crash 终极解决方案,注册 ErrorHandler

1
复制代码RxJavaPlugins.setErrorHandler { e -> handleError(e) }

它将捕获上述任何情况下的所有 RxJava 未捕获的错误,just() 除外,接下来我们来了解一下 RxJavaPlugins.setErrorHandler

关于 RxJavaPlugins.setErrorHandler

这是 RxJava2.x 的一个重要设计,以下几种类型的错误 RxJava 是无法捕获的:

  • 由于下游的生命周期已经达到其终端状态导致的异常
  • 下游取消了将要发出错误的序列而无法发出的错误
  • 发生了 crash ,但是 crash 不在您的控制范围内
  • 一些第三方库的代码在被取消 或者 调用中断时会抛出该异常,这通常会导致无法交付的异常。

RxJava 无法判断这些超出生命周期的、不可交付的异常中哪些应该或不应该导致应用程序崩溃。

这些无法捕获的错误,最后会发送到 RxJavaPlugins.onError 处理程序中。这个处理程序可以用方法RxJavaPlugins.setErrorHandler() 重写,RxJava 默认情况下会将 Throwable 的 stacktrace 打印到控制台,并调用当前线程的未捕获异常处理程序。

所以我们可以采用一种全局的处理方式,注册一个 RxJavaPlugins.setErrorHandler() ,添加一个非空的全局错误处理,下面的示例演示了上面列出来的几种无法交付的异常。

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
复制代码RxJavaPlugins.setErrorHandler(e -> {
if (e instanceof UndeliverableException) {
e = e.getCause();
}
if ((e instanceof IOException) || (e instanceof SocketException)) {
// fine, irrelevant network problem or API that throws on cancellation
return;
}
if (e instanceof InterruptedException) {
// fine, some blocking code was interrupted by a dispose call
return;
}
if ((e instanceof NullPointerException) || (e instanceof IllegalArgumentException)) {
// that's likely a bug in the application
Thread.currentThread().getUncaughtExceptionHandler()
.handleException(Thread.currentThread(), e);
return;
}
if (e instanceof IllegalStateException) {
// that's a bug in RxJava or in a custom operator
Thread.currentThread().getUncaughtExceptionHandler()
.handleException(Thread.currentThread(), e);
return;
}
Log.warning("Undeliverable exception received, not sure what to do", e);
});

我相信到这里关于 RxJava Crash 处理方案,应该了解的很清楚了,选择最有效的解决方案,多个方案配合一起使用,可以更健壮地处理程序所发生的的 Crash。

接下来我们来了解一下 just 和 fromCallable 区别 ,作者在另外一篇文章 just-vs-fromcallable 做了详细的介绍

just 和 fromCallable 区别

1. 值获取来源不一样

just 值从外部获取的,而 fromCallable 值来自于内部生成,为了更清楚的了解,我们来看一下下面的代码:

1
2
3
4
5
6
7
8
9
复制代码println("From Just")
val justSingle = Single.just(Random.nextInt())
justSingle.subscribe{ it -> println(it) }
justSingle.subscribe{ it -> println(it) }

println("\nFrom Callable")
val callableSingle = Single.fromCallable { Random.nextInt() }
callableSingle.subscribe{ it -> println(it) }
callableSingle.subscribe{ it -> println(it) }

对于 Just 和 fromCallable 分别调用 2 次 subscribe 执行结果如下所示:

1
2
3
4
5
6
7
复制代码From Just
801570614
801570614

From Callable
1251601849
2033593269

你会发现对于 just 无论 subscribe 多少次,生成的随机值都保持不变,因为该值是从 Observable 外部生成的,而 Observable 只是将其存储供以后使用。

但是对于 fromCallable 它是从 Observable 内部生成的,每次 subscribe 都会都会生成一个新的随机数。

2. 立即执行和延迟执行

  • just 在调用 subscribe 方法之前值已经生成了,属于立即执行。
  • 而 fromCallable 是调用 subscribe 方法之后执行的,属于延迟执行。

为了更清楚的了解,我们来看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码fun main() {
println("From Just")
val justSingle = Single.just(getRandomMessage())
println("start subscribing")
justSingle.subscribe{ it -> println(it) }

println("\nFrom Callable")
val callableSingle = Single.fromCallable { getRandomMessage() }
println("start subscribing")
callableSingle.subscribe{ it -> println(it) }
}

fun getRandomMessage(): Int {
println("-Generating-")
return Random.nextInt()
}

结果如下所示:

1
2
3
4
5
6
7
8
9
复制代码From Just
-Generating-
start subscribing
1778320787

From Callable
start subscribing
-Generating-
1729786515

对于 just 在调用 subscribe 之前打印了 -Generating-,而 fromCallable 是在调用 subscribe 之后才打印 -Generating-。

到这里文章就结束了,我们来一起探讨一个问题, 在 Java 时代 RxJava 确实帮助我们解决了很多问题,但是相对而言不好的地方 RxJava 里面各种操作符,理解起来确实很费劲,随着 Google 将 Kotlin 作为 Android 首选语言,那么 RxKotlin,有能给我们带来哪些好处呢?

参考文献

  • Beyond Basic RxJava Error Handling
  • RxJava 2 : Understanding Hot vs. Cold with just vs. fromCallable
  • What’s different in 2.0

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,请持续关注,除了翻译还有对每篇欧美文章思考,如果对你有帮助,请帮我点个赞,感谢!!!期待与你一起成长。

文章列表

Android 10 源码系列

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构

Android 应用系列

  • 如何高效获取视频截图
  • 如何在项目中封装 Kotlin + Android Databinding
  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

1…809810811…956

开发者博客

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