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

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


  • 首页

  • 归档

  • 搜索

SpringCloud升级之路20200x版-42Sp

发表于 2021-11-27

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

本系列代码地址:github.com/JoJoTec/spr…

网关由于是所有外部用户请求的入口,记录这些请求中我们需要的元素,对于线上监控以及业务问题定位,是非常重要的。并且,在这些元素中,链路信息也是非常重要的。通过链路信息,我们可以找到请求调用全链路相关的日志。并且,网关也是大部分请求链路起始的地方,记录请求中的元素的同时,也要带上链路信息。

我们需要在网关记录每个请求的:

  • HTTP 相关元素:
    • URL 相关信息
    • 请求信息,例如 HTTP HEADER,请求时间等等
    • 某些类型的请求体
    • 响应信息,例如响应码
    • 某些类型响应的响应体
  • 链路信息

现有的可供分析的日志以及缺陷

首先我们来看 Spring Cloud Gateway 中本身我们可以利用的日志。Spring Cloud Gateway 基于 Spring-WebFlux,Spring-WebFlux 基于 Project Reactor,我们没有在网关加入额外的 Web 容器依赖,所以 Web 容器用的是默认的基于 Project Reactor 的 reactor-netty 实现的 Web 容器。

netty 抓包日志

使用了 netty 我们可以联想到 netty 的抓包日志,Spring Cloud Gateway 封装了这个功能,并暴露了配置。Spring Cloud Gateway 是一个网关,他会作为 HTTP 服务器接受 HTTP 请求的同时,还在作为 HTTP 客户端将请求转发到下游的微服务。所以,这里有两种抓包日志,一种是作为 HTTP 服务器接收到的请求和响应,另一种是做为 HTTP 客户端发出的请求和响应。分别对应两个配置:

1
2
3
4
5
yaml复制代码spring.cloud.gateway:
httpserver:
wiretap: true
httpclient:
wiretap: true

经常有读者私信我问如何看 spring cloud 有哪些配置,官方文档感觉不够清楚全面。我一般是看源码,但是鉴于很多人没有精力去研究源码,一种偷懒的方式是去看 jar 包里面的 spring-configuration-metadata.json。里面包含了比较全的配置,以及配置类(如果存在的话),这是很方便的。例如我们这里的两个配置,在这个 json 中对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
"name": "spring.cloud.gateway.httpclient.wiretap",
"type": "java.lang.Boolean",
"description": "Enables wiretap debugging for Netty HttpClient.",
"sourceType": "org.springframework.cloud.gateway.config.HttpClientProperties",
"defaultValue": false
},
{
"name": "spring.cloud.gateway.httpserver.wiretap",
"type": "java.lang.Boolean",
"description": "Enables wiretap debugging for Netty HttpServer.",
"defaultValue": "false"
},

可以看出,spring.cloud.gateway.httpclient.wiretap 对应配置类 org.springframework.cloud.gateway.config.HttpClientProperties(这个配置类里面的配置我们后面还会用到,到时候会详细分析其中的配置项),默认为 false。spring.cloud.gateway.httpserver.wiretap 没有配置类,他是被直接使用的,对应源码:

GatewayAutoConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.httpserver.wiretap")
public NettyWebServerFactoryCustomizer nettyServerWiretapCustomizer(Environment environment,
ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties) {
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.addServerCustomizers(httpServer -> httpServer.wiretap(true));
super.customize(factory);
}
};
}

增加这两个配置为 true 之后,增加日志输出配置,我这里使用的是 log4j2(其实就是设置 reactor.netty.http.server.HttpServer 和 reactor.netty.http.server.HttpClient 的日志级别为 DEBUG):

1
2
3
4
5
6
ini复制代码<AsyncLogger name="reactor.netty.http.server.HttpServer" level="debug" additivity="false" includeLocation="true">
<appender-ref ref="console" />
</AsyncLogger>
<AsyncLogger name="reactor.netty.http.client.HttpClient" level="debug" additivity="false" includeLocation="true">
<appender-ref ref="console" />
</AsyncLogger>

然后,再次请求我们前面实现的网关,可以看到类似于下面的日志:

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
ini复制代码2021-11-27 01:16:06,262 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] READ: 466B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 74 65 73 74 2d 73 73 2f 61 6e 79 |GET /test-ss/any|
|00000010| 74 68 69 6e 67 20 48 54 54 50 2f 31 2e 31 0d 0a |thing HTTP/1.1..|
|00000020| 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a |Host: 127.0.0.1:|
|00000030| 38 31 38 31 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e |8181..Connection|
|00000040| 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a 43 61 |: keep-alive..Ca|
|00000050| 63 68 65 2d 43 6f 6e 74 72 6f 6c 3a 20 6d 61 78 |che-Control: max|
|00000060| 2d 61 67 65 3d 30 0d 0a 55 70 67 72 61 64 65 2d |-age=0..Upgrade-|
|00000070| 49 6e 73 65 63 75 72 65 2d 52 65 71 75 65 73 74 |Insecure-Request|
|00000080| 73 3a 20 31 0d 0a 55 73 65 72 2d 41 67 65 6e 74 |s: 1..User-Agent|
|00000090| 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 57 |: Mozilla/5.0 (W|
|000000a0| 69 6e 64 6f 77 73 20 4e 54 20 31 30 2e 30 3b 20 |indows NT 10.0; |
|000000b0| 57 4f 57 36 34 29 20 41 70 70 6c 65 57 65 62 4b |WOW64) AppleWebK|
|000000c0| 69 74 2f 35 33 37 2e 33 36 20 28 4b 48 54 4d 4c |it/537.36 (KHTML|
|000000d0| 2c 20 6c 69 6b 65 20 47 65 63 6b 6f 29 20 43 68 |, like Gecko) Ch|
|000000e0| 72 6f 6d 65 2f 37 30 2e 30 2e 33 35 33 38 2e 32 |rome/70.0.3538.2|
|000000f0| 35 20 53 61 66 61 72 69 2f 35 33 37 2e 33 36 20 |5 Safari/537.36 |
|00000100| 43 6f 72 65 2f 31 2e 37 30 2e 33 38 37 39 2e 34 |Core/1.70.3879.4|
|00000110| 30 30 20 51 51 42 72 6f 77 73 65 72 2f 31 30 2e |00 QQBrowser/10.|
|00000120| 38 2e 34 35 35 32 2e 34 30 30 0d 0a 41 63 63 65 |8.4552.400..Acce|
|00000130| 70 74 3a 20 74 65 78 74 2f 68 74 6d 6c 2c 61 70 |pt: text/html,ap|
|00000140| 70 6c 69 63 61 74 69 6f 6e 2f 78 68 74 6d 6c 2b |plication/xhtml+|
|00000150| 78 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e 2f |xml,application/|
|00000160| 78 6d 6c 3b 71 3d 30 2e 39 2c 69 6d 61 67 65 2f |xml;q=0.9,image/|
|00000170| 77 65 62 70 2c 69 6d 61 67 65 2f 61 70 6e 67 2c |webp,image/apng,|
|00000180| 2a 2f 2a 3b 71 3d 30 2e 38 0d 0a 41 63 63 65 70 |*/*;q=0.8..Accep|
|00000190| 74 2d 45 6e 63 6f 64 69 6e 67 3a 20 67 7a 69 70 |t-Encoding: gzip|
|000001a0| 2c 20 64 65 66 6c 61 74 65 2c 20 62 72 0d 0a 41 |, deflate, br..A|
|000001b0| 63 63 65 70 74 2d 4c 61 6e 67 75 61 67 65 3a 20 |ccept-Language: |
|000001c0| 7a 68 2d 43 4e 2c 7a 68 3b 71 3d 30 2e 39 0d 0a |zh-CN,zh;q=0.9..|
|000001d0| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
2021-11-27 01:16:06,262 DEBUG [sports,,] [10632] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:249]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@317ee5cc
2021-11-27 01:16:06,378 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] READ COMPLETE
2021-11-27 01:16:06,381 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1] REGISTERED
2021-11-27 01:16:06,437 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1] CONNECT: httpbin.org/18.232.227.86:80
2021-11-27 01:16:06,656 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1, L:/172.168.160.198:52819 - R:httpbin.org/18.232.227.86:80] ACTIVE
2021-11-27 01:16:06,672 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1-1, L:/172.168.160.198:52819 - R:httpbin.org/18.232.227.86:80] WRITE: 775B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 61 6e 79 74 68 69 6e 67 20 48 54 |GET /anything HT|
|00000010| 54 50 2f 31 2e 31 0d 0a 43 61 63 68 65 2d 43 6f |TP/1.1..Cache-Co|
|00000020| 6e 74 72 6f 6c 3a 20 6d 61 78 2d 61 67 65 3d 30 |ntrol: max-age=0|
|00000030| 0d 0a 55 70 67 72 61 64 65 2d 49 6e 73 65 63 75 |..Upgrade-Insecu|
|00000040| 72 65 2d 52 65 71 75 65 73 74 73 3a 20 31 0d 0a |re-Requests: 1..|
|00000050| 55 73 65 72 2d 41 67 65 6e 74 3a 20 4d 6f 7a 69 |User-Agent: Mozi|
|00000060| 6c 6c 61 2f 35 2e 30 20 28 57 69 6e 64 6f 77 73 |lla/5.0 (Windows|
|00000070| 20 4e 54 20 31 30 2e 30 3b 20 57 4f 57 36 34 29 | NT 10.0; WOW64)|
|00000080| 20 41 70 70 6c 65 57 65 62 4b 69 74 2f 35 33 37 | AppleWebKit/537|
|00000090| 2e 33 36 20 28 4b 48 54 4d 4c 2c 20 6c 69 6b 65 |.36 (KHTML, like|
|000000a0| 20 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65 2f 37 | Gecko) Chrome/7|
|000000b0| 30 2e 30 2e 33 35 33 38 2e 32 35 20 53 61 66 61 |0.0.3538.25 Safa|
|000000c0| 72 69 2f 35 33 37 2e 33 36 20 43 6f 72 65 2f 31 |ri/537.36 Core/1|
|000000d0| 2e 37 30 2e 33 38 37 39 2e 34 30 30 20 51 51 42 |.70.3879.400 QQB|
|000000e0| 72 6f 77 73 65 72 2f 31 30 2e 38 2e 34 35 35 32 |rowser/10.8.4552|
|000000f0| 2e 34 30 30 0d 0a 41 63 63 65 70 74 3a 20 74 65 |.400..Accept: te|
|00000100| 78 74 2f 68 74 6d 6c 2c 61 70 70 6c 69 63 61 74 |xt/html,applicat|
|00000110| 69 6f 6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c 61 70 |ion/xhtml+xml,ap|
|00000120| 70 6c 69 63 61 74 69 6f 6e 2f 78 6d 6c 3b 71 3d |plication/xml;q=|
|00000130| 30 2e 39 2c 69 6d 61 67 65 2f 77 65 62 70 2c 69 |0.9,image/webp,i|
|00000140| 6d 61 67 65 2f 61 70 6e 67 2c 2a 2f 2a 3b 71 3d |mage/apng,*/*;q=|
|00000150| 30 2e 38 0d 0a 41 63 63 65 70 74 2d 45 6e 63 6f |0.8..Accept-Enco|
|00000160| 64 69 6e 67 3a 20 67 7a 69 70 2c 20 64 65 66 6c |ding: gzip, defl|
|00000170| 61 74 65 2c 20 62 72 0d 0a 41 63 63 65 70 74 2d |ate, br..Accept-|
|00000180| 4c 61 6e 67 75 61 67 65 3a 20 7a 68 2d 43 4e 2c |Language: zh-CN,|
|00000190| 7a 68 3b 71 3d 30 2e 39 0d 0a 46 6f 72 77 61 72 |zh;q=0.9..Forwar|
|000001a0| 64 65 64 3a 20 70 72 6f 74 6f 3d 68 74 74 70 3b |ded: proto=http;|
|000001b0| 68 6f 73 74 3d 22 31 32 37 2e 30 2e 30 2e 31 3a |host="127.0.0.1:|
|000001c0| 38 31 38 31 22 3b 66 6f 72 3d 22 31 32 37 2e 30 |8181";for="127.0|
|000001d0| 2e 30 2e 31 3a 35 32 37 39 37 22 0d 0a 58 2d 46 |.0.1:52797"..X-F|
|000001e0| 6f 72 77 61 72 64 65 64 2d 46 6f 72 3a 20 31 32 |orwarded-For: 12|
|000001f0| 37 2e 30 2e 30 2e 31 0d 0a 58 2d 46 6f 72 77 61 |7.0.0.1..X-Forwa|
|00000200| 72 64 65 64 2d 50 72 6f 74 6f 3a 20 68 74 74 70 |rded-Proto: http|
|00000210| 0d 0a 58 2d 46 6f 72 77 61 72 64 65 64 2d 50 72 |..X-Forwarded-Pr|
|00000220| 65 66 69 78 3a 20 2f 74 65 73 74 2d 73 73 0d 0a |efix: /test-ss..|
|00000230| 58 2d 46 6f 72 77 61 72 64 65 64 2d 50 6f 72 74 |X-Forwarded-Port|
|00000240| 3a 20 38 31 38 31 0d 0a 58 2d 46 6f 72 77 61 72 |: 8181..X-Forwar|
|00000250| 64 65 64 2d 48 6f 73 74 3a 20 31 32 37 2e 30 2e |ded-Host: 127.0.|
|00000260| 30 2e 31 3a 38 31 38 31 0d 0a 68 6f 73 74 3a 20 |0.1:8181..host: |
|00000270| 68 74 74 70 62 69 6e 2e 6f 72 67 0d 0a 58 2d 42 |httpbin.org..X-B|
|00000280| 33 2d 54 72 61 63 65 49 64 3a 20 65 66 39 36 35 |3-TraceId: ef965|
|00000290| 33 61 30 30 30 33 65 64 36 38 65 0d 0a 58 2d 42 |3a0003ed68e..X-B|
|000002a0| 33 2d 53 70 61 6e 49 64 3a 20 38 32 62 36 61 30 |3-SpanId: 82b6a0|
|000002b0| 32 30 32 64 63 31 35 36 36 62 0d 0a 58 2d 42 33 |202dc1566b..X-B3|
|000002c0| 2d 50 61 72 65 6e 74 53 70 61 6e 49 64 3a 20 65 |-ParentSpanId: e|
|000002d0| 66 39 36 35 33 61 30 30 30 33 65 64 36 38 65 0d |f9653a0003ed68e.|
|000002e0| 0a 58 2d 42 33 2d 53 61 6d 70 6c 65 64 3a 20 30 |.X-B3-Sampled: 0|
|000002f0| 0d 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 |..content-length|
|00000300| 3a 20 30 0d 0a 0d 0a |: 0.... |
+--------+-------------------------------------------------+----------------+
2021-11-27 01:16:06,672 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1-1, L:/172.168.160.198:52819 - R:httpbin.org/18.232.227.86:80] FLUSH
2021-11-27 01:16:06,890 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1-1, L:/172.168.160.198:52819 - R:httpbin.org/18.232.227.86:80] READ: 1315B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
|00000010| 0a 44 61 74 65 3a 20 53 61 74 2c 20 32 37 20 4e |.Date: Sat, 27 N|
|00000020| 6f 76 20 32 30 32 31 20 30 31 3a 31 36 3a 30 34 |ov 2021 01:16:04|
|00000030| 20 47 4d 54 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 | GMT..Content-Ty|
|00000040| 70 65 3a 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f |pe: application/|
|00000050| 6a 73 6f 6e 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 |json..Content-Le|
|00000060| 6e 67 74 68 3a 20 31 30 38 34 0d 0a 43 6f 6e 6e |ngth: 1084..Conn|
|00000070| 65 63 74 69 6f 6e 3a 20 6b 65 65 70 2d 61 6c 69 |ection: keep-ali|
|00000080| 76 65 0d 0a 53 65 72 76 65 72 3a 20 67 75 6e 69 |ve..Server: guni|
|00000090| 63 6f 72 6e 2f 31 39 2e 39 2e 30 0d 0a 41 63 63 |corn/19.9.0..Acc|
|000000a0| 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 6c 6c 6f |ess-Control-Allo|
|000000b0| 77 2d 4f 72 69 67 69 6e 3a 20 2a 0d 0a 41 63 63 |w-Origin: *..Acc|
|000000c0| 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 6c 6c 6f |ess-Control-Allo|
|000000d0| 77 2d 43 72 65 64 65 6e 74 69 61 6c 73 3a 20 74 |w-Credentials: t|
|000000e0| 72 75 65 0d 0a 0d 0a 7b 0a 20 20 22 61 72 67 73 |rue....{. "args|
|000000f0| 22 3a 20 7b 7d 2c 20 0a 20 20 22 64 61 74 61 22 |": {}, . "data"|
|00000100| 3a 20 22 22 2c 20 0a 20 20 22 66 69 6c 65 73 22 |: "", . "files"|
|00000110| 3a 20 7b 7d 2c 20 0a 20 20 22 66 6f 72 6d 22 3a |: {}, . "form":|
|00000120| 20 7b 7d 2c 20 0a 20 20 22 68 65 61 64 65 72 73 | {}, . "headers|
|00000130| 22 3a 20 7b 0a 20 20 20 20 22 41 63 63 65 70 74 |": {. "Accept|
|00000140| 22 3a 20 22 74 65 78 74 2f 68 74 6d 6c 2c 61 70 |": "text/html,ap|
|00000150| 70 6c 69 63 61 74 69 6f 6e 2f 78 68 74 6d 6c 2b |plication/xhtml+|
|00000160| 78 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e 2f |xml,application/|
|00000170| 78 6d 6c 3b 71 3d 30 2e 39 2c 69 6d 61 67 65 2f |xml;q=0.9,image/|
|00000180| 77 65 62 70 2c 69 6d 61 67 65 2f 61 70 6e 67 2c |webp,image/apng,|
|00000190| 2a 2f 2a 3b 71 3d 30 2e 38 22 2c 20 0a 20 20 20 |*/*;q=0.8", . |
|000001a0| 20 22 41 63 63 65 70 74 2d 45 6e 63 6f 64 69 6e | "Accept-Encodin|
|000001b0| 67 22 3a 20 22 67 7a 69 70 2c 20 64 65 66 6c 61 |g": "gzip, defla|
|000001c0| 74 65 2c 20 62 72 22 2c 20 0a 20 20 20 20 22 41 |te, br", . "A|
|000001d0| 63 63 65 70 74 2d 4c 61 6e 67 75 61 67 65 22 3a |ccept-Language":|
|000001e0| 20 22 7a 68 2d 43 4e 2c 7a 68 3b 71 3d 30 2e 39 | "zh-CN,zh;q=0.9|
|000001f0| 22 2c 20 0a 20 20 20 20 22 43 61 63 68 65 2d 43 |", . "Cache-C|
|00000200| 6f 6e 74 72 6f 6c 22 3a 20 22 6d 61 78 2d 61 67 |ontrol": "max-ag|
|00000210| 65 3d 30 22 2c 20 0a 20 20 20 20 22 43 6f 6e 74 |e=0", . "Cont|
|00000220| 65 6e 74 2d 4c 65 6e 67 74 68 22 3a 20 22 30 22 |ent-Length": "0"|
|00000230| 2c 20 0a 20 20 20 20 22 46 6f 72 77 61 72 64 65 |, . "Forwarde|
|00000240| 64 22 3a 20 22 70 72 6f 74 6f 3d 68 74 74 70 3b |d": "proto=http;|
|00000250| 68 6f 73 74 3d 5c 22 31 32 37 2e 30 2e 30 2e 31 |host=\"127.0.0.1|
|00000260| 3a 38 31 38 31 5c 22 3b 66 6f 72 3d 5c 22 31 32 |:8181\";for=\"12|
|00000270| 37 2e 30 2e 30 2e 31 3a 35 32 37 39 37 5c 22 22 |7.0.0.1:52797\""|
|00000280| 2c 20 0a 20 20 20 20 22 48 6f 73 74 22 3a 20 22 |, . "Host": "|
|00000290| 68 74 74 70 62 69 6e 2e 6f 72 67 22 2c 20 0a 20 |httpbin.org", . |
|000002a0| 20 20 20 22 55 70 67 72 61 64 65 2d 49 6e 73 65 | "Upgrade-Inse|
|000002b0| 63 75 72 65 2d 52 65 71 75 65 73 74 73 22 3a 20 |cure-Requests": |
|000002c0| 22 31 22 2c 20 0a 20 20 20 20 22 55 73 65 72 2d |"1", . "User-|
|000002d0| 41 67 65 6e 74 22 3a 20 22 4d 6f 7a 69 6c 6c 61 |Agent": "Mozilla|
|000002e0| 2f 35 2e 30 20 28 57 69 6e 64 6f 77 73 20 4e 54 |/5.0 (Windows NT|
|000002f0| 20 31 30 2e 30 3b 20 57 4f 57 36 34 29 20 41 70 | 10.0; WOW64) Ap|
|00000300| 70 6c 65 57 65 62 4b 69 74 2f 35 33 37 2e 33 36 |pleWebKit/537.36|
|00000310| 20 28 4b 48 54 4d 4c 2c 20 6c 69 6b 65 20 47 65 | (KHTML, like Ge|
|00000320| 63 6b 6f 29 20 43 68 72 6f 6d 65 2f 37 30 2e 30 |cko) Chrome/70.0|
|00000330| 2e 33 35 33 38 2e 32 35 20 53 61 66 61 72 69 2f |.3538.25 Safari/|
|00000340| 35 33 37 2e 33 36 20 43 6f 72 65 2f 31 2e 37 30 |537.36 Core/1.70|
|00000350| 2e 33 38 37 39 2e 34 30 30 20 51 51 42 72 6f 77 |.3879.400 QQBrow|
|00000360| 73 65 72 2f 31 30 2e 38 2e 34 35 35 32 2e 34 30 |ser/10.8.4552.40|
|00000370| 30 22 2c 20 0a 20 20 20 20 22 58 2d 41 6d 7a 6e |0", . "X-Amzn|
|00000380| 2d 54 72 61 63 65 2d 49 64 22 3a 20 22 52 6f 6f |-Trace-Id": "Roo|
|00000390| 74 3d 31 2d 36 31 61 31 38 36 64 34 2d 34 31 38 |t=1-61a186d4-418|
|000003a0| 32 30 65 31 66 36 33 39 30 32 64 34 64 33 63 63 |20e1f63902d4d3cc|
|000003b0| 64 38 62 39 64 22 2c 20 0a 20 20 20 20 22 58 2d |d8b9d", . "X-|
|000003c0| 42 33 2d 50 61 72 65 6e 74 73 70 61 6e 69 64 22 |B3-Parentspanid"|
|000003d0| 3a 20 22 65 66 39 36 35 33 61 30 30 30 33 65 64 |: "ef9653a0003ed|
|000003e0| 36 38 65 22 2c 20 0a 20 20 20 20 22 58 2d 42 33 |68e", . "X-B3|
|000003f0| 2d 53 61 6d 70 6c 65 64 22 3a 20 22 30 22 2c 20 |-Sampled": "0", |
|00000400| 0a 20 20 20 20 22 58 2d 42 33 2d 53 70 61 6e 69 |. "X-B3-Spani|
|00000410| 64 22 3a 20 22 38 32 62 36 61 30 32 30 32 64 63 |d": "82b6a0202dc|
|00000420| 31 35 36 36 62 22 2c 20 0a 20 20 20 20 22 58 2d |1566b", . "X-|
|00000430| 42 33 2d 54 72 61 63 65 69 64 22 3a 20 22 65 66 |B3-Traceid": "ef|
|00000440| 39 36 35 33 61 30 30 30 33 65 64 36 38 65 22 2c |9653a0003ed68e",|
|00000450| 20 0a 20 20 20 20 22 58 2d 46 6f 72 77 61 72 64 | . "X-Forward|
|00000460| 65 64 2d 48 6f 73 74 22 3a 20 22 31 32 37 2e 30 |ed-Host": "127.0|
|00000470| 2e 30 2e 31 3a 38 31 38 31 22 2c 20 0a 20 20 20 |.0.1:8181", . |
|00000480| 20 22 58 2d 46 6f 72 77 61 72 64 65 64 2d 50 72 | "X-Forwarded-Pr|
|00000490| 65 66 69 78 22 3a 20 22 2f 74 65 73 74 2d 73 73 |efix": "/test-ss|
|000004a0| 22 0a 20 20 7d 2c 20 0a 20 20 22 6a 73 6f 6e 22 |". }, . "json"|
|000004b0| 3a 20 6e 75 6c 6c 2c 20 0a 20 20 22 6d 65 74 68 |: null, . "meth|
|000004c0| 6f 64 22 3a 20 22 47 45 54 22 2c 20 0a 20 20 22 |od": "GET", . "|
|000004d0| 6f 72 69 67 69 6e 22 3a 20 22 31 32 37 2e 30 2e |origin": "127.0.|
|000004e0| 30 2e 31 2c 20 35 39 2e 31 35 32 2e 32 35 34 2e |0.1, 59.152.254.|
|000004f0| 32 33 38 22 2c 20 0a 20 20 22 75 72 6c 22 3a 20 |238", . "url": |
|00000500| 22 68 74 74 70 3a 2f 2f 31 32 37 2e 30 2e 30 2e |"http://127.0.0.|
|00000510| 31 3a 38 31 38 31 2f 61 6e 79 74 68 69 6e 67 22 |1:8181/anything"|
|00000520| 0a 7d 0a |.}. |
+--------+-------------------------------------------------+----------------+
2021-11-27 01:16:06,904 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] WRITE: 207B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
|00000010| 0a 44 61 74 65 3a 20 53 61 74 2c 20 32 37 20 4e |.Date: Sat, 27 N|
|00000020| 6f 76 20 32 30 32 31 20 30 31 3a 31 36 3a 30 34 |ov 2021 01:16:04|
|00000030| 20 47 4d 54 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 | GMT..Content-Ty|
|00000040| 70 65 3a 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f |pe: application/|
|00000050| 6a 73 6f 6e 0d 0a 53 65 72 76 65 72 3a 20 67 75 |json..Server: gu|
|00000060| 6e 69 63 6f 72 6e 2f 31 39 2e 39 2e 30 0d 0a 41 |nicorn/19.9.0..A|
|00000070| 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 6c |ccess-Control-Al|
|00000080| 6c 6f 77 2d 4f 72 69 67 69 6e 3a 20 2a 0d 0a 41 |low-Origin: *..A|
|00000090| 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 6c |ccess-Control-Al|
|000000a0| 6c 6f 77 2d 43 72 65 64 65 6e 74 69 61 6c 73 3a |low-Credentials:|
|000000b0| 20 74 72 75 65 0d 0a 63 6f 6e 74 65 6e 74 2d 6c | true..content-l|
|000000c0| 65 6e 67 74 68 3a 20 31 30 38 34 0d 0a 0d 0a |ength: 1084.... |
+--------+-------------------------------------------------+----------------+
2021-11-27 01:16:06,904 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] FLUSH
2021-11-27 01:16:06,909 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] WRITE: 1084B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 0a 20 20 22 61 72 67 73 22 3a 20 7b 7d 2c 20 |{. "args": {}, |
|00000010| 0a 20 20 22 64 61 74 61 22 3a 20 22 22 2c 20 0a |. "data": "", .|
|00000020| 20 20 22 66 69 6c 65 73 22 3a 20 7b 7d 2c 20 0a | "files": {}, .|
|00000030| 20 20 22 66 6f 72 6d 22 3a 20 7b 7d 2c 20 0a 20 | "form": {}, . |
|00000040| 20 22 68 65 61 64 65 72 73 22 3a 20 7b 0a 20 20 | "headers": {. |
|00000050| 20 20 22 41 63 63 65 70 74 22 3a 20 22 74 65 78 | "Accept": "tex|
|00000060| 74 2f 68 74 6d 6c 2c 61 70 70 6c 69 63 61 74 69 |t/html,applicati|
|00000070| 6f 6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c 61 70 70 |on/xhtml+xml,app|
|00000080| 6c 69 63 61 74 69 6f 6e 2f 78 6d 6c 3b 71 3d 30 |lication/xml;q=0|
|00000090| 2e 39 2c 69 6d 61 67 65 2f 77 65 62 70 2c 69 6d |.9,image/webp,im|
|000000a0| 61 67 65 2f 61 70 6e 67 2c 2a 2f 2a 3b 71 3d 30 |age/apng,*/*;q=0|
|000000b0| 2e 38 22 2c 20 0a 20 20 20 20 22 41 63 63 65 70 |.8", . "Accep|
|000000c0| 74 2d 45 6e 63 6f 64 69 6e 67 22 3a 20 22 67 7a |t-Encoding": "gz|
|000000d0| 69 70 2c 20 64 65 66 6c 61 74 65 2c 20 62 72 22 |ip, deflate, br"|
|000000e0| 2c 20 0a 20 20 20 20 22 41 63 63 65 70 74 2d 4c |, . "Accept-L|
|000000f0| 61 6e 67 75 61 67 65 22 3a 20 22 7a 68 2d 43 4e |anguage": "zh-CN|
|00000100| 2c 7a 68 3b 71 3d 30 2e 39 22 2c 20 0a 20 20 20 |,zh;q=0.9", . |
|00000110| 20 22 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c 22 | "Cache-Control"|
|00000120| 3a 20 22 6d 61 78 2d 61 67 65 3d 30 22 2c 20 0a |: "max-age=0", .|
|00000130| 20 20 20 20 22 43 6f 6e 74 65 6e 74 2d 4c 65 6e | "Content-Len|
|00000140| 67 74 68 22 3a 20 22 30 22 2c 20 0a 20 20 20 20 |gth": "0", . |
|00000150| 22 46 6f 72 77 61 72 64 65 64 22 3a 20 22 70 72 |"Forwarded": "pr|
|00000160| 6f 74 6f 3d 68 74 74 70 3b 68 6f 73 74 3d 5c 22 |oto=http;host=\"|
|00000170| 31 32 37 2e 30 2e 30 2e 31 3a 38 31 38 31 5c 22 |127.0.0.1:8181\"|
|00000180| 3b 66 6f 72 3d 5c 22 31 32 37 2e 30 2e 30 2e 31 |;for=\"127.0.0.1|
|00000190| 3a 35 32 37 39 37 5c 22 22 2c 20 0a 20 20 20 20 |:52797\"", . |
|000001a0| 22 48 6f 73 74 22 3a 20 22 68 74 74 70 62 69 6e |"Host": "httpbin|
|000001b0| 2e 6f 72 67 22 2c 20 0a 20 20 20 20 22 55 70 67 |.org", . "Upg|
|000001c0| 72 61 64 65 2d 49 6e 73 65 63 75 72 65 2d 52 65 |rade-Insecure-Re|
|000001d0| 71 75 65 73 74 73 22 3a 20 22 31 22 2c 20 0a 20 |quests": "1", . |
|000001e0| 20 20 20 22 55 73 65 72 2d 41 67 65 6e 74 22 3a | "User-Agent":|
|000001f0| 20 22 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 57 | "Mozilla/5.0 (W|
|00000200| 69 6e 64 6f 77 73 20 4e 54 20 31 30 2e 30 3b 20 |indows NT 10.0; |
|00000210| 57 4f 57 36 34 29 20 41 70 70 6c 65 57 65 62 4b |WOW64) AppleWebK|
|00000220| 69 74 2f 35 33 37 2e 33 36 20 28 4b 48 54 4d 4c |it/537.36 (KHTML|
|00000230| 2c 20 6c 69 6b 65 20 47 65 63 6b 6f 29 20 43 68 |, like Gecko) Ch|
|00000240| 72 6f 6d 65 2f 37 30 2e 30 2e 33 35 33 38 2e 32 |rome/70.0.3538.2|
|00000250| 35 20 53 61 66 61 72 69 2f 35 33 37 2e 33 36 20 |5 Safari/537.36 |
|00000260| 43 6f 72 65 2f 31 2e 37 30 2e 33 38 37 39 2e 34 |Core/1.70.3879.4|
|00000270| 30 30 20 51 51 42 72 6f 77 73 65 72 2f 31 30 2e |00 QQBrowser/10.|
|00000280| 38 2e 34 35 35 32 2e 34 30 30 22 2c 20 0a 20 20 |8.4552.400", . |
|00000290| 20 20 22 58 2d 41 6d 7a 6e 2d 54 72 61 63 65 2d | "X-Amzn-Trace-|
|000002a0| 49 64 22 3a 20 22 52 6f 6f 74 3d 31 2d 36 31 61 |Id": "Root=1-61a|
|000002b0| 31 38 36 64 34 2d 34 31 38 32 30 65 31 66 36 33 |186d4-41820e1f63|
|000002c0| 39 30 32 64 34 64 33 63 63 64 38 62 39 64 22 2c |902d4d3ccd8b9d",|
|000002d0| 20 0a 20 20 20 20 22 58 2d 42 33 2d 50 61 72 65 | . "X-B3-Pare|
|000002e0| 6e 74 73 70 61 6e 69 64 22 3a 20 22 65 66 39 36 |ntspanid": "ef96|
|000002f0| 35 33 61 30 30 30 33 65 64 36 38 65 22 2c 20 0a |53a0003ed68e", .|
|00000300| 20 20 20 20 22 58 2d 42 33 2d 53 61 6d 70 6c 65 | "X-B3-Sample|
|00000310| 64 22 3a 20 22 30 22 2c 20 0a 20 20 20 20 22 58 |d": "0", . "X|
|00000320| 2d 42 33 2d 53 70 61 6e 69 64 22 3a 20 22 38 32 |-B3-Spanid": "82|
|00000330| 62 36 61 30 32 30 32 64 63 31 35 36 36 62 22 2c |b6a0202dc1566b",|
|00000340| 20 0a 20 20 20 20 22 58 2d 42 33 2d 54 72 61 63 | . "X-B3-Trac|
|00000350| 65 69 64 22 3a 20 22 65 66 39 36 35 33 61 30 30 |eid": "ef9653a00|
|00000360| 30 33 65 64 36 38 65 22 2c 20 0a 20 20 20 20 22 |03ed68e", . "|
|00000370| 58 2d 46 6f 72 77 61 72 64 65 64 2d 48 6f 73 74 |X-Forwarded-Host|
|00000380| 22 3a 20 22 31 32 37 2e 30 2e 30 2e 31 3a 38 31 |": "127.0.0.1:81|
|00000390| 38 31 22 2c 20 0a 20 20 20 20 22 58 2d 46 6f 72 |81", . "X-For|
|000003a0| 77 61 72 64 65 64 2d 50 72 65 66 69 78 22 3a 20 |warded-Prefix": |
|000003b0| 22 2f 74 65 73 74 2d 73 73 22 0a 20 20 7d 2c 20 |"/test-ss". }, |
|000003c0| 0a 20 20 22 6a 73 6f 6e 22 3a 20 6e 75 6c 6c 2c |. "json": null,|
|000003d0| 20 0a 20 20 22 6d 65 74 68 6f 64 22 3a 20 22 47 | . "method": "G|
|000003e0| 45 54 22 2c 20 0a 20 20 22 6f 72 69 67 69 6e 22 |ET", . "origin"|
|000003f0| 3a 20 22 31 32 37 2e 30 2e 30 2e 31 2c 20 35 39 |: "127.0.0.1, 59|
|00000400| 2e 31 35 32 2e 32 35 34 2e 32 33 38 22 2c 20 0a |.152.254.238", .|
|00000410| 20 20 22 75 72 6c 22 3a 20 22 68 74 74 70 3a 2f | "url": "http:/|
|00000420| 2f 31 32 37 2e 30 2e 30 2e 31 3a 38 31 38 31 2f |/127.0.0.1:8181/|
|00000430| 61 6e 79 74 68 69 6e 67 22 0a 7d 0a |anything".}. |
+--------+-------------------------------------------------+----------------+
2021-11-27 01:16:06,909 DEBUG [sports,,] [10632] [reactor-http-nio-6][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:6666c1d1, L:/172.168.160.198:52819 - R:httpbin.org/18.232.227.86:80] READ COMPLETE
2021-11-27 01:16:06,910 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] FLUSH
2021-11-27 01:16:06,910 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] WRITE: 0B
2021-11-27 01:16:06,910 DEBUG [sports,,] [10632] [reactor-http-nio-2][io.netty.util.internal.logging.AbstractInternalLogger:145]:[id:1277d54e-2, L:/127.0.0.1:8181 - R:/127.0.0.1:52797] FLUSH

可以从日志中看出下面比较重要的信息:

  • 从抓包日志从可以得出 HTTP 请求与响应包中的所有内容。
  • 这里的日志,由于没被 spring-cloud-sleuth 包装,所以日志本身的占位符没有相关链路信息
  • 但是从包中,可以看到 spring-cloud-sleuth 的链路信息,这里是: "X-B3-Parentspanid": "ef9653a0003ed68e", "X-B3-Sampled": "0", "X-B3-Spanid": "82b6a0202dc1566b", "X-B3-Traceid": "ef9653a0003ed68e",
  • netty 本身有 traceId,即这里的 6666c1d1 和 1277d54e (分别是作为 HttpServer 接受请求和作为 HttpClient 转发请求后台微服务),这样其实就可以将 spring-cloud-sleuth 的链路信息与 netty 的链路信息结合在一起

这些日志很全面,但是有下面一些不好用的问题:

  • 只能输出所有请求响应的包内容,并不能定制输出哪些内容。例如对于文件上传请求,我们其实并不想看他的请求体,但是这里无法定制化。
  • 日志太多了,会影响我们的性能。

HttpWebHandlerAdapter 的 TRACE 日志

在系列前面文章的源码分析中,我们知道在入口的 HttpWebHandlerAdapter 中有请求与响应日志,可以通过下面的配置进行启用:

1
2
3
ini复制代码<AsyncLogger name="org.springframework.web.server.adapter.HttpWebHandlerAdapter" level="trace" additivity="false" includeLocation="true">
<appender-ref ref="console" />
</AsyncLogger>

这样就能启用类似于下面的日志:

1
2
ini复制代码2021-11-27 01:25:28,472 TRACE [sports,,] [16188] [reactor-http-nio-2][org.springframework.core.log.LogFormatUtils:88]:[8d5138d1-3] HTTP GET "/test-ss/anything", headers={masked}
2021-11-27 01:25:28,696 TRACE [sports,,] [16188] [reactor-http-nio-2][org.springframework.core.log.LogFormatUtils:88]:[8d5138d1-3] Completed 200 OK, headers={masked}

如何让 headers 不要被掩码,可以通过下面的配置实现:

1
2
yaml复制代码spring.codec:
log-request-details: true

这个配置对应的配置类是:CodecProperties,通过源码我们也可以发现,我们还可以在这里限制 body 的大小:

CodecProperties.java

1
2
3
4
5
kotlin复制代码@ConfigurationProperties(prefix = "spring.codec")
public class CodecProperties {
private boolean logRequestDetails;
private DataSize maxInMemorySize;
}

加上配置后,日志如下:

1
2
ini复制代码2021-11-27 01:32:50,501 TRACE [sports,,] [17668] [reactor-http-nio-2][org.springframework.core.log.LogFormatUtils:88]:[ecf9f55a-1] HTTP GET "/test-ss/anything", headers=[Host:"127.0.0.1:8181", Connection:"keep-alive", Cache-Control:"max-age=0", Upgrade-Insecure-Requests:"1", User-Agent:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3879.400 QQBrowser/10.8.4552.400", Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", Accept-Encoding:"gzip, deflate, br", Accept-Language:"zh-CN,zh;q=0.9"]
2021-11-27 01:32:51,212 TRACE [sports,,] [17668] [reactor-http-nio-2][org.springframework.core.log.LogFormatUtils:88]:[ecf9f55a-1] Completed 200 OK, headers=[Date:"Sat, 27 Nov 2021 01:32:48 GMT", Content-Type:"application/json", Server:"gunicorn/19.9.0", Access-Control-Allow-Origin:"*", Access-Control-Allow-Credentials:"true", content-length:"1084"]

但是这个日志内容太少,只包含请求和响应头,并且没有 spring-cloud-sleuth 相关的链路信息。

AccessLog

Spring Cloud Gateway 使用基于 reactor-netty 的 http 服务器,在 spring-boot 的封装中,可以通过添加 NettyServerCustomizer Bean 来实现添加自定义 AccessLog。AccessLog 包含的元素有:

AccessLogArgProvider.java

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

@Nullable
@Deprecated
String zonedDateTime();

@Nullable
ZonedDateTime accessDateTime();

@Nullable
SocketAddress remoteAddress();

@Nullable
CharSequence method();

@Nullable
CharSequence uri();

@Nullable
String protocol();

@Nullable
String user();

@Nullable
CharSequence status();

long contentLength();

long duration();

@Nullable
CharSequence requestHeader(CharSequence name);

@Nullable
CharSequence responseHeader(CharSequence name);

@Nullable
Map<CharSequence, Set<Cookie>> cookies();
}

可以看出里面没有 spring cloud sleuth 相关的链路信息,但是我们可以通过实现 Global Filter 将链路信息加入 Response 的 Header 中,并且通过 AccessLogArgProvider 的 requestHeader 将这些 Header 输出到 accesslog:

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
ini复制代码package com.github.jojotech.spring.cloud.apigateway.filter;

import java.util.List;

import lombok.extern.log4j.Log4j2;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.TraceContext;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Log4j2
@Component
public class CommonTraceFilter implements GlobalFilter, Ordered {
public static final String TRACE_ID = "traceId";
public static final String SPAN_ID = "spanId";

@Autowired
private Tracer tracer;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Span span = tracer.currentSpan();
if (span == null) {
span = tracer.nextSpan();
}
TraceContext context = span.context();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.put(TRACE_ID, List.of(context.traceId()));
headers.put(SPAN_ID, List.of(context.spanId()));

return chain.filter(exchange);
}

@Override
public int getOrder() {
//需要在所有的 Filter 之前,拿到 trace 信息
return Ordered.HIGHEST_PRECEDENCE;
}
}

然后,通过实现 NettyServerCustomizer 注册一个 Bean 来配置 Access Log。

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
java复制代码package com.github.jojotech.spring.cloud.apigateway.filter;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.AccessLog;
import reactor.netty.http.server.logging.AccessLogFactory;
import reactor.util.annotation.Nullable;

import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
import org.springframework.stereotype.Component;

@Component
public class AccessLogNettyServerCustomizer implements NettyServerCustomizer {
static final String DEFAULT_LOG_FORMAT =
"{},{} -> {} - {} [{}] \"{} {} {}\" {} {} {} ms";
static final String MISSING = "-";

@Override
public HttpServer apply(HttpServer httpServer) {
httpServer = httpServer.accessLog(true, AccessLogFactory.createFilter(
accessLogArgProvider -> {
//所有的都打印
return true;
}, accessLogArgProvider -> {
return AccessLog.create(DEFAULT_LOG_FORMAT, accessLogArgProvider
.responseHeader(CommonTraceFilter.TRACE_ID) == null ? MISSING : accessLogArgProvider
.responseHeader(CommonTraceFilter.TRACE_ID), accessLogArgProvider
.responseHeader(CommonTraceFilter.SPAN_ID) == null ? MISSING : accessLogArgProvider
.responseHeader(CommonTraceFilter.SPAN_ID), applyAddress(accessLogArgProvider
.remoteAddress()), accessLogArgProvider.user(),
accessLogArgProvider.zonedDateTime(), accessLogArgProvider.method(), accessLogArgProvider
.uri(), accessLogArgProvider.protocol(), accessLogArgProvider.status(),
accessLogArgProvider.contentLength() > -1 ? accessLogArgProvider
.contentLength() : MISSING, accessLogArgProvider.duration());
})
);
return httpServer;
}

static String applyAddress(@Nullable SocketAddress socketAddress) {
if (socketAddress instanceof InetSocketAddress) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
return inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort();
}
else {
return MISSING;
}
}
}

这样,我们就可以得到像下面的 access log:

1
ini复制代码2021-11-27 03:56:10,948  INFO [sports,,] [8536] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:269]:baa69b779a8497eb,baa69b779a8497eb -> 127.0.0.1:64196 - - [2021-11-27T03:56:10.720844200Z[Etc/GMT]] "GET /test-ss/anything HTTP/1.1" 200 1084 228 ms

accesslog 的也不能输出 body,所以我们还是需要定制自己的日志 Filter。

我们在下一节,会开始实现我们自己定制的日志 Filter

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

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

画了一堆图来解释AQS源码 1 简介 2 AQS线程队列 3

发表于 2021-11-27

1 简介

AbstractQueuedSynchronizer抽象的队列式同步器(抽象类)。提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。

其中比较重要的概念有:

  • state共享资源
  • FIFO
  • CAS
  • park unpark

2 AQS线程队列

Node是整个队列中最核心的部分,包含CANCELLED、SIGNAL、CONDITION、PROPAGATE四种状态、指向前后节点的指针、thread是节点存储的值。

ReentrantLock中的公平锁与非公平锁都继承了这个抽象类。

)​

ReentrantLock包含公平锁和非公平锁,每种锁里面还包含了抽象的锁Sync,抽象锁执行AQS。

如果当前是线程1拥有锁的话,线程2,3,4会调用park操作来被挂起加入队列中。

当拥有锁的Thread1执行完毕后会调用unpark方法,head指向下一个节点。由于ReentrantLock是一个可重入锁,重入次数被AQS.state记录,每重入一次值 + 1,退出一次 - 1。

3 ReentrantLock实例构建

ReentrantLock默认构造函数创建的是非公平锁,如果想要公平锁的话需要在构造方法中传入true。

)​

4 多线程抢锁图示(非公平锁)

线程抢到锁的过程使用CAS实现。

初始时等待队列只有一个Head节点,其中存储的Thread是null,用来占位,线程B,C在队列中使用双向链表的方式与Head相连,调用park挂起。

)​

上图的分步过程见下文。

4.1 A线程加锁

A线程抢锁时通过CAS操作将state改为1,并将AQS的exclusiveOwnerThread属性设为A,表示当前锁的拥有者。

)​

4.2 B线程加锁

此时B线程要进行抢锁,发现state已经是1了,所以CAS操作失败,进入到队列中,waitStatus设置为0

第一次new Node的时候,即创建head,thread设置为了null,waitStatus设置为SIGNAL

)​

4.3 C线程加锁

接着C线程抢锁时,操作同B,会将B的waitStatus设置为SIGNAL,自己的是初始化是的值0

)​

新插入的节点waitStatus值都设为0原因是用来标记队列中最后一个节点,此时不必再进行对后续节点的unpark操作,等待队列中没有节点时会有一个null的节点用作占位,这两个操作都是防止等待队列中没有等待的线程时而变为空。

4.4 A线程解锁

A线程进行解锁操作之后,B线程可以抢到锁,流程如下:

A线程解锁操作:

  • state设为0
  • exclusiveOwnerThread设为null表示锁被释放
  • head节点向后移动一位进行unpark操作唤醒线程B
  • 之前的head节点要被释放

)​

4.5 B线程解锁

)​

4.6 C线程解锁

)​

5 源码分析(以非公平锁为例)

5.1 线程A先抢占锁

构造方法:

1
2
3
java复制代码public ReentrantLock() {
sync = new NonfairSync();
}

lock():

1
2
3
java复制代码public void lock() {
sync.lock();
}

假设现在有两个线程A和B,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

// 线程A进来
// 线程B进来
final void lock() {
// 线程A执行成功,将AQS.state置为1,表示已抢占该锁
// 线程B,由于线程A已将AQS.state置为1,所以线程B执行CAS操作为false
if (compareAndSetState(0, 1))
// 线程A,将AbstractOwnableSynchronizer.exclusiveOwnerThread设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 线程B执行该部分,尝试获取一个锁
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

其中acquire(1)方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 线程B调用:arg = 1
public final void acquire(int arg) {
// 线程B
// 尝试获得非公平锁,由于已被线程A抢到,所以tryAcquire(arg) = false
// addWaiter方法,构建承载线程B的Node,然后添加到队列末尾,返回该node
// acquireQueued方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)) {
// 设置当前线程的中断标识
selfInterrupt();
}
}

尝试抢锁的方法 tryAcquire(arg) 最终调用的:

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
java复制代码/**
* 尝试抢锁
*
* 处理内容:
* 1 如果抢到锁,返回true
* 1.1 如果当前线程第一次抢到锁:
* AQS.status由0变为1
* AQS.exclusiveOwnerThread = Thread.currentThread()
* 返回true
* 1.2 如果当前线程再次抢到锁(重入)
* AQS.status++
* 返回true
* 2 没抢到锁,返回false
*
*/
// 线程B acquires = 1
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 由于线程A已将state设为1,所以c=1
int c = getState();
if (c == 0) { // 线程B,不满足不执行
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 线程B,不满足不执行
/**
* 获得当前独享线程,如果就是当前线程,那么执行重入操作
* 执行tryLock()时:
* 如果第二次进入,则nextc = 0 + 1 = 1
* 如果第三次进入,则nextc = 1 + 1 = 2
* 如果第四次进入,则nextc = 2 + 1 = 3
*/
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 线程B,返回false
return false;
}

回到 acquire 代码中 addWaiter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 线程B:mode = Node.EXCLUSIVE = null
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 线程B:由于线程A并没有进入链表,所以tail = null不会进入if方法
if (pred != null) {
node.prev = pred; // (老的尾部node) <--- 新node.prev
if (compareAndSetTail(pred, node)) {
pred.next = node; // (老的尾部node) next ---> (新node)
return node;
}
}
// 线程B:node = 承载线程B的node
enq(node);
return node;
}

addWaiter 方法使用到了 Node 的构造方法:

1
2
3
4
5
java复制代码// 线程B:thread = Thread.currentThread()  mode = null
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}

对照下面的图来看,nextWaiter 是null,Node的构造方法里没有对 waitStatus 赋值,所以默认为0

)​

回到addWaiter方法,其中还用到了enq方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码/**
* 将node节点插入队列末尾
* 1 如果是空队列,则初始化一个空内容node作为第一个节点,然后将入参node加到队列末尾
* 2 如果不是空队列,则直接入参node加到队列末尾
*
* @param node
* @return
*/
// 线程B:node = 承载线程B的node
private Node enq(final Node node) {
for (;;) {
// 线程B:第一次循环 tail = null
// 线程B:第二次循环 tail = head = 空内容node
Node t = tail;
/**
* 第一次进入由于队列为null,所以t = null
* 第二次进入,由于队列已经被初始化1个节点,故 t != null
*/
if (t == null) { // 线程B:第一次循环 满足t == null,进入该模块内
if (compareAndSetHead(new Node())) // 初始化一个空内容节点,作为AQS.head节点
tail = head;
} else { // 线程B:第二次循环 满足 t != null进入该模块内,即空内容node <-> 新节点
node.prev = t; // 老的尾部node <--- 入参node.prev
if (compareAndSetTail(t, node)) { // 将AQS.tailOffset内容更新为入参node
t.next = node; // 老的尾部node ---> 入参node
return t;
}
}
}
}

此时addWaiter执行完毕,又回到acquire方法,addWaiter返回了承载B的node,现在要执行acquireQueued方法:

)​

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复制代码// 线程B:node = 承载B的节点,arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 线程B:会一直循环,直到拿到锁
for (;;) {
// 线程B:p=空内容node(head)
final Node p = node.predecessor();
// 线程B:p == head等于true;但是由于线程A先抢到锁,所以tryAcquire(arg)=false,所以不会进入下面的if
if (p == head && tryAcquire(arg)) { // tryAcquire(arg):抢锁操作
setHead(node); // 更新头结点为入参node
p.next = null; // help GC
failed = false;
return interrupted;
}
// 线程B:shouldParkAfterFailedAcquire方法,设置p节点的waitStatus = Node.SIGNAL(原先是0),如果自旋两次没抢到锁的话会返回true挂起线程
// 线程B:parkAndCheckInterrupt方法,挂起线程B
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

5.2 线程A释放锁

如果A调用unlock操作:

1
2
3
java复制代码public void unlock() {
sync.release(1);
}

释放锁的方法如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 线程A:arg = 1
public final boolean release(int arg) {
if (tryRelease(arg)) { // 线程A:AQS.state = 0,AQS.exclusiveOwnerThread = null
Node h = head;
// 线程A:满足条件 h.waitStatus == SIGNAL(值为-1)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

其中调用了tryRelease方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 线程A:release = 1
protected final boolean tryRelease(int releases) {
// 线程A:c = 1 - 1 = 0
int c = getState() - releases;
// 如果当前线程不是之前抢占的线程,则抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 线程A:c == 0 为 true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

对应:

)​

tryRelease执行完毕之后又回到了release方法中,运行unparkSuccessor方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private void unparkSuccessor(Node node) {
// 线程A:node.waitStatus == SIGNAL(-1)
int ws = node.waitStatus;
if (ws < 0) {
// 线程A:设置 waitStatus == 0
compareAndSetWaitStatus(node, ws, 0);
}
Node s = node.next;
// 线程A:s == 线程B,s.waitStatus == SIGNAL(-1),所以不满足
if (s == null || s.waitStatus > 0) { // node是tail 或者 node的tail节点waitStatus > 0
s = null; // 断开node与node.next的连接
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 线程A:s == 线程B,激活线程B
if (s != null)
LockSupport.unpark(s.thread);
}

释放完毕之后,在线程B的lock方法的一些列调用中有 parkAndCheckInterrupt() 方法,即线程B被挂起了,A释放之后又回到被挂起那个位置继续执行,所以线程B的 acquireQueued() 方法可以抢到锁

对应:

)​

​

本文转载自: 掘金

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

SpringloC容器的依赖注入源码解析(10)—— pop

发表于 2021-11-27

populateBean的前置逻辑文章

在这里插入图片描述

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
java复制代码protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
if (bw == null) {
if (mbd.hasPropertyValues()) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
}
else {
// Skip property population phase for null instance.
return;
}
}

// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
// state of the bean before properties are set. This can be used, for example,
// to support styles of field injection.
boolean continueWithPropertyPopulation = true;

if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

// 如果上面设置continueWithPropertyPopulation = false,表明用户可能已经自己填充了
// bean的属性,不需要Spring帮忙填充了。此时直接返回即可
if (!continueWithPropertyPopulation) {
return;
}
// pvs是一个MutablePropertyValues实例,里面实现了PropertyValues接口,
// 提供属性的读写操作实现,同时可以通过调用构造函数实现深拷贝
// 获取BeanDefinition里面为Bean设置上的属性值
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

PropertyDescriptor[] filteredPds = null;
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 在这里会对@Autowired标记的属性进行依赖注入
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
// 依赖检查,对应depend-on属性,3.0已经弃用此属性
if (needsDepCheck) {
// 过滤出所有需要进行依赖检查的属性编辑器
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
checkDependencies(beanName, mbd, filteredPds, pvs);
}

if (pvs != null) {
// 最终将属性注入到Bean的Wrapper实例里,这里的注入主要是供
// 显式配置了autowiredbyName或者ByType的属性注入,
// 针对注解来讲,由于在AutowiredAnnotationBeanPostProcessor已经完成了注入,
// 所以此处不执行
applyPropertyValues(beanName, mbd, bw, pvs);
}
}

完成了按名字或按类型自动装配后,来到脑图里第三步,对解析完但还未设置的属性进行再处理。

请添加图片描述
这里需要关注AutowiredAnnotationBeanPostProcessor实现的postProcessProperties方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 获取指定类中@Autowired相关注解的元信息
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
// 对Bean的属性进行自动注入
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}

首先会去尝试获取InjectionMetadata对象,findAutowiringMetadata方法之前在分析该类的postProcessMergedBeanDefinition方法时,已经获取到了存储有该bean实例里被@Autowired或@Value标签修饰的属性列表的InjectionMetaData对象,并且将其已经放置到了缓存中。再次进入findAutowiringMetadata

请添加图片描述
在此次调用时,相关的InjectionMetadata实例已经从缓存中获取到了,不需要再进到if里面去解析bean了,此时又回到postProcessProperties方法里,取到了InjectionMetadata实例之后直接对bean注入属性

1
java复制代码metadata.inject(bean, beanName, pvs);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
// 要注入的字段集合
Collection<InjectedElement> elementsToIterate =
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
// 遍历每个字段并进行注入
for (InjectedElement element : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
}

该方法遍历每一个属性元素去调用元素的inject方法,进入inject发现又回到了AutowiredAnnotationBeanPostProcessor类里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
// 获取要注入的成员变量
Field field = (Field) this.member;
Object value;
// 如果成员变量的值先前缓存过
if (this.cached) {
// 从缓存中获取成员变量的值
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
}
// 没有缓存
else {
// 创建一个成员变量的依赖描述符实例
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
Assert.state(beanFactory != null, "No BeanFactory available");
// 获取容器的类型转换器
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
// 获取注入的值
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
}
synchronized (this) {
if (!this.cached) {
if (value != null || this.required) {
this.cachedFieldValue = desc;
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
}
else {
this.cachedFieldValue = null;
}
this.cached = true;
}
}
}
if (value != null) {
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
}

在调用的过程中复用了其他类的装配能力,此时是给boyfriend装配上girlfriend实例,首先去缓存里看下之前是否解析过girlfriend,第一次执行会进入到else里,先用DependencyDescriptor包装一下属性field:

1
java复制代码DependencyDescriptor desc = new DependencyDescriptor(field, this.required);

之后给desc注册上宿主的类名(Boyfriend):

1
java复制代码desc.setContainingClass(bean.getClass());

之后会尝试获取之前容器初始化时注册上去的转换器TypeConverter:

1
java复制代码TypeConverter typeConverter = beanFactory.getTypeConverter();

converter用来做类型转换,默认获取spring自带的simpleTypeConverter用来处理简单类型的转换,之后执行

1
2
java复制代码// 获取注入的值
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);

进入resolveDependency方法里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Override
@Nullable
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}

在方法里会依据依赖描述符的不同类型进行不同的处理,但是最终都会到else里,真正起作用的是doResolveDependency方法

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
java复制代码@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
try {
// 该方法最终调用了beanFactory.getBean(String, Class),从容器中获取依赖
Object shortcut = descriptor.resolveShortcut(this);
// 如果容器缓存中存在所需依赖,这里进行短路路操作,提前结束依赖解析逻辑
if (shortcut != null) {
return shortcut;
}

Class<?> type = descriptor.getDependencyType();
// 处理@Value注解
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String) {
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ?
getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
try {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
catch (UnsupportedOperationException ex) {
// A custom TypeConverter which does not support TypeDescriptor resolution...
return (descriptor.getField() != null ?
converter.convertIfNecessary(value, type, descriptor.getField()) :
converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
}
// 如果标识@Autowired注解的成员变量是复合类型,如Array,Collection,Map
// 从这个方法获取@Autowired里的值
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}

Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}

String autowiredBeanName;
Object instanceCandidate;

if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
}
else {
// In case of an optional Collection/Map, silently ignore a non-unique case:
// possibly it was meant to be an empty collection of multiple regular beans
// (before 4.3 in particular when we didn't even look for collection beans).
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
else {
// We have exactly one match.
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}

if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}
if (instanceCandidate instanceof Class) {
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
}
Object result = instanceCandidate;
if (result instanceof NullBean) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
result = null;
}
if (!ClassUtils.isAssignableValue(type, result)) {
throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
}
return result;
}
finally {
ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
}
}

首先尝试调用依赖描述符实例的resolveShortcut方法,尝试从容器缓存里获取属性名对应的bean实例

1
2
3
4
java复制代码@Nullable
public Object resolveShortcut(BeanFactory beanFactory) throws BeansException {
return null;
}

相对于注解的这种情况,并没有实现该方法。


之后尝试从依赖描述符实例里面去获取目标实例的属性

1
java复制代码Class<?> type = descriptor.getDependencyType();

这里的目标实例是girlfriend,之后就会调用注解候选解析器的getSuggestedValue方法尝试获取属性值,但是对于@Autowired修饰的属性来说,这一步无法获取到值

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
@Nullable
public Object getSuggestedValue(DependencyDescriptor descriptor) {
Object value = findValue(descriptor.getAnnotations());
if (value == null) {
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
value = findValue(methodParam.getMethodAnnotations());
}
}
return value;
}

该方法调用findValue方法

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Nullable
protected Object findValue(Annotation[] annotationsToSearch) {
if (annotationsToSearch.length > 0) { // qualifier annotations have to be local
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
if (attr != null) {
return extractValue(attr);
}
}
return null;
}

该方法主要是提取@Value修饰的属性值的


回到doResolveDependency方法

本文转载自: 掘金

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

Bash命令 入门

发表于 2021-11-27

Bash 是用于管理 Linux 计算机的重要工具。 此名称是 Bourne Again Shell 的缩写

Shell 是命令操作系统执行操作的程序。 可以在计算机的控制台中输入命令,然后直接运行命令,也可以使用脚本运行批量命令。 PowerShell 和 Bash 等 Shell 为系统管理员提供了对其所负责的计算机进行优化控制所需的功能和精度。

Bash 基础知识

若要了解 Bash,首先要了解 Bash 语法。 了解语法后,可以将其应用到你运行的每个 Bash 命令

Bash 命令的完整语法如下:

1
bash复制代码command [options] [arguments]

Bash 将其遇到的第一个字符串视为命令。 以下命令使用 Bash 的 ls(表示“list”)命令显示当前工作目录的内容:

1
bash复制代码ls

Bash 命令通常带有参数。 例如,可以在 ls 命令中添加路径名称,以列出其他目录的内容:

1
bash复制代码ls /etc

大多数 Bash 命令都具有用于修改其工作方式的选项。 选项(也称为标志)为命令传达更具体的指令。 例如,名称以句点开头的文件和目录会对用户隐藏,而不会由 ls 显示。 但可以在 ls 命令中添加 -a(表示“all”)标志,以便查看目标目录中的所有内容:

1
bash复制代码ls -a /etc

甚至可以为了简洁合并标志。 例如,与其输入 ls -a -l /etc 以长格式显示 Linux 的 /etc 目录中的所有文件和目录,不如输入以下内容:

1
bash复制代码ls -al /etc

Bash 非常简洁。 有时使用一个命令即可完成大量任务(这是 Bash 爱好者引以为豪之处)

获取帮助

可以使用或必须使用的选项和参数因命令而异。 幸运的是,Bash 文档内置到了操作系统中。 只需要使用一个命令即可获得所需帮助。 若要了解命令的选项,请使用 man(表示“manual”)命令。 例如,若要查看 mkdir(表示“make directory”)命令的所有选项,请执行以下操作:

1
arduino复制代码man mkdir

man 有助于你了解 Bash。 man 可用于获取了解命令工作方式所需的信息。

大多数 Bash 和 Linux 命令都支持 --help 选项。 这会显示命令的语法和选项的说明。 为进行演示,请输入 mkdir --help以这种方式获取的帮助通常比使用 man 获取的帮助更简洁。

使用通配符

通配符是表示 Bash 命令中的一个或多个字符的符号。 最常用的通配符是星号。 它表示无字符或字符序列。 假设当前目录中包含数百个图像文件,但你只想查看文件名以 .png 结尾的 PNG 文件。 以下命令仅列出上述文件:

1
bash复制代码ls *.png

备注

与其他操作系统不同,Linux 对于文件扩展名并没有正式概念。 这不表示 PNG 文件没有 .png 扩展名。 这只是表示 Linux 不会对文件名以 .png 结尾这一点附加任何特殊意义。

现在,假设当前目录中还包含 JPEG 文件。 一些文件以 .jpg 结尾,而其他文件以 .jpeg 结尾。 以下是列出所有 JPEG 文件的一种方法:

1
bash复制代码ls *.jpg *.jpeg

以下是另一种方法:

1
bash复制代码ls *.jp*g

* 通配符与零字符或多字符匹配,但 ? 通配符表示单字符。 如果当前目录中包含名为 0001.jpg、0002.jpg 以此类推到 0009.jpg 的文件,以下命令会将其全部列出:

1
bash复制代码ls 000?.jpg

但使用通配符筛选输出的另一种方法是使用表示字符组的方括号。 以下命令会列出当前目录中名称包含句点且句点后跟小写 J 或 P 的所有文件。该命令会列出所有 .jpg、.jpeg 和 .png 文件,但不会列出 .gif 文件:

1
bash复制代码ls *.[jp]*

在 Linux 中,文件名和对文件名进行操作的命令区分大小写。 因此,若要列出当前目录中名称包含句点且句点后跟大写或小写 J 或 P 的所有文件,可以输入以下内容:

1
bash复制代码ls *.[jpJP]*

方括号中的表达式可以表示字符范围。 例如,以下命令会列出当前目录中名称以小写字母开头的所有文件:

1
bash复制代码ls [a-z]*

相反,以下命令会列出当前目录中名称以大写字母开头的所有文件:

1
bash复制代码ls [A-Z]*

以下命令则会列出当前目录中名称以大写字母或小写字母开头的所有文件:

1
bash复制代码ls [a-zA-Z]*

如果需要将某个通配符用作普通字符,可以在该字符前加上一个反斜杠,使其成为文本或将其“转义”。 因此,如果由于某种原因在文件名中使用了星号(不应故意执行此操作),可以使用如下命令进行搜索:

1
shell复制代码$ ls ***

Bash 命令和运算符

ls 命令

ls 列出当前目录中的内容或命令参数所指定目录中的内容。 它本身会列出当前目录中的文件和目录:

1
bash复制代码ls

名称以句点开头的文件和目录默认隐藏。 若要在目录列表中添加这些项,请使用 -a 标志:

1
bash复制代码ls -a

若要详细了解当前目录中的文件和目录,请使用 -l 标志:

1
bash复制代码ls -l

cat 命令

假设要查看文件中的内容。 可以使用 cat 命令进行查看。 文件是文本文件时,输出才有意义。 以下命令显示存储在 /etc 目录中的 os-release 文件的内容:

1
bash复制代码cat /etc/os-release

此命令有用,因为它会告诉你正在运行哪个 Linux 分发版:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.2 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

/etc 目录是 Linux 中的一种特殊目录。 它包含系统配置文件。 不建议删除此目录中的任何文件,除非你了解要执行的操作。

sudo 命令

某些 Bash 命令只能由根用户(系统管理员或超级用户)运行。 如果在权限不足的情况下尝试其中一个命令,该命令会失败。 例如,只有以超级用户身份登录的用户才可以使用 cat 显示 /etc/at.deny 的内容:

1
bash复制代码cat /etc/at.deny

at.deny 是一个特殊文件,用于确定可使用其他 Bash 命令提交作业以供后续执行的用户。

不建议在大部分时间内以根身份运行。 太过危险。 若要在不以超级用户身份登录的情况下运行需要管理员权限的命令,请在命令前加上 sudo:

1
bash复制代码sudo cat /etc/at.deny

sudo 表示“superuser do”。 你使用它时,shell 知道对于此命令而言,你具有根用户级别的权限。

cd、mkdir 和 rmdir 命令

cd 表示“change directory”,其功能也恰如其名:将当前目录更改为其他目录。 与 Windows 中的对应命令相同,它可实现从一个目录到另一个目录的移动。 使用以下命令将移动到名为 orders 的当前目录的子目录:

1
bash复制代码cd orders

可以通过将 .. 指定为目录名称来向上移动目录:

1
bash复制代码cd ..

使用此命令将移动到主目录,即首次登录时所访问的目录:

1
bash复制代码cd ~

可以使用 mkdir 命令创建目录。 以下命令会在当前的工作目录中创建名为 orders 的子目录:

1
arduino复制代码mkdir orders

如果要使用一个命令创建子目录及其所属子目录,请使用 --parents 标志:

1
arduino复制代码mkdir --parents orders/2019

rmdir 命令可以删除目录,前提是目录不为空。 幸运的是,可以使用 rm 命令删除不为空的目录。

rm 命令

rm 命令是“remove”的缩写。 如你所料,rm 可以删除文件。 因此,此命令可以删除 0001.jpg:

1
bash复制代码rm 0001.jpg

此命令可以删除当前目录中的所有文件:

1
bash复制代码rm *

请谨慎使用 rm。 此命令太过危险。

运行带有 -i 标志的 rm 可以让你在删除之前有考虑时间:

1
bash复制代码rm -i *

养成在每个 rm 命令中添加 -i 的习惯,避免造成 Linux 中的重大误操作。 功能强大的 rm -rf / 命令可以删除整个驱动器上的所有文件。 其工作方式是递归删除根的所有子目录及其所属子目录。 -f(表示“force”)标志会隐藏提示从而将问题复杂化。 请勿这样操作。

如果要删除名为 orders 且不为空的子目录,可以通过以下方式使用 rm 命令:

1
bash复制代码rm -r orders

这会删除 orders 子目录和其中的所有内容,包括其他子目录。

cp 命令

如果需要,cp 命令不仅可以复制文件,还可以复制整个目录(和子目录)。 若要创建名为 0002.jpg 的 0001.jpg 副本,请使用以下命令:

1
yaml复制代码cp 0001.jpg 0002.jpg

如果 0002.jpg 已存在,则 Bash 会在无提示情况下将其替换。 如果这是你所需的结果,这样做很好,但如果你没有意识到将要覆盖旧版本,就很糟糕。

幸运的是,如果使用 -i(表示“interactive”)标志,Bash 会在删除现有文件之前发出警告。 以下命令更安全:

1
yaml复制代码cp -i 0001.jpg 0002.jpg

当然,可以使用通配符一次复制多个文件。 若要将当前目录中的所有文件复制到名为 photos 的子目录中,请使用以下命令:

1
bash复制代码cp * photos

若要将名为 photos 的子目录中的所有文件复制到名为 images 的子目录中,请使用以下命令:

1
bash复制代码cp photos/* images

此命令假定 images 目录已存在。 如果不存在,则可以创建该目录,然后使用以下命令复制 photos 目录的内容:

1
bash复制代码cp -r photos images

-r 表示“recursive”。 -r 标志的另一个优点是,如果 photos 包含自己的子目录,则子目录也会复制到 images 目录中。

ps 命令

ps 命令可以提供当前运行的所有进程的快照。 它本身不带参数,可显示所有 shell 进程,换言之,它无法显示太多进程。 但如果你添加 -e 标志,情况就会有所变化:

1
复制代码ps -e

-e 会列出所有运行中的进程,通常数量很多。

若要更全面地了解系统中正在运行的进程,请使用 -ef 标志:

1
复制代码ps -ef

此标志显示所有正在运行的进程的名称、其进程标识号 (PID)、其父进程的 PID (PPID) 及其开始时间 (STIME)。 还显示其连接到 (TTY) 的终端(如果有)、其占用的 CPU 时间 (TIME) 及其完整路径名。

另外,你可能会发现显示通过以下方式使用的 ps 的文档:

1
复制代码ps aux

ps aux 和 ps -ef 是相同的。 这种双重性可追溯到 POSIX Unix 系统(Linux 属其中之一)与 BSD Unix 系统(最常见的是 macOS)之间的历史差异。 最初,POSIX 使用 -ef,而 BSD 采用 aux。 现在,两种操作系统系列都接受这两种格式。

这很好地提醒了你应仔细阅读相关手册,了解所有的 Linux 命令。 学习 Bash 如同学习第二语言英语。 规则存在很多例外。

w 命令

为查找出服务器上的用户,Linux 提供了 w(表示“who”)命令。 它显示当前计算机系统上的用户及其活动相关的信息。 w 显示用户名、用户 IP 地址、用户登录时间、用户当前正在运行的进程及其运行时间。 它是系统管理员的重要工具。

Bash I/O 运算符

通过使用 Bash 命令及其许多选项可以在 Linux 中执行许多操作。 但你也可以在合并命令时使用以下 I/O 运算符执行这些操作:

  • <,用于将输入重定向到键盘以外的源
  • >,用于将输出重定向到屏幕以外的目标
  • >>,用于执行相同的操作,但只是追加,并不进行覆盖
  • |,用于将一个命令的输出通过管道传输到另一个命令的输入

假设要列出当前目录中的所有内容,并捕获名为 list.txt 的文件中的输出。 以下命令可实现此操作:

1
bash复制代码ls > listing.txt

如果 listing.txt 已存在,它会被覆盖。 如果改用 >> 运算符,则 ls 的输出将追加到 listing.txt 的现有内容中:

1
bash复制代码ls >> listing.txt

管道运算符功能非常强大(且经常使用)。 它可以将第一个命令的输出重定向到第二个命令的输入。 假设使用 cat 显示大型文件的内容,但内容滚动过快,你无法阅读。 可以将结果管道传输给 more 等其他命令,使输出更易于管理。 以下命令列出所有当前正在运行的进程。 但屏幕填满后,输出会暂停显示,直到你选择“Enter”后才会显示下一行:

1
复制代码ps -ef | more

还可以将输出管道传输到 head,以查看前几行:

1
bash复制代码ps -ef | head

或者,假设要筛选输出,使其只包括含词“daemon”的行。 实现此目的的一种方法是将 ps 的输出管道传输到 Linux 的 grep 工具:

1
perl复制代码ps -ef | grep daemon

还可以将文件用作输入。 标准输入默认来自键盘,但也可以进行重定向。 若要从文件而不是键盘获取输入,请使用 < 运算符。 系统管理员的一项常见任务是对文件内容进行排序。 顾名思义,sort 会按字母顺序对文本进行排序:

1
bash复制代码sort < file.txt

若要将排序结果保存到新文件,可以重定向输入和输出:

1
bash复制代码sort < file.txt > sorted_file.txt

可以根据需要使用 I/O 运算符链接 Linux 命令。 请考虑以下命令:

1
bash复制代码cat file.txt | fmt | pr | lpr

cat 的输出转到 fmt,fmt 的输出转到 pr,依此类推。 fmt 将结果格式化为整齐段落。 pr 对结果进行分页处理。 lpr 将分页输出发送到打印机。 一行命令即可完成所有操作!

本文转载自: 掘金

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

Python用10行代码爬取大批美女图片

发表于 2021-11-27

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

前言

说到美女,第一个想到的就是美女云集的相亲网站了。所以今天也是选取某个相亲网站作为素材,爬取美女图片。

1、准备工作

首先需要一个相亲网站的账号,我这里选取的是“我主良缘”。注册登陆就可以了:登陆后的界面
登陆后界面大致如上,填一些筛选条件,然后点搜缘分,就是我们要的结果了。但是我们要做的是爬取其中的美女图片,我们右击->检查->Network,然后我们再点一下搜缘分,发现多了下面这条东西:
点击搜缘分后多了一个请求内容
我们点一下,查看一下header中有什么:点进去之后发现一个API
其它我们都不需要看了,我们直接看一下这个网址。就是一个api,哈哈这就是我们要的美女图片api了。API如下:www.7799520.com/api/user/pc…

2、API解析

我们可以从URL中分析出这个API的参数,主要参数如下:

参数 参数类型
startage int
endage int
gender int
startheight int
endheight int
salary int
page int
对于这些参数哪些是必要的哪些是非必要的这个可以自己试出来,对参数值的限定也可以自己试试。因为博主比较年轻,所以今天测试的是21-30岁、身高151-160的女性。这个你们可以根据自己的爱好修改😄。

3、Json数据分析

在测试之后,发现上面的API返回的数据为Json数据,返回数据如下:

1
bash复制代码{    "data": {        "list": [            {                "avatar": "http://img.7799520.com/2019-11-27-1574867191-MXUdY0Fc.png",                "birthdayyear": "1994",                "city": "上海",                "education": "初中",                "gender": "2",                "height": "159",                "marry": "未婚",                "monolog": "愿得一人心,白首不相离",                "monologflag": "1",                "province": "上海",                "salary": "5千-1万",                "userid": "3018330",                "username": "单身笑山岚"            },            {                "avatar": "http://img.7799520.com/FhTV65n3mQ-X-PjfR3W9OpsFs5SO",                "birthdayyear": "1991",                "city": "北京",                "education": "本科",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "土生土长北京人一枚,91年底小天蝎~lxt1103程序猿,高薪资,没房有车小有存款~胖胖哒还不高,唉:-(喜爱旅游,美食,旅游吃美食~想找个喜欢运动的小哥哥陪我减肥,或者不介意胖姑娘的男生哦~男孩子最好也是北京的,这样共同话题多,不能离北京太远了,赶春运也很痛苦的希望你是个逗比或者心思灵巧的蓝孩纸,在一起开心快乐聊得来就很幸福了",                "monologflag": "-1",                "province": "北京",                "salary": "2万-5万",                "userid": "3018171",                "username": "桐桐桐桐桐"            },            {                "avatar": "http://img.7799520.com/00d0ba6e-5807-44fd-88af-eb379b325835",                "birthdayyear": "1991",                "city": "深圳",                "education": "高中",                "gender": "2",                "height": "155",                "marry": "未婚",                "monolog": "如果真心实意可以加微信you02457本人对年龄要求30--35",                "monologflag": "-1",                "province": "广东",                "salary": "1万-2万",                "userid": "3017206",                "username": "(坦诚相待)"            },            {                "avatar": "http://img.7799520.com/2019-11-27-1574817016-6JBhbUyU.png",                "birthdayyear": "1989",                "city": "西安",                "education": "大专",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "再晚也要嫁给爱情",                "monologflag": "-2",                "province": "陕西",                "salary": "2千-5千",                "userid": "3015509",                "username": "Best媛"            },            {                "avatar": "http://img.7799520.com/0e1ed4fa3b5ca22ed120bf08a452506b53c0da49-2019-11-27-15748275951574827595051-hSw85JrS.png",                "birthdayyear": "1995",                "city": "上海",                "education": "硕士",                "gender": "2",                "height": "155",                "marry": "未婚",                "monolog": "这个真的不知道咋写哇......爹妈每天催婚.....算是独白吗...",                "monologflag": "1",                "province": "上海",                "salary": "2万-5万",                "userid": "3014896",                "username": "。。。123"            },            {                "avatar": "http://img.7799520.com/f9e573e4-728a-4a05-8abd-9688c6d1c156",                "birthdayyear": "1997",                "city": "宁波",                "education": "初中",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "愿得一人心,白首不分离,15058276626",                "monologflag": "-1",                "province": "浙江",                "salary": "2千-5千",                "userid": "3014476",                "username": "季节娇气"            },            {                "avatar": "http://img.7799520.com/8c328b6a-f34a-4d91-a869-10f6e47627e9",                "birthdayyear": "1992",                "city": "深圳",                "education": "初中",                "gender": "2",                "height": "158",                "marry": "未婚",                "monolog": "愿得一人心,白首不分离我微信号chen123456qing",                "monologflag": "-1",                "province": "广东",                "salary": "5千-1万",                "userid": "3013067",                "username": "音响回眸勤奋"            },            {                "avatar": "http://img.7799520.com/9f74fb99444547a1408575c346008f22ac4bb1f7-2019-11-25-15746785901574678589876-kHZrSfnc.png",                "birthdayyear": "1992",                "city": "济南",                "education": "大专",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "也许我很平凡,但是我绝不缺乏生活的热情和生命的梦想,也许我会孤单,但是我会一路找寻你的踪迹。遇见你,将是我生命中最绚烂的时刻。",                "monologflag": "1",                "province": "山东",                "salary": "5千-1万",                "userid": "3009076",                "username": "骄傲的猫大王"            },            {                "avatar": "http://img.7799520.com/7da0c781-3115-467f-9fcc-d46d2aa1bb4a",                "birthdayyear": "1994",                "city": "国外",                "education": "高中",                "gender": "2",                "height": "155",                "marry": "未婚",                "monolog": "我有一壶酒,足以慰风尘",                "monologflag": "1",                "province": "国外",                "salary": "2千-5千",                "userid": "3007139",                "username": "墨染."            },            {                "avatar": "http://img.7799520.com/2019-11-24-1574575893-JYE0Y9nz.png",                "birthdayyear": "1994",                "city": "北海",                "education": "大专",                "gender": "2",                "height": "157",                "marry": "未婚",                "monolog": "愿得一人心,白首不相离,非会员哦,所以很多信息都看不到呢,抱歉",                "monologflag": "1",                "province": "广西",                "salary": "5千-1万",                "userid": "3006914",                "username": "蔓鲸"            },            {                "avatar": "http://img.7799520.com/2019-11-24-1574565615-2p6Q37YC.png",                "birthdayyear": "1995",                "city": "广州",                "education": "本科",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "如果在一起是因为合适,那希望是合适一辈子。",                "monologflag": "1",                "province": "广东",                "salary": "5千-1万",                "userid": "3006237",                "username": "长颈鹿向淡淡"            },            {                "avatar": "http://img.7799520.com/4c69af45f1f9763bc33b7322cd025c90157a93b9-2019-11-23-15745152791574515278714-5F2a7dhi.png",                "birthdayyear": "1997",                "city": "上海",                "education": "大专",                "gender": "2",                "height": "158",                "marry": "未婚",                "monolog": "好看的皮囊千篇一律,有趣的灵魂万里挑一。。。",                "monologflag": "1",                "province": "上海",                "salary": "1万-2万",                "userid": "3004596",                "username": "solely"            },            {                "avatar": "http://img.7799520.com/aaf297dd-af30-48de-8027-5c7e57ec2cdc",                "birthdayyear": "1993",                "city": "深圳",                "education": "高中",                "gender": "2",                "height": "155",                "marry": "未婚",                "monolog": "在现在快节奏的社会,忙碌的工作之余,希望有个知心人陪伴,偶尔逛街,看电影吃饭,一起旅游,运动,分享彼此的喜怒哀乐,希望相互欣赏,包容,理解。我认为最好的爱情莫过于为彼此成为最好的自己,成为最默契的搭档,一起发现这个世界的美好。",                "monologflag": "1",                "province": "广东",                "salary": "5千-1万",                "userid": "3003499",                "username": "一木木"            },            {                "avatar": "http://img.7799520.com/2019-11-22-1574436265-oOHCA0Pi.png",                "birthdayyear": "1991",                "city": "上海",                "education": "高中",                "gender": "2",                "height": "153",                "marry": "未婚",                "monolog": "爱吃西瓜的跳舞女少年?",                "monologflag": "-2",                "province": "上海",                "salary": "5千-1万",                "userid": "3001594",                "username": "西瓜西瓜瓜"            },            {                "avatar": "http://img.7799520.com/6351f7c2-734d-484f-95ae-7881b3b65132",                "birthdayyear": "1996",                "city": "南昌",                "education": "中专",                "gender": "2",                "height": "158",                "marry": "未婚",                "monolog": "事事有回应,渐渐有着落",                "monologflag": "1",                "province": "江西",                "salary": "2千-5千",                "userid": "2999190",                "username": "977"            },            {                "avatar": "http://img.7799520.com/bc692905b97d0deeb6df0f73356d3de82b1d6261-2019-11-23-15745076641574507664470-zV4AFL8O.png",                "birthdayyear": "1990",                "city": "成都",                "education": "大专",                "gender": "2",                "height": "156",                "marry": "未婚",                "monolog": "在成都的东北人!照片是很多年前的了。不喜欢拍照所以没有现在的照片!我身高155体重42公斤。不喜欢:属羊的男生,最好不抽烟不喝酒!我属蛇天蝎座♏️",                "monologflag": "1",                "province": "四川",                "salary": "2千-5千",                "userid": "2998289",                "username": "水壶苦恋无语"            },            {                "avatar": "http://img.7799520.com/7384954e-5c0d-4a5c-92c6-4493ba1be3d4",                "birthdayyear": "1995",                "city": "苏州",                "education": "大专",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "嗨 你好 能带给我一份超大杯快乐嘛",                "monologflag": "1",                "province": "江苏",                "salary": "2千-5千",                "userid": "2991868",                "username": "小呀么小静静"            },            {                "avatar": "http://img.7799520.com/FlUJTeR0REKbLhtoR5RNVeuOXRy1",                "birthdayyear": "1992",                "city": "苏州",                "education": "大专",                "gender": "2",                "height": "160",                "marry": "未婚",                "monolog": "爱好看动漫和小说,比较宅,做事喜欢有计划,喜欢独处,自在。理想伴侣就是要有稳定的工作。。。。",                "monologflag": "1",                "province": "江苏",                "salary": "2千-5千",                "userid": "2989769",                "username": "青一木"            },            {                "avatar": "http://img.7799520.com/edbb6516-2b07-401e-b56e-6aee6c2620ca",                "birthdayyear": "1994",                "city": "巴中",                "education": "高中",                "gender": "2",                "height": "156",                "marry": "未婚",                "monolog": "我是找对象的,感觉我还行的,可以加JC718829",                "monologflag": "-1",                "province": "四川",                "salary": "5千-1万",                "userid": "2989629",                "username": "星愿回首悲凉"            },            {                "avatar": "http://img.7799520.com/e648d317faacffb4f03b1ca31fdbed2b4c6ec5e4-2019-11-25-15746776401574677640473-C6Z1QX0K.png",                "birthdayyear": "1990",                "city": "深圳",                "education": "初中",                "gender": "2",                "height": "159",                "marry": "未婚",                "monolog": "愿得一人心,白首不相离",                "monologflag": "1",                "province": "广东",                "salary": "2千-5千",                "userid": "2988102",                "username": "兰玛珊蒂"            }        ],        "num": 20,        "page": 1    },    "error_code": 0}

json数据的大致结构
我们可以分析这个结构来获取自己需要的信息。

4、代码讲解

如果使用过爬虫一般都会觉得Python的爬虫是非常简单的,正如标题所言,只需要10行代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import requests		#导入request包
dir = 'C:/Users/zaxwz/Desktop/xqImg/' #用来存储图片的文件夹路径
#图片的url,我这里page没给参数,为了方便后面换页
url = 'http://www.7799520.com/api/user/pc/list/search?startage=21&endage=30&gender=2&startheight=151&endheight=160&marry=1&salary=3&page='
#用循环,爬取40页的美女
for i in range(40):
#其返回值为json数据,直接获取其json字典
jsonData = requests.get(url + str(i+1)).json()
#通过jsonData['data']['list']获取美女列表
for j in jsonData['data']['list']:
#其中j['avatar']为图片网址
imgUrl = j['avatar']
#发送网络请求
resp = requests.get(imgUrl)
#创建图片文件,并将流写入图片
img = open(dir + j['username'] + '.jpg', 'wb')
img.write(resp.content)

这样爬取美女图片就完成了,去掉注释的话正好是10行代码。爬取图片如下:爬取的图片

本文转载自: 掘金

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

Oracle移动数据文件 11G and before 12

发表于 2021-11-27

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

11G and before

分为不停机和停机两种方式:

一、不停机移动数据文件

完整步骤:

1、确认开启归档模式

2、offline数据文件

3、物理层移动数据文件(可重命名)

4、逻辑层rename数据文件路径及名称

5、recover恢复数据文件

6、online数据文件

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
sql复制代码--开启归档模式
SQL> archive log list
Database log mode No Archive Mode
Automatic archival Disabled
Archive destination /archivelog
Oldest online log sequence 1
Current log sequence 2
SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL>
SQL>
SQL> startup mount
ORACLE instance started.

Total System Global Area 1603411968 bytes
Fixed Size 2253664 bytes
Variable Size 452988064 bytes
Database Buffers 1140850688 bytes
Redo Buffers 7319552 bytes
Database mounted.
SQL> alter database archivelog;

Database altered.

SQL> alter database open;

Database altered.

--offline数据文件
SQL> /

FILE# NAME STATUS
---------- ------------------------------------------------------------ -------
1 /oradata/orcl11g/system01.dbf SYSTEM
2 /oradata/orcl11g/sysaux01.dbf ONLINE
3 /oradata/orcl11g/undotbs01.dbf ONLINE
4 /oradata/orcl11g/users01.dbf ONLINE
5 /oradata/orcl11g/example01.dbf ONLINE
6 /oradata/orcl11g/test01.dbf ONLINE
7 /oradata/ORCL11G/datafile/o1_mf_test_j7jgpq7k_.dbf ONLINE

7 rows selected.

SQL> alter database datafile 7 offline;

Database altered.

--物理层移动数据文件
SQL> !mv /oradata/ORCL11G/datafile/o1_mf_test_j7jgpq7k_.dbf /oradata/orcl11g/test02.dbf

SQL> !ls /oradata/orcl11g/test02.dbf
/oradata/orcl11g/test02.dbf

--逻辑层rename数据文件
SQL> alter database rename file '/oradata/ORCL11G/datafile/o1_mf_test_j7jgpq7k_.dbf' to '/oradata/orcl11g/test02.dbf';

Database altered.

--恢复数据文件
SQL> recover datafile 7;
Media recovery complete.

--online数据文件
SQL> alter database datafile 7 online;

Database altered.

SQL> select file#,name,status from v$datafile;

FILE# NAME STATUS
---------- ------------------------------------------------------------ -------
1 /oradata/orcl11g/system01.dbf SYSTEM
2 /oradata/orcl11g/sysaux01.dbf ONLINE
3 /oradata/orcl11g/undotbs01.dbf ONLINE
4 /oradata/orcl11g/users01.dbf ONLINE
5 /oradata/orcl11g/example01.dbf ONLINE
6 /oradata/orcl11g/test01.dbf ONLINE
7 /oradata/orcl11g/test02.dbf ONLINE

7 rows selected.

二、停机移动数据文件

完整步骤:

1、关闭数据库

2、物理层移动数据文件(可重命名)

3、开启数据库到mount

4、逻辑层rename数据文件路径及名称

5、开启数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sql复制代码--创建一个TEST表空间,发现建在了/oradata/ORCL11G/下,希望移动到/oradata/orcl11g/下
SQL> create tablespace TEST;

Tablespace created.

SQL> select name from v$datafile;

NAME
--------------------------------------------------------------------------------
/oradata/orcl11g/system01.dbf
/oradata/orcl11g/sysaux01.dbf
/oradata/orcl11g/undotbs01.dbf
/oradata/orcl11g/users01.dbf
/oradata/orcl11g/example01.dbf
/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf

--尝试在线移动数据文件
SQL> alter database rename file '/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf' to '/oradata/orcl11g/test01.dbf';
alter database rename file '/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf' to '/oradata/orcl11g/test01.dbf'
*
ERROR at line 1:
ORA-01511: error in renaming log/data files
ORA-01121: cannot rename database file 6 - file is in use or recovery
ORA-01110: data file 6: '/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf'

--报错ORA-01121
[oracle@orcl11g:/home/oracle]$ oerr ORA 01121
01121, 00000, "cannot rename database file %s - file is in use or recovery"
// *Cause: Attempted to use ALTER DATABASE RENAME to rename a
// datafile that is online in an open instance or is being recovered.
// *Action: Close database in all instances and end all recovery sessions.

明确无法在线移动数据文件,需要关闭数据库。

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
sql复制代码--操作系统层面移动数据文件,并且重命名
[oracle@orcl11g:/oradata/ORCL11G/datafile]$ ll
total 102408
-rw-r----- 1 oracle oinstall 104865792 Apr 15 20:55 o1_mf_test_j7jfm30c_.dbf
[oracle@orcl11g:/oradata/ORCL11G/datafile]$ mv o1_mf_test_j7jfm30c_.dbf /oradata/orcl11g/
control01.ctl example01.dbf redo01.log redo02.log redo03.log sysaux01.dbf system01.dbf temp01.dbf undotbs01.dbf users01.dbf
[oracle@orcl11g:/oradata/ORCL11G/datafile]$ mv o1_mf_test_j7jfm30c_.dbf /oradata/orcl11g/test01.dbf
[oracle@orcl11g:/oradata/ORCL11G/datafile]$ ll /oradata/orcl11g/test01.dbf

--开启数据库到mount
SQL> startup mount
ORACLE instance started.

Total System Global Area 1603411968 bytes
Fixed Size 2253664 bytes
Variable Size 452988064 bytes
Database Buffers 1140850688 bytes
Redo Buffers 7319552 bytes
Database mounted.

--rename数据文件名称
SQL> select name from v$datafile;

NAME
--------------------------------------------------------------------------------
/oradata/orcl11g/system01.dbf
/oradata/orcl11g/sysaux01.dbf
/oradata/orcl11g/undotbs01.dbf
/oradata/orcl11g/users01.dbf
/oradata/orcl11g/example01.dbf
/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf

6 rows selected.

SQL> alter database rename file '/oradata/ORCL11G/datafile/o1_mf_test_j7jfm30c_.dbf' to '/oradata/orcl11g/test01.dbf';

Database altered.

SQL> select name from v$datafile;

NAME
--------------------------------------------------------------------------------
/oradata/orcl11g/system01.dbf
/oradata/orcl11g/sysaux01.dbf
/oradata/orcl11g/undotbs01.dbf
/oradata/orcl11g/users01.dbf
/oradata/orcl11g/example01.dbf
/oradata/orcl11g/test01.dbf

6 rows selected.

--开启数据库
SQL> alter database open;

Database altered.

12C and later

支持在线移动数据文件:

可参考:Online Move Datafile in Oracle Database 12c Release 1 (12.1)

语法如下:

1
2
3
sql复制代码ALTER DATABASE MOVE DATAFILE ( 'filename' | 'ASM_filename' | file_number )
[ TO ( 'filename' | 'ASM_filename' ) ]
[ REUSE ] [ KEEP ]

blog.csdn.net/m0_50546016…

本文转载自: 掘金

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

我们是这样设计对外安全接口的

发表于 2021-11-27

背景

在日常工作中难免会调用第三方系统的接口,一个项目会有多个服务组合而成,负责各自核心的服务,在接触的项目中他主要以门户服务、核心服务组合而成。

大部分门户服务会调用核心服务。当然如果有支付业务,会调用其他部门的支付服务,这样的话设计一个统一安全的对外接口就极为重要。

安全措施

AppId机制

并不是谁都可以来调用的服务的。需要使用接口的服务需要在后台开通appId和相应密钥。

数据加密

数据在传输的过程中是很容易被抓包的,常见的做法对请求的内容做数据加密。

数据加签

数据在传输的过程中很容易被篡改,常见的做法是利用数据加签发送者产生一段无法伪造的数字串,来保证数据在传输的过程中不被篡改。

限流机制

本来就是真实的用户,并且开通了appid,但是出现频繁调用接口的情况,这种情况需要给相关appid限流处理,常用的限流算法有令牌桶。

实际项目中是如何实现的?

AppId机制

我们项目中AppId机制是由应用+终端组合的,也就是说一个应用会有多个终端。在运营平台里开通相应应用AppId,应用底下添加相应的终端TermId,之后就可以请求我们的服务要带上AppId、TermId。当然添加应用的时候会产生有对应的密钥(随机32位),这个密钥是用来加密+签名的。

运营平台提供运营商、商户入驻。每个商户入驻都会得到一个终端Id。如果是提供给公司其他部门,运营平台会用admin账户提前分配一套一个AppId和TermId给其他部门使用。

数据加密加签SDK

相信大家在开发过程中如果有接触调用第三方API的时候会映入对方的SDK,之后调用对方API如下:

我们的做法也是类似。我们有一个专门的项目SDK。该SDK包含加密解密加签验签功能,别人的服务要调用我们的服务,先引入我们的SDK。SDK由TransRequest 、Handler、TransResponse三大部分组成

TransRequest是请求模板,对应的app_id、app_key、term_id、加密类型、签名类型、请求内容、请求方法等。Handler主要是对信息签名加密、请求具体的服务、信息解密验签。请求具体的服务本质还是用HttpClient调用。TransResponse是统一返回模板

数据加密

加密方式有对称加密和非对称加密

对称加密:对称加密在加密和解密的过程中使用的密钥是相同的,常见的对称加密算法有DES,AES;有点计算速度快。缺点:发送方接收方商定好密钥。

非对称加密:服务端会生成一对密钥,私钥存放在服务端,公钥可以发给任何人使用。优点:安全。缺点:速度慢。

我们采取的是使用密钥进行AES加密。

数据加签

数据签名我们是将TransRequest里按照key value拼接生成用sm3算法签名的。

关于加签验签一些基本的概念 可参考

juejin.cn/post/685457…

总结

  1. 调用者引入我们的SDK,有一套请求模板,我们通过运营平台管理AppId、TermId、AppKey,把这些下发给调用者。
  1. SDK对信息加密签名去请求我们的服务,服务端在进行解密验证签名进行相应的处理业务返回,返回时候再对信息进行签名加密。
  1. SDK对返回的信息在进行解密验签返回给调用者是明文,整个过程中加密验证签名对用户是无感的。

本文转载自: 掘金

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

并查集(例子+画图+Code详细解析) 并查集

发表于 2021-11-27

并查集

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

什么是并查集

举个例子简单理解,就是相同血缘的人组成了一个个家族(不考虑家庭伦理剧!)

  1. 两个人都没有家族但是血缘相同,那么他们俩成立一个家族

image.png

  1. 如果某人和某个家族的人有血缘关系,就把他加入该家族

image.png

  1. 如果我们发现两个不同家族的人有人血缘相同,就把两个家族合并为一个家族

image.png
最后得到的情况是,各个家族真的没有血缘关系了

总结下来并查集就是:

  1. 并查集可以进行集合合并的操作(并)
  2. 并查集可以查找元素在哪个集合中(查)
  3. 并查集维护的是一堆集合(集)

根据具体场景深入理解

上述例子只是有了初步的理解,具体怎么使用和如何考虑,可以在下面的例子中更有效的学习。

背景介绍

冗余连接

树可以看成是一个连通且无环的无向图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。

图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

image.png

输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]


输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]

题目分析

一共有n个点n条边,如果没有环的情况下应该仅有n-1条边才对

我们的任务是删除一条边,但不会让任何点孤立,失去联系

可以考虑按边出现次序依次选择,对每条边上的两个点,设为A,B进行分析

其情况有以下三种:

  1. 两个点都没有本访问过,那么我们就让A作为父亲节点,B作为子节点,AB构成了一颗树
    并将点AB访问情况设置为true
  2. 有一个节点被访问过,例如B节点被访问过,A节点没有被访问过
    那么B节点可能是一个树的根,叶子节点或者中间节点,但无论哪一种只需要让A的父亲为B即可,这样A也属于B家族的一个成员了
    并将B设置为ture
  3. A,B都被访问过

(1)我们首先查找A和B节点的根节点,他们俩是不是一个家族的,如果是一个家族的说明他们俩已经被链接起来了,那么这一条边便是多余的

根据本题只会有一条多余的边,因此返回这一条即可啦

(2)如果A和B家族不一样,那么我们将A和B两大家族合并即可,我们可以把B家族的族长也就是根结点并入到A节点,或者A节点的族长之下即可

解题代码

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
ini复制代码    public int[] findRedundantConnection(int[][] edges) {
boolean[] visited=new boolean[edges.length+1];//创建n长度大小的数组,用来记录节点是否被访问过了
HashMap<Integer,Integer>map=new HashMap<>();//我们不用单独创建树结构,用mao数组保存 孩子索引和父亲索引即可
for (int i = 0; i < edges.length; i++) {
int a=edges[i][0];
int b=edges[i][1];
if(visited[a]==false&&visited[b]==false){//两者都没有归属
map.put(b,a);//让a当b的父节点
map.put(a,-1);//设a的父节点为-1,用来判断到头了
visited[a]=true;
visited[b]=true;
}else if(visited[a]==false){
map.put(a,b);
visited[a]=true;
}else if(visited[b]==false){
map.put(b,a);
visited[b]=true;
}else {
int rootOfA=getRoot(a,map);
int rootOfB=getRoot(b,map);
if(rootOfA==rootOfB)return edges[i];
map.put(rootOfB,rootOfA);//让a当b的父节点
}
}
return null;
}
public int getRoot(int cur,HashMap<Integer,Integer>map){
if(map.get(cur)==-1)return cur;
return getRoot(map.get(cur),map);
}

并查集的经典案例-克鲁斯卡尔算法

克鲁斯卡尔算法简介

克鲁斯卡尔算法是一种用来寻找最小生成树的算法(用来求加权连通图的最小生成树的算法)。在剩下的所有未选取的边中,找最小边,如果和已选取的边构成回路,则放弃,选取次小边。

具体的操作过程为:

  1. 将图的所有连接线去掉,只剩顶点
  2. 从图的边集数组中找到权值最小的边,将边的两个顶点连接起来
  3. 继续寻找权值最小的边,将两个顶点之间连接起来,如果选择的边使得最小生成树出现了环路,则放弃该边,选择权值次小的边
  4. 直到所有的顶点都被连接在一起并且没有环路,最小生成树就生成了。

两个核心问题

问题一 对图的所有边按照权值大小进行排序。

直接采用排序算法进行排序即可,或者构建最小堆,也是不错的选择。

问题二 将边添加到最小生成树中时,怎么样判断是否形成了回路。

核心思想是记录处理,运用了并查集的处理思想

处理方式是:记录顶点在”最小生成树”中的终点,顶点的终点是”在最小生成树中与它连通的最大顶点”。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

完整版代码

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


class Edge implements Comparable<Edge> {
//起始点
private int begin;
//终止点
private int end;
//权值
private int weight;

public Edge(int begin, int end, int weight) {
this.begin = begin;
this.end = end;
this.weight = weight;
}

public int getBegin() {
return begin;
}

public void setBegin(int begin) {
this.begin = begin;
}

public int getEnd() {
return end;
}

public void setEnd(int end) {
this.end = end;
}

public int getWeight() {
return weight;
}

public void setWeight(int weight) {
this.weight = weight;
}

@Override
public int compareTo(Edge o) {
if (o.weight > this.weight) {
return -1;
} else {
return 1;
}
}
}

public class Kruskal {

public static void main(String[] args) {
//默认以a为根节点的最小生成树
List<Edge> list = new ArrayList<>();
int[][] arr = new int[][]{
{-1, 4, 0, 0, 0, 0, 0, 8, 0},
{0, -1, 8, 0, 0, 0, 0, 11, 0},
{0, 0, -1, 7, 0, 4, 0, 0, 2},
{0, 0, 0, -1, 9, 14, 0, 0, 0},
{0, 0, 0, 0, -1, 10, 0, 0, 0},
{0, 0, 0, 0, 0, -1, 2, 0, 0},
{0, 0, 0, 0, 0, 0, -1, 1, 6},
{0, 0, 0, 0, 0, 0, 0, -1, 7},
{0, 0, 0, 0, 0, 0, 0, 0, -1}
};
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i][j] > 0) {
list.add(new Edge(i, j, arr[i][j]));
}
}
}
Collections.sort(list);
//数组中每一个节点都只知道他的父节点是什么,-1表示不存在父节点,0位置是根节点
int[] parent = new int[arr.length];
for (int i = 1; i < arr.length; i++) {
parent[i] = -1;
}
int m = 0, n = 0;
for (Edge edge : list) {
//寻找这两个点有没有相同的父节点
m = find(parent, edge.getBegin());
n = find(parent, edge.getEnd());
if (m != n ) {
parent[m] = n;
System.out.println("加入边("+edge.getBegin()+","+edge.getEnd()+") 权重 "+ edge.getWeight());
}
}
System.out.println(Arrays.toString(parent));
}

private static int find(int[] parent, int ch) {
while (parent[ch] > 0) {
ch = parent[ch];
}
return ch;
}
}

代码结果输出

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码加入边(6,7) 权重 1
加入边(2,8) 权重 2
加入边(5,6) 权重 2
加入边(0,1) 权重 4
加入边(2,5) 权重 4
加入边(2,3) 权重 7
加入边(0,7) 权重 8
加入边(3,4) 权重 9
[1, 3, 8, 4, -1, 7, 7, 3, 7]

Process finished with exit code 0

本文转载自: 掘金

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

线程的三种创建方式

发表于 2021-11-27

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

1、Thread

继承Thread类,并重写run方法

1
2
3
4
5
6
scala复制代码class ThreadDemo1 extends Thread {
@Override
public void run() {
log.info("{}", Thread.currentThread().getName());
}
}

线程启动方式

1
2
3
ini复制代码ThreadDemo1 t1 = new ThreadDemo1();
t1.setName("t1");
t1.start();

简便写法

1
2
3
4
5
6
7
8
java复制代码Thread t1 = new Thread() {
@Override
public void run() {
log.info("{}", Thread.currentThread().getName());
}
};
t1.setName("t1");
t1.start();

2、Runnable和Thread

Thread类的构造函数支持传入Runnable的实现类

1
2
3
4
5
6
7
8
csharp复制代码public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}


Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}

Runnable是一个函数式接口(FunctionalInterface)

1
2
3
4
5
csharp复制代码@FunctionalInterface
public interface Runnable {
// 没有返回值
public abstract void run();
}

因此需要创建类实现Runnable接口,重写run方法

1
2
3
4
5
6
7
typescript复制代码class ThreadDemo2 implements Runnable {

@Override
public void run() {
log.info("{}", Thread.currentThread().getName());
}
}

简便写法

1
2
ini复制代码Thread t2 = new Thread(() -> log.info("{}", Thread.currentThread().getName()), "t2");
t2.start();

3、Callable和Thread

Callable和Runnable一样,也是一个函数式接口,二者的区别非常明显,Runnable中run方法没有返回值,Callable中的run方法有返回值(可以通过泛型约束返回值类型)。因此在需要获取线程执行的返回值时,可以使用Callable。

1
2
3
4
5
csharp复制代码@FunctionalInterface
public interface Callable<V> {
// 带返回值
V call() throws Exception;
}

在Thread的构造函数中,并没有看到Callable,只有Runnable

此时需要一个可以提交Callable给Thread的类,这类就是FutureTask;FutureTask实现类Runnable接口。

并且FutureTask提供了传入Callable的构造函数

1
2
3
4
5
6
ini复制代码public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}

因此可以通过FutureTask传入Callable实现,再将FutureTask传给Thread即可

1
2
3
4
5
6
7
8
java复制代码ThreadDemo3 implements Callable<Integer> {

@Override
public Integer call() throws Exception {
log.info("{}", Thread.currentThread().getName());
return 1998;
}
}
1
2
3
4
5
6
7
ini复制代码// Callable 实现类
ThreadDemo3 callable = new ThreadDemo3();
// 通过Callable创建FutureTask
FutureTask<Integer> task = new FutureTask(callable);
// 通过FutureTask创建Thread
Thread t3 = new Thread(task, "t3");
t3.start();

简便写法

1
2
3
4
5
ini复制代码Thread t3 = new Thread(new FutureTask<Integer>(() -> {
log.info("{}", Thread.currentThread().getName());
return 1998;
}), "t3");
t3.start();

4、三者对比

创建线程的方式有三种,Thread、Runnable+Thread、Callable+FutureTask+Thread;这三者如何选择呢?

  • 首先在实际的开发过程中,我们不会直接创建线程,因为频繁创建和销毁线程开销比较大,此外不利于管理和释放,因此项目中都是通过设计线程池来管理线程资源
  • Thread、Runnable+Thread相比,Runnable+Thread将线程的创建和任务模块解耦了,代码设计更加灵活,此外更加利于任务的提交,更方便和线程池结合使用
  • Callable+FutureTask+Thread适用于需要获取线程返回结果的场景

5、注意项

文中多次使用thread.start();需要注意的是,调用线程的start()方法表示启动线程,但是线程是否执行并不确定,这需要操作系统调度,线程分配到CPU执行时间片才能执行。多核CPU下多个线程同时启动,线程之间交替执行,执行顺序是不确定的。

本文转载自: 掘金

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

HarmonyOS(鸿蒙)——Image(图片)组件介绍

发表于 2021-11-27

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

1、简介

Image是用来显示图片的组件,我们在开发中使用的非常频繁!

2、属性表

Image也是组件,它继承自:ohos.agp.components.Component


我们在使用Image组件的时候,只需要事先搞懂它的属性,使用起来就很方便了(官方指定的全部Image属性,都在这里):

属性名称 中文描述 取值 取值说明 使用案例
clip_alignment 图像裁剪对齐方式 left 表示按左对齐裁剪。 ohos:clip_alignment=”left”
right 表示按右对齐裁剪。 ohos:clip_alignment=”right”
top 表示按顶部对齐裁剪。 ohos:clip_alignment=”top”
bottom 表示按底部对齐裁剪。 ohos:clip_alignment=”bottom”
center 表示按居中对齐裁剪。 ohos:clip_alignment=”center”
image_src 图像 Element类型 可直接配置色值,也可引用color资源或引用media/graphic下的图片资源。 ohos:image_src=”#FFFFFFFF”ohos:image_src=”color:black”ohos:imagesrc=”color:black”ohos:image_src=”color:black”ohos:images​rc=”media:warning”ohos:image_src=”$graphic:graphic_src”
scale_mode 图像缩放类型 zoom_center 表示原图按照比例缩放到与Image最窄边一致,并居中显示。 ohos:scale_mode=”center”
zoom_start 表示原图按照比例缩放到与Image最窄边一致,并靠起始端显示。
zoom_end 表示原图按照比例缩放到与Image最窄边一致,并靠结束端显示。
stretch 表示将原图缩放到与Image大小一致。
center 表示不缩放,按Image大小显示原图中间部分。
inside 表示将原图按比例缩放到与Image相同或更小的尺寸,并居中显示。
clip_center 表示将原图按比例缩放到与Image相同或更大的尺寸,并居中显示。

3、使用

3.1 上传资源

在使用Image组件之前,需要知道HarmonyOS的应用程序结构中,图片资源的存放位置。

我们创建一个项目之后,打开项目的entry > src > main > resources > base > media目录,这个下面会有一张默认的icon.png图片。这里就是图片指定存放位置了。

我们接下来使用Java之父(詹姆斯·高斯林 (James Gosling)),来做测试,保佑大家都成为Java之母,哈哈哈哈哈!!!

将图片放置到entry > src > main > resources > base > media即可!

3.2 代码中使用

HarmonyOS的Java语义开发中,组件可以通过XML配置和Java代码直接构建,这里两种方式都演示一下。

3.2.1 XML创建Image

在src -> main -> resources -> base -> layout -> ability_main.xml中实现如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="vertical">


<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
/>

</DirectionalLayout>

启动应用程序看下效果,我们来瞅瞅Java之父(嗯嗯嗯,有点小呀!!)

3.2.2 Java代码创建Image

注释掉XML中配置的Image,我们改用Java代码实现。

在com.liziba.image.slice.MainAbilitySlice类的onStart方法中,创建Image代码如下:

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
java复制代码package com.liziba.image.slice;

import com.liziba.image.ResourceTable;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.Image;

public class MainAbilitySlice extends AbilitySlice {
@Override
public void onStart(Intent intent) {
super.onStart(intent);

//创建一个Image组件
Image image = new Image(getContext());
image.setPixelMap(ResourceTable.Media_JamesGosling);
image.setHeight(500);
image.setWidth(500);
image.setScaleMode(Image.ScaleMode.STRETCH);


//创建一个布局
DirectionalLayout layout = new DirectionalLayout(getContext());
//Image组件添加到DirectionalLayout布局中
layout.addComponent(image);

super.setUIContent(layout);
}

@Override
public void onActive() {
super.onActive();
}

@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
}

这里做了一个简单的缩放,将原图缩放到与Image大小一致,看下效果(嗯不错,长大了点,哈哈哈!!!):

3.3 属性

由于在实际开发中,XML配置UI的还是多一些,因为方便改动和统一管理,也更加灵活。所以属性这里,都用XML配置来演示啦!!!

3.3.1 透明度

设置透明度为0.2,透明度设置的越小,图片越透明,原图透明度为1。

ohos:alpha=”0.2”

1
2
3
4
5
6
7
ini复制代码<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
ohos:alpha="0.2"
/>

3.3.2 缩放系数

当我们给定的图片大小和Image组件设置的大小不一致的时候,我们往往需要通过缩放来实现兼容。(但是为了图片不失真,最后还是少用缩放,图片大小不符合尽量找美工做一张新的图片。)

缩放系数可设置X轴和Y轴方向两个参数,其实就是宽、高

如下是缩放x和y轴都配置0.5的效果

ohos:scale_x=”0.5”

ohos:scale_y=”0.5”

1
2
3
4
5
6
7
8
9
ini复制代码<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
ohos:scale_x="0.5"
ohos:scale_y="0.5"
/>

如下是配置为1的效果,默认为1,可以不配置:

ohos:scale_x=”1”

ohos:scale_y=”1”

1
2
3
4
5
6
7
8
9
ini复制代码<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
ohos:scale_x="1"
ohos:scale_y="1"
/>

\

能不能配置为比1大的数呢?当然是可以的,这样会更加配置的缩放参数,进行放大。

比如配置一个2试试:

ohos:scale_x=”2”

ohos:scale_y=”2”

1
2
3
4
5
6
7
8
9
ini复制代码<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
ohos:scale_x="2"
ohos:scale_y="2"
/>

3.3.3 裁剪

如果给定的图片太大了,超过了我们的Image设置的大小,那该怎么办呢?

这个时候我们可以使用裁剪,裁剪有下面这些参数,只演示一个吧!

属性名称 中文描述 取值 取值说明 使用案例
clip_alignment 图像裁剪对齐方式 left 表示按左对齐裁剪。 ohos:clip_alignment=”left”
right 表示按右对齐裁剪。 ohos:clip_alignment=”right”
top 表示按顶部对齐裁剪。 ohos:clip_alignment=”top”
bottom 表示按底部对齐裁剪。 ohos:clip_alignment=”bottom”
center 表示按居中对齐裁剪。 ohos:clip_alignment=”center”

我们先将Image组件设置小一点,这样我们能看出裁剪的效果,这里我们采取按左对齐裁剪:

1
2
3
4
5
6
7
8
xml复制代码<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="50vp"
ohos:width="50vp"
ohos:image_src="$media:JamesGosling"
ohos:clip_alignment="left"
/>

3.3.4 缩放

当图片和Image组件的大小不同的时候,我们可以通过缩放的形式来自行适配。

比如我们此时设置的Image组件大小宽高均为200vp,但是图片的肯定没得这么大,所以我们可以考虑放大图片,让图片放大到Image组件的大小即可。

我们采用stretch,将原图缩放到与Image大小一致。

ohos:scale_mode=”stretch”

1
2
3
4
5
6
7
8
xml复制代码<!--XML配置Image-->
<Image
ohos:id="$+id:imageComponent"
ohos:height="200vp"
ohos:width="200vp"
ohos:image_src="$media:JamesGosling"
ohos:scale_mode="stretch"
/>

Image组件非常简单,多试试就会了!!!

本文转载自: 掘金

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

1…156157158…956

开发者博客

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