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

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


  • 首页

  • 归档

  • 搜索

Gradle真能干掉Maven?今天体验了一把,贼爽!

发表于 2021-04-13

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

作为Java Web开发,很多朋友都在使用Maven作为构建工具。Gradle作为Google大力拥护的构建工具,被广泛地运用到了Android开发中,在Java Web方面也大有取代Maven上位的趋势。Gradle真的有那么香么?今天我们来体验一把,以我的脚手架项目mall-tiny为例,看看Gradle到底行不行!

Gradle简介

Gradle是一款开源的自动化构建工具,使用灵活且性能极佳,可以使用 Groovy 或者 Kotlin DSL 来编写构建脚本。从移动开发到微服务,从小团队到大企业,Gradle提高了开发人员的生产力。

Gradle具有如下特性:

  • 可以高度定制:Gradle使用可定制、可扩展的方式进行建模,非常灵活。
  • 构建速度快:Gradle通过重用先前执行的输出,仅处理已更改的输入且通过并行执行任务来快速完成构建。
  • 功能强大:Gradle是Android的官方构建工具,并支持许多流行的语言和技术。

Gradle使用体验

接下来将把我的脚手架项目mall-tiny从使用Maven改造成使用Gradle,来体验一把Gradle的使用。

创建Gradle项目

  • 首先需要下载Gradle的安装包,建议下载带源码的完整版本(否则Gradle中属性点进去不会有注释)下载地址:gradle.org/releases/

  • 下载完成后进行解压,之后在IDEA中创建一个SpringBoot项目;

  • 选择创建一个Gradle项目;

  • 之后选择使用我们之前下载好的Gradle版本,输入你解压的目录;

  • 项目创建完成后,一个非常简单的Gradle项目目录结构如下,需要注意的是build.gradle和settings.gradle这两个文件。

Gradle插件介绍

在新创建的build.gradle文件中,我们可以发现下面3个插件:

1
2
3
4
5
groovy复制代码plugins {
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}

org.springframework.boot

SpringBoot官方提供的Gradle插件,方便我们使用SpringBoot,通过修改version可以控制使用的SpringBoot版本。

io.spring.dependency-management

一个可以提供依赖版本管理功能的Gradle插件(类似于Maven)。

比如之前我们使用Maven管理Druid版本时,会先在<dependencyManagement>中定义好依赖的版本。

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependencyManagement>
<dependencies>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
</dependencyManagement>

然后在引入依赖的时候就无需再填写版本号了,这样做的好处就是可以统一依赖的版本。

1
2
3
4
5
6
7
xml复制代码<dependencies>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
</dependencies>

在Gradle中你可以这样用,是不是简洁不少!

1
2
3
4
5
6
7
8
9
groovy复制代码dependencies {
implementation 'com.alibaba:druid-spring-boot-starter'
}

dependencyManagement {
dependencies {
dependency 'com.alibaba:druid-spring-boot-starter:1.1.10'
}
}

java

Java插件将Java编译、测试等常用功能添加到项目中,它是许多其他JVM语言Gradle插件的基础。

Maven转Gradle

Maven项目转Gradle非常简单,只需要把pom.xml中的依赖转为build.gradle中的依赖即可。

  • 比如说Hutool这个依赖,Maven中的写法是这样的:
1
2
3
4
5
xml复制代码<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
  • Gradle中的写法是这样的,一行即可搞定:
1
2
3
groovy复制代码dependencies {
implementation 'cn.hutool:hutool-all:4.5.7'
}
  • 有时候Gradle下载依赖比较慢,这里将url修改为阿里云的Maven仓库地址可以加速;
1
2
3
4
groovy复制代码repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
mavenCentral()
}
  • 再来个完整的build.gradle,已经添加所有依赖;
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
groovy复制代码plugins {
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}

group = 'com.macro.mall.tiny'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'com.alibaba:druid-spring-boot-starter'
implementation 'mysql:mysql-connector-java'
implementation 'io.springfox:springfox-swagger2'
implementation 'io.springfox:springfox-swagger-ui'
implementation 'io.swagger:swagger-models'
implementation 'io.swagger:swagger-annotations'
implementation 'cn.hutool:hutool-all'
implementation 'io.jsonwebtoken:jjwt'
implementation 'com.baomidou:mybatis-plus-boot-starter'
implementation 'com.baomidou:mybatis-plus-generator'
implementation 'org.apache.velocity:velocity-engine-core'
}

dependencyManagement {
dependencies {
dependency 'com.alibaba:druid-spring-boot-starter:1.1.10'
dependency 'mysql:mysql-connector-java:8.0.16'
dependency 'io.springfox:springfox-swagger2:2.9.2'
dependency 'io.springfox:springfox-swagger-ui:2.9.2'
dependency 'io.swagger:swagger-models:1.6.0'
dependency 'io.swagger:swagger-annotations:1.6.0'
dependency 'cn.hutool:hutool-all:4.5.7'
dependency 'io.jsonwebtoken:jjwt:0.9.0'
dependency 'com.baomidou:mybatis-plus-boot-starter:3.3.2'
dependency 'com.baomidou:mybatis-plus-generator:3.3.2'
dependency 'org.apache.velocity:velocity-engine-core:2.2'
}
}

test {
useJUnitPlatform()
}
  • 最后你需要做的就是把原来的代码都复制过来就行了,至此Gradle改造完成。

对比Maven

都说Gradle构建速度快,官方自己也在说,我们将项目clean以后构建下试试,看看到底有多快!

  • 首先使用之前的Maven项目,直接clean之后再package,打包构建下;

  • 控制台输出如下,耗时32s;

  • 再使用现在的Gradle项目,也是clean之后再package(Gradle中使用bootjar命令),打包构建下;

  • 控制台输出如下,耗时15s,快了不止一倍!

  • 再放张官方的对比图,Gradle构建比Maven快1倍,那是妥妥的!

总结

Gradle作为Google官方推荐的构建工具,确实很不错!如果你会写Groovy脚本的话,使用起来是非常灵活的,而且语法简洁,构建速度也很快!

参考资料

Gradle官方文档:docs.gradle.org

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

希尔排序听起来有点难,其实很简单

发表于 2021-04-13

前言

直接插入排序当待排序数据的顺序和期望排序结果相反时,排序效率是最差的;上次聊到的折半插入排序只是减少有序列表的比较次数,而对于整体数据遍历次数还是没有得到优化;接下来要说的希尔排序就是针对整体数据进行优化,从而提升排序效率。

正文

1.1 希尔排序算法思想

**希尔排序(Shell’s Sort)**是直接插入排序算法的改进版,又称“缩小增量排序”(Diminishing Increment Sort);

算法思想

将待排序数据根据指定步长进行分组,分别进行直接插入排序;减小步长,重复分组,重复直接插入排序,直到步长为1时进行最后一次插入排序。

对于第一次步长可以根据需要自定义,但一般推荐会设置为元素个数除以2(lenght/2),后续的步长依次是上一次步长除以2(stepk=stepk-1/2),直到步长为1,如下图:

image-20210407235130599

上面说到步长可以理解为增量,而减少步长的过程,也就是缩小增量,即希尔排序又称为缩小增量排序。

分组原理(第一次分组的step1=6/2=3):

  • 第一组:0索引位的元素为2,0+step1的索引位为3,对应的元素是1,3+step1越界了,则第一组的元素为2、1;
  • 第二组:1索引位的元素为5,1+step1的索引位为4,对应的元素是9,4+step1越界了,则第二组的元素为5、9;
  • 第三组:2索引位的元素为6,2+step1的索引位为5,对应的元素是3,5+step1越界了,则第三组的元素为6、3;此时元素就分组完成了;

接下来的分组就依次递减步长,即上一次步长除以2取整; 然后根据新算出来的步长继续将上一次的排序的结果分组即可;直到步长递减到为1时,整体进行最后一次直接插入排序为止;

1.2 希尔排序算法实现与解析

代码实现(升序):

代码

运行结果如下:

结果

步骤解析:

步骤

上图步骤说明:

  • 将原始数据array复制到新数组中arrayb中,这步的主要目的是后续不需要声明额外临时变量,也为了后续核心代码实现逻辑简单易懂,减少过多的判断;0索引位也充当为哨兵位;
  • 第一步根据元素个数算出第一次步长step1=3,根据步长将待排序数据进行虚拟分组,索引位为1的元素和索引位为1+step的元素为一组,索引位为2的元素和索引位为2+step的元素为一组,索引位为3的元素和索引位为3+step的元素为一组;则将待排序数据分为2、1;5、9;6、3 三组;
  • 第二步开始遍历每一组数据,针对每一组数据进行直接插入排序; 首先是第一组数据2、1,将待排序数据1放入哨兵位(即0索引位),哨兵位的数据1和有序列表中的2进行比较,2大于1,则需要腾出空位,所以2移到分组中索引位为4的位置;然后将哨兵位的数据1插入到腾出的空位中;
  • 第三步遍历第二组数据5、9,首先将待排序数据9放入哨兵位(即0索引位),哨兵位的数据9和有序列表中的5进行比较,5小于9,则不需改变位置;
  • 第四步遍历第三组数据6、3,首先将待排序数据3放入哨兵位(即0索引位),哨兵位的数据3和有序列表中的6进行比较,6大于3,则需要腾出空位,所以6移到分组中索引位为6的位置;然后将哨兵位的数据3插入到腾出的空位中;
  • 分组排序完成之后,最终得出第一次分组排序结果:

第一次分组排序完成之后,调整步长,继续进行分组,由于第二次计算出的步长step2=step1/2=1,即将所有上一次分组的数据全部为一组进行最后一次直接插入排序即可;这里就不在重复演示了,具体步骤和之前说到的直接插入排序一样,参照这篇大牛领导单独找我聊了两句:搞框架的同时别忘了算法。

通过第二次插入排序完成之后就得到最后的排序结果啦。

1.3 希尔排序算法分析

时间复杂度

时间复杂度最坏情况和直接插入排序的时间复杂一样,都是O(n2),但有其他大神经过大量演示,希尔排序的时间复杂度一般为O(n(1.3~2)),比O(n2)性能好。

空间复杂度
在算法核心部分只采用了固定的几个中间变量((i,j,step,arrayb[0])),所以算法过程中消耗的内存是一个常量,则空间复杂度为O(1);

稳定性
由于在排序过程中是根据步长将原始数据进行分组,这样就可能会导致相同的元素分到不同组,在最终排序时就不能保证原来两个相同元素的顺序啦,所以希尔排序是不稳定的。

综上所述,希尔排序的时间复杂度为O(n2),空间复杂度为O(1),是不稳定算法;

总结

到这里,插入排序的三种排序介绍完毕,下期开始介绍交换排序;这里先总结一下插入排序的相关关键点(下图绿色部分);如下:

总结

感谢小伙伴的:点赞、收藏和评论,下期继续**

1
2
3


一个被程序搞丑的帅小伙,关注"Code综艺圈",跟我一起学

本文转载自: 掘金

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

Go 实现 AWS4 请求认证 Go主题月

发表于 2021-04-12

什么是 Amazon S3?它是 AWS 提供的一种对象存储服务,提供行业领先的可扩展性、数据可用性、安全性和性能。Amazon S3 可达到 99.999999999%(11 个 9)的持久性。

Amazon S3 里用到了 AWS Signature Version 4(下面简称 AWS4)做认证请求,这篇文章将会讲解如何使用 Go 实现 AWS4 请求认证。

AWS4 是一种用于对所有 AWS 区域服务的入站 API 请求进行身份验证的协议。

AWS4

AWS4 对请求进行签名有以下优势(但这也取决于你如何使用):

  • 验证请求者的身份 - 经过身份验证的请求需要使用 AccessKeyID 和 SecretAccessKey 创建签名。
  • 保护传输中的数据 - 为了防止在传输过程中对请求进行篡改,可以使用一些请求元素(比如请求路径、请求头等)来计算请求签名。Amazon S3 在收到请求后,使用相同的请求元素来计算签名。如果 Amazon S3 接收到的任何请求组件与用于计算签名的组件不匹配,Amazon S3 将拒绝该请求。
  • 防止重用请求的签名部分 - 请求的签名部分在请求中的时间戳的一段时间内有效。

授权方式

  • HTTP 身份验证头,例如 Authorization 请求头:
1
2
3
4
ini复制代码Authorization: AWS4-HMAC-SHA256 
Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
SignedHeaders=host;range;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
  • URL 查询字符串参数,例如预签名 URL:
1
2
3
4
5
6
7
ini复制代码https://s3.amazonaws.com/examplebucket/test.txt
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=<your-access-key-id>/20130721/us-east-1/s3/aws4_request
&X-Amz-Date=20130721T201207Z
&X-Amz-Expires=86400
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<signature-value>

Go 实现 HTTP 身份验证头

HTTP 身份验证头组成部分

  • AWS4-HMAC-SHA256 - 该字符串指定 AWS4 和签名算法(HMAC-SHA256)。
  • Credential - 指定 AccessKeyID、计算签名的日期、区域和服务。它的格式是 <your-access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request,date 的格式是 YYYYMMDD。
  • SignedHeaders - 指定用于计算签名的请求头列表,以分号分隔。仅包含请求头的名称,且必须是小写,例如:host;range;x-amz-date。
  • Signature - 表示为 64 个小写十六进制字符的 256 位签名,例如:fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024。

上传图片至掘金

根据对 AWS4 请求认证的学习,并参考 simples3 的代码实现了 上传图片至掘金(字节跳动的存储服务,其服务名称是 imagex)的功能,下面是部分的代码实现(主要省略了 Client 部分的实现,完整代码有待后续完善后进行开源):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
go复制代码package juejin

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"

"github.com/tidwall/gjson"
)

const (
amzDateISO8601TimeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
algorithm = "AWS4-HMAC-SHA256"
serviceName = "imagex"
serviceID = "k3u1fbpfcp"
version = "2018-08-01"
uploadURLFormat = "https://%s/%s"

RegionCNNorth = "cn-north-1"

actionApplyImageUpload = "ApplyImageUpload"
actionCommitImageUpload = "CommitImageUpload"

polynomialCRC32 = 0xEDB88320
)

var (
newLine = []byte{'\n'}

// if object matches reserved string, no need to encode them
reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$")
)

type ImageX struct {
AccessKey string
SecretKey string
Region string
Client *http.Client

Token string
Version string
BaseURL string
}

type UploadToken struct {
AccessKeyID string `json:"AccessKeyID"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
}

func (c *Client) UploadImage(region, imgPath string) (string, error) {
uploadToken, err := c.GetUploadToken()
if err != nil {
return "", err
}

ix := &ImageX{
AccessKey: uploadToken.AccessKeyID,
SecretKey: uploadToken.SecretAccessKey,
Token: uploadToken.SessionToken,
Region: region,
}

applyRes, err := ix.ApplyImageUpload()
if err != nil {
return "", err
}

storeInfo := gjson.Get(applyRes, "Result.UploadAddress.StoreInfos.0")
storeURI := storeInfo.Get("StoreUri").String()
storeAuth := storeInfo.Get("Auth").String()
uploadHost := gjson.Get(applyRes, "Result.UploadAddress.UploadHosts.0").String()
uploadURL := fmt.Sprintf(uploadURLFormat, uploadHost, storeURI)
if err := ix.Upload(uploadURL, imgPath, storeAuth); err != nil {
return "", err
}

sessionKey := gjson.Get(applyRes, "Result.UploadAddress.SessionKey").String()
if _, err = ix.CommitImageUpload(sessionKey); err != nil {
return "", err
}

return c.GetImageURL(storeURI)
}

func (c *Client) GetImageURL(uri string) (string, error) {
endpoint := "/imagex/get_img_url"
params := &url.Values{
"uri": []string{uri},
}
raw, err := c.Get(APIBaseURL, endpoint, params)
if err != nil {
return "", err
}
rawurl := gjson.Get(raw, "data.main_url").String()
return rawurl, nil
}

func (c *Client) GetUploadToken() (*UploadToken, error) {
endpoint := "/imagex/gen_token"
params := &url.Values{
"client": []string{"web"},
}
raw, err := c.Get(APIBaseURL, endpoint, params)
if err != nil {
return nil, err
}
var token *UploadToken
err = json.Unmarshal([]byte(gjson.Get(raw, "data.token").String()), &token)
return token, err
}

func (ix *ImageX) ApplyImageUpload() (string, error) {
rawurl := fmt.Sprintf("https://imagex.bytedanceapi.com/?Action=%s&Version=%s&ServiceId=%s",
actionApplyImageUpload, version, serviceID)
req, err := http.NewRequest(http.MethodGet, rawurl, nil)
if err != nil {
return "", err
}

if err := ix.signRequest(req); err != nil {
return "", err
}

res, err := ix.getClient().Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
raw := string(b)
if res.StatusCode != 200 || gjson.Get(raw, "ResponseMetadata.Error").Exists() {
return "", fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return raw, nil
}

func (ix *ImageX) CommitImageUpload(sessionKey string) (string, error) {
rawurl := fmt.Sprintf("https://imagex.bytedanceapi.com/?Action=%s&Version=%s&SessionKey=%s&ServiceId=%s",
actionCommitImageUpload, version, sessionKey, serviceID)
req, err := http.NewRequest(http.MethodPost, rawurl, nil)
if err != nil {
return "", err
}

if err := ix.signRequest(req); err != nil {
return "", err
}

res, err := ix.getClient().Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
raw := string(b)
if res.StatusCode != 200 || gjson.Get(raw, "ResponseMetadata.Error").Exists() {
return "", fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return raw, nil
}

func (ix *ImageX) getClient() *http.Client {
if ix.Client == nil {
return http.DefaultClient
}
return ix.Client
}

func (ix *ImageX) signKeys(t time.Time) []byte {
h := makeHMac([]byte("AWS4"+ix.SecretKey), []byte(t.Format(shortTimeFormat)))
h = makeHMac(h, []byte(ix.Region))
h = makeHMac(h, []byte(serviceName))
h = makeHMac(h, []byte("aws4_request"))
return h
}

func (ix *ImageX) writeRequest(w io.Writer, r *http.Request) error {
r.Header.Set("host", r.Host)

w.Write([]byte(r.Method))
w.Write(newLine)
writeURI(w, r)
w.Write(newLine)
writeQuery(w, r)
w.Write(newLine)
writeHeader(w, r)
w.Write(newLine)
w.Write(newLine)
writeHeaderList(w, r)
w.Write(newLine)
return writeBody(w, r)
}

func (ix *ImageX) writeStringToSign(w io.Writer, t time.Time, r *http.Request) error {
w.Write([]byte(algorithm))
w.Write(newLine)
w.Write([]byte(t.Format(amzDateISO8601TimeFormat)))
w.Write(newLine)

w.Write([]byte(ix.creds(t)))
w.Write(newLine)

h := sha256.New()
if err := ix.writeRequest(h, r); err != nil {
return err
}
fmt.Fprintf(w, "%x", h.Sum(nil))
return nil
}

func (ix *ImageX) creds(t time.Time) string {
return t.Format(shortTimeFormat) + "/" + ix.Region + "/" + serviceName + "/aws4_request"
}

func (ix *ImageX) signRequest(req *http.Request) error {
t := time.Now().UTC()
req.Header.Set("x-amz-date", t.Format(amzDateISO8601TimeFormat))

req.Header.Set("x-amz-security-token", ix.Token)

k := ix.signKeys(t)
h := hmac.New(sha256.New, k)

if err := ix.writeStringToSign(h, t, req); err != nil {
return err
}

auth := bytes.NewBufferString(algorithm)
auth.Write([]byte(" Credential=" + ix.AccessKey + "/" + ix.creds(t)))
auth.Write([]byte{',', ' '})
auth.Write([]byte("SignedHeaders="))
writeHeaderList(auth, req)
auth.Write([]byte{',', ' '})
auth.Write([]byte("Signature=" + fmt.Sprintf("%x", h.Sum(nil))))

req.Header.Set("authorization", auth.String())
return nil
}

func writeURI(w io.Writer, r *http.Request) {
path := r.URL.RequestURI()
if r.URL.RawQuery != "" {
path = path[:len(path)-len(r.URL.RawQuery)-1]
}
slash := strings.HasSuffix(path, "/")
path = filepath.Clean(path)
if path != "/" && slash {
path += "/"
}
w.Write([]byte(path))
}
func writeQuery(w io.Writer, r *http.Request) {
var a []string
for k, vs := range r.URL.Query() {
k = url.QueryEscape(k)
for _, v := range vs {
if v == "" {
a = append(a, k)
} else {
v = url.QueryEscape(v)
a = append(a, k+"="+v)
}
}
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write([]byte{'&'})
}
w.Write([]byte(s))
}
}

func writeHeader(w io.Writer, r *http.Request) {
i, a := 0, make([]string, len(r.Header))
for k, v := range r.Header {
sort.Strings(v)
a[i] = strings.ToLower(k) + ":" + strings.Join(v, ",")
i++
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write(newLine)
}
io.WriteString(w, s)
}
}

func writeHeaderList(w io.Writer, r *http.Request) {
i, a := 0, make([]string, len(r.Header))
for k := range r.Header {
a[i] = strings.ToLower(k)
i++
}
sort.Strings(a)
for i, s := range a {
if i > 0 {
w.Write([]byte{';'})
}
w.Write([]byte(s))
}
}

func writeBody(w io.Writer, r *http.Request) error {
var (
b []byte
err error
)
// If the payload is empty, use the empty string as the input to the SHA256 function
// http://docs.amazonwebservices.com/general/latest/gr/sigv4-create-canonical-request.html
if r.Body == nil {
b = []byte("")
} else {
b, err = ioutil.ReadAll(r.Body)
if err != nil {
return err
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
}

h := sha256.New()
h.Write(b)
fmt.Fprintf(w, "%x", h.Sum(nil))
return nil
}

func makeHMac(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}

func (ix *ImageX) Upload(rawurl, fp, auth string) error {
crc32, err := hashFileCRC32(fp)
if err != nil {
return err
}
file, err := os.Open(fp)
if err != nil {
return err
}
defer file.Close()

req, err := http.NewRequest(http.MethodPost, rawurl, file)
if err != nil {
return err
}
req.Header.Add("authorization", auth)
req.Header.Add("Content-Type", "application/octet-stream")
req.Header.Add("content-crc32", crc32)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
raw := string(b)
if gjson.Get(raw, "success").Int() != 0 {
return fmt.Errorf("raw: %s, response: %+v", raw, res)
}
return nil
}

// hashFileCRC32 generate CRC32 hash of a file
// Refer https://mrwaggel.be/post/generate-crc32-hash-of-a-file-in-golang-turorial/
func hashFileCRC32(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
tablePolynomial := crc32.MakeTable(polynomialCRC32)
hash := crc32.New(tablePolynomial)
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

总结

就一句话,掘金牛啊牛啊!

本文转载自: 掘金

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

盘点认证框架 SpringSecurity OAuth

发表于 2021-04-12

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

这一篇我们继续深入 SpringSecurity , 看看其 OAuth2.0 的流程逻辑.

二 . 简易使用

2.1 Maven 依赖

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


<!-- OAuth 包 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>

2.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
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
java复制代码@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler myAuthenctiationFailureHandler;

@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;

@Autowired
private UserService userService;

@Autowired
private ClientDetailsService clientDetailsService;

@Bean
public AuthenticationManager authenticationManagerBean(DataSource dataSource) throws Exception {
OAuth2AuthenticationManager authenticationManager = new OAuth2AuthenticationManager();
authenticationManager.setTokenServices(new DefaultTokenServices());
authenticationManager.setClientDetailsService(new JdbcClientDetailsService(dataSource));
return authenticationManager;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//该方法用于用户认证,此处添加内存用户,并且指定了权限
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}

@Autowired
public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
configurer.configure(clientDetails);
}
}

@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String authorizeEndpointPath = handlerMapping.getServletPath("/oauth/authorize");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}

// PS : 注意 , OAuth 本身有一个 WebSecurityConfigurerAdapter ,我这里选择覆盖自定义
http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/before/**").permitAll()
.antMatchers("/index").permitAll()
.antMatchers(authorizeEndpointPath).authenticated()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.anyRequest().authenticated() //其它请求都需要校验才能访问
.and()
.requestMatchers()
// .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.formLogin()
.loginPage("/login") //定义登录的页面"/login",允许访问
.defaultSuccessUrl("/home") //登录成功后默认跳转到"list"
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
.logout() //默认的"/logout", 允许访问
.logoutSuccessUrl("/index")
.permitAll();
http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}

@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png");
}

protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
configurer.configure(oauthServer);
}
}
}

Resource 资源配置

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复制代码@Configuration
@EnableResourceServer
public class ResServerConfig extends ResourceServerConfigurerAdapter {

@Autowired
private TokenStore tokenStore;

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenStore(tokenStore).resourceId("resourceId");
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers()
.antMatchers("/user", "/res/**")
.and()
.authorizeRequests()
.antMatchers("/user", "/res/**")
.authenticated();

}
}

OAuthConfig 专属属性

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
java复制代码@Configuration
@EnableAuthorizationServer
@Order(2)
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {


@Autowired
private DataSource dataSource;

@Autowired
@Lazy
private AuthenticationManager authenticationManager;

@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}

@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}


@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}

//检查token的策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
security.checkTokenAccess("permitAll()");
}

//OAuth2的主配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// .approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}

}

2.3 数据库

详见项目

2.4 使用方式

请求方式

1
2
3
java复制代码http://localhost:8080/security/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=app

https://www.baidu.com/?code=jYgDO3

AccessToken

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
js复制代码var settings = {
"url": "http://localhost:8080/security/oauth/token",
"method": "POST",
"timeout": 0,
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"data": {
"grant_type": "authorization_code",
"client_id": "client",
"client_secret": "secret",
"code": "CFUFok",
"redirect_uri": "http://www.baidu.com"
}
};

$.ajax(settings).done(function (response) {
console.log(response);
});

// 失败
{
"error": "invalid_grant",
"error_description": "Invalid authorization code: CFUFok"
}

// 成功
{
"access_token": "c0955d7f-23fb-4ca3-8a52-c715867cbef2",
"token_type": "bearer",
"refresh_token": "55f53af0-1133-46dc-a32d-fbb9968e5938",
"expires_in": 7199,
"scope": "app"
}

check Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码var settings = {
"url": "http://localhost:8080/security/oauth/check_token?token=c0955d7f-23fb-4ca3-8a52-c715867cbef2",
"method": "GET",
"timeout": 0,
};

$.ajax(settings).done(function (response) {
console.log(response);
});

// 返回
{
"aud": [
"resourceId"
],
"exp": 1618241690,
"user_name": "gang",
"client_id": "client",
"scope": [
"app"
]
}

三 . 源码解析

3.1 基础类

TokenStore
TokenStore 是一个接口 , 既然是一个接口 , 就意味着使用中是可以完全定制的

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复制代码public interface TokenStore {

// 通过 OAuth2AccessToken 对象获取一个 OAuth2Authentication
OAuth2Authentication readAuthentication(OAuth2AccessToken token);
OAuth2Authentication readAuthentication(String token);

// 持久化关联 OAuth2AccessToken 和 OAuth2Authentication
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);

// OAuth2AccessToken 的获取和移除
OAuth2AccessToken readAccessToken(String tokenValue);
void removeAccessToken(OAuth2AccessToken token);
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

// OAuth2RefreshToken 的直接操作
void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
OAuth2RefreshToken readRefreshToken(String tokenValue);
OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);
void removeRefreshToken(OAuth2RefreshToken token);

// 使用刷新令牌删除访问令牌 , 该方法会被用于控制令牌数量
void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);

// Client ID 查询令牌
Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
Collection<OAuth2AccessToken> findTokensByClientId(String clientId);
}

我们再来看看 TokenStore 主要的实现类 , 默认提供了 以下几种实现 :

OAuth001.jpg

InMemoryTokenStore

  • 从名字就能看到 , 该类是将 Token 放在内存中 ,
  • 点进去就能看到 , 类中准备了大量的 ConcurrentHashMap , 保证多线程访问的安全性
1
2
3
4
5
6
7
8
9
10
11
java复制代码// 整体其实没什么看的  , 唯一有点特殊的就是 , 里面不是一个集合 , 而是每个业务一个集合 , 这其实相当于分库分表的处理思路
C- InMemoryTokenStore
- private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore
- private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore
- private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> userNameToAccessTokenStore

// 内部类 TokenExpiry
PSC- TokenExpiry implements Delayed
?- Delayed 是延迟处理的接口 , 用于判断 Token 是否过期
- private final long expiry;
- private final String value;

JdbcTokenStore

JdbcTokenStore 是可行的处理方式 , 但是并不是最优解 , 数据库处理对高并发 , 高性能会带来不小的挑战

1
2
3
4
5
6
JAVA复制代码// 关键点一 : SQL 写死了 , 点开就能看到 , sql 是定死的 , 但是提供了 Set 方法 , 即可定制
private static final String DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT = "insert into oauth_access_token (toke....."
private String insertAccessTokenSql = DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT;

// 关键点二 : 使用 JDBCTemplate , 意味着常规Spring 配置即可
private final JdbcTemplate jdbcTemplate;

RedisTokenStore

Redis 存储 Token , 比较常见的存储方式 , 一般是首选方案 , 环境影响不能使用才会次选 JDBC

  • 冒号区分文件夹
  • RedisConnectionFactory 需要 redis 包
  • JdkSerializationStrategy 序列化策略
    • 序列化这一块反而是最应该关注的 , 部分监控框架可能会和序列化方式存在冲突

后面说一下它的另外2个特别的实现类 , 他们不是一种持久化的方式

JwkTokenStore

  • 提供了对使用JSON Web密钥(JWK)验证JSON Web令牌(JWT)的JSON Web签名(JWS)的支持
  • 令牌库实现专门用于资源服务器 , 唯一责任是解码JWT并使用相应的JWK验证其签名(JWS)
  • 从这个介绍大概就知道了 , 他是用于资源服务器的 , 他的主要目的是转换 , 所以点开后不难发现 , 里面有一个对象用于底层调用
1
2
3
4
5
6
7
8
9
java复制代码private final TokenStore delegate : 通过该对象再去处理底层的方式

// 常见的构造器
public JwkTokenStore(String jwkSetUrl)
public JwkTokenStore(List<String> jwkSetUrls)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter)
public JwkTokenStore(String jwkSetUrl, JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(List<String> jwkSetUrls, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)

扩展资料 :

  • JWK RFC : tools.ietf.org/html/rfc751…
  • JWT RFC : tools.ietf.org/html/rfc751…
  • JWS RFC : tools.ietf.org/html/rfc751…

JwtTokenStore

这个对象其实是一个全新的体系 , 是 Token 的 JWT 实现 , 而不仅仅只是一种存储方式

1
2
3
4
5
java复制代码- private JwtAccessTokenConverter jwtTokenEnhancer;
- private ApprovalStore approvalStore;
- JdbcApprovalStore
- TokenApprovalStore
- InMemoryApprovalStore
  • 可以看到提供了一个 转换的成员变量和一个 存储的 store 对象
  • 这里要注意的是 , 他只从令牌本身读取数据。不是真正的存储,它从不持久化任何东西.
    • 因为 , JWT 本身就存储了数据

3.2 事件类型和处理

事件用于推送 , 主要使用的有 DefaultAuthenticationEventPublisher ,我们来看看他

OAuth005.jpg

DefaultAuthenticationEventPublisher

从构造器里面可以看到大概的事件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public DefaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

this.applicationEventPublisher = applicationEventPublisher;
addMapping(BadCredentialsException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
addMapping(UsernameNotFoundException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
addMapping(AccountExpiredException.class.getName(),AuthenticationFailureExpiredEvent.class);
addMapping(ProviderNotFoundException.class.getName(),AuthenticationFailureProviderNotFoundEvent.class);
addMapping(DisabledException.class.getName(),AuthenticationFailureDisabledEvent.class);
addMapping(LockedException.class.getName(),AuthenticationFailureLockedEvent.class);
addMapping(AuthenticationServiceException.class.getName(),AuthenticationFailureServiceExceptionEvent.class);
addMapping(CredentialsExpiredException.class.getName(),AuthenticationFailureCredentialsExpiredEvent.class);
addMapping( "org.springframework.security.authentication.cas.ProxyUntrustedException",
AuthenticationFailureProxyUntrustedEvent.class);
}


M- publishAuthenticationSuccess
?- 发布认证成功事件

M- publishAuthenticationFailure
- AbstractAuthenticationEvent event = constructor.newInstance(authentication, exception);
?- 构建一个 AbstractAuthenticationEvent
- applicationEventPublisher.publishEvent(event)
?- 发布事件


M- setAdditionalExceptionMappings
?- 将额外的异常设置为事件映射。它们会自动与ProviderManager定义的事件映射的默认异常合并

3.3 Service 处理类

ResourceServerTokenServices 接口

1
2
3
4
JAVA复制代码public interface ResourceServerTokenServices {
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
OAuth2AccessToken readAccessToken(String accessToken);
}

DefaultTokenServices
默认的 Token 处理类 , 没看到特别的东西, 主要是对 TokenStore 的调用

RemoteTokenServices

  • 查询/check_token端点以获取访问令牌的内容。如果端点返回400响应,这表明令牌无效。
1
2
3
4
java复制代码C- RemoteTokenServices
F- private RestOperations restTemplate;
- 简单点说就是通过这个对象调用 check_token 接口查询 token 信息
- 注意 , 区别于本地类 , 这种方式目的应该是当前 OAuth 服务作为一个 SP 的情况

3.4 Token 管理体系

TokenGranter 是一个接口 , 他有很多实现类,

其中最常见的应该是 AuthorizationCodeTokenGranter 和 ImplicitTokenGranter , RefreshTokenGranter

1
2
3
4
5
java复制代码C- AuthorizationCodeTokenGranter
M- getOAuth2Authentication
- Paramters 中获取 Code , 并且判空 -> InvalidRequestException
- authorizationCodeServices.consumeAuthorizationCode(authorizationCode) : 通过 Code 获取 OAuth2Authentication
- 判断 redirectUri 和 clientId 是否存在 -> RedirectMismatchException/InvalidClientException

OAuth002.jpg

3.5 Token Conversion 体系

1
2
3
4
java复制代码C- DefaultAccessTokenConverter
?- 默认 Token 处理体系 ,我们来看一下主要做了什么
M- convertAccessToken
?- 可以看到 , 整个转换逻辑中会通过不同的开关 , 决定显示哪些

OAuth003.jpg

3.6 配置类详情

OAuth 中除了原本的 User 概念 ,同时还有个 Client 概念 ,每个 Client 都可以看成一类待认证的对象 , **Spring OAuth 中提供了 OAuth 协议的自动配置 **, 主要包含2个类 :

  • 实现 User 的认证
    • AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter
  • 实现 Resource 的认证
    • ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered
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
java复制代码
// ResourceServerConfiguration
F- private TokenStore tokenStore; // token 管理实现
F- private AuthenticationEventPublisher eventPublisher; // 事件发布
F- private Map<String, ResourceServerTokenServices> tokenServices; // Token Service 集合
F- private ApplicationContext context;
F- private List<ResourceServerConfigurer> configurers = Collections.emptyList();
?- 这里的集合可以用于自己定制 ResourceServerConfigurer 类
F- private AuthorizationServerEndpointsConfiguration endpoints;
?- 对 EndPoint 接口做一个初始化操作
PSC- NotOAuthRequestMatcher
M- configure(HttpSecurity http)
?- 核心配置方法 , 主要生成了一个 ResourceServerSecurityConfigurer 放在 HttpSecurity 中
?- 这里实际上是克隆了一个当前对象给 HttpSecurity ,而不是一个引用
- 前面几步分别是 : 配置 tokenServices + tokenStore + eventPublisher
- 然后发现一个有意思的地方 : 从结构上讲 , 这应该算是装饰器的应用
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(resources);
}
- 后面几步开始对 HttpSecurity 本身做配置 , 分别是
- authenticationProvider : AnonymousAuthenticationProvider
- exceptionHandling
- accessDeniedHandler
- sessionManagement : session 管理
- sessionCreationPolicy
- 跨域处理 csrf
- 添加 requestMatcher
- 然后又发现了一个有趣的地方 , 双方互相持有对象
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(http);
}

// AuthorizationServerSecurityConfiguration
protected void configure(HttpSecurity http) throws Exception {

// 看样子和上面一样 , 构建一个新得 AuthorizationServerSecurityConfigurer 放入 HttpSecurity 中
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();

//
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);

// 装饰器持有对象 , 获得更多的扩展功能
configure(configurer);
http.apply(configurer);

// 此处就是获取 OAuth 的相关接口 , 并且在下面为其配置对应的权限要求
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}

// 略.... 没什么关键的 , 都是通用的东西
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}

四 . 运行逻辑

看完了配置逻辑 , 我们来看一下主要的运行逻辑 :

4.1 请求入口

我们从请求的 入口 authorize 来开始 , 看看请求经过了哪些途径 , 来到了这里

流程开始

跳过一系列 Invoke 和 MVC 的流程 , 找到了 FilterChainProxy 类 , 大概就能知道 , OAuth 协议是在Filter 整体流程里面对请求进行的过滤

下面来找一下是哪个过滤类讲请求拦截到登录页的 :

Step 1 : SecurityContextPersistenceFilter

这里主要是执行 Filter 链后在 finally 中处理

Step 2 : BasicAuthenticationFilter

可以看到 , 代码中做了这些事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码C01- BasicAuthenticationFilter
String header = request.getHeader("Authorization");
?- Basic Z2FuZzoxMjM0NTY=
// 如果 header 为空 , 则继续执行 Filter
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}

// 如果认证信息存在
- String[] tokens = extractAndDecodeHeader(header, request);
- String username = tokens[0];
- new UsernamePasswordAuthenticationToken(username, tokens[1]);
?- 构建一个 UsernamePasswordAuthenticationToken
- .... (PS : 这里的逻辑 Filter 详细说过了 , 就不反复说了)
- SecurityContextHolder.getContext().setAuthentication(authResult);

Filter 的逻辑其实之前就已经讲了 , 这里也就不太深入了

其他扩展 Filter

TokenEndpointAuthenticationFilter

TokenEndpoint的可选身份验证过滤器。它位于客户端的另一个过滤器(通常是BasicAuthenticationFilter)的下游,如果请求也包含用户凭证,它就会为Spring SecurityContext创建一个OAuth2Authentication .

如果使用这个过滤器,Spring安全上下文将包含一个OAuth2Authentication封装(作为授权请求)、进入过滤器的表单参数和来自已经经过身份验证的客户端身份验证的客户端id,以及从请求中提取并使用身份验证管理器验证的已验证用户令牌。

OAuth2AuthenticationProcessingFilter

针对OAuth2受保护资源的认证前过滤器。

从传入请求提取一个OAuth2令牌,并使用它用OAuth2Authentication(如果与OAuth2AuthenticationManager一起使用)填充Spring安全上下文。

4.2 接口详情

注意 , 到这个接口时候 ,认证其实已经完成了 , 拦截的过程详见上文 Filter , 这一部分只分析内部的流程

接口一 : authorize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码http://localhost:8080/security/oauth/authorize
C06- AuthorizationEndpoint
M601- authorize
P- Map<String, Object> model
P- Map<String, String> parameters : 传入的参数
P- SessionStatus sessionStatus
P- Principal principal : 因为实际上已经认证完了 , 所以能拿到 Principal
- getOAuth2RequestFactory().createAuthorizationRequest(parameters);
?- 通过 parameters 生成了一个 AuthorizationRequest , 该对象为认证过程中的流转对象
- authorizationRequest.getResponseTypes() : 获取 tResponseTypes 的Set<String>
?- 如果集合类型正确 -> UnsupportedResponseTypeException
?- TODO : 为什么是集合 ?
- authorizationRequest.getClientId() : 校验 ClientId 是否存在 -> InvalidClientException
- principal.isAuthenticated() : 校验是否认证 -> InsufficientAuthenticationException
- authorizationRequest.setRedirectUri(resolvedRedirect) : 生成并且设置重定向地址
?- 注意 , 这个地址此时还不带 Code
- oauth2RequestValidator.validateScope(authorizationRequest, client)
?- 校验当前 client 的作用域是否包含当前请求
- userApprovalHandler.checkForPreApproval(authorizationRequest,(Authentication) principal)
?- TODO
- userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal)
?- 请求是否已被最终用户(或其他流程)批准
- getAuthorizationCodeResponse(authorizationRequest,(Authentication) principal)
?- ResponseType = code 时的最终处理逻辑 :M602
?- ResponseType = token 时的最终处理逻辑 :M605
M602- getAuthorizationCodeResponse
- getSuccessfulRedirect(authorizationRequest,generateCode(authorizationRequest, authUser)):M603
M603- getSuccessfulRedirect
- Map<String, String> query = new LinkedHashMap<String, String>();
- query.put("code", authorizationCode);
?- 插入 Code
- String state = authorizationRequest.getState();
- if (state != null) query.put("state", state);
?- 插入 State
M604- generateCode
- OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
- OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
- String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);
?- 核心方法 , 注意 这里的 AuthorizationCodeServices 是一个接口 , 意味着该对象是可以自定义实现的
?- 这里的生成类是 RandomValueStringGenerator

// 补充 Token 模式
当使用 Implicit 模式进行认证的时候 , 这里是怎么处理的呢 ?
M605- getImplicitGrantResponse(authorizationRequest)
- TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
- OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
- OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
?- 此方法中生成最后的 Token , 如果为空会抛出异常
- getTokenGranter().grant("implicit",new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));

接口二 : AccessToken 接口 /oauth/token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码C07- TokenEndpoint
?- Token 的处理主要集中在该类中 , 该类中提供了 POST 和 GET 两种请求能力 , 这2种无明显区别
M701- postAccessToken(Principal principal,Map<String, String> parameters)
- 判断是否已经认证
- getClientDetailsService().loadClientByClientId(clientId)
?- 先获取 clientId , 再获取一个 ClientDetails
- getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient)
?- 构建出一个 TokenRequest
- 校验 Client ID , 再校验 ClientDetails 的 Scope 域
- 校验 GrantType 是否合理 , 不能为 空 , 不能为 implicit
- tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
?-对 RefreshToken 类型 进行处理
- getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)
?- 核心 , 生成 AccessToken , 详见上文 CodeToken 生成逻辑
- getResponse(token) : 生成一个 Response 对象

接口三 : CheckTokenEndpoint - /oauth/check_token

1
2
3
4
5
6
7
8
java复制代码C08- CheckTokenEndpoint
M801- checkToken
- OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
?- 通过 Token 获取 OAuth2AccessToken 对象
- OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
?- 如果 OAuth2AccessToken 存在且没过期 , 获取 OAuth2Authentication
- accessTokenConverter.convertAccessToken(token, authentication)
?- 返回用户信息

Client 核心处理

Client 也是 OAuth 中一个非常核心的概念 , 毫无意外 , Client 的校验仍然是通过 Filter 处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JAVA复制代码C- ClientCredentialsTokenEndpointFilter
M- attemptAuthentication
- String clientId = request.getParameter("client_id");
- String clientSecret = request.getParameter("client_secret");
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
?- 如果认证过了 , 则直接返回 (PS : 这里是 Client 认证)
- UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,clientSecret);
?- 构建一个 UsernamePasswordAuthenticationToken , 用于认证 Client
- his.getAuthenticationManager().authenticate(authRequest)

// 后面会调用 ProviderManager 用于认证处理
C- ProviderManager
M- authenticate
FOR - getProviders()
- result = provider.authenticate(authentication) : 完成认证

// DaoAuthenticationProvider

可以看到 , 这里完全是将 Client 当成一个用户在认证 , 整个体系都得到了通用

业务处理之 : OAuth2ErrorHandler 异常处理

TODO

AuthenticationProvider 体系结构

AuthenticationProvider 的整个体系结构异常庞大 , 他是认证的主体

TODO

4.3 其他重要工具类

DefaultStateKeyGenerator

默认state 构建工具

1
2
java复制代码private RandomValueStringGenerator generator = new RandomValueStringGenerator();
?- 使用的是随机数

4.4 异常类

  • OAuth2AccessDeniedException
    • 当访问被拒绝时,我们通常想要一个403,但是我们想要与所有其他OAuth2Exception类型一样的处理
  • UserApprovalRequiredException
    • 许可异常
  • UserRedirectRequiredException
    • 抛出该异常 , 许可令牌重定向
  • AccessTokenRequiredException
  • JwkException

4.5 补充类

OAuth2RestTemplate

OAuth2 的定制 RestTemplate 使用所提供资源的凭据发出oauth2认证的Rest请求

1
2
3
4
5
6
7
8
9
java复制代码// 其中包含了一些和 OAuth 相关的定制
- appendQueryParameter : 构建token 请求的 parameter
- acquireAccessToken : 构建一个 OAuth2AccessToken ??
- getAccessToken : 必要情况下获取或更新当前上下文的访问令牌
- createRequest : 创建一个请求 , 会调用 DefaultOAuth2RequestAuthenticator 生成一个 Token

C- DefaultOAuth2RequestAuthenticator
?- 通过 AccessToken 生成一个 OAuth2Request
- Authorization Bearer ....

ProviderDiscoveryClient

看这代码 , OAuth2 应该还支持 OIDC 呢 , 该类用于发现 OIDC 规范配置的提供者的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public ProviderConfiguration discover() {

// 发起请求
Map responseAttributes = this.restTemplate.getForObject(this.providerLocation, Map.class);
ProviderConfiguration.Builder builder = new ProviderConfiguration.Builder();

// 获取 OIDC 信息
builder.issuer((String)responseAttributes.get(ISSUER_ATTR_NAME));
builder.authorizationEndpoint((String)responseAttributes.get(AUTHORIZATION_ENDPOINT_ATTR_NAME));
if (responseAttributes.containsKey(TOKEN_ENDPOINT_ATTR_NAME)) {
builder.tokenEndpoint((String)responseAttributes.get(TOKEN_ENDPOINT_ATTR_NAME));
}
if (responseAttributes.containsKey(USERINFO_ENDPOINT_ATTR_NAME)) {
builder.userInfoEndpoint((String)responseAttributes.get(USERINFO_ENDPOINT_ATTR_NAME));
}
if (responseAttributes.containsKey(JWK_SET_URI_ATTR_NAME)) {
builder.jwkSetUri((String)responseAttributes.get(JWK_SET_URI_ATTR_NAME));
}

return builder.build();
}
1
2
3
4
5
6
7
8
java复制代码
// 基本上能看到这些 OIDC 的属性
private static final String PROVIDER_END_PATH = "/.well-known/openid-configuration";
private static final String ISSUER_ATTR_NAME = "issuer";
private static final String AUTHORIZATION_ENDPOINT_ATTR_NAME = "authorization_endpoint";
private static final String TOKEN_ENDPOINT_ATTR_NAME = "token_endpoint";
private static final String USERINFO_ENDPOINT_ATTR_NAME = "userinfo_endpoint";
private static final String JWK_SET_URI_ATTR_NAME = "jwks_uri";

4.6 业务的扩展定制

我们最终的目的是为了知道哪些节点可以扩展 :

Security 可以扩展的地方主要有这几类 :

  • 使用接口的地方
    • TokenStore
    • ResourceServerTokenServices
    • ClientDetailsService
    • AuthorizationServerConfigurer
  • 使用开关的地方
    • 扩展 Filter , 监听 AccessToken
    • OAuth2AuthenticationFailureEvent
    • OAuth2ClientAuthenticationProcessingFilter
  • 提供Set 方法的地方

TODO : 这个光说没用 , 后续会尝试做个 Demo 出来

总结

整体来说走了个大概 , 其中还有些零零散散的小点就不想走了 , 精力有限 , 后面如果涉及到 ,再完善到其中去

参考和感谢

blog.csdn.net/weixin_4184…

本文转载自: 掘金

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

呦呦,这些代码有点臭,重构大法带你秀(SPI接口化),skr

发表于 2021-04-12

大家好,我是狼王,一个爱打球的程序员

如果说 正常的重构是为了消除代码的坏味道, 那么高层次的重构就是消除架构的坏味道

最近由于需要将公司基础架构的组件进行各种兼容,适配以及二开,所以很多时候就需要对组件进行重构,大家是不是在拿到公司老项目老代码,又需要二开或者重构的时候,会头很大,无从下手,我之前也一直是这样的状态,不过在慢慢熟悉了一些重构的思想和方法之后,就能稍微的得心应手一些,下面我就开始讲下重构,然后会着重讲下重构中的SPI接口化。

先给大家看看最近通过使用SPI接口化,重构的一个组件-分布式存储。

重构前的代码结构

好家伙,所有的第三方存储都是写在一个模块中的,各种阿里云,腾讯云,华为云等等,这样的代码架构在前期可能在不需要经常扩展,二开的时候,还是能用的。

但是当某个新需求来的时候,比如我遇到的:需要支持多个云的多个账号上传下载功能,这个是因为在不同的云上,不同账号的权限,安全认证等都是不太一样的,所以在某一刻,这个需求就被提出来了,也就是你想上传到哪个云的哪个账号都可以。

然后拿到这个代码,看了下这样的架构,可能在这样的基础上完成需求也是没有问题的,但是扩展很麻烦,而且代码会越来越繁重,架构会越来越复杂,不清晰。

所以我索性趁着这个机会,就重构一把,和其他同事也商量了下,决定分模块,SPI化,好处就是根据你想使用的引入对应的依赖,让代码架构更加清晰,后续更加容易扩展了!下面就是重构后的大体架构:

是不是清楚多了,之后哪怕某个云存储需要增加新功能,或者需要兼容更多的云也是比较容易的了。

好了,下面就让我们开始讲讲重构大法~


重构

重构是什么?

重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

重构最重要的思想就是让普通程序员也能写出优秀的程序。

把优化代码质量的过程拆解成一个个小的步骤,这样重构一个项目的巨大工作量就变成比如修改变量名、提取函数、抽取接口等等简单的工作目标。

作为一个普通的程序员就可以通过实现这些易完成的工作目标来提升自己的编码能力,加深自己的项目认识,从而为最高层次的重构打下基础。

而且高层次的重构依然是由无数个小目标构成,而不是长时间、大规模地去实现。

重构本质是极限编程的一部分,完整地实现极限编程才能最大化地发挥重构的价值。而极限编程本身就提倡拥抱变化,增强适应性,因此分解极限编程中的功能去适应项目的需求、适应团队的现状才是最好的操作模式。

重构的重点

重复代码,过长函数,过大的类,过长参数列,发散式变化,霰弹式修改,依恋情结,数据泥团,基本类型偏执,平行继承体系,冗余类等

下面举一些常用的或者比较基础的例子:

一些基本的原则我觉得还是需要了解的

  1. 尽量避免过多过长的创建Java对象
  2. 尽量使用局部变量
  3. 尽量使用StringBuilder和StringBuffer进行字符串连接
  4. 尽量减少对变量的重复计算
  5. 尽量在finally块中释放资源
  6. 尽量缓存经常使用的对象
  7. 不使用的对象及时设置为null
  8. 尽量考虑使用静态方法
  9. 尽量在合适的场合使用单例
  10. 尽量使用final修饰符

下面是关于类和方法优化:

  1. 重复代码的提取
  2. 冗长方法的分割
  3. 嵌套条件分支或者循环递归的优化
  4. 提取类或继承体系中的常量
  5. 提取继承体系中重复的属性与方法到父类

这里先简单介绍这些比较常规的重构思想和原则,方法,毕竟今天的主角是SPI,下面有请SPI登场!

SPI

什么是SPI?

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

它是一种服务发现机制,它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

下面就是SPI的机制过程

SPI实际上是基于接口的编程+策略模式+配置文件组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。

一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

SPI使用介绍

要使用Java SPI,一般需要遵循如下约定:

  1. 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以接口全限定名`为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的jar包放在主程序的classpath中;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  4. SPI的实现类必须携带一个不带参数的构造方法;

SPI使用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

以下是比较常见的例子:

  1. 数据库驱动加载接口实现类的加载 JDBC加载不同类型数据库的驱动
  2. 日志门面接口实现类加载 SLF4J加载不同提供商的日志实现类
  3. Spring Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  4. Dubbo Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

SPI简单例子

先定义接口类

1
2
3
4
5
6
go复制代码package com.test.spi.learn;
import java.util.List;

public interface Search {
    public List<String> searchDoc(String keyword);   
}

文件搜索实现

1
2
3
4
5
6
7
8
9
10
go复制代码package com.test.spi.learn;
import java.util.List;

public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 "+keyword);
        return null;
    }
}

数据库搜索实现

1
2
3
4
5
6
7
8
9
10
go复制代码package com.test.spi.learn;
import java.util.List;

public class DBSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据库搜索 "+keyword);
        return null;
    }
}

接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.test.spi.learn.Search

里面加上我们需要用到的实现类

1
2
go复制代码com.test.spi.learn.FileSearch
com.test.spi.learn.DBSearch

然后写一个测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package com.test.spi.learn;
import java.util.Iterator;
import java.util.ServiceLoader;

public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}

可以看到输出结果:

1
2
go复制代码文件搜索 hello world
数据库搜索 hello world

SPI原理解析

通过查看ServiceLoader的源码,梳理了一下,实现的流程如下:

  1. 应用程序调用ServiceLoader.load方法 ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括以下:

loader(ClassLoader类型,类加载器) acc(AccessControlContext类型,访问控制器) providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类) lookupIterator(实现迭代器功能)

  1. 应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,

如果有缓存,直接返回。如果没有缓存,执行类的装载,实现如下:

(1) 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件

(2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。

(3) 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。

总结

优点

使用SPI机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。

缺点

  1. 不能按需加载。虽然ServiceLoader做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  3. 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
  4. 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

看到上面这么多的缺点,你肯定会想,有这些弊端为什么还要使用呢,没错,在重构的过程中,SPI接口化是一个非常有用的方式,当你需要扩展的时候,适配的时候,越早的使用你就会受利越早,在一个合适的时间,恰当的机会的时候,就鼓起勇气,重构吧!


好了。今天就说到这了,我还会不断分享自己的所学所想,希望我们一起走在成功的道路上!

乐于输出干货的Java技术公众号:狼王编程。公众号内有大量的技术文章、海量视频资源、精美脑图,不妨来关注一下!回复资料领取大量学习资源和免费书籍!

转发朋友圈是对我最大的支持!

\

觉得有点东西就点一下“赞和在看”吧!感谢大家的支持了!

\

本文转载自: 掘金

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

《k8s 集群搭建》不要让贫穷扼杀了你学 k8s 的兴趣!

发表于 2021-04-12

大家好,欢迎来到小菜个人 solo 学堂。在这里,知识免费,不吝吸收!关注免费,不吝动手!
死鬼~看完记得给我来个三连哦!

本文主要介绍 kubernetes集群的搭建

如有需要,可以参考

如有帮助,不忘 点赞 ❥

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

阅读这篇文章先需要对 docker 的基本知识有所了解!相关阅读请移步:Docker上手,看完觉得自己又行了!

相信点进来的小伙伴应该都对 k8s 有所耳闻,甚至于已经使用上了。而如果是因为对标题感到好奇的小伙伴也别急着划走,因为我劝你一定要学习 Kubernetes(k8s)。而标题也并非标题党,由于 k8s 集群大体上分为两大类:

  • 一主多从:一台 master 节点和多台 node 节点,搭建比较简单,但是有可能出现 master 单机故障
  • 多主多从: 多台 master 节点和多台 node 节点,搭建比较麻烦,但是安全性高

不管是 一主多从 异或者是 多主多从 ,这里至少都是需要三台服务器,而且每台服务器的规格至少得在 2G内存 2颗CPU 配置起步,而我们如果纯属为了平时练习使用,花费一笔钱去投资服务器,可能有部分人是不愿意的,所以这里就呼应了标题~接下来小菜将带给你比较节省的方案去学习 k8s集群的搭建!

开头说到,我劝你一定要学习 k8s 这并非是一句空话。当下,云原生也并非是一个新名词,它已经指定了一条新的开发道路,**敏捷、可扩展、可复制、最大化利用…**便是这条道路的代名词!这篇文章不单单介绍 Kubernetes 的搭建,如果对 Kubernetes 有所熟悉的同学可以直接跳转到 Kubernetes 集群搭建 的部分,如果不熟悉的同学建议先看看前半部分,先对 Kubernetes 有所了解一下。

Kubernetes

一、K8s 事前了解

有些同学可能感到有点奇怪,为什么一会说 kubernetes,一会说 k8s,这两个是同一个东西吗?答案是肯定的

Kubernetes 简称 k8s,是用 8 来代替 8 个字符 “ubernete” 的缩写

这个一个用来管理云平台中多个主机上的容器化应用,它的目的便是让部署容器化的应用简单且高效,它提供了应用部署,规划,更新,维护的一种机制。

我们先来看看部署应用的迭代过程:

  • 传统部署: 直接将应用程序部署在物理机上
  • 虚拟化部署: 可以在一台物理机上运行多个虚拟机,每个虚拟机都是独立的一个环境
  • 容器化部署: 与虚拟机类似, 但是共享了操作系统

看了以上部署方式,想想看你们公司现在是用的哪一种~说到容器化部署,学过docker的同学肯定第一时间想到docker ,docker 的容器化部署方式确实给我们带来了很多便利,但是问题也是存在的,有时候这些问题会被我们刻意性的回避,因为 docker 实在是有点好用,让人有点不忍心对它产生质疑,但是又不得不面对:

  • 一个容器故障停机了,怎么样保证高可用让另外一个容器立刻启动去替补上停机的容器
  • 当并发访问量上来的时候,是否可以做到自动扩容,并发访问量下去的时候是否可以做到自动缩容
  • …

容器的问题确实有时候挺值得深思的,而这些容器管理的问题统称为 容器编排 问题,我们能想到的问题,自然有人去解决,例如 docker 自家就推出了 Docker Swarm 容器编排工具,Apache 退出了 Mesos 资源统一管控工具,Google 推出了Kubernetes容器编排工具,而这也是我们要说到的主角!

1)K8s优点
  • 自我修复:一旦某一个容器崩溃,能够在1秒左右迅速启动新的容器
  • 弹性伸缩:可以根据需要,自动对集群中正在运行的容器数量进行调整
  • 服务发现:服务可以通过自动发现的形式找到它所依赖的服务
  • 负载均衡:如果一个服务启动了多个容器,能够自动实现请求的负载均衡
  • 版本回退:如果发现新发布的程序版本有问题,可以立即回退到原来的版本
  • 存储编排:可以根据容器自身的需求自动创建存储卷
2)K8s 构成组件

一个完整的 Kubernetes 集群是由 控制节点 master 、工作节点 node 构成的,因此这种集群方式也分为 一主多从 和 多主多从,而每个节点上又会安装不同组件以提供服务。

1、Master

集群的控制平面,负责集群的决策(管理)。它旗下存在以下组件

  • ApiServer :资源操作的唯一入口,接收用户输入的命令,提供认证、授权、Api 注册和发现等机制
  • Scheduler:负责集群资源调度,按照预定的调度策略将 pod 调度到相应的 node 节点上
  • ControllerManager:负责维护集群的状态,比如程序部署安排,故障检测,自动扩展,滚动更新等
  • Etcd:负责存储集群中各种资源对象的信息
2、Node

集群的数据平面,负责为容器提供运行环境(干活)。它旗下存在以下组件

  • Kubelet:负责维护容器的生命周期,即通过控制 docker 来创建、更新、销毁容器
  • KubeProxy:负责提供集群内部的服务发现和负载均衡

看完了以上介绍,那我们接下来就开始进行 k8s 集群的搭建!

二、k8s 集群搭建

1)Centos7 安装

首先我们需要软件:

  • VMware Workstation Pro
  • Centos 7.0 镜像

虚拟机软件可百度查找下载,如若没有联系小菜,小菜给你提供

镜像可访问阿里云进行下载,如下图:下载地址

完成虚拟机的安装后我们便可在 VMware 中安装 Centos7

  • 我们选择 创建新的虚拟机

  • 选择自定义安装

典型安装:VMware会将主流的配置应用在虚拟机的操作系统上,对于新手来很友好。

自定义安装:自定义安装可以针对性的把一些资源加强,把不需要的资源移除。避免资源的浪费。

  • 兼容性一般向下兼容

  • 选择我们下载好的 centos 镜像

  • 给自己的虚拟机分配名称和安装地址,我们需要安装三台,所以名称我这里分别命名为(master、node01、node02)

  • 给自己的虚拟机分配资源,最低要求一般是 2核2G内存

  • 这里使用 NAT 网络类型

桥接:选择桥接模式的话虚拟机和宿主机在网络上就是平级的关系,相当于连接在同一交换机上。

NAT:NAT模式就是虚拟机要联网得先通过宿主机才能和外面进行通信。

仅主机:虚拟机与宿主机直接连起来

  • 接下来一直下一步,然后点击完成即可

  • 安装完后我们便可以在页面看到以下结果,点击开启此虚拟机:

  • 选择 install CentOS7

image-20210410200006452

  • 然后就可以看到安装过程:

  • 过一会便会看到让我们选择语言的界面,这里选择中文并继续

  • 软件选择我们可以选基础设施服务器,安装位置可选 自动分区

  • 然后我们需要点击 网络和主机名 进入网络配置

  • 我们在 tarbar 栏点击 编辑 -> 虚拟网络编辑器 查看虚拟机的子网IP

  • 这边我们手动自定义添加 Ipv4 的地址,DNS服务器可填阿里云的

我们分配的地址需要排除 255 和 02 这两个地址,分别是广播和网关地址。

我是这样配置的:

master 节点 : 192.168.108.100

node01 节点 :192.168.108.101

node02 节点 :192.168.108.102

  • 配置完选择保存并点击完成,然后设置主机名

我是这样配置的:

master 节点 : master

node01 节点 :node01

node02 节点 :node02

  • 完成以上配置后,大致是如下样子

  • 点击开始安装后,我们来到了以下页面,然后配置以下两个信息

完成以上配置后,重启便可以使用,其他两个节点也是同样的配置,可以直接选择克隆,网络配置和主机名 记得改~ 然后我们便得到以下配置的三个服务器:

主机名 IP 配置
master 192.168.108.100 2 核 2G内存 30G硬盘
node01 192.168.108.101 2 核 2G内存 30G硬盘
node02 192.168.108.102 2 核 2G内存 30G硬盘
2)环境配置

完成以上服务器的搭建后,我们可以利用 shell 工具 进行连接,开始搭建 k8s 环境

  • 主机名解析

为了集群节点间的直接调用,我们需要配置一下主机名解析,分别在三台服务器上编辑 /etc/hosts

  • 同步时间

集群中的时间必须要精确一致,我们可以直接使用chronyd服务从网络同步时间,三台服务器需做同样的操作

  • 禁用iptables和firewalld服务

kubernetes和docker在运行中会产生大量的iptables规则,为了不让系统规则跟它们混淆,直接关闭系统的规则。三台虚拟机需做同样操作:

1
2
3
4
5
6
shell复制代码# 1 关闭firewalld服务
[root@master ~]# systemctl stop firewalld
[root@master ~]# systemctl disable firewalld
# 2 关闭iptables服务
[root@master ~]# systemctl stop iptables
[root@master ~]# systemctl disable iptables
  • 禁用selinux

selinux是linux系统下的一个安全服务,如果不关闭它,在安装集群中会产生各种各样的奇葩问题

1
2
3
4
shell复制代码# 永久关闭
[root@master ~]# sed -i 's/enforcing/disabled/' /etc/selinux/config
# 临时关闭
[root@master ~]# setenforce 0
  • 禁用swap分区

swap分区指的是虚拟内存分区,它的作用是在物理内存使用完之后,将磁盘空间虚拟成内存来使用启用swap设备会对系统的性能产生非常负面的影响,因此kubernetes要求每个节点都要禁用swap设备但是如果因为某些原因确实不能关闭swap分区,就需要在集群安装过程中通过明确的参数进行配置说明

1
2
3
4
shell复制代码# 临时关闭
[root@master ~]# swapoff -a
# 永久关闭
[root@master ~]# vim /etc/fstab

注释掉swap分区那一行

  • 修改linux的内核参数

我们需要修改linux的内核参数,添加网桥过滤和地址转发功能,编辑/etc/sysctl.d/kubernetes.conf文件,添加如下配置:

1
2
3
shell复制代码net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1

添加后进行以下操作:

1
2
3
4
5
6
shell复制代码# 重新加载配置
[root@master ~]# sysctl -p
# 加载网桥过滤模块
[root@master ~]# modprobe br_netfilter
# 查看网桥过滤模块是否加载成功
[root@master ~]# lsmod | grep br_netfilter

同样是在三台服务器都进行操作,成功信息如下:

  • 配置 ipvs 功能

在kubernetes中service有两种代理模型,一种是基于iptables的,一种是基于ipvs的
相比较的话,ipvs的性能明显要高一些,但是如果要使用它,需要手动载入ipvs模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码# 安装ipset和ipvsadm
[root@master ~]# yum install ipset ipvsadmin -y

# 添加需要加载的模块写入脚本文件
[root@master ~]# cat <<EOF > /etc/sysconfig/modules/ipvs.modules
#!/bin/bash
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
EOF
# 为脚本文件添加执行权限
[root@master ~]# chmod +x /etc/sysconfig/modules/ipvs.modules
# 执行脚本文件
[root@master ~]# /bin/bash /etc/sysconfig/modules/ipvs.modules
# 查看对应的模块是否加载成功
[root@master ~]# lsmod | grep -e ip_vs -e nf_conntrack_ipv4

  • 完成以上配置后重启服务器
1
shell复制代码[root@master ~]# reboot
3)docker安装

第一步:

1
2
shell复制代码# 获取镜像源
[root@master ~]# wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo

第二步:

1
2
3
shell复制代码# 安装特定版本的docker-ce
# 必须指定--setopt=obsoletes=0,否则yum会自动安装更高版本
[root@master ~]# yum install --setopt=obsoletes=0 docker-ce-18.06.3.ce-3.el7 -y

第三步:

1
2
3
shell复制代码# 添加一个配置文件
# Docker在默认情况下使用的Cgroup Driver为cgroupfs,而kubernetes推荐使用systemd来代替cgroupfs
[root@master ~]# mkdir /etc/docker

第四步:

1
2
3
4
5
6
shell复制代码# 添加阿里云 yum 源, 可从阿里云容器镜像管理中复制镜像加速地址
[root@master ~]# cat <<EOF > /etc/docker/daemon.json
{
"registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"]
}
EOF

第五步:

1
2
shell复制代码# 启动docker
[root@master ~]# systemctl enable docker && systemctl start docker

完成以上5步,也就完成了 docker 的安装,离成功更近一步~

4)集群初始化

1、由于 kubernetes 的镜像源在国外,速度比较慢,因此我们需要切换成国内的镜像源

1
2
3
4
5
6
7
8
9
10
shell复制代码# 编辑 /etc/yum.repos.d/kubernetes.repo 添加一下配置
[root@master ~]# vim /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg

2、然后安装kubeadm、kubelet和kubectl 三个组件

1
2
shell复制代码yum install --setopt=obsoletes=0 kubeadm-1.17.4-0 kubelet-1.17.4-0
kubectl-1.17.4-0 -y

3、配置 kubelet 的group

1
2
3
shell复制代码# 编辑 /etc/sysconfig/kubelet,添加下面的配置
KUBELET_CGROUP_ARGS="--cgroup-driver=systemd"
KUBE_PROXY_MODE="ipvs"

4、这步是来初始化集群的,因此只需在 master 服务器上执行即可,上面那些是每个服务器都需要执行!

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码# 创建集群
# 由于默认拉取镜像地址 k8s.gcr.io 国内无法访问,这里指定阿里云镜像仓库地址
[root@master ~]# kubeadm init \
--apiserver-advertise-address=192.168.108.100 \
--image-repository registry.aliyuncs.com/google_containers \
--kubernetes-version=v1.17.4 \
--pod-network-cidr=10.244.0.0/16 \
--service-cidr=10.96.0.0/12

#使用 kubectl 工具
[root@master ~]# mkdir -p $HOME/.kube
[root@master ~]# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
[root@master ~]# sudo chown $(id -u):$(id -g) $HOME/.kube/config

然后我们需要将node 节点加入集群中,在 node 服务器 上执行上述红框的命令:

1
2
shell复制代码[root@master ~]# kubeadm join 192.168.108.100:6443 --token xxx \ 
--discovery-token-ca-cert-hash sha256:xxx

便可在 master 节点 获取到节点信息:

但是我们这个时候查看集群状态都是为NotReady,这是因为还没有配置网络插件

5、安装网络插件

kubernetes支持多种网络插件,比如flannel、calico、canal等等,这里选择使用flanne

下载 flanneld-v0.13.0-amd64.docker :下载地址

下载完成后,上传至 master 服务器 执行以下命令

1
shell复制代码docker load < flanneld-v0.13.0-amd64.docker

执行完成后便可看到多了个 flannel 镜像:

然后我们需要获取flannel的配置文件来部署 flannel 服务

1
2
3
4
5
6
7
shell复制代码[root@master ~]# wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

# 使用配置文件启动fannel
[root@master ~]# kubectl apply -f kube-flannel.yml

# 再次查看集群节点的状态
[root@master ~]# kubectl get nodes

这个时候所有节点的状态都是Ready 的状态,到此为止,我们的 k8s 集群就算搭建完成了!

5)集群功能验证

接下来就是我们的验证时间,之前我们学 docker 的时候往往会启动一个 nginx 容器来测试是否可用,k8s 我们也同样来部署一个 nginx 来测试下服务是否可用~

(下面例子为测试例子,如果不清楚每个指令的作用也不要紧,后面我们会出篇 k8s 的教学文章来说明 k8s 如果使用!)

  • 首先我们创建一个 deployment
1
2
3
4
5
6
shell复制代码[root@master ~]# kubectl create deployment nginx --image=nginx:1.14-alpine
deployment.apps/nginx created

[root@master ~]# kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 31s
  • 然后创建一个 service 来让外界能够访问到我们 nginx 服务
1
2
3
4
5
6
shell复制代码[root@master ~]# kubectl expose deploy nginx --port=80 --target-port=80 --type=NodePort
service/nginx exposed

[root@master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx NodePort 10.110.224.214 <none> 80:31771/TCP 5s

然后我们通过 node 节点的 IP 加上service 暴露出来的 nodePort 来访问我们的 nginx 服务:

也可以直接在集群中通过 service 的 IP 加上映射出来的 port 来访问我们的服务:

从结果上看两种访问都是可用的,说明我们的 nginx 服务部署成功,不妨点个关注助助兴~

公众号搜索:小菜良记

更多干货值得阅读哦!

那么为什么我们可以访问到 nginx?我们不妨结合上面说到的 k8s 组件来梳理一下各个组件的调用关系:

  1. kubernetes 启动后,无论是 master 节点 亦或者 node 节点,都会将自身的信息存储到 etcd 数据库中
  2. 创建 nginx 服务,首先会将安装请求发送到 master 节点上的 apiServer 组件中
  3. apiServer 组件会调用 scheduler 组件来决定应该将该服务安装到哪个 node 节点上。这个时候就需要用到 etcd 数据库了,scheduler会从 etcd 中读取各个 node 节点的信息,然后按照一定的算法进行选择,并将结果告知给 apiServer
  4. apiServer 调用 controllerManager 去调度 node 节点,并安装 nginx 服务
  5. node 节点上的 kubelet 组件接收到指令后,会通知docker,然后由 docker 来启动一个 nginx 的pod

pod 是 kubernetes 中的最小操作单元,容器都是跑在 pod 中
6. 以上步骤完成后,nginx 服务便运行起来了,如果需要访问 nginx,就需要通过 kube-proxy 来对 pod 产生访问的代理,这样外部用户就能访问到这个 nginx 服务

以上便是运行一个服务的全过程,不知道看完之后有没有一种 肃然起敬 的感觉,设计是在太巧妙了,因此到这里,难道就不准备看 k8s 使用下文!如果准备看的话,小手将关注点起来哦!

END

以上便是 k8s 集群的搭建过程,有了 k8s 的环境,你还怕学不会 k8s 的使用吗!在自己的虚拟机上尽情折腾,弄坏了也就一个恢复快照的事~ 我是小菜,路漫漫,与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

本文转载自: 掘金

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

Cloudreve 自建云盘实践,我说了没人能限得了我的容量

发表于 2021-04-12

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

为啥要用自建网盘,市面上的云盘不香了?

每一个用户需求的背后都是因为有场景存在,而这些差异化的场景也都是因为不同的用户类型产生的。

就像我作为技术号主想分享一些自己总结的资料,放到一些云盘以后有时候会被其他不知道从哪冒出来的小伙伴给举报,举报链接就取消了,取消了链接也就影响了我的资料分享。同时我可能还希望我的分享内容能被记录到下载次数、允许几次下载、下载时是否要做一些引流动作等等。

所以类似这样的特殊场景下就需要自建网盘来维护个人需要的资料,与之类似的还有一些公司或者组织都会建相对私域的网盘功能服务功能,给予内部用户使用。

所以,也并不一定市面的网盘不香了,只是因为我有需要自建网盘。在这条路上我尝试过自建、kodexplorer、Owncloud等,恰巧最近发现了 Cloudreve 尝试体验后感觉更香,支持的功能更多。所以准备给小伙伴分享下关于 Cloudreve 的安装、配置和使用,也让有需要的小伙伴可以尝尝鲜。

二、Cloudreve 介绍

Cloudreve,帮助您以最低的成本快速搭建公私兼备的网盘系统。

🔉 功能

✨ 特性

  • ☁️ 支持本机、从机、七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive (包括世纪互联版) 作为存储端
  • 📤 上传/下载 支持客户端直传,支持下载限速
  • 💾 可对接 Aria2 离线下载
  • 📚 在线 压缩/解压缩、多文件打包下载
  • 💻 覆盖全部存储策略的 WebDAV 协议支持
  • ⚡ 拖拽上传、目录上传、流式上传处理
  • 🗃️ 文件拖拽管理
  • 👩‍👧‍👦 多用户、用户组
  • 🔗 创建文件、目录的分享链接,可设定自动过期
  • 👁️‍🗨️ 视频、图像、音频、文本、Office 文档在线预览
  • 🎨 自定义配色、黑暗模式、PWA 应用、全站单页应用
  • 🚀 All-In-One 打包,开箱即用

📌 资料

  1. 官网:cloudreve.org
  2. 文档:docs.cloudreve.org/getting-sta…
  3. 社区:forum.cloudreve.org
  4. 源码:github.com/cloudreve/C…
  5. 演示:demo.cloudreve.org

三、环境准备

  1. 云服务器资源或本地服务器,推荐腾讯云轻量服务器,内含宝塔组件,算是是几个云服务里最简单的:console.cloud.tencent.com/lighthouse/…
  2. 已备案过的域名,如果不需要域名访问,可以直接使用云服务提供的公网IP
  3. Cloudreve安装包:github.com/cloudreve/C…

本章节的案例是基于腾讯云的,如果你使用的是其他云服务器,找到对应的位置配置即可。这些云服务使用方式基本大同小异,遇到问题可以联系对应的云服务客服,不要联系我哈哈哈😄

四、宝塔配置

宝塔是一个简单好用的Linux/Windows服务器运维管理面板,在宝塔后台页面上可以非常方便的安全软件和配置环境。一般可以在云服务器上安装宝塔,有一些厂商也把宝塔集成到自己的云服务器上了。

1. 获取用户名和密码

  • 地址:console.cloud.tencent.com/lighthouse/…
  • 进入服务的应用管理会看到应用内软件信息:宝塔,在这里点击登录按钮后,会获取到宝塔的登录地址、用户名和密码信息「这些信息可以后期在宝塔后台修改」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码 * Socket connection established *
Last login: Sat Apr 10 09:33:50 2021 from 119.29.96.147
[lighthouse@VM-8-9-centos ~]$ sudo /etc/init.d/bt default
==================================================================
BT-Panel default info!
==================================================================
外网面板地址: http://80.71.255.122:8888/cloudtencent
内网面板地址: http://10.0.8.9:8888/cloudtencent
*以下仅为初始默认账户密码,若无法登录请执行bt命令重置账户/密码登录
username: 3kkjecc3
password: 3f7d2743018b
If you cannot access the panel,
release the following panel port [8888] in the security group
若无法访问面板,请检查防火墙/安全组是否有放行面板[8888]端口
==================================================================

2. 8888 端口授权

  • 在获取到面板的用户名和密码后,还不能直接访问,因为你的端口还没有授权开通。
  • 这时可以在云服务平台上,点击防火墙这个配置,添加 8888 端口。

3. 登录宝塔后台

地址:http://80.71.255.122:8888/cloudtencent - 你需要更换为自己的地址
说明:在初次进入宝塔时会有一些提示和软件安装,选择自己需要的安装即可。
页面:

五、服务安装

在宝塔面板的左侧菜单栏有一个终端菜单,点击进入是一个黑窗口,接下来我们就在这里安装整个服务。

1. 在宝塔终端查看服务内核

因为不同云服务下可能是 adm 或者 arm 架构,对应下载的 Cloudreve 也会有所不同 cloudreve_版本号_操作系统_CPU架构.tar.gz,所以这里我们需要使用 arch 命令查看下服务信息。

1
2
3
4
5
java复制代码Last failed login: Sat Apr 10 11:38:41 CST 2021 from 194.165.16.68 on ssh:notty
There were 8 failed login attempts since the last successful login.
Last login: Sat Apr 10 09:57:33 2021 from 127.0.0.1
[root@VM-8-9-centos ~]# arch
x86_64
  • x86_64:代表 amd64
  • aarch64:代表 arm64

2. 下载和安装

确定好我们的云服务架构后,选择对应的 Cloudreve 版本,复制地址。我的是:github.com/cloudreve/C…

安装命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码mkdir /www/wwwroot/cloudreve    # 创建一个新文件夹存放程序
cd /www/wwwroot/cloudreve # 进入这个文件夹
wget https://github.com/cloudreve/Cloudreve/releases/download/3.3.1/cloudreve_3.3.1_linux_amd64.tar.gz # 下载你复制的链接
tar -zxvf cloudreve_3.3.1_linux_amd64.tar.gz # 解压获取到的主程序
chmod +x ./cloudreve # 赋予执行权限
./cloudreve # 启动 Cloudreve

# 运行信息截取
[Info] 2021-04-10 10:39:59 初始化数据库连接
[Info] 2021-04-10 10:39:59 开始进行数据库初始化...
[Info] 2021-04-10 10:39:59 初始管理员账号:admin@cloudreve.org
[Info] 2021-04-10 10:39:59 初始管理员密码:U4BfStlm
[Info] 2021-04-10 10:40:00 数据库初始化结束
[Info] 2021-04-10 10:40:00 初始化任务队列,WorkerNum = 10
[Info] 2021-04-10 10:40:00 初始化定时任务...
[Info] 2021-04-10 10:40:00 当前运行模式:Master
[Info] 2021-04-10 10:40:00 开始监听 :5212
  • wget,替换为你的 Cloudreve 地址
  • tar,是对应名称一起替换
  • 最后把这些命令复制到你的终端黑窗口,它就开始运行安装了。安装完成以后你会得到一个初始的用户名和密码,复制粘贴保存起来

3. 开放端口 5212

  • Cloudreve 安装完成以后,访问地址为你的服务IP:5212,但此时5212并不能直接访问还需要授权。
  • 仅在宝塔后台授权还不够,还需要在云服务平台的防火墙进行授权,如下:

4. 登录服务

  • 地址:http://80.71.255.122:5212

  • 如果一切顺利现在你就可以使用自己的网盘了,但有一点要知道如果你还需要设置域名,那么这个时候先不要使用,先去设置域名,否则一些图片在IP下上传和在域名下上传,分享是有问题的。

六、进程守护

其实在服务安装完成后就已经可以正常使用了,但我们很难保证宝塔面板不被重启或者出现异常时也难免要我们自己再启动云盘服务。那么,就需要一个守护进程来自动重启服务。

在宝塔面板的软件商店中,找到 Supervisor 安装。Supervisor是用Python开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台daemon,并监控进程状态,异常退出时能自动重启。

1. Supervisor 配置

  • 名称:Cloudreve
  • 启动用户:root 默认的
  • 运行目录:/www/wwwroot/cloudreve/
  • 启动命令:/www/wwwroot/cloudreve/cloudreve

2. Supervisor 启动

  • 配置守护进程后,点开宝塔面板右上角的重启,进入后重启服务
  • 重启后再进入到宝塔面板就会看到守护进程已经在启动了,现在启动这个事就交给了 Supervisor 管理

七、配置域名

1. 解析域名

  • 在配置域名之前,需要在你已经准备好的域名下配置一个A记录解析,这样后面才能配置反向代理。

2. 反向代理

  • 点击宝塔面板左侧菜单中的网站按钮,添加一个站点。站点里的域名就是配置解析域名时的信息,我的是pan.itedus.cn
  • 配置完站点后就需要给这个站点设置一个反向代理,点击它的设置即可进入。在反向代理中添加并设置目标URL:127.0.0.1:5212
  • 最后,如果你的域名已经解析完成,那么现在你就可以通过域名访问你的云盘服务了,还可以上传和分享文件。例如我分享的文件:pan.itedus.cn/s/qofO

八、数据库切换

系统默认的数据库是自带的 SQLite,你可改为 Mysql,如下:

  1. 数据库类型,目前支持 sqlite | mysql
    Type = mysql
  2. 用户名
    User = Cloudreve
  3. 密码
    Password = Cloudreve
  4. 数据库地址
    Host = 127.0.0.1
  5. 数据库名称
    Name = Cloudreve
  6. 数据表前缀
    TablePrefix = cd_
  • 切换完记得使用命令的方式进行重启,因为此时它需要重新创建账号和密码
  • 如果你没有看见账号和密码,那么可以把创建的数据库删掉,重新来一次

九、总结

  • 关于 Cloudreve 云盘的安装和使用就演示到这里了,如果你感兴趣也可以自己搭建一个。另外 Cloudreve 可以获取到它的源码,在源码的基础上可以添加一些想要的功能,比如在下载的时候设置为关注某些东西在下载等等。
  • 除了 Cloudreve 云盘还可以尝试下有道云,这个云盘直接在简单的服务器上就可以直接安装,也可以自动升级,使用起来会简单一些。
  • 无论是云服务还是各类工具,多尝试一些这样的东西,可以给自己增加很多其他知识面的理解。也许弄着弄着,你就不只是一个简单的CRUD开发工程师了,可能还是运维、产品、业务!

十、系列推荐

  • 另外一种可道云网盘的搭建,也很不错
  • 一天建4个,小傅哥教你搭博客!
  • 为了省钱,我用1天时间把PHP学了!
  • Github被攻击。我的GitPage博客也挂了,紧急修复之路,也教会你搭建 Jekyll 博客!
  • Netty+JavaFx实战:仿桌面版微信聊天

本文转载自: 掘金

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

软件测试:测试一个网站

发表于 2021-04-11

一、软件测试的原则
1、软件测试应尽早执行,并贯穿于整个软件生命周期
2、软件测试应追溯需求
3、测试应由第三方来构造
4、穷举测试是不可能的,要遵循 Good-enough 原则
5、必须确定预期输出(或结果)
6、必须彻底检查每个测试结果
7、充分注意测试中的群集现象
8、缺陷的二八定理
9、严格执行测试计划,排除测试的随意性
10、注意合法合理的输入,也要注意非法的非预期的输入
11、检查程序是否做了不该做的
12、测试应从“小规模”开始,逐步转向“大规模”
13、反复使用同样的测试会使软件具有抵抗力
14、关注缺陷的修复

二、网站测试的步骤
1、文档阅读查看
查找需求说明、网站设计等相关文档,分析测试需求。
2、测试计划的制定
功能性测试;界面测试;性能测试;数据库测试;安全性测试;兼容性测试
3、测试用例的设计
等价类划分方法
边界值分析方法
错误推测方法
因果图方法
判定表驱动分析方法
正交实验设计方法
功能图分析方法
4、接口测试和性能测试
接口测试和性能测试可以根据接口文档,然后使用相应的的工具进行测试。
接口测试工具:apipost

01010.png
性能测试工具:jmeter
5、安全测试
————————————————
版权声明:本文为CSDN博主「海淀码农」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:blog.csdn.net/phpwechat/a…

本文转载自: 掘金

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

Golang 5分钟读懂GMP并发模型

发表于 2021-04-11

进程与线程与协程goroutine

多个线程属于同一个进程并共享内存空间,线程之间的通讯基于共享的内存进行。

Go语言的调度器使用与CPU数量相等的线程来调度多个Goroutine。

进程、线程存在问题:

  1. CPU高消耗
    • 切换线程上下文需要申请、销毁资源消耗时间高
  2. 内存高占用
    • 线程占用1M以上的内存空间

协程(Goroutine)的优点:

  1. 占用的内存更小(几kb)
    • 初始为2kb,如果栈空间不足则自动扩容
  2. 调度更灵活(runtime调度)
    • Go自己实现的调度器,创建和销毁的消耗非常小,是用户级。
  3. 抢占式调度(10ms)
    • 编译器插入抢占指令,函数调用时检查当前Goroutine是否发起抢占请求
  4. 1.14版本后支持基于信号的异步抢占(20ms)
    • 垃圾回收扫描栈时触发抢占调度
    • 解决抢占式调度因垃圾回收和循环长时间占用资源(无法执行抢占指令)导致程序暂停

GMP并发模型

GM老版调度器:

  1. 激烈的锁竞争
    • 从全局队列中获取G,需要加锁
  2. 局部性差
    • 比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
  3. 系统开销大
    • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。
GM

GMP新版调度器

在这里插入图片描述

G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G,M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。

G: 取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值

M: 取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人

P:取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。

  1. 解决GM老版调度器的问题
  2. M(线程):N(协程)关系
* 创建 M 个线程(CPU 执行调度的单位),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行。
* 在同一时刻,一个线程上只能跑一个 goroutine。当 goroutine 发生阻塞时,runtime 会把当前 goroutine 调度走,让其他 goroutine 来执行。
  1. 任务偷取(work stealing)
* 全局队列已经没有 G,那 m 就要执行 work stealing (偷取):从其他有 G 的 P 哪里偷取一半 G 过来,放到自己的 P 本地队列
  1. 让出执行权(hand off)
* 某个G堵塞,线程释放绑定的P,把P转移给其它空闲线程

go func() 执行过程

go func

  1. go关键字创建一个goroutine入队,如果本地P队列满了则入队全局G队列
  2. 从P队列中队头的G交给M执行
  3. P有两个关键特性
    1. work stealing
    2. hand off

本文转载自: 掘金

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

学会这10种定时任务,我有点飘了 前言 一 linux自带

发表于 2021-04-11

前言

最近有几个读者私信给我,问我他们的业务场景,要用什么样的定时任务。确实,在不用的业务场景下要用不同的定时任务,其实我们的选择还是挺多的。我今天给大家总结10种非常实用的定时任务,总有一种是适合你的。

一. linux自带的定时任务

crontab

不知道你有没有遇到过这种场景:有时需要临时统计线上的数据,然后导出到excel表格中。这种需求有时较为复杂,光靠写sql语句是无法满足需求的,这就需要写java代码了。然后将该程序打成一个jar包,在线上环境执行,最后将生成的excel文件下载到本地。

为了减小对线上环境的影响,我们一般会选择在凌晨1-2点,趁用户量少的时候,执行统计程序。(其实凌晨4点左右,用户才是最少的)

由于时间太晚了,我们完全没必要守在那里等执行结果,一个定时任务就能可以搞定。

那么,这种情况用哪种定时任务更合适呢?

答案是:linux系统的crontab。(不过也不排除有些项目没部署在linux系统中)

运行crontab -e,可以编辑定时器,然后加入如下命令:

1
bash复制代码0 2 * * * /usr/local/java/jdk1.8/bin/java -jar /data/app/tool.jar > /logs/tool.log &

就可以在每天凌晨2点,定时执行tool.jar程序,并且把日志输出到tool.log文件中。当然你也可以把后面的执行java程序的命令写成shell脚本,更方便维护。

使用这种定时任务支持方便修改定时规则,有界面可以统一管理配置的各种定时脚本。

crontab命令的基本格式如下:

1
css复制代码crontab [参数] [文件名]

如果没有指定文件名,则接收键盘上输入的命令,并将它载入到crontab。

参数功能对照表如下:

image.png

以上参数,如果没有使用-u指定用户,则默认使用的当前用户。

通过crontab -e命令编辑文件内容,具体语法如下:

1
css复制代码[分] [小时] [日期] [月] [星期] 具体任务

其中:

  • 分,表示多少分钟,范围:0-59
  • 小时,表示多少小时,范围:0-23
  • 日期,表示具体在哪一天,范围:1-31
  • 月,表示多少月,范围:1-12
  • 星期,表示多少周,范围:0-7,0和7都代表星期日

还有一些特殊字符,比如:

  • *代表如何时间,比如:*1*** 表示每天凌晨1点执行。
  • /代表每隔多久执行一次,比如:*/5 **** 表示每隔5分钟执行一次。
  • ,代表支持多个,比如:10 7,9,12 *** 表示在每天的7、9、12点10分各执行一次。
  • -代表支持一个范围,比如:10 7-9 *** 表示在每天的7、8、9点10分各执行一次。

此外,顺便说一下crontab需要crond服务支持,crond是linux下用来周期地执行某种任务的一个守护进程,在安装linux操作系统后,默认会安装crond服务工具,且crond服务默认就是自启动的。crond进程每分钟会定期检查是否有要执行的任务,如果有,则会自动执行该任务。

可以通过以下命令操作相关服务:

1
2
3
4
5
arduino复制代码service crond status // 查看运行状态
service crond start //启动服务
service crond stop //关闭服务
service crond restart //重启服务
service crond reload //重新载入配置

使用crontab的优缺点:

  • 优点:方便修改定时规则,支持一些较复杂的定时规则,通过文件可以统一管理配好的各种定时脚本。
  • 缺点:如果定时任务非常多,不太好找,而且必须要求操作系统是linux,否则无法执行。

二. jdk自带的定时任务

1.Thread

各位亲爱的朋友,你没看错,Thread类真的能做定时任务。如果你看过一些定时任务框架的源码,你最后会发现,它们的底层也会使用Thread类。

实现这种定时任务的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public static void init() {
new Thread(() -> {
while (true) {
try {
System.out.println("doSameThing");
Thread.sleep(1000 * 60 * 5);
} catch (Exception e) {
log.error(e);
}
}
}).start();
}

使用Thread类可以做最简单的定时任务,在run方法中有个while的死循环(当然还有其他方式),执行我们自己的任务。有个需要特别注意的地方是,需要用try...catch捕获异常,否则如果出现异常,就直接退出循环,下次将无法继续执行了。

这种方式做的定时任务,只能周期性执行,不能支持定时在某个时间点执行。

此外,该线程可以定义成守护线程,在后台默默执行就好。

使用场景:比如项目中有时需要每隔10分钟去下载某个文件,或者每隔5分钟去读取模板文件生成静态html页面等等,一些简单的周期性任务场景。

使用Thread类的优缺点:

  • 优点:这种定时任务非常简单,学习成本低,容易入手,对于那些简单的周期性任务,是个不错的选择。
  • 缺点:不支持指定某个时间点执行任务,不支持延迟执行等操作,功能过于单一,无法应对一些较为复杂的场景。

2.Timer

Timer类是jdk专门提供的定时器工具,用来在后台线程计划执行指定任务,在java.util包下,要跟TimerTask一起配合使用。

图片

Timer类其实是一个任务调度器,它里面包含了一个TimerThread线程,在这个线程中无限循环从TaskQueue中获取TimerTask(该类实现了Runnable接口),调用其run方法,就能异步执行定时任务。我们需要继承TimerTask类,实现它的run方法,在该方法中加上自己的业务逻辑。

实现这种定时任务的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public class TimerTest {

public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("doSomething");
}
},2000,1000);
}
}

先实例化一个Timer类,然后调用它的schedule方法,在该方法中实例化TimerTask类,业务逻辑写在run方法中。schedule方法最后的两次参数分别表示:延迟时间 和 间隔时间,单位是毫秒。上面例子中,设置的定时任务是每隔1秒执行一次,延迟2秒执行。

主要包含6个方法:

  • schedule(TimerTask task, Date time), 指定任务task在指定时间time执行
  • schedule(TimerTask task, long delay), 指定任务task在指定延迟delay后执行
  • schedule(TimerTask task, Date firstTime,long period),指定任务task在指定时间firstTime执行后,进行重复固定延迟频率peroid的执行
  • schedule(TimerTask task, long delay, long period), 指定任务task 在指定延迟delay 后,进行重复固定延迟频率peroid的执行
  • scheduleAtFixedRate(TimerTask task,Date firstTime,long period), 指定任务task在指定时间firstTime执行后,进行重复固定延迟频率peroid的执行
  • scheduleAtFixedRate(TimerTask task, long delay, long period), 指定任务task 在指定延迟delay 后,进行重复固定延迟频率peroid的执行

不过使用Timer实现定时任务有以下问题:

  1. 由于Timer是单线程执行任务,如果其中一个任务耗时非常长,会影响其他任务的执行。
  2. 如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行。

使用Timer类的优缺点:

  • 优点:非常方便实现多个周期性的定时任务,并且支持延迟执行,还支持在指定时间之后支持,功能还算强大。
  • 缺点:如果其中一个任务耗时非常长,会影响其他任务的执行。并且如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行,所以阿里巴巴开发者规范中不建议使用它。

3.ScheduledExecutorService

ScheduledExecutorService是JDK1.5+版本引进的定时任务,该类位于java.util.concurrent并发包下。

ScheduledExecutorService是基于多线程的,设计的初衷是为了解决Timer单线程执行,多个任务之间会互相影响的问题。

它主要包含4个方法:

  • schedule(Runnable command,long delay,TimeUnit unit),带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至任务执行完毕。
  • schedule(Callable<V> callable,long delay,TimeUnit unit),带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果。
  • scheduleAtFixedRate,表示以固定频率执行的任务,如果当前任务耗时较多,超过定时周期period,则当前任务结束后会立即执行。
  • scheduleWithFixedDelay,表示以固定延时执行任务,延时是相对当前任务结束为起点计算开始时间。

实现这种定时任务的具体代码如下:

1
2
3
4
5
6
7
8
9
typescript复制代码public class ScheduleExecutorTest {

public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("doSomething");
},1000,1000, TimeUnit.MILLISECONDS);
}
}

调用ScheduledExecutorService类的scheduleAtFixedRate方法实现周期性任务,每隔1秒钟执行一次,每次延迟1秒再执行。

这种定时任务是阿里巴巴开发者规范中用来替代Timer类的方案,对于多线程执行周期性任务,是个不错的选择。

ScheduledExecutorService的优缺点:

  • 优点:基于多线程的定时任务,多个任务之间不会相关影响,支持周期性的执行任务,并且带延迟功能。
  • 缺点:不支持一些较复杂的定时规则。

三. spring支持的定时任务

1.spring task

spring task是spring3以上版本自带的定时任务,实现定时任务的功能时,需要引入spring-context包,目前它支持:xml 和 注解 两种方式。

1. 项目实战

由于xml方式太古老了,我们以springboot项目中注解方式为例。

第一步,在pom.xml文件中引入spring-context相关依赖。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>

第二步,在springboot启动类上加上@EnableScheduling注解。

1
2
3
4
5
6
7
8
less复制代码@EnableScheduling
@SpringBootApplication
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}

第三步,使用@Scheduled注解定义定时规则。

1
2
3
4
5
6
7
8
kotlin复制代码@Service
public class SpringTaskTest {

@Scheduled(cron = "${sue.spring.task.cron}")
public void fun() {
System.out.println("doSomething");
}
}

第四步,在applicationContext.properties文件中配置参数:

1
ini复制代码sue.spring.task.cron=*/10 * * * * ?

这样就能每隔10秒执行一次fun方法了。

2. cron规则

spring4以上的版本中,cron表达式包含6个参数:

1
css复制代码[秒] [分] [时] [日期] [月] [星期]

还支持几个常用的特殊符号:

  • *:表示任何时间触发任务
  • ,:表示指定的时间触发任务
  • -:表示一段时间内触发任务
  • /:表示从哪一个时刻开始,每隔多长时间触发一次任务。
  • ?:表示用于月中的天和周中的天两个子表达式,表示不指定值。

cron表达式参数具体含义:

  1. 秒,取值范围:0-59,支持*、,、-、/。
  2. 分,取值范围:0-59,支持*、,、-、/。
  3. 时,取值范围:0-23,支持*、,、-、/。
  4. 日期,取值范围:1-31,支持*、,、-、/。比秒多了?,表示如果指定的星期触发了,则配置的日期变成无效。
  5. 月,取值范围:1-12,支持*、,、-、/。
  6. 星期,取值范围:1~7,1代表星期天,6代表星期六,其他的以此类推。支持*、,、-、/、?。比秒多了?,表示如果指定的日期触发了,则配置的星期变成无效。

常见cron表达式使用举例:

  • 0 0 0 1 * ? 每月1号零点执行
  • 0 0 2 * * ? 每天凌晨2点执行
  • 0 0 2 * * ? 每天凌晨2点执行
  • 0 0/5 11 * * ? 每天11点-11点55分,每隔5分钟执行一次
  • 0 0 18 ? * WED 每周三下午6点执行

spring task先通过ScheduledAnnotationBeanPostProcessor类的processScheduled方法,解析和收集Scheduled注解中的参数,包含:cron表达式。

然后在ScheduledTaskRegistrar类的afterPropertiesSet方法中,默认初始化一个单线程的ThreadPoolExecutor执行任务。

对spring task感兴趣的小伙伴,可以加我微信找我私聊。

使用spring task的优缺点:

  • 优点:spring框架自带的定时功能,springboot做了非常好的封装,开启和定义定时任务非常容易,支持复杂的cron表达式,可以满足绝大多数单机版的业务场景。单个任务时,当前次的调度完成后,再执行下一次任务调度。
  • 缺点:默认单线程,如果前面的任务执行时间太长,对后面任务的执行有影响。不支持集群方式部署,不能做数据存储型定时任务。

2.spring quartz

quartz是OpenSymphony开源组织在Job scheduling领域的开源项目,是由java开发的一个开源的任务日程管理系统。

quartz能做什么?

  • 作业调度:调用各种框架的作业脚本,例如shell,hive等。
  • 定时任务:在某一预定的时刻,执行你想要执行的任务。

架构图如下:

图片

quartz包含的主要接口如下:

  • Scheduler 代表调度容器,一个调度容器中可以注册多个JobDetail和Trigger。
  • Job 代表工作,即要执行的具体内容。
  • JobDetail 代表具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容。
  • JobBuilder 用于定义或构建JobDetail实例。
  • Trigger 代表调度触发器,决定什么时候去调。
  • TriggerBuilder 用于定义或构建触发器。
  • JobStore 用于存储作业和任务调度期间的状态。

1. 项目实战

我们还是以springboot集成quartz为例。

第一步,在pom.xml文件中引入quartz相关依赖。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

第二步,创建真正的定时任务执行类,该类继承QuartzJobBean。

1
2
3
4
5
6
7
scala复制代码public class QuartzTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String userName = (String) context.getJobDetail().getJobDataMap().get("userName");
System.out.println("userName:" + userName);
}
}

第三步,创建调度程序JobDetail和调度器Trigger。

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
kotlin复制代码@Configuration
public class QuartzConfig {
@Value("${sue.spring.quartz.cron}")
private String testCron;

/**
* 创建定时任务
*/
@Bean
public JobDetail quartzTestDetail() {
JobDetail jobDetail = JobBuilder.newJob(QuartzTestJob.class)
.withIdentity("quartzTestDetail", "QUARTZ_TEST")
.usingJobData("userName", "susan")
.storeDurably()
.build();
return jobDetail;
}

/**
* 创建触发器
*/
@Bean
public Trigger quartzTestJobTrigger() {
//每隔5秒执行一次
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(testCron);

//创建触发器
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(quartzTestDetail())
.withIdentity("quartzTestJobTrigger", "QUARTZ_TEST_JOB_TRIGGER")
.withSchedule(cronScheduleBuilder)
.build();
return trigger;
}
}

第四步,在applicationContext.properties文件中配置参数:

1
ini复制代码sue.spring.quartz.cron=*/5 * * * * ?

这样就能每隔5秒执行一次QuartzTestJob类的executeInternal方法了。

CronTrigger配置格式:

1
css复制代码[秒] [分] [小时] [日] [月] [周] [年]

spring quartz跟spring task的cron表达式规则基本一致,只是spring4以上的版本去掉了后面的年,而quartz的CronTrigger的年是非必填的,这里我就不做过多介绍了。

使用spring quartz的优缺点:

  • 优点:默认是多线程异步执行,单个任务时,在上一个调度未完成时,下一个调度时间到时,会另起一个线程开始新的调度,多个任务之间互不影响。支持复杂的cron表达式,它能被集群实例化,支持分布式部署。
  • 缺点:相对于spring task实现定时任务成本更高,需要手动配置QuartzJobBean、JobDetail和Trigger等。需要引入了第三方的quartz包,有一定的学习成本。不支持并行调度,不支持失败处理策略和动态分片的策略等。

四. 分布式定时任务

1.xxl-job

xxl-job是大众点评(许雪里)开发的一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

xxl-job框架对quartz进行了扩展,使用mysql数据库存储数据,并且内置jetty作为RPC服务调用。

主要特点如下:

  1. 有界面维护定时任务和触发规则,非常容易管理。
  2. 能动态启动或停止任务
  3. 支持弹性扩容缩容
  4. 支持任务失败报警
  5. 支持动态分片
  6. 支持故障转移
  7. Rolling实时日志
  8. 支持用户和权限管理

管理界面:

图片

整体架构图如下:图片

使用quartz架构图如下:

图片

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

项目实战

xxl-admin管理后台部署和mysql脚本执行等这些前期准备工作,我就不过多介绍了,有需求的朋友可以找我私聊,这些更偏向于运维的事情。

假设前期工作已经OK了,接下来我们需要:

第一步,在pom.xml文件中引入xxl-job相关依赖。

1
2
3
4
xml复制代码<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>

第二步,在applicationContext.properties文件中配置参数:

1
2
3
4
yaml复制代码xxl.job.admin.address: http://localhost:8088/xxl-job-admin/
xxl.job.executor.appname: xxl-job-executor-sample
xxl.job.executor.port: 8888
xxl.job.executor.logpath: /data/applogs/xxl-job/

第三步,创建HelloJobHandler类继承IJobHandler类:

1
2
3
4
5
6
7
8
9
10
scala复制代码@JobHandler(value = "helloJobHandler")
@Component
public class HelloJobHandler extends IJobHandler {

@Override
public ReturnT<String> execute(String param) {
System.out.println("XXL-JOB, Hello World.");
return SUCCESS;
}
}

这样定时任务就配置好了。

建议把定时任务单独部署到另外一个服务中,跟api服务分开。根据我以往的经验,job大部分情况下,会对数据做批量操作,如果操作的数据量太大,可能会对服务的内存和cpu资源造成一定的影响。

使用xxl-job的优缺点:

  • 优点:有界面管理定时任务,支持弹性扩容缩容、动态分片、故障转移、失败报警等功能。它的功能非常强大,很多大厂在用,可以满足绝大多数业务场景。
  • 缺点:和quartz一样,通过数据库分布式锁,来控制任务不能重复执行。在任务非常多的情况下,有一些性能问题。

2.elastic-job

elastic-job是当当网开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片。它是专门为高并发和复杂业务场景开发。

elastic-job目前是apache的shardingsphere项目下的一个子项目,官网地址:shardingsphere.apache.org/elasticjob/…

elastic-job在2.x之后,出了两个产品线:Elastic-Job-Lite和Elastic-Job-Cloud,而我们一般使用Elastic-Job-Lite就能够满足需求。Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务,外部仅依赖于Zookeeper。。

主要特点如下:

  • 分布式调度协调
  • 弹性扩容缩容
  • 失效转移
  • 错过执行作业重触发
  • 作业分片一致性,保证同一分片在分布式环境中仅一个执行实例
  • 自诊断并修复分布式不稳定造成的问题
  • 支持并行调度

整体架构图:

图片

项目实战

第一步,在pom.xml文件中引入elastic-job相关依赖。

1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
</dependency>

第二步,增加ZKConfig类,配置zookeeper:

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Configuration
@ConditionalOnExpression("'${zk.serverList}'.length() > 0")
public class ZKConfig {

@Bean
public ZookeeperRegistryCenter registry(@Value("${zk.serverList}") String serverList,
@Value("${zk.namespace}") String namespace) {
return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));
}

}

第三步,定义一个类实现SimpleJob接口:

1
2
3
4
5
6
7
8
typescript复制代码public class TestJob implements SimpleJob {

@Override
public void execute(ShardingContext shardingContext){
System.out.println("ShardingTotalCount:"+shardingContext.getShardingTotalCount());
System.out.println("ShardingItem:"+shardingContext.getShardingItem());
}
}

第四步,增加JobConfig配置任务:

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
typescript复制代码@Configuration
public class JobConfig {
@Value("${sue.spring.elatisc.cron}")
private String testCron;
@Value("${sue.spring.elatisc.itemParameters}")
private String shardingItemParameters;
@Value("${sue.spring.elatisc.jobParameters}")
private String jobParameters =;
@Value("${sue.spring.elatisc.shardingTotalCount}")
private int shardingTotalCount;

@Autowired
private ZookeeperRegistryCenter registryCenter;

@Bean
public SimpleJob testJob() {
return new TestJob();
}

@Bean
public JobScheduler simpleJobScheduler(final SimpleJob simpleJob) {
return new SpringJobScheduler(simpleJob, registryCenter, getConfiguration(simpleJob.getClass(),
cron, shardingTotalCount, shardingItemParameters, jobParameters));
}

private geConfiguration getConfiguration(Class<? extends SimpleJob> jobClass,String cron,int shardingTotalCount,String shardingItemParameters,String jobParameters) {
JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), testCron, shardingTotalCount).
shardingItemParameters(shardingItemParameters).jobParameter(jobParameters).build();
SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());
LiteJobConfiguration jobConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();
return jobConfig;
}
}

其中:

  • cron:cron表达式,定义触发规则。
  • shardingTotalCount:定义作业分片总数
  • shardingItemParameters:定义分配项参数,一般用分片序列号和参数用等号分隔,多个键值对用逗号分隔,分片序列号从0开始,不可大于或等于作业分片总数。
  • jobParameters:作业自定义参数

第五步,在applicationContext.properties文件中配置参数:

1
2
3
4
5
6
7
ini复制代码spring.application.name=elasticjobDemo
zk.serverList=localhost:2181
zk.namespace=elasticjobDemo
sue.spring.elatisc.cron=0/5 * * * * ?
sue.spring.elatisc.itemParameters=0=A,1=B,2=C,3=D
sue.spring.elatisc.jobParameters=test
sue.spring.elatisc.shardingTotalCount=4

这样定时任务就配置好了,创建定时任务的步骤,相对于xxl-job来说要繁琐一些。

使用elastic-job的优缺点:

  • 优点:支持分布式调度协调,支持分片,适合高并发,和一些业务相对来说较复杂的场景。
  • 缺点:需要依赖于zookeeper,实现定时任务相对于xxl-job要复杂一些,要对分片规则非常熟悉。

3.其他分布式定时任务

1. Saturn

Saturn是唯品会开源的一个分布式任务调度平台。取代传统的Linux Cron/Spring Batch Job的方式,做到全域统一配置,统一监控,任务高可用以及分片并发处理。

Saturn是在当当开源的Elastic-Job基础上,结合各方需求和我们的实践见解改良而成。使用案例:唯品会、酷狗音乐、新网银行、海融易、航美在线、量富征信等。

github地址:github.com/vipshop/Sat…

2. TBSchedule

TBSchedule是阿里开发的一款分布式任务调度平台,旨在将调度作业从业务系统中分离出来,降低或者是消除和业务系统的耦合度,进行高效异步任务处理。

目前被广泛应用在阿里巴巴、淘宝、支付宝、京东、聚美、汽车之家、国美等很多互联网企业的流程调度系统中。

github地址:github.com/taobao/TBSc…

老实说优秀的定时任务还是挺多的,不是说哪种定时任务牛逼我们就一定要用哪种,而是要根据实际业务需求选择。每种定时任务都有优缺点,合理选择既能满足业务需求,又能避免资源浪费,才是上上策。当然在实际的业务场景,通常会多种定时任务一起配合使用。

顺便说一句,欢迎亲爱的小伙伴们,找我一起聊聊:你用过哪些定时任务,遇到过哪些问题,以及如何解决问题的。如果有相关问题也可以问我。

希望我们能够共同进步,一起成长。
最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

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

1…687688689…956

开发者博客

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