-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
1138 lines (1138 loc) · 169 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>ClickHouse 笔记</title>
<url>/2024/08/30/ClickHouseOne/</url>
<content><![CDATA[<p>项目最近需要用到 ClickHouse 来做大数据的实时分析,但是 ClickHouse 相关文档过少,只能通过做笔记的形式手动记录</p>
<h3 id="支持的数据类型">支持的数据类型</h3>
<p><a href="https://clickhouse.com/docs/zh/sql-reference/data-types" title="ClickHouse数据类型">参考地址</a></p>
<h4 id="数值类型">数值类型:</h4>
<ul>
<li>Int8、Int16、Int32、Int64、Int128、Int256:有符号整数型,分别使用 1、2、4、8 个字节存储。</li>
<li>UInt8、UInt16、UInt32、UInt64、UInt128、UInt256:无符号整数型,分别使用 1、2、4、8 个字节存储。</li>
<li>Float32、Float64:浮点数型,分别使用 4、8 个字节存储。
<blockquote>
<p>对浮点数进行计算可能引起四舍五入的误差。<br>
SELECT 1 - 0.9 => 0.09999999999999998</p>
</blockquote>
</li>
<li>Decimal (P, S):定点数型,支持指定精度和小数位数。</li>
</ul>
<h4 id="时间日期类型:">时间日期类型:</h4>
<ul>
<li>Date:日期类型,使用 4 个字节存储,表示自 1970 年 1 月 1 日以来的天数。</li>
<li>DateTime:日期时间类型,使用 8 个字节存储,精确到纳秒级。</li>
</ul>
<h4 id="字符串类型:">字符串类型:</h4>
<ul>
<li>FixedString (N):定长字符串类型,占用 N 个字节存储。</li>
<li>String:变长字符串类型,使用相对较少的内存来存储字符串。</li>
</ul>
<span id="more"></span>
<h3 id="数据库引擎及基本语法">数据库引擎及基本语法</h3>
<h4 id="MergeTree">MergeTree</h4>
<p>主键由关键字 PRIMARY KEY 指定,如果没有使用 PRIMARY KEY 显式指定的主键,ClickHouse 会使用排序键 ORDER BY 指定的列作为主键。</p>
<p>需要注意的是,如果同时指定了主键与排序键,那么排序键必须包含主键的所有列,比如主键为(a,b),排序键就必须为 (a,b,**)。</p>
<p>和关系型数据库的主键具备唯一性不同,<strong>ClickHouse 可以存在相同主键的数据行</strong>。这也是 ClickHouse 为了性能而做出的考量。</p>
<div class="note danger"><p><code>PARTITION BY</code> 分区键,可选项。大多数情况下,不需要使用分区键。即使需要使用,也不需要使用比月更细粒度的分区键。分区不会加快查询(这与 ORDER BY 表达式不同)。永远也别使用过细粒度的分区键。不要使用客户端指定分区标识符或分区字段名称来对数据进行分区(而是将分区字段标识或名称作为 ORDER BY 表达式的第一列来指定分区)。</p>
<p>要按月分区,可以使用表达式 toYYYYMM (date_column) ,这里的 date_column 是一个 Date 类型的列。分区名的格式会是 “YYYYMM” 。</p>
</div>
<h4 id="ReplacingMergeTree">ReplacingMergeTree</h4>
<p>前面提到 MergeTree 的主键没有唯一性约束。所以即使多行数据的主键相同,它们还是能够正常写入。</p>
<p>ReplacingMergeTree 建表语句与 MergeTree 相同,只需要替换掉表引擎就好,不过需要指定一个列作为版本列。</p>
<p>当排序键相同的数据行插入时,它会删除重复行,保留版本列值最大的一行。如果没有指定版本列,则会保留最后插入的一行。</p>
<p>通常来说,ReplacingMergeTree 删除重复行,是因为两种情况。</p>
<ul>
<li>一起批量插入的数据,属于同一个数据片段的数据,所以会在插入时候按排序键去重。</li>
<li>分批插入的数据,属于不同的数据片段,需要依托后台数据片段合并的时候才进行。</li>
</ul>
<p>所以即使你用了 ReplacingMergeTree 表引擎,在查询表数据的时候,仍然可能会查询到重复数据,不过只要使用 final 关键字去重即可。或者你也可以使用如下命令强制合并分区,这样数据片段就会合并。</p>
<h3 id="四层数仓建模">四层数仓建模</h3>
<p>在数仓建模中,有一个比较通用的建模方法论,就是将数据分为 4 层:包括:ODS(Operational Data Store)、DWD(Data Warehouse Detail)、DWS(Data Warehouse Summary)和 ADS(Application Data Store)</p>
<h4 id="ODS">ODS</h4>
<p>直接存放从各个渠道收集的原始数据,比如业务系统收集的、前后端埋点收集的等等。有时候会考虑做一定程度的数据清洗,比如处理异常字段、规范字段命名、统一时间字段等</p>
<h4 id="DWD">DWD</h4>
<p>以业务过程作为建模驱动,基于每个具体的业务过程特点,构建最细粒度的明细事实表,比如浏览明细表、交易明细表。DWD 也会清洗 ODS 层的数据(去除空值、脏数据、异常数据)、降维处理、脱敏等,并基于维度建模,将某些字段做适当冗余,做宽表化</p>
<h4 id="DWS">DWS</h4>
<p>在 DWD 层的基础上进行数据聚合和汇总,比如用户交易汇总表、用户指标汇总宽表。一般会依据特定业务需求或报表要求进行数据汇总和预计算,提高数据查询效率和性能</p>
<h4 id="ADS">ADS</h4>
<p>在 DWS 之上,面向特定应用场景设计的数据层,比如我们 CDP 场景里面,用户标签、人群画像,是通过计算后直接给到前端应用系统使用的表</p>
<h3 id="搭建集群">搭建集群</h3>
<p>ClickHouse 集群的配置很简单,只需要修改配置文件 /etc/clickhouse-server/config.xml,在配置文件的 标签下,增加集群的节点配置即可</p>
<h4 id="remote-server">remote_server</h4>
<p>配置集群时,注意不要配置 <code><host></code> 和 <code><port></code> 为转发端口。ck 使用 zookeeper 作为分布式查询的同步节点,其中存储了分布式查询任务,可使用 zookeeper 客户端通过路径 <code>/clickhouse/task_queue/ddl/</code> 查看执行情况,查看其中任意任务内容即可发现,ck 是根据配置中的 host 和 port 来明确需要执行分布式任务的服务</p>
<p><img src="/2024/08/30/ClickHouseOne/ck1.png" alt=""></p>
<p><a href="https://stackoverflow.com/questions/64947277/clickhouse-create-database-on-cluster-ends-with-timeout" title="ClickHouse的配置问题">stackoverflow</a> 也有人反馈此问题</p>
<h3 id="常用命令">常用命令</h3>
<h4 id="查看进程及杀死进程">查看进程及杀死进程</h4>
<p>当出现 ClickHouse 查询过慢,所在服务器 cpu 和内存飙升的情况,说明可能出现慢 sql,此时可通过以下命令查询正在执行的 sql 信息</p>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">query_id,read_rows,total_rows_approx,memory_usage,</span><br><span class="line">initial_user,initial_address,elapsed,query</span><br><span class="line"><span class="keyword">from</span> system.processes;</span><br></pre></td></tr></tbody></table></figure>
<p>字段含义</p>
<ul>
<li>query_id 查询 id</li>
<li>read_rows 从表中读取的行数</li>
<li> total_rows_approx 应读取的行总数的近似值</li>
<li> memory_usage 请求使用的内存量</li>
<li> initial_user 进行查询的用户</li>
<li> initial_address 请求的 IP 地址</li>
<li> elapsed 从执行开始以来的秒数</li>
<li> query 查询语句</li>
</ul>
<p>通过 sql 语句的查询行数和查询已经执行的时间来判断 sql 是不是在慢查询,或者是同事在查询的时候没有日期限定而直接查全表。一般的话如果 grafana 监控的 CK 节点出现 cpu 飙升的情况,就需要我们去判断 CK 中是否有垃圾 sql 在执行,根据 query_id 杀死该进程</p>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line">kill query <span class="keyword">where</span> query_id <span class="operator">=</span> <span class="string">'70442d9b-7fc5-4a0e-81be-9543431a4882'</span> ;</span><br></pre></td></tr></tbody></table></figure>
<h4 id="清表">清表</h4>
<p>如果发现数据集错误,可以执行下述命令清除表中数据,但是执行较慢,谨慎执行</p>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> cdp.cdp_user_local;</span><br></pre></td></tr></tbody></table></figure>
<h4 id="分批导入数据">分批导入数据</h4>
<p>可用官方客户端工具 clickhouse-client 导入数据,参考命令</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">clickhouse-client --password 123456 --port 9000 --query="INSERT INTO cdp.cdp_user_all FORMAT CSV" --input_format_allow_errors_ratio=0.1 --input_format_allow_errors_num=0 < cdp_user_data.csv</span><br></pre></td></tr></tbody></table></figure>
<p>在 ClickHouse 里,其实不建议表分区数量太多。默认情况下一次插入数据的分区超过 100 个,就会报下面的错误。</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">Received exception from server (version 24.3.2):</span><br><span class="line">Code: 252. DB::Exception: Received from localhost:9002. DB::Exception: Too many partitions for single INSERT block (more than 100). The limit is controlled by 'max_partitions_per_insert_block' setting. Large number of partitions is a common misconception. It will lead to severe negative performance impact, including slow server startup, slow INSERT queries and slow SELECT queries. Recommended total number of partitions for a table is under 1000..10000. Please note, that partitioning is not intended to speed up SELECT queries (ORDER BY key is sufficient to make range queries fast). Partitions are intended for data manipulation (DROP PARTITION, etc).. (TOO_MANY_PARTS)</span><br><span class="line">(query: INSERT INTO cdp.cdp_user_all FORMAT CSV)</span><br></pre></td></tr></tbody></table></figure>
<p>可以通过修改 /etc/clickhouse-server/users.xml 配置文件的 max_partitions_per_insert_block 的值来调大每次 INSERT 可以承载的分区数量。</p>
<figure class="highlight xml"><table><tbody><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">profiles</span>></span></span><br><span class="line"> <span class="comment"><!-- Default settings. --></span></span><br><span class="line"> <span class="tag"><<span class="name">default</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">max_partitions_per_insert_block</span>></span>10000<span class="tag"></<span class="name">max_partitions_per_insert_block</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">default</span>></span></span><br><span class="line"><span class="tag"></<span class="name">profiles</span>></span></span><br></pre></td></tr></tbody></table></figure>
<p>还有另一种方式,拆解要插入的 csv 文件,可在 linux 下执行如下命令</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">split -l 1000 cdp_user_data.csv output_chunk_</span><br></pre></td></tr></tbody></table></figure>
<p>这里,<code>-l 1000</code> 参数指定每个生成的文件应该包含 1000 行,<code>cdp_user_data.csv</code> 是要拆分的原始文件,<code>output_chunk_</code> 是每个小文件的前缀。执行上述命令后,你会得到类似 <code>output_chunk_aa</code>, <code>output_chunk_ab</code>, <code>output_chunk_ac</code> 等以 <code>output_chunk_</code> 开头的文件,每个文件包含大约 1000 行(最后一个文件可能少于 1000 行,如果原始文件的行数不是 1000 的倍数)。</p>
<p>随后执行如下命令即可批量将拆分后的文件导入到 ClickHouse</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">cat *.csv | clickhouse-client --password 123456 --port 9000 --query="INSERT INTO cdp.cdp_user_all FORMAT CSV" --input_format_allow_errors_ratio=0.1 --input_format_allow_errors_num=0</span><br></pre></td></tr></tbody></table></figure>
<h3 id="踩坑问题">踩坑问题</h3>
<h4 id="分区数量过多">分区数量过多</h4>
<p>数据测试时,错误使用了 <code>PARTITION BY</code>,选择了一个区分度过高,类似于主键的字段作为分区键,导致每插入一条数据就会创建一个分区,当插入 20w 数据时,就达到分区上限(搭建了两个节点组建集群,单个节点默认上限 10w 个分区),建表语句如下</p>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> cdp_user_local <span class="keyword">on</span> cluster <span class="keyword">default</span>(</span><br><span class="line"> unique_user_id UInt64 COMMENT <span class="string">'用户全局唯一ID,ONE-ID'</span>,</span><br><span class="line"> name String COMMENT <span class="string">'用户姓名'</span>,</span><br><span class="line"> nickname String COMMENT <span class="string">'用户昵称'</span>,</span><br><span class="line"> gender Int8 COMMENT <span class="string">'性别:1-男;2-女;3-未知'</span>,</span><br><span class="line"> birthday String COMMENT <span class="string">'用户生日:yyyyMMdd'</span>,</span><br><span class="line"> user_level Int8 COMMENT <span class="string">'用户等级:1-5'</span>,</span><br><span class="line"> register_date DateTime64 COMMENT <span class="string">'注册日期'</span>,</span><br><span class="line"> last_login_time DateTime64 COMMENT <span class="string">'最后一次登录时间'</span></span><br><span class="line">) ENGINE <span class="operator">=</span> MergeTree()</span><br><span class="line"> <span class="keyword">PARTITION</span> <span class="keyword">BY</span> register_date</span><br><span class="line"> <span class="keyword">PRIMARY</span> KEY unique_user_id</span><br><span class="line"> <span class="keyword">ORDER</span> <span class="keyword">BY</span> unique_user_id;</span><br></pre></td></tr></tbody></table></figure>
<p>register_date 时间格式为年月日时分秒,区分度过高,错误如下。此时应参考官方建议使用函数 <code>toYYYYMM(register_date)</code></p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">Code: 252. DB::Exception: Too many parts (100025) in all partitions in total. This indicates wrong choice of partition key. The threshold can be modified with 'max_parts_in_total' setting in <merge_tree> element in config.xml or with per-table setting. (TOO_MANY_PARTS) (version 21.11.11.1 (official build))</span><br></pre></td></tr></tbody></table></figure>
<p>正常情况下不需要按照报错提示修改分区上限,大部分情况是因为错误的设置了细粒度分区键导致的,请自行检查建表 <code>sql</code>,如需要修改,可编辑 <code>/etc/clickhouse-server/users.xml</code> 文件,添加如下配置项。</p>
<figure class="highlight xml"><table><tbody><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">merge_tree</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">max_parts_in_total</span>></span>100000<span class="tag"></<span class="name">max_parts_in_total</span>></span></span><br><span class="line"><span class="tag"></<span class="name">merge_tree</span>></span></span><br></pre></td></tr></tbody></table></figure>
<h4 id="datagrip查询超时">datagrip 查询超时</h4>
<p>有时会执行一些时间较长的慢 <code>sql</code>,但是在 datagrip 软件下,默认 30s 会超时提示 <code>Read timed out</code>,datagrip 默认使用 jdbc 链接,可手动修改配置项</p>
<p><img src="/2024/08/30/ClickHouseOne/ck2.png" alt=""></p>
<p>亦可直接在链接上添加参数 <code>jdbc:clickhouse://localhost:8123?socket_timeout=300000</code></p>
]]></content>
<categories>
<category>ClickHouse</category>
</categories>
<tags>
<tag>ClickHouse</tag>
</tags>
</entry>
<entry>
<title>Hexo 使用指北</title>
<url>/2022/10/16/UseHexoOne/</url>
<content><![CDATA[<p>第一次使用 Hexo 还是在 2017 年,那时候到处了解学习新兴技术,go 也是在那段时间听说并进行了简单学习;当时应该是在搜如何建一个个人博客,然后就搜到了 Hexo 的相关文章,因为其借用 GitPage 相关特性无需自建服务器,直接依托 github 就能建立个人博客,并且也能支持搜索引擎收录,这些亮点在当时的我看来还是很酷的(现在依然觉得很酷~),但是当时在研究完如何安装并成功放到 GitPage 之后,因为没有良好的书写文档的习惯,个人博客也就搁置了,不过当时还是很惊叹于 Hexo 运行后的博客页面,风格很吸引人。</p>
<p>最近使用 github 时,看了看自己的仓库发现了这个被遗弃的博客仓库,正好最近意识到了文档的重要性,并且也在尝试书写一些,而且用的也是 markdown 格式,能够无缝衔接到 Hexo 上,就准备把这个仓库重新用起来,希望不算太晚吧。</p>
<div class="note info"><p>说起来,markdown 也是 2017 那年学习使用的,因为当时正值毕业季,需要写个人简历,可能大部分人都是用的智联吧,后来我同学推荐我用乔布简历,写完效果拔群~比智联不知道高到哪里去了,后来同学又推荐我使用 markdown,简单的编写规则加上自由的样式控制可以写出非常好看的简历,并且也足够小众足够酷(但是现在基本程序员已经都会用这个工具了),这才是程序员写个人简历的工具~从此之后我写文档基本都是用 markdown 来写</p>
</div>
<span id="more"></span>
<h2 id="Hexo">Hexo</h2>
<h3 id="Hexo安装方式">Hexo 安装方式</h3>
<p>鉴于国情,很多人学习技术更多的是在 csdn 或者其他个人博客,通过别人学习后总结的文章来学习,确实这样学起来更快,并且很多官方网站都是英文的,而 csdn 或者个人博客相当于一个汉化的过程,但是 csdn 会存在一个问题就是文章中的内容会出现过时的情况,所以还是建议直接前往官网学习,并且 <a href="https://hexo.io/zh-cn/docs/" title="Hexo官网">Hexo 官网</a>是有简体中文的。</p>
<h4 id="安装git">安装 git</h4>
<p>直接去 git 官网下载 64 位的 exe 安装文件即可</p>
<h4 id="安装node">安装 node</h4>
<p>node 版本是个坑,很多 npm 包是对 node 版本有要求的,并且有的时候过新的 node 版本也会导致 npm 包失败,我尝试 Hexo 安装成功的 node 环境版本是 <em>14.16.1</em></p>
<h4 id="安装Hexo">安装 Hexo</h4>
<p>npm 全局安装 Hexo</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install -g hexo-cli</span></span><br></pre></td></tr></tbody></table></figure>
<p>Hexo 初始化</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">hexo init</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install</span></span><br></pre></td></tr></tbody></table></figure>
<p>配置文件</p>
<p>_config.yml 是 Hexo 的配置文件,可以在其中设置一些基本参数,例如博客名称、作者名称及一些插件的配置信息</p>
<h4 id="安装主题NEXT">安装主题 NEXT</h4>
<p>Hexo 默认主题外观还是比较一般的,Hexo 支持很多扩展主题,我使用的是 NexT 主题,国内大部分网站也是推荐使用这个</p>
<p>但是这就引出了一个坑,因国内大部分介绍 Hexo 的博客文章都停留在 2020 年或 2020 年以前了,假如没有去 Hexo 官方学习安装方式,而是直接按照国内博客来,那么你安装主题的命令一定是</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">clone</span> https://github.com/theme-next/hexo-theme-next themes/next</span></span><br></pre></td></tr></tbody></table></figure>
<p>在安装完这个主题后,你会发现使用起来都很正常,但是几个第三方插件却会出现不好使的情况,并且也没有任何参考解决方案,例如当你要用 leancloud 来当浏览量插件时,页面会出现报错的情况。这实际上是因为 NexT 原作者出现了长期不在线的情况,导致其他成员无法管理已有仓库,所以重新创建了新的分支<a href="https://github.com/next-theme/hexo-theme-next/issues/4" title="解释博客">解释博客</a></p>
<p>正确的安装主题命令</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">clone</span> https://github.com/next-theme/hexo-theme-next themes/next</span></span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>命令后缀的 themes/next 不必须按照示例来,next 是指 clone 后的所在文件夹名,这个可以任意命名,只需要 clone 后,将根目录下的_config.yml 中 theme 参数配置为对应的文件夹名就可以了</p>
</blockquote>
<p>Hexo 新版本下,修改主题的配置文件有了新的方式,只需要在根目录下建立_config.xxx.yml,xxx 对应的是 themes 下使用主题所在的文件夹名</p>
<h3 id="Hexo常用命令">Hexo 常用命令</h3>
<ul>
<li><code>hexo clean</code> 删除编译文件</li>
<li><code>hexo g</code> 编译文件</li>
<li><code>hexo d</code> 推送编译文件到远端。如果存储在 github 上,需配置 setting 中的 <strong>SSH keys</strong></li>
</ul>
<h3 id="常用-Markdown-语法">常用 Markdown 语法</h3>
<h4 id="Note-引用">Note 引用</h4>
<p>Note 引用包含 <code>success / primary / warning / default / info / danger</code> 类型</p>
<pre><code class="language-Markdown">{% note warning %} warning {% endnote %}
</code></pre>
]]></content>
<categories>
<category>Hexo</category>
</categories>
<tags>
<tag>Hexo</tag>
</tags>
</entry>
<entry>
<title>WSL 使用笔记</title>
<url>/2024/09/13/WSL/</url>
<content><![CDATA[<p>记录在 windows 中使用 WSL 技术出现的一系列问题</p>
<h3 id="安装WSL">安装 WSL</h3>
<p>默认需要 window10 或 11 的系统,并且版本号有一定要求,可去 <a href="https://learn.microsoft.com/zh-cn/windows/wsl/install" title="WSL官方安装教程">官方</a> 检查</p>
<h4 id="开启cpu虚拟化技术">开启 cpu 虚拟化技术</h4>
<p>使用 WSL 需要电脑 cpu 支持 Hyper-V 虚拟化技术,首先要在 BIOS 中开启对应选项,具体型号笔记本的选项不同,可参考 <a href="https://prod.support.services.microsoft.com/zh-cn/windows/%E5%9C%A8-windows-%E4%B8%8A%E5%90%AF%E7%94%A8%E8%99%9A%E6%8B%9F%E5%8C%96-c5578302-6e43-4b4b-a449-8ced115f58e1" title="在Windows 上启用虚拟化">在 Windows 上启用虚拟化</a></p>
<span id="more"></span>
<h4 id="Windows启用linux子系统">Windows 启用 linux 子系统</h4>
<p>在 Windows 桌面任务栏的搜索框输入<strong>启用或关闭 Windows 功能</strong>,启用截图中圈选的功能</p>
<p><img src="/2024/09/13/WSL/WSL1.png" alt=""></p>
<p>但是实测发现,Windows 中未开启 Hyper-V 和适用于 Linux 的 Windows 子系统,仅开启了虚拟机平台,WSL 一样可用</p>
<h4 id="WSL-update">WSL update</h4>
<p>经过上述两步,可在终端控制台中输入 <code>wsl --install</code>,自动安装所需工具,安装完成后输入 <code>wsl -v</code> 检查当前版本号和内核</p>
<p>如果此时提示命令行选项无效,可输入 <code>wsl --update</code>,完成所需组件的更新</p>
<p>输入 <code>wsl -v</code> 最终显示如下信息</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">WSL 版本: 2.2.4.0</span><br><span class="line">内核版本: 5.15.153.1-2</span><br><span class="line">WSLg 版本: 1.0.61</span><br><span class="line">MSRDC 版本: 1.2.5326</span><br><span class="line">Direct3D 版本: 1.611.1-81528511</span><br><span class="line">DXCore 版本: 10.0.26091.1-240325-1447.ge-release</span><br><span class="line">Windows 版本: 10.0.22631.4037</span><br></pre></td></tr></tbody></table></figure>
<h3 id="常用命令">常用命令</h3>
<h4 id="wsl-l-v">wsl -l -v</h4>
<p>查看当前已安装的 Linux 发行版及对应的 wsl 版本号</p>
<h4 id="wsl-shutdown">wsl --shutdown</h4>
<p>关闭所有正在运行的 Linux 发行版,但是可能出现关不上的情况</p>
<h3 id="存储迁移">存储迁移</h3>
<p>WSL 默认安装的发型本存储在 C 盘,随着使用的增多,会占用较多空间,建议提早移出到别的盘符</p>
<h4 id="查看运行状态">查看运行状态</h4>
<p>打开终端控制台,输入 <code>wsl -l -v</code> 查看 WSL 虚拟机的状态</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"> NAME STATE VERSION</span><br><span class="line">* Ubuntu-20.04 Running 2</span><br></pre></td></tr></tbody></table></figure>
<h4 id="关闭正在运行的虚拟机">关闭正在运行的虚拟机</h4>
<p>使用命令 <code>wsl --shutdown</code>,再次查看 WSL 虚拟机的状态,可发现已经关闭</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line"> NAME STATE VERSION</span><br><span class="line">* Ubuntu-20.04 Stopped 2</span><br></pre></td></tr></tbody></table></figure>
<h4 id="导出备份">导出备份</h4>
<p>首先在自己选择想要存放 wsl 的盘符(这里选择 D 盘)下创建文件夹(名称为 WSL)。注意命令中 export 后是自己 Ubuntu 的版本名称。</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">wsl --export Ubuntu-20.04 D:\WSL\Ubuntu.tar</span><br></pre></td></tr></tbody></table></figure>
<h4 id="注销原有版本">注销原有版本</h4>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">wsl --unregister Ubuntu-20.04</span><br></pre></td></tr></tbody></table></figure>
<h4 id="将备份文件恢复到自己新建的文件夹(这里为WSL)下">将备份文件恢复到自己新建的文件夹(这里为 WSL)下</h4>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">wsl --import Ubuntu-20.04 D:\WSL D:\WSL\Ubuntu.tar</span><br></pre></td></tr></tbody></table></figure>
<h4 id="恢复默认用户">恢复默认用户</h4>
<p>导入成功后,打开虚拟机默认当前用户会变成 root,如果想还原到之前的用户,可执行下述命令,将用户名替换成默认进入虚拟机的用户名即可</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">Ubuntu2004 config -default-user 用户名</span><br></pre></td></tr></tbody></table></figure>
<div class="note warning"><p>执行配置默认用户命令时,控制台可能会报错,如果百度后无法解决的情况下, 可以 root 身份先进入虚拟机,执行 <code>sudo vim /etc/wsl.conf</code> 创建或修改文件,在其中添加如下内容</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">[user]</span><br><span class="line">default = root</span><br></pre></td></tr></tbody></table></figure>
<p>保存配置并退出,同样在关闭 wsl 之后重新进入,便会发现默认用户已经修改了。</p>
<p>需要注意的是 wsl.conf 配置优先级要高于 Ubuntu2004.exe config --default-user,因此如果两个都配置了的话,会以 wsl.conf 中的配置优先。</p>
</div>
<h3 id="网络问题">网络问题</h3>
<p>使用 wsl2 版本,在宿主机和 wsl 中使用 localhost 都可访问到部署的服务</p>
<blockquote>
<p>使用远程 IP 地址连接到应用程序时,它们将被视为来自局域网 (LAN) 的连接。 这意味着你需要确保你的应用程序可以接受 LAN 连接。</p>
<p>例如,你可能需要将应用程序绑定到 0.0.0.0 而非 127.0.0.1。 以使用 Flask 的 Python 应用为例,可以通过以下命令执行此操作:app.run (host=‘0.0.0.0’)。 进行这些更改时请注意安全性,因为这将允许来自你的 LAN 的连接。</p>
</blockquote>
<h4 id="从局域网-LAN-访问-WSL-2-分发版">从局域网 (LAN) 访问 WSL 2 分发版</h4>
<p>WSL 2 有一个带有其自己独一无二的 IP 地址的虚拟化以太网适配器。目前,若要启用此工作流,你需要执行与常规虚拟机相同的步骤。</p>
<p>下面是使用 <a href="https://learn.microsoft.com/zh-cn/windows-server/networking/technologies/netsh/netsh-interface-portproxy1" title="Netsh 接口 portproxy">Netsh 接口 portproxy</a> Windows 命令添加端口代理的示例,该代理侦听主机端口并将该端口代理连接到 WSL 2 VM 的 IP 地址。</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">netsh interface portproxy add v4tov4 listenport=<yourPortToForward> listenaddress=0.0.0.0 connectport=<yourPortToConnectToInWSL> connectaddress=(wsl hostname -I)</span><br></pre></td></tr></tbody></table></figure>
<p>在此示例中,需要更新 <code><yourPortToForward></code> 到端口号,例如 <code>listenport=4000</code>。 <code>listenaddress=0.0.0.0</code> 表示将接受来自任何 IP 地址的传入请求。 侦听地址指定要侦听的 IPv4 地址,可以更改为以下值:IP 地址、计算机 NetBIOS 名称或计算机 DNS 名称。 如果未指定地址,则默认值为本地计算机。 需要将 <code><yourPortToConnectToInWSL></code> 值更新为希望 WSL 连接的端口号,例如 <code>connectport=4000</code>。 最后,<code>connectaddress</code> 值必须是通过 WSL 2 安装的 Linux 分发版的 IP 地址(WSL 2 VM 地址),可通过输入命令:<code>wsl.exe hostname -I</code> 找到。</p>
<p>因此,此命令可能如下所示:</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">netsh interface portproxy add v4tov4 listenport=4000 listenaddress=0.0.0.0 connectport=4000 connectaddress=192.168.101.100</span><br></pre></td></tr></tbody></table></figure>
<p>要获取 IP 地址,请使用:</p>
<ul>
<li><code>wsl hostname -I</code> 标识通过 WSL 2 安装的 Linux 分发版 IP 地址(WSL 2 VM 地址)</li>
<li><code>cat /etc/resolv.conf</code> 表示从 WSL 2 看到的 WINDOWS 计算机的 IP 地址 (WSL 2 VM)</li>
</ul>
<div class="note info"><p>在主机名命令中使用小写 “i” 将生成与使用大写 “I” 不同的结果。 <code>wsl hostname -i</code> 是本地计算机(127.0.1.1 是占位符诊断地址),而 <code>wsl hostname -I</code> 会返回其他计算机所看到的本地计算机的 IP 地址,应该用于识别通过 WSL 2 运行的 Linux 发行版的 <code>connectaddress</code>。</p>
</div>
<h4 id="netsh-interface-portproxy-常用命令">netsh interface portproxy 常用命令</h4>
<p>Portproxy 服务器从服务器侦听的 IPv4 端口和地址列表中删除 IPv4 地址。</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">netsh interface portproxy delete v4tov4 listenport=<yourPortToForward> listenaddress=0.0.0.0</span><br></pre></td></tr></tbody></table></figure>
<p>显示所有 portproxy 参数,包括 v4tov4、v4tov6、v6tov4 和 v6tov6 的端口 / 地址对。</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">netsh interface portproxy show all</span><br></pre></td></tr></tbody></table></figure>
<h4 id="镜像模式">镜像模式</h4>
<p>部分情况下,wsl 中的应用程序需要 wsl 虚拟机所在 ip 和宿主机保持一致,这时可启用 wsl 的镜像模式,<a href="https://learn.microsoft.com/zh-cn/windows/wsl/wsl-config#wslconfig" title="WSL 中的高级设置配置">参考</a></p>
<p>开启方式为修改 .wslconfig 文件,使用 .wslconfig 为 WSL 上运行的所有已安装的发行版配置全局设置。</p>
<ul>
<li>默认情况下,.wslconfig 文件不存在。 它必须创建并存储在 <code>%UserProfile%</code> 目录中才能应用这些配置设置。</li>
<li>用于在作为 WSL 2 版本运行的所有已安装的 Linux 发行版中全局配置设置。</li>
<li>只能用于 WSL 2 运行的发行版。 作为 WSL 1 运行的发行版不受此配置的影响,因为它们不作为虚拟机运行。</li>
<li>要访问 <code>%UserProfile%</code> 目录,请在 PowerShell 中使用 <code>cd ~</code> 访问主目录(通常是用户配置文件 <code>C:\Users\<UserName></code>),或者可以打开 Windows 文件资源管理器并在地址栏中输入 <code>%UserProfile%</code>。 该目录路径应类似于:<code>C:\Users\<UserName>\.wslconfig</code>。</li>
</ul>
<p>WSL 将检测这些文件是否存在,读取内容,并在每次启动 WSL 时自动应用配置设置。 如果文件缺失或格式错误(标记格式不正确),则 WSL 将继续正常启动,而不应用配置设置。</p>
<p>文件中添加 <code>[wsl2]</code> 标签,并在下方添加 <code>networkingMode=mirrored</code></p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">[wsl2]</span><br><span class="line">networkingMode=mirrored</span><br></pre></td></tr></tbody></table></figure>
<h3 id="Windows-与-WSL-文件交互">Windows 与 WSL 文件交互</h3>
<p>在 wsl 中可直接访问宿主机,宿主机盘符为 <code>/mnt/</code></p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">cp -r /mnt/e/BaiduNetdiskDownload/ls16 /home/soap/</span><br></pre></td></tr></tbody></table></figure>
<p><code>/mnt/e</code> 表示本地的 e 盘,Windows 的文件路径为 <code>/mnt/e/BaiduNetdiskDownload/ls16</code>,wsl 的目标路径为 <code>/home/soap/</code></p>
]]></content>
<categories>
<category>WSL</category>
</categories>
<tags>
<tag>WSL</tag>
</tags>
</entry>
<entry>
<title>chrome 浏览器内置谷歌翻译无法使用解决方案</title>
<url>/2022/10/10/chromeTranslate/</url>
<content><![CDATA[<p>google 近期关闭了国内谷歌翻译网站,但是 chrome 浏览器内置的翻译功能用起来比较顺手,可以通过修改 host 的方式临时解决下</p>
<h3 id="修改host教程">修改 host 教程</h3>
<h4 id="进入etc文件夹">进入 etc 文件夹</h4>
<ul>
<li>mac: 右键访达 - 前往文件夹 - 输入 “etc/hosts”</li>
<li>windows: C:\Windows\System32\drivers\etc</li>
</ul>
<p>修改文件,添加以下内容:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="code"><pre><span class="line">203.208.40.66 translate.google.com</span><br><span class="line">142.250.4.90 translate.googleapis.com</span><br></pre></td></tr></tbody></table></figure>
<p>保存修改后,chrome 即可正常使用</p>
]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>chrome</tag>
</tags>
</entry>
<entry>
<title>java 内存泄露事故分析</title>
<url>/2022/10/10/bugFix/</url>
<content><![CDATA[<h3 id="问题描述">问题描述</h3>
<p><strong>电影票</strong>长期存在 <strong>ticket</strong> 模块堆栈内存溢出导致系统崩溃的问题,初期一直认为是订单量过大导致比价功能调用次数太多引起的内存溢出问题。经过排查 dump 日志未能定位到具体问题,最终特权选择多开 <strong>ticket</strong> 模块的方式来避免单机崩溃导致服务不可用的情况,但是此方法治标不治本,运维需要经常重启崩溃的 <strong>ticket</strong> 模块。</p>
<blockquote>
<p><em>电影票 ticket 模块在 h5 用户下单时,会调用异步比价功能,此功能使用 @Async 注解实现,意味着每有一笔订单都会启用一个比价线程</em></p>
</blockquote>
<p>本问题历时一年才得以解决,这其中经历了四个问题分析阶段</p>
<ol>
<li>
<p><strong>ticket</strong> 模块崩溃,根据 dump 日志发现内存中存在大量比价功能生成的对象,初步判断是短时订单量过大导致的,未有较好的解决办法,选择多开 <strong>ticket</strong> 模块顶住大量订单的压力。</p>
</li>
<li>
<p>运维侧报告 <strong>ticket</strong> 模块存在凌晨崩溃的情况,排查日志发现凌晨订单只有几笔,此时怀疑是比价功能存在一定问题,导致其产生的对象停留较久,但是依旧未排查到问题。</p>
<blockquote>
<p><em>因还是未定位到能导致内存溢出的问题,只能判断认为是虚拟机内存回收较慢导致的</em></p>
</blockquote>
</li>
<li>
<p>技术侧发现 <code>@Async</code> 注解支持选择线程池来将异步线程改为用线程池来执行,可以防止大量调用 <code>@Async</code> 导致生成了海量线程从而内存溢出。但是在本功能上线后,依然出现了 <strong>ticket</strong> 模块崩溃的问题。</p>
</li>
<li>
<p>经过以上三次排查,已基本确定本次问题是由代码问题引起的内存泄露,并不是因为电影票订单数据量过大导致的系统崩溃。随后重新开始排查问题,并最终定位到是因为 json 序列化引起的内存泄露。</p>
</li>
</ol>
<h3 id="定位思路">定位思路</h3>
<h4 id="步骤1">步骤 1</h4>
<p>首先使用 <strong>JProfiler</strong> 工具查看 <strong>dump</strong> 日志,按照保留大小倒序排序,可以发现最多的是 <code>String</code> 对象,这是因为 <code>YtbShowListResponse</code> 内存溢出对象中存储有请求接口返回的字符串内容,此内容包含一个影院下面的所有场次,所以内容较大。</p>
<p><img src="/2022/10/10/bugFix/JProfiler1.png" alt=""></p>
<center>
(JProfiler 工具查看 dump 日志)
</center>
<span id="more"></span>
<h4 id="步骤2">步骤 2</h4>
<p>查看对象的数据结构,发现本质是一个 <code>Map</code>,初步怀疑是不是 <strong>NutMap</strong> 第三方工具引起的</p>
<p><img src="/2022/10/10/bugFix/YtbShowListResponse.png" alt=""></p>
<center>
(YtbShowListResponse 数据结构)
</center>
<h4 id="步骤3">步骤 3</h4>
<p>使用 <strong>JProfiler</strong> 工具的图表功能,查看对象到 GC 根 (root) 的路径,可以发现最终是被 <strong>org.nutz.lang.Mirror</strong> 对象所持有,导致无法被回收</p>
<p><img src="/2022/10/10/bugFix/JProfiler2.png" alt=""></p>
<center>
(JProfiler 工具查看图标)
</center>
<h4 id="步骤4">步骤 4</h4>
<p>此时查看比价功能代码,可以发现 <code>YtbShowListResponse</code> 对象的使用都是正常的,并没有放置到 java 静态变量中等操作,但是根据 <strong>dump</strong> 日志分析其中的 <code>YtbShowListResponse</code> 对象,<code>YtbShowListResponse</code> 中 <code>content</code> 字段都是成功,即请求失败就不会出现内存泄露问题,问题代码范围进一步缩小。</p>
<p><img src="/2022/10/10/bugFix/queryYtbOrderInfo.png" alt=""></p>
<center>
(比价功能代码)
</center>
<h4 id="步骤5">步骤 5</h4>
<p>排查代码发现<em><strong> getResponse</strong></em> 方法有非常高的可疑性,使用了第三方序列化方式<em><strong> Json.fromJson</strong></em>,并且使用了 <strong>PType</strong> 第三方工具来标识序列化类型,更加可疑,此序列化方法和 <strong>org.nutz.lang.Mirror</strong> 同属于 <strong>nutz</strong> 第三方工具包,基本已锁定到问题代码。</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> List<YtbShowBo> <span class="title function_">getResponse</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">data</span> <span class="operator">=</span> <span class="built_in">this</span>.getData();</span><br><span class="line"> <span class="keyword">if</span> (Strings.isBlank(data)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">YtbException</span>(<span class="string">"影托邦场次列表响应为空"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Json.fromJson(<span class="keyword">new</span> <span class="title class_">PType</span><List<YtbShowBo>>() {</span><br><span class="line"> }, data);</span><br><span class="line"> }</span><br></pre></td></tr></tbody></table></figure>
<h4 id="步骤6">步骤 6</h4>
<p>为了验证是否是 json 序列化导致,在 <code>YtbShowListResponse</code> 上重写了<em><strong> finalize</strong></em> 方法,并在其中打印了日志,验证对象是否被回收了,采用对照法,使用 <strong>fastjson</strong> 包替换了序列化方法,执行相同比价功能,最终验证结果如下:</p>
<ol>
<li><em><strong>Json.fromJson</strong></em> 方法和 <strong>PType</strong> 共同使用情况下,会导致<em><strong> finalize</strong></em> 不被执行,也就意味着此对象不会被虚拟机回收</li>
<li><em><strong> JSON.parseObject</strong></em> 方法和 <strong>TypeReference</strong> 共同使用情况下,<em><strong>finalize</strong></em> 很快就执行了,意味着对象被回收了</li>
</ol>
<blockquote>
<p>java 的<em><strong> finalize</strong></em> 方法在生产环境是禁止使用的,<strong>对其的重写可能会造成虚拟机性能下降或者对象无法被回收的问题</strong>,最终导致系统崩溃(网上说的,未验证ㄟ (▔, ▔) ㄏ),本次只是在测试环境打印日志,方便检查对象是否被回收,<strong>切勿在生产环境使用本方法</strong>。</p>
</blockquote>
<blockquote>
<p><em>其实 java 虚拟机垃圾回收执行的很快</em></p>
</blockquote>
<h4 id="总结">总结</h4>
<p>到这里,其实问题已经得到了解决,但是为了提现我们热爱技术的一面,也为了彰显我们的钻研精神,那么还要提出疑问,为什么这会导致内存泄露?总结出以下几个问题:</p>
<ul>
<li>为什么 <strong>fastjson</strong> 不会出现内存泄露的问题?</li>
<li>明明序列化的对象是字符串 <strong>data</strong> 数据,生成后的对象也是 <code>List<YtbShowBo></code>,怎么结果就成了 <code>YtbShowListResponse</code> 存到了虚拟机中无法被回收?</li>
<li>json 序列化的原理是什么?</li>
</ul>
<p>本着以上几个问题,我们进入了下一阶段,我们要找到问题本质~我们要搞清楚到底是什么东西,影响了虚拟机内存回收?</p>
<h3 id="追本溯源">追本溯源</h3>
<p>进入 <strong>nutz</strong> 的 jar 包源码,可以发现他使用了 <code>mirrorCache</code> 把所有使用到的序列化类型都缓存了起来,问题就出在了这里</p>
<p><img src="/2022/10/10/bugFix/Mirror.png" alt=""></p>
<center>
(json 序列化源码)
</center>
<p>这时就要开始使用我们的好帮手百度了,<strong>PType</strong> 实现了 <strong>ParameterizedType</strong> 接口,使用 <strong>ParameterizedType</strong> + 内存溢出为标题查到一篇文章<sup>①</sup>,说明 <strong>fastjson</strong> 也存在使用 <strong>ParameterizedType</strong> 不当导致的内存泄露问题,此问题已经被 <strong>fastjson</strong> 修复了。不过 <strong>nutz</strong> 包和 <strong>fastjson</strong> 包并不相同,但是这也印证了我们的观点,json 序列化如果使用不当确实会导致内存泄露的问题。</p>
<blockquote>
<p><strong>ParameterizedType</strong> 是 jdk 提供的用于处理带泛型类型的类的序列化方式<sup>②</sup></p>
</blockquote>
<p>比对了 <strong>nutz</strong> 包和 <strong>fastjson</strong> 的源码,发现了区别,<strong>fastjson</strong> 并没有直接使用序列化类型 <strong>Type</strong>,而是在其内部进行了重新实现,到这里,我们就能回答出前面三个问题中的两个了:</p>
<ul>
<li><strong>fastjson</strong> 采用了在 <strong>Type</strong> 中重新生成子序列化类型的方式,避免了内存泄露的问题</li>
<li> json 序列化的原理就是通过声明一个序列化类型,然后将字符串内容进行转换的过程</li>
</ul>
<p><img src="/2022/10/10/bugFix/nuztAndFastjson.png" alt=""></p>
<center>
(**nutz** 包和 **fastjson** 源码比较,左 **nutz**,右 **fastjson**)
</center>
<p>但是,第二个问题没有得到解答,为什么最终在内存中存储了大量的 <code>YtbShowListResponse</code> 而不是 <code>List<YtbShowBo></code> 呢?</p>
<p>这个问题最终通过研究 <code>new TypeReference</code> 和 <code>new PType</code> 两者之间的区别得以解决</p>
<p>先看下声明序列化类型的写法,<code>new xxx(){}</code> 是 java 匿名内部类的写法。</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line">List<YtbShowBo> ytbShowBos = Json.fromJson(<span class="keyword">new</span> <span class="title class_">PType</span><List<YtbShowBo>>() {</span><br><span class="line">}, data);</span><br></pre></td></tr></tbody></table></figure>
<p>此时,你只需要回想下学过的 java 基础,就能明白一点,java 的匿名内部类是依赖于其父类的,这就导致你在 dump 日志或者 debug 模式下查看匿名内部类对象时,都会惊奇的发现他的指针并不是指向他的内存地址,而是直接指向其父类</p>
<blockquote>
<p><em>匿名内部类其实极易导致内存泄露问题<sup>③</sup></em></p>
</blockquote>
<p><img src="/2022/10/10/bugFix/debug.png" alt=""></p>
<center>
(debug)
</center>
<p>到这里,你也就明白了为什么会出现内存泄露的问题,为什么内存中会有大量的 <code>YtbShowListResponse</code> 对象,因为每一个 <code>new PType</code> 都持有了其父类对象,当 <strong>PType</strong> 被放置到 <code>mirrorCache</code> 中缓存起来时,也就导致他持有的父对象再也无法被回收了,我们也就解答了前面第二个问题,也就明白了这次内存泄露的本质</p>
<h5 id="这就结束了吗?">这就结束了吗?</h5>
<p>问题找到了,但是我们还没有去解决问题,前面好像已经找到了解决办法,就是替换第三方序列化工具,使用 <strong>fastjson</strong> 替代 <strong>nutz</strong>,但是这样好像我们学到的知识没用上,如何使用 java 基础解决这个问题?让我们倒回前文有关匿名内部类的部分,解决办法就藏在这里面 ↑</p>
<h5 id="终极解决方案:">终极解决方案:</h5>
<p>将在方法内部创建的 <strong>Type</strong> 改为静态变量</p>
<p><code>private static final PType<List<YtbShowBo>> YTB_SHOW_BO_TYPE = new PType<List<YtbShowBo>>() {};</code></p>
<p>本问题随即解决</p>
<blockquote>
<p>当匿名内部类成为静态变量时,根据我们学过的 java 基础知识,java 类下的静态变量属于类,被所有类实例对象所共享,当且仅当在类初次加载时会被初始化</p>
</blockquote>
<h5 id="到此为止了,但是没有完全为止">到此为止了,但是没有完全为止</h5>
<p>本次内存泄露问题已经圆满解决,大家也跟着本文档学习了解了如何排查问题,如何分析问题,如何解决问题。但是本文档只是个引子,希望在以后的工作中大家都能保持钻研精神,不怕问题,解决问题。</p>
<h3 id="附录">附录</h3>
<p>收录相关在排查问题时查询到的有参考意义的博客内容</p>
<p>附录① <a href="https://blog.csdn.net/weixin_33358099/article/details/114828657">Java 反序列化 json 内存溢出_fastjson 反序列化使用不当导致内存泄露</a></p>
<p>附录② <a href="https://blog.csdn.net/JustBeauty/article/details/81116144">ParameterizedType 详解</a></p>
<p>附录③ <a href="https://blog.csdn.net/feiying0canglang/article/details/124946774">JVM–Java 内存泄露–匿名内部类–原因 / 解决方案</a></p>
]]></content>
<categories>
<category>开发日常</category>
</categories>
<tags>
<tag>bug</tag>
</tags>
</entry>
<entry>
<title>Hello World</title>
<url>/2022/10/10/hello-world/</url>
<content><![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p>
<h2 id="Quick-Start">Quick Start</h2>
<h3 id="Create-a-new-post">Create a new post</h3>
<figure class="highlight bash"><table><tbody><tr><td class="code"><pre><span class="line">$ hexo new <span class="string">"My New Post"</span></span><br></pre></td></tr></tbody></table></figure>
<p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p>
<h3 id="Run-server">Run server</h3>
<figure class="highlight bash"><table><tbody><tr><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></tbody></table></figure>
<p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p>
<h3 id="Generate-static-files">Generate static files</h3>
<figure class="highlight bash"><table><tbody><tr><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></tbody></table></figure>
<p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p>
<h3 id="Deploy-to-remote-sites">Deploy to remote sites</h3>
<figure class="highlight bash"><table><tbody><tr><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></tbody></table></figure>
<p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>
]]></content>
<categories>
<category>helloworld</category>
</categories>
<tags>
<tag>helloworld</tag>
</tags>
</entry>
<entry>
<title>前端使用 iframe 嵌套我方 h5 页面,出现 session 跨域问题如何解决</title>
<url>/2023/03/10/iframeCors/</url>
<content><![CDATA[<h3 id="问题描述">问题描述</h3>
<p>提供给客户对接的 H5 页面正常都是通过 webview 或者页面跳转的形式来做,部分客户可能会采用 iframe 嵌套的形式,如果是 H5 页面用户信息采用 token 的形式,不会有影响,但是采用 session 的形式,会引起 session 跨域从而导致浏览器无法拿到对应的用户信息</p>
<span id="more"></span>
<h3 id="问题解决">问题解决</h3>
<h4 id="SameSite">SameSite</h4>
<blockquote>
<p>Chrome 51 开始,浏览器的 Cookie 新增加了一个 SameSite 属性,用来防止 CSRF 攻击和用户追踪。Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险</p>
</blockquote>
<p>目前 SameSite 有三个常用的值 <code>Strict</code>, <code>Lax</code>, <code>None</code>,他们的作用是对跨站请求的 cookie 做不同程度的限制,而根据请求的不同,其对应关系如下:</p>
<table>
<thead>
<tr>
<th>请求类型</th>
<th>示例</th>
<th>默认</th>
<th> Lax</th>
<th>Strict</th>
</tr>
</thead>
<tbody>
<tr>
<td> 链接</td>
<td><code><a href="..."></a></code></td>
<td>发送 Cookie</td>
<td> 发送 Cookie</td>
<td> 不发送</td>
</tr>
<tr>
<td>预加载</td>
<td><code><link rel="prerender" href="..."/></code></td>
<td>发送 Cookie</td>
<td> 发送 Cookie</td>
<td> 不发送</td>
</tr>
<tr>
<td> GET 表单</td>
<td><code><form method="GET" action="..."></code></td>
<td>发送 Cookie</td>
<td> 发送 Cookie</td>
<td> 不发送</td>
</tr>
<tr>
<td> POST 表单</td>
<td><code><form method="POST" action="..."></code></td>
<td>发送 Cookie</td>
<td> 不发送</td>
<td>不发送</td>
</tr>
<tr>
<td> iframe</td>
<td><code><iframe src="..."></iframe></code></td>
<td>发送 Cookie</td>
<td> 不发送</td>
<td>不发送</td>
</tr>
<tr>
<td> AJAX</td>
<td><code>$.get("...")</code></td>
<td>发送 Cookie</td>
<td> 不发送</td>
<td>不发送</td>
</tr>
<tr>
<td> Image</td>
<td><code><img src="..."></code></td>
<td>发送 Cookie</td>
<td> 不发送</td>
<td>不发送</td>
</tr>
</tbody>
</table>
<p>从 Chrome 80 开始,默认会对 cookie 设置 SameSite=Lax,ChromeLab 对此有比较清楚的说明:地址,目前项目中大部分 cookie 是没有设置任何的 SameSite 属性,所以可能出现跨站 cookie 无法传递的情况,需要对这部分内容进行处理,否则,可能出现 cookie 无法传递导致类似登陆失败问题。</p>
<p>所以默认较新浏览器版本下,没有正常声明处理 SameSite 属性的系统,浏览器都会默认设置成 Lax 从而导致 iframe 中失效</p>
<p>我们只需要设置 SameSite=None,同时,从 Chrome 从 76 版本开始,开始对设置为 None 必须和 Secure 配合使用,所以最后设置内容是</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="code"><pre><span class="line">SameSite=None;Secure;</span><br></pre></td></tr></tbody></table></figure>
<h4 id="解决措施">解决措施</h4>
<ol>
<li>创建自定义 CookieSerializer,修改设置 SameSite 属性的逻辑 </li>
</ol>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 自定义cookie序列化方式</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> anifengx</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2022-04-02 14:21:21</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CustomCookieSerializer</span> <span class="keyword">extends</span> <span class="title class_">DefaultCookieSerializer</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * http协议</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">HTTP</span> <span class="operator">=</span> <span class="string">"http"</span>;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 阿里云cdn添加header头帮助判断协议类型</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">X_CLIENT_SCHEME</span> <span class="operator">=</span> <span class="string">"X-Client-Scheme"</span>;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * nginx添加header头帮助判断协议类型</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">X_FORWARDED_PROTO</span> <span class="operator">=</span> <span class="string">"X-Forwarded-Proto"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">writeCookieValue</span><span class="params">(CookieValue cookieValue)</span> {</span><br><span class="line"> <span class="type">HttpServletRequest</span> <span class="variable">request</span> <span class="operator">=</span> cookieValue.getRequest();</span><br><span class="line"> <span class="type">HttpServletResponse</span> <span class="variable">response</span> <span class="operator">=</span> cookieValue.getResponse();</span><br><span class="line"> <span class="built_in">super</span>.writeCookieValue(cookieValue);</span><br><span class="line"> <span class="type">StringBuffer</span> <span class="variable">requestURL</span> <span class="operator">=</span> request.getRequestURL();</span><br><span class="line"> <span class="type">String</span> <span class="variable">scheme</span> <span class="operator">=</span> request.getHeader(X_CLIENT_SCHEME);</span><br><span class="line"> log.info(<span class="string">"requestURL X-Client-Scheme : [{}] scheme: [{}]"</span>, requestURL, scheme);</span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(scheme)) {</span><br><span class="line"> scheme = request.getHeader(X_FORWARDED_PROTO);</span><br><span class="line"> log.info(<span class="string">"requestURL X-Forwarded-Proto : [{}] scheme: [{}]"</span>, requestURL, scheme);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(scheme)) {</span><br><span class="line"> scheme = request.getScheme();</span><br><span class="line"> log.info(<span class="string">"requestURL schema : [{}] scheme: [{}]"</span>, requestURL, scheme);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(scheme) || HTTP.equals(scheme)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);</span><br><span class="line"> <span class="keyword">if</span> (CollectionUtils.isEmpty(headers)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="type">String</span> <span class="variable">userAgent</span> <span class="operator">=</span> request.getHeader(HttpHeaders.USER_AGENT);</span><br><span class="line"> log.info(<span class="string">"shouldSendSameSiteNone start userAgent: [{}] SET_COOKIE: [{}]"</span>, userAgent, headers);</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">firstHeader</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line"> <span class="comment">// there can be multiple Set-Cookie attributes</span></span><br><span class="line"> <span class="keyword">for</span> (String header : headers) {</span><br><span class="line"> <span class="keyword">if</span> (firstHeader) {</span><br><span class="line"> log.info(<span class="string">"shouldSendSameSiteNone userAgent: [{}] SET_COOKIE: [{}]"</span>, userAgent, header);</span><br><span class="line"> <span class="type">boolean</span> shouldSendSameSiteNone;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> shouldSendSameSiteNone = shouldSendSameSiteNone(userAgent);</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> log.error(<span class="string">"shouldSendSameSiteNone error userAgent: [{}] SET_COOKIE: [{}]"</span>, userAgent, header, e);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> log.info(<span class="string">"shouldSendSameSiteNone userAgent: [{}] SET_COOKIE: [{}] boolean [{}]"</span>, userAgent, header, shouldSendSameSiteNone);</span><br><span class="line"> <span class="keyword">if</span> (shouldSendSameSiteNone) {</span><br><span class="line"> header = header.replaceAll(<span class="string">"SameSite=Lax"</span>, <span class="string">"SameSite=None;secure;"</span>);</span><br><span class="line"> response.setHeader(HttpHeaders.SET_COOKIE, header);</span><br><span class="line"> }</span><br><span class="line"> log.info(<span class="string">"shouldSendSameSiteNone change userAgent: [{}] SET_COOKIE: [{}] boolean [{}] headers [{}] "</span>, userAgent, header, shouldSendSameSiteNone, response.getHeaders(HttpHeaders.SET_COOKIE));</span><br><span class="line"> firstHeader = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">shouldSendSameSiteNone</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="keyword">return</span> !isSameSiteNoneIncompatible(useragent);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isSameSiteNoneIncompatible</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="keyword">return</span> hasWebKitSameSiteBug(useragent) || dropsUnrecognizedSameSiteCookies(useragent);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">hasWebKitSameSiteBug</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="keyword">return</span> isIosVersion(<span class="number">12</span>, useragent) || (isMacosxVersion(<span class="number">10</span>, <span class="number">14</span>, useragent) && (isSafari(useragent) || isMacEmbeddedBrowser(useragent)));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">dropsUnrecognizedSameSiteCookies</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="keyword">if</span> (isUcBrowser(useragent)) {</span><br><span class="line"> <span class="keyword">return</span> !isUcBrowserVersionAtLeast(<span class="number">12</span>, <span class="number">13</span>, <span class="number">2</span>, useragent);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> isChromiumBased(useragent) && isChromiumVersionAtLeast(<span class="number">51</span>, useragent) && !isChromiumVersionAtLeast(<span class="number">67</span>, useragent);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isIosVersion</span><span class="params">(<span class="type">int</span> major, String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"\\(iP.+; CPU .*OS (\\d+)[\\d]*.*\\) AppleWebKit\\/"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">if</span> (m.find()) {</span><br><span class="line"> log.info(<span class="string">"isIosVersion Found value: "</span> + m.group(<span class="number">0</span>));</span><br><span class="line"> log.info(<span class="string">"isIosVersion Found value: "</span> + m.group(<span class="number">1</span>));</span><br><span class="line"> <span class="keyword">return</span> m.group(<span class="number">1</span>).equals(String.valueOf(major));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isMacosxVersion</span><span class="params">(<span class="type">int</span> major, <span class="type">int</span> minor, String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"\\(Macintosh;.*Mac OS X (\\d+)(\\d+)[\\d]*.*\\) AppleWebKit\\/"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">if</span> (m.find()) {</span><br><span class="line"> log.info(<span class="string">"isMacosxVersion Found value: "</span> + m.group(<span class="number">0</span>));</span><br><span class="line"> log.info(<span class="string">"isMacosxVersion Found value: "</span> + m.group(<span class="number">1</span>));</span><br><span class="line"> log.info(<span class="string">"isMacosxVersion Found value: "</span> + m.group(<span class="number">2</span>));</span><br><span class="line"> <span class="keyword">return</span> m.group(<span class="number">1</span>).equals(String.valueOf(major)) && m.group(<span class="number">2</span>).equals(String.valueOf(minor));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isSafari</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"Version\\/.* Safari\\/"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">return</span> m.find() && !isChromiumBased(useragent);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isChromiumBased</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"Chrom(e|ium)"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">return</span> m.find();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isMacEmbeddedBrowser</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"^Mozilla\\/[\\.\\d]+ \\(Macintosh;.*Mac OS X [\\d]+\\) "</span> + <span class="string">"AppleWebKit\\/[\\.\\d]+ \\(KHTML, like Gecko\\)$"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">return</span> m.find();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isChromiumVersionAtLeast</span><span class="params">(<span class="type">int</span> major, String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"Chrom[^ \\/]+\\/(\\d+)[\\.\\d]* "</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">if</span> (m.find()) {</span><br><span class="line"> <span class="type">int</span> <span class="variable">version</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(m.group(<span class="number">1</span>));</span><br><span class="line"> <span class="keyword">return</span> version >= major;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isUcBrowser</span><span class="params">(String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"UCBrowser\\/"</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">return</span> m.find();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isUcBrowserVersionAtLeast</span><span class="params">(<span class="type">int</span> major, <span class="type">int</span> minor, <span class="type">int</span> build, String useragent)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">"UCBrowser\\/(\\d+)\\.(\\d+)\\.(\\d+)[\\.\\d]* "</span>;</span><br><span class="line"> <span class="type">Pattern</span> <span class="variable">r</span> <span class="operator">=</span> Pattern.compile(pattern);</span><br><span class="line"> <span class="type">Matcher</span> <span class="variable">m</span> <span class="operator">=</span> r.matcher(useragent);</span><br><span class="line"> <span class="keyword">if</span> (m.find()) {</span><br><span class="line"> <span class="type">int</span> <span class="variable">majorVersion</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(m.group(<span class="number">1</span>));</span><br><span class="line"> <span class="type">int</span> <span class="variable">minorVersion</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(m.group(<span class="number">2</span>));</span><br><span class="line"> <span class="type">int</span> <span class="variable">buildVersion</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(m.group(<span class="number">3</span>));</span><br><span class="line"> <span class="keyword">if</span> (majorVersion != major) {</span><br><span class="line"> <span class="keyword">return</span> majorVersion > major;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (minorVersion != minor) {</span><br><span class="line"> <span class="keyword">return</span> minorVersion > minor;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> buildVersion >= build;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<ol start="2">
<li>在 spring 中注解式注入自定义 CookieSerializer</li>
</ol>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringSessionConfig</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> CookieSerializer <span class="title function_">cookieSerializer</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">CustomCookieSerializer</span> <span class="variable">cookieSerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CustomCookieSerializer</span>();</span><br><span class="line"> cookieSerializer.setCookieMaxAge(<span class="number">60</span> * <span class="number">60</span> * <span class="number">24</span> * <span class="number">30</span>);</span><br><span class="line"> <span class="keyword">return</span> cookieSerializer;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>]]></content>
<categories>
<category>cors</category>
</categories>
<tags>
<tag>java</tag>
<tag>cors</tag>
</tags>
</entry>
<entry>
<title>java 面试题汇总</title>
<url>/2022/11/02/javaAudition/</url>
<content><![CDATA[<p>本文记录一些比较贴合实际开发的常遇到的 java 面试题,方便后续面试</p>
<h3 id="【易】StringBuilder和StringBuffer区别是什么?">【易】StringBuilder 和 StringBuffer 区别是什么?</h3>
<p>StringBuffer 是线程安全的,StringBuilder 是非线程安全的,但是 StringBuilder 性能高于 StringBuffer,单线程下推荐使用 StringBuilder,<strong>StringBuffer 内部采用 synchronized 来实现并发下的线程安全,也导致性能较于 StringBuilder 更低</strong> <em>【加分项】</em></p>
<h3 id="【中】在-Java-程序中怎么保证多线程的运行安全?">【中】在 Java 程序中怎么保证多线程的运行安全?</h3>
<ul>
<li>使用安全类,比如 Java. util. concurrent 下的类</li>
<li>使用自动锁 synchronized</li>
<li> 使用手动锁 Lock</li>
<li>volatile 关键字</li>
</ul>
<h3 id="【难】volatile关键字作用,如何实现的原子性、可见性、有序性">【难】volatile 关键字作用,如何实现的原子性、可见性、有序性</h3>
<p>volatile 只能保证可见性和有序性,对于一些复杂操作无法保证原子性</p>
<ol>
<li>可见性:</li>
</ol>
<blockquote>
<p>CPU 的运行速度是远远高于内存的读写速度的,为了不让 cpu 为了等待读写内存数据,现代 cpu 和内存之间都存在一个高速缓存 cache,线程在运行的过程中会把主内存的数据拷贝一份到线程内部 cache 中,也就是 working memory。这个时候多个线程访问同一个变量,其实就是访问自己的内部 cache。这就导致当线程 A 把变量 flag 加载到自己的内部缓存 cache 中,线程 B 修改变量 flag 后,即使重新写入主内存,但是线程 A 不会重新从主内存加载变量 flag,看到的还是自己 cache 中的变量 flag。所以线程 A 是读取不到线程 B 更新后的值。</p>
</blockquote>
<p>volatile 会对修饰的变量添加 LOCK 指令,这个指令有两个最用</p>
<ul>
<li>将当前处理器缓存的数据刷新到主内存</li>
<li>刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效</li>
</ul>
<p>这也就保证了变量的可见性</p>
<ol start="2">
<li>有序性</li>
</ol>
<p>计算机在执行程序的过程中,编译器和处理器通常会对指令进行重排序,这样做的目的是为了提高性能。而 volatile 通过借助内存屏障这个概念保证了操作的有序性。</p>
<p>volatile 重排序规则:</p>
<ul>
<li>当第一个操作是 volatile 读时,无论第二个操作是什么都不能进行重排序。</li>
<li>当第二个操作是 volatile 写时,无论第一个操作是什么都不能进行重排序。</li>
<li>当第一个操作是 volatile 写,第二个操作为 volatile 读时,不能进行重排序。</li>
</ul>
<p>根据 volatile 重排序规则,Java 内存模型采取的是保守的屏障插入策略,volatile 写是在前面和后面分别插入内存屏障,volatile 读是在后面插入两个内存屏障,具体如下:</p>
<ul>
<li>
<p>volatile 读:在每个 volatile 读后面分别插入 LoadLoad 屏障及 LoadStore 屏障(根据 volatile 重排序规则第一条)</p>
<ul>
<li>LoadLoad 屏障的作用:禁止上面的所有普通读操作和上面的 volatile 读操作进行重排序</li>
<li> LoadStore 屏障的作用:禁止下面的普通写和上面的 volatile 读进行重排序。</li>
</ul>
</li>
<li>
<p>volatile 写:在每个 volatile 写前面插入一个 StoreStore 屏障(为满足 volatile 重排序规则第二条),在每个 volatile 写后面插入一个 StoreLoad 屏障(为满足 voaltile 重排序规则第三条</p>
<ul>
<li>StoreStore 屏障的作用:禁止上面的普通写和下面的 volatile 写重排序</li>
<li> StoreLoad 屏障的作用:防止上面的 volatile 写与下面可能出现的 volatile 读 / 写重排序。</li>
</ul>
</li>
</ul>
<blockquote>
<p>因为 Java 内存模型所采用的屏障插入策略比较保守,所以在实际的执行过程中,只要不改变 volatile 读 / 写的内存语义,编译器通常会省略一些不必要的内存屏障。</p>
</blockquote>
<ol start="3">
<li>原子性</li>
</ol>
<p>为什么一些复杂操作无法保证原子性?例如 i++,此操作实际可以分解为</p>
<ul>
<li>线程读取 i 的值</li>
<li> i 进行自增计算</li>
<li>刷新回 i 的值</li>
</ul>
<p>会出现 ab 两个线程取到同一个 i 值,例如 5,然后进行自增运算得到要刷新的值为 6,这是 a 线程率先完成赋值操作,i 变为 6,随后 b 线程进行赋值操作,因 b 线程已完成自增运算,所以也会用得到的值 6 来进行赋值操作,从而导致不满足原子性</p>
<h3 id="【难】synchronized的底层实现原理是什么?锁升级原理是什么?">【难】synchronized 的底层实现原理是什么?锁升级原理是什么?</h3>
<span id="more"></span>
<h3 id="【易】遍历List操作的几种方式?">【易】遍历 List 操作的几种方式?</h3>
<ul>
<li>for 循环遍历</li>
<li>增强 for 循环</li>
<li>迭代器循环</li>
<li> jdk8 后的 lambda 表达式,foreach 方法 <em>【加分项】</em></li>
</ul>
<h3 id="【中】遍历循环删除元素的话你会用那种方法来操作?">【中】遍历循环删除元素的话你会用那种方法来操作?</h3>
<p>使用迭代器循环删除</p>
<h3 id="【中】说一下-session-的工作原理?">【中】说一下 session 的工作原理?</h3>
<p>session 的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session 的 id 发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着 sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了</p>
<h3 id="【中】try-catch-finally-中,如果-catch-中-return-了,finally-还会执行吗?">【中】try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?</h3>
<p>finally 一定会执行,即使是 catch 中 return 了,catch 中的 return 会等 finally 中的代码执行完之后,才会执行</p>
<h3 id="【低】spring-事务实现方式有哪些?">【低】spring 事务实现方式有哪些?</h3>
<p><strong>声明式事务</strong>:声明式事务也有两种实现方式,基于 xml 配置文件的方式和注解方式(在类上添加 @Transaction 注解)</p>
<p><strong>编码方式</strong>:提供编码的形式管理和维护事务</p>
<h3 id="【低】MySQL-的内连接、左连接、右连接有什么区别?">【低】MySQL 的内连接、左连接、右连接有什么区别?</h3>
<p>内连接关键字:inner join;左连接:left join;右连接:right join。</p>
<p>内连接是把匹配的关联数据显示出来;左连接是左边的表全部显示出来,右边的表显示出符合条件的数据;右连接正好相反。</p>
<h3 id="【低】mysql默认事务隔离级别?">【低】mysql 默认事务隔离级别?</h3>
<p>REPEATABLE-READ 可重复读</p>
<h3 id="【中】乐观锁和悲观锁的区别?">【中】乐观锁和悲观锁的区别?</h3>
<p><strong>乐观锁</strong>:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据</p>
<p><strong>悲观锁</strong>:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放</p>
<h3 id="【难】如何保证接口幂等性?">【难】如何保证接口幂等性?</h3>
<div class="note info"><p>没有唯一正确的答案,更多地考察面试者的实际经验和总结能力</p>
</div>
<ol>
<li>定义唯一 msgId,根据 msgId 判断是否重复</li>
<li>结合业务逻辑使用乐观锁来限制,例如订单回调的业务逻辑,可以根据回调时订单的当前状态来判断本次回调内容是否要消费,并且在消费时借助乐观锁来进行限制,添加前置状态校验,类似<br>
<em>【加分项】</em></li>
</ol>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">update</span> `<span class="keyword">order</span>` <span class="keyword">set</span> `status` <span class="operator">=</span> <span class="string">'status'</span> <span class="keyword">where</span> `status` <span class="operator">=</span> <span class="string">'beforeStatus'</span>;</span><br></pre></td></tr></tbody></table></figure>
<ol start="3">
<li>每个请求操作必须有唯一的 ID,而这个 ID 就是用来表示此业务是否被执行过的关键凭证,例如,订单支付业务的请求,就要使用订单的 ID 作为幂等性验证的 Key;每次执行业务之前必须要先判断此业务是否已经被处理过;第一次业务处理完成之后,要把此业务处理的状态进行保存,比如存储到 Redis 中或者是数据库中,这样才能防止业务被重复处理</li>
</ol>
<h3 id="【难】JVM中有哪些垃圾回收算法?">【难】JVM 中有哪些垃圾回收算法?</h3>
<h3 id="【中】什么是Redis缓存穿透、击穿、雪崩?">【中】什么是 Redis 缓存穿透、击穿、雪崩?</h3>
<p><strong>缓存穿透</strong>:指访问一个缓存和数据库中都不存在的 key,由于这个 key 在缓存中不存在,则会到数据库中查询,数据库中也不存在该 key,无法将数据添加到缓存中,所以每次都会访问数据库导致数据库压力增大</p>
<blockquote>
<ol>
<li>将空 key 添加到缓存中</li>
<li>使用布隆过滤器过滤空 key</li>
</ol>
</blockquote>
<p><strong>缓存击穿</strong>:指大量请求访问缓存中的一个 key 时,该 key 过期了,导致这些请求都去直接访问数据库,短时间大量的请求可能会将数据库击垮</p>
<blockquote>
<ol>
<li>添加互斥锁或分布式锁,让一个线程去访问数据库,将数据添加到缓存中后,其他线程直接从缓存中获取</li>
<li>热点数据 key 不过期,定时更新缓存</li>
</ol>
</blockquote>
<p><strong>缓存雪崩</strong>:指在系统运行过程中,缓存服务宕机或大量的 key 值同时过期,导致所有请求都直接访问数据库导致数据库压力增大</p>
<blockquote>
<ol>
<li>将 key 的过期时间打散,避免大量 key 同时过期</li>
<li>使用互斥锁重建缓存</li>
</ol>
</blockquote>
<h3 id="【中】Redis怎么实现分布式锁?">【中】Redis 怎么实现分布式锁?</h3>
<p>使用命令 setnx 来实现,假如当前 key 有值则说明锁已经被占有,程序在使用完锁后需要调用 del 释放锁;</p>
<blockquote>
<p>redis 的分布式锁会存在锁超时的情况,如果程序执行时间过长会导致锁自动解开,从而分布式锁就失效了,可以借助 Redission 的 watchdog 机制来解决;因为设置锁和设置过期时间是分开操作的,可能存在设置完锁服务宕机的情况,导致锁没有正确释放,这些可以通过 lua 脚本来实现,合并到一条命令中,也可以通过 Redission 来实现。<em>【加分项】</em></p>
</blockquote>
<h3 id="【中】RPC请求,服务端在收到方法名、接口名、参数名后怎么完成一次调用?">【中】RPC 请求,服务端在收到方法名、接口名、参数名后怎么完成一次调用?</h3>
<p>服务端通过 rpc 解析到参数后,通过反射的方式,调用具体方法</p>
<h3 id="【低】最近有在学习新的东西吗?有在了解哪块技术吗?">【低】最近有在学习新的东西吗?有在了解哪块技术吗?</h3>
<p>答案不限,关注面试者是否有自律性和学习能力</p>
]]></content>
<categories>
<category>面试题</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title>linux 常用命令</title>
<url>/2023/03/15/linuxCommonCommands/</url>
<content><![CDATA[<p>平常查询日志等工作都是通过 ELK 来完成,并且 ELK 用起来也比较方便,但是有时候存在单条日志信息过多导致 ELK 收集器忽略或者截断的情况,导致无法查看需要的全部日志来定位问题,所以会需要连到服务器上去查看日志,这时候就需要用到一些 linux 命令,在此记下方便以后直接复制使用</p>
<h3 id="查看日志的命令">查看日志的命令</h3>
<h4 id="tail">tail</h4>
<p>服务器启动失败,只需要查看最近的失败日志;查看当前服务运行情况等,可以使用本命令</p>
<p><code>-f</code> 实时读取最新内容,默认 10 行</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">tail -f task_info.log</span><br></pre></td></tr></tbody></table></figure>
<p><code>-n</code> 指定要读取的行数,读取最后 n 行</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">tail -n 30 task_info.log</span><br></pre></td></tr></tbody></table></figure>
<span id="more"></span>
<h4 id="cat">cat</h4>
<p>查看文件内容,服务器上大文件使用时要注意控制显示内容</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">cat task_info.log</span><br></pre></td></tr></tbody></table></figure>
<h4 id="grep">grep</h4>
<p>根据一些关键词搜索要查看的内容,一般结合 cat 使用</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">cat task_info-20221216.5.log | grep '锁座'</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>一些额外的选项,方便查看日志</p>
</blockquote>
<ul>
<li>-A n : 如果匹配成功,则将匹配行及其后 n 行一起打印出来<br></li>
<li>-B n : 如果匹配成功,则将匹配行及其前 n 行一起打印出来<br></li>
<li>-C n : 如果匹配成功,则将匹配行及其 (前后) n 行一起打印出来<br></li>
<li>–color=auto :可以将找到的关键词部分加上颜色的显示</li>
</ul>
<p><em><strong>e.g.</strong></em></p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">cat task_info-20221216.5.log | grep -n -A2 -B1 --color=auto '锁座'</span><br></pre></td></tr></tbody></table></figure>
<h4 id="sz">sz</h4>
<p>从服务器上下载文件</p>
<figure class="highlight shell"><table><tbody><tr><td class="code"><pre><span class="line">sz apiclient_cert.p12</span><br></pre></td></tr></tbody></table></figure>]]></content>
<categories>
<category>linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title>记管理人员拓展活动思考</title>
<url>/2022/10/27/manage/</url>
<content><![CDATA[<p>2022 年 10 月 15 日参加了管理人员拓展活动,也是第一次参加拓展活动,认识了很多人,也同时有了一些感悟,本文仅做自身感悟记录。</p>
<p>当日活动有一个七巧板比赛,是个多人分组协同合作的项目,赛后教练也是组织大家做了复盘,本次的笔记也主要是来源于此次复盘的一些发言和自身感悟。</p>
<h3 id="一致性">一致性</h3>
<p>如何理解这个标题?一致性指的就是身为管理高层,要有统一的思想,要有一致认可的行动方案。不能小明有小明的方案,小强有小强的方案,并且这两个方案还在同时执行,这只会导致下属行动混乱,没有固定的目标。</p>
<span id="more"></span>
<h3 id="项目实施前规划方案的重要性">项目实施前规划方案的重要性</h3>
<p>每次做什么事前,你是会准备一个计划方案来执行,还是先执行起来,再慢慢修正呢?</p>
<p>这次七巧板就遇到了这个问题,当教练公布完游戏规则后,游戏就已经开始了,每个组手上都有了一份属于他们自己的任务,游戏时间只有 30 分钟,这时你是选择暂停所有人的行动,花 3-5 分钟甚至更多的时间来收集任务并归纳输出一套解决方案,还是先让整个游戏运转起来,然后等遇到问题再解决问题呢?</p>
<p>当时我们选择了先行动,再思考,最终在游戏进行了约 20 分钟后,才逐渐摸清了套路,并进行了调整,不过船头调转的太晚,没能拿到游戏的满分。但是假如我们先花些时间,收集并查看每个小组的任务,总结其中的共同点,那么可能只需要 20 分钟就能完成这场游戏。</p>
<p>由此可见,实际行动前的准备何其重要,提前了解任务目标、游戏规则并制定方案,不正如我们实际工作中的了解客户需求,按照客户的规定提供项目实施计划吗?但是当我们跳过这些准备,直接开始实施,想必项目实施的结果如同游戏结果一般,杂乱无章,一团糟糕,最终引起客户的投诉。</p>
<p>当然在我们的日常工作中,很少会出现这么紧迫的情况,更多的是先得到需求并与客户研讨,然后逐步执行最终落地。这就更需要我们眼光放长远,不要专注于眼前利益、急干蛮干;提前统筹规划,制定出详细的计划方案,让项目的运行始终符合我们的预期,最终完美落地。</p>
<h3 id="动态修正">动态修正</h3>
<p>什么是动态修正?没有什么计划一开始就是完美无缺的,做到万无一失。但是我们可以在计划进行中穿插早会、周会等多种形式,及时对当前进度进行总结复盘,发现其中的问题并进行修正,形成闭环。最不应该的是项目已出现许多问题,却依然视而不见,坚持既定计划,最终导致计划失败或者计划完成但是效果很差。不惧怕问题,视项目中出现的问题为自己改正的契机,正是暴露出的这些问题,才能逐步让自己的计划变得完善,也让自己有了应对经验。</p>
]]></content>
<categories>
<category>管理</category>
</categories>
<tags>
<tag>管理</tag>
</tags>
</entry>
<entry>
<title>mysql 中多表 LEFT JOIN 的误区</title>
<url>/2022/11/12/mysqlLeftJoin/</url>
<content><![CDATA[<p>最近在日常看项目慢 SQL 日志的时候,发现项目里的用户管理查询 SQL 略慢,基本要在一秒以上,决定优化一下,结果优化了半天还优化出来个问题,研究了好久才发现原因,其实是我的使用姿势不对</p>
<h4 id="背景介绍">背景介绍</h4>
<p>用户列表查询使用了多表关联,其中使用了 7 张表做 LEFT JOIN 关联查询,拼接了很多查询条件,并且还穿插了几个子查询。所以做了一下优化,重新梳理了表逻辑</p>
<p>其中有三张表分别是 <code>user</code> 用户表,<code>user_brand</code> 用户品牌表,<code>user_add_account</code> 用户加款账户表,查询逻辑是</p>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> u.name, group_concat(ub.name) brandName, group_concat(uac.account) account</span><br><span class="line"><span class="keyword">FROM</span> `<span class="keyword">user</span>` u </span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span> `user_brand` ub <span class="keyword">ON</span> u.id <span class="operator">=</span> ub.user_id </span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span> `user_add_account` uac <span class="keyword">ON</span> u.id <span class="operator">=</span> uac.user_id</span><br></pre></td></tr></tbody></table></figure>
<p>结果出现了 brandName 和 account 相同内容重复的问题,之前 brandName 是通过子查询得出的,所以没有存在重复的情况,结果改成 LEFT JOIN 出现了重复问题,这个问题也是研究了好久才发现,其实是因为对 SQL 查询原理掌握不牢导致的</p>
<span id="more"></span>
<h4 id="原因">原因</h4>
<p>三表的关系是,<code>user</code> 和 <code>user_brand</code> 是一对多的关系,<code>user</code> 和 <code>user_add_account</code> 也是一对多的关系,在 mysql 中,如果多表使用 LEFT JOIN 来进行关联查询,并且存在多个左表和右表的一对多关系,mysql 会将多个右表的条数进行笛卡尔积对应到左表上,就导致了 <code>user_brand</code> 和 <code>user_add_account</code> 都存在重复出现的情况</p>
<h5 id="问题SQL">问题 SQL</h5>
<figure class="highlight sql"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> u.id, ub.brand_id, uaa.account </span><br><span class="line"><span class="keyword">FROM</span> `<span class="keyword">user</span>` u </span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span> `user_brand` ub <span class="keyword">ON</span> u.id <span class="operator">=</span> ub.user_id </span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span> `user_add_account` uaa <span class="keyword">ON</span> uaa.user_id <span class="operator">=</span> u.id </span><br></pre></td></tr></tbody></table></figure>
<h5 id="查询结果">查询结果</h5>
<table>
<thead>
<tr>
<th>id</th>
<th>brand_id</th>
<th>account</th>
</tr>
</thead>
<tbody>
<tr>
<td>8</td>
<td>1</td>
<td>5719xxxxxxxx901</td>
</tr>
<tr>
<td>8</td>
<td>1</td>
<td>5719xxxxxxxx555</td>
</tr>
<tr>
<td>11</td>
<td>1</td>
<td>1209xxxxxxxx902</td>
</tr>
<tr>
<td>11</td>
<td>2</td>
<td>1209xxxxxxxx902</td>
</tr>
<tr>
<td>11</td>
<td>3</td>
<td>1209xxxxxxxx902</td>
</tr>
</tbody>
</table>
<p>很明显可以看出,id 为 8 的数据因为 <code>user_add_account</code> 存在两条数据,导致会查出来两条数据,id 为 11 的数据因为 <code>user_brand</code> 存在三条数据,导致会查出来三条数据,假如有一个 id 为 20 的数据,关联到 <code>user_brand</code>2 条,关联到 <code>user_add_account</code> 三条,那最终会查出 6 条数据</p>
<h4 id="怎么解决?">怎么解决?</h4>
<p>两种方案,第一种可以在 group_concat 中加 DISTINCT 关键词去重,因为实际业务查询只是为了看关联到 user_brand 的数据,去重并不会影响业务的展示</p>
<p>第二种可以去除 LEFT JOIN 关联的表,采用单独查询后拼接的方式,也能避免本情况,顺便还能减少表关联,减轻数据库查询压力</p>
]]></content>
<categories>
<category>mysql</category>
</categories>
<tags>
<tag>mysql</tag>
</tags>
</entry>
<entry>
<title>shiro 框架版本升级出现请求 400 拦截处理</title>
<url>/2023/01/18/shiroUpdate/</url>
<content><![CDATA[<h3 id="问题描述">问题描述</h3>
<p>因阿里云服务器安全提示,项目中的 shiro 框架近期做了一次升级,升级后检查项目运行无任何问题,几天后运营突然反应说管理平台图片全部无法展示,进行了初步排查,发现在请求后端获取图片的请求全部返回 400 被拦截了,结合最近更新内容锁定目标在 shiro 版本升级上</p>
<div class="note info"><p>运营需要在管理平台查看图片后直接右键另存为对应中文名的文件,并且要求是原本文件名称。图片默认都上传到了 oss,如果直接右键下载会是乱序英文数字文件名,所以在管理平台后端做了一次转发并设置了中文文件名称</p>
</div>
<span id="more"></span>
<h3 id="问题解决">问题解决</h3>
<p>实际问题是由于 shiro 新框架新增了几个默认 filter,其中 <code>InvalidRequestFilter</code> 会拦截 url 上的特殊字符,所以只需要修改 <code>InvalidRequestFilter</code> 的方法跳过拦截即可</p>
<h4 id="解决办法">解决办法</h4>
<p>重写 <code>ShiroFilterFactoryBean</code></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CustomShiroFilterFactoryBean</span> <span class="keyword">extends</span> <span class="title class_">ShiroFilterFactoryBean</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> LoggerFactory.getLogger(CustomShiroFilterFactoryBean.class);</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Class <span class="title function_">getObjectType</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> CustomSpringShiroFilter.class;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> AbstractShiroFilter <span class="title function_">createInstance</span><span class="params">()</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> logger.debug(<span class="string">"Creating Shiro Filter instance."</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">SecurityManager</span> <span class="variable">securityManager</span> <span class="operator">=</span> getSecurityManager();</span><br><span class="line"> <span class="keyword">if</span> (securityManager == <span class="literal">null</span>) {</span><br><span class="line"> <span class="type">String</span> <span class="variable">msg</span> <span class="operator">=</span> <span class="string">"SecurityManager property must be set."</span>;</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanInitializationException</span>(msg);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!(securityManager <span class="keyword">instanceof</span> WebSecurityManager)) {</span><br><span class="line"> <span class="type">String</span> <span class="variable">msg</span> <span class="operator">=</span> <span class="string">"The security manager does not implement the WebSecurityManager interface."</span>;</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanInitializationException</span>(msg);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">FilterChainManager</span> <span class="variable">manager</span> <span class="operator">=</span> createFilterChainManager();</span><br><span class="line"> <span class="comment">//Expose the constructed FilterChainManager by first wrapping it in a</span></span><br><span class="line"> <span class="comment">// FilterChainResolver implementation. The AbstractShiroFilter implementations</span></span><br><span class="line"> <span class="comment">// do not know about FilterChainManagers - only resolvers:</span></span><br><span class="line"> <span class="type">PathMatchingFilterChainResolver</span> <span class="variable">chainResolver</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PathMatchingFilterChainResolver</span>();</span><br><span class="line"> chainResolver.setFilterChainManager(manager);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// URL携带中文400,servletPath中文校验bug</span></span><br><span class="line"> Map<String, Filter> filterMap = manager.getFilters();</span><br><span class="line"> <span class="type">Filter</span> <span class="variable">invalidRequestFilter</span> <span class="operator">=</span> filterMap.get(DefaultFilter.invalidRequest.name());</span><br><span class="line"> <span class="keyword">if</span> (invalidRequestFilter <span class="keyword">instanceof</span> InvalidRequestFilter) {</span><br><span class="line"> ((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(<span class="literal">false</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built</span></span><br><span class="line"> <span class="comment">//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class</span></span><br><span class="line"> <span class="comment">//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts</span></span><br><span class="line"> <span class="comment">//injection of the SecurityManager and FilterChainResolver:</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">CustomSpringShiroFilter</span>((WebSecurityManager) securityManager, chainResolver);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>将重写的 <code>ShiroFilterFactoryBean</code> 注入进 shiro 中</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta">@Bean(name = "shiroFilter")</span></span><br><span class="line"><span class="keyword">public</span> BaseShiroFactoryBean <span class="title function_">shiroFilter</span><span class="params">(<span class="meta">@Qualifier(value = "securityManager")</span> SecurityManager securityManager)</span> {</span><br><span class="line"> <span class="comment">//根据配置加载是否启用redis</span></span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">redis</span> <span class="operator">=</span> customerConfig.getRedis();</span><br><span class="line"> <span class="type">BaseShiroFactoryBean</span> <span class="variable">shiroFilterFactoryBean</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CustomShiroFilterFactoryBean</span>();</span><br><span class="line"> Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();</span><br><span class="line"> filters.put(Constants.CUSTOMER_SHIRO_FILTER, customRolesAuthorizationFilter());</span><br><span class="line"> shiroFilterFactoryBean.setFilters(filters);</span><br><span class="line"> shiroFilterFactoryBean.setSecurityManager(securityManager);</span><br><span class="line"> <span class="comment">//加载默认配置.</span></span><br><span class="line"> Map<String, String> filterChainDefinitionMap = <span class="keyword">new</span> <span class="title class_">HashMap</span><>(<span class="number">16</span>);</span><br><span class="line"> defaultFilterChain(filterChainDefinitionMap);</span><br><span class="line"> <span class="comment">//加载自定义配置</span></span><br><span class="line"> loadCustomerChain(filterChainDefinitionMap);</span><br><span class="line"> <span class="comment">//登录地址</span></span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(customerConfig.getLoginUrl())) {</span><br><span class="line"> shiroFilterFactoryBean.setLoginUrl(Constants.DEFAULT_LOGIN_URL);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> shiroFilterFactoryBean.setLoginUrl(customerConfig.getLoginUrl());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//登录成功后要跳转的链接</span></span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(customerConfig.getSuccessUrl())) {</span><br><span class="line"> shiroFilterFactoryBean.setSuccessUrl(Constants.DEFAULT_INDEX);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> shiroFilterFactoryBean.setSuccessUrl(customerConfig.getSuccessUrl());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//未授权界面;</span></span><br><span class="line"> <span class="keyword">if</span> (StringUtils.isBlank(customerConfig.getUnauthorizedUrl())) {</span><br><span class="line"> shiroFilterFactoryBean.setUnauthorizedUrl(Constants.DEFAULT_403);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> shiroFilterFactoryBean.setUnauthorizedUrl(customerConfig.getUnauthorizedUrl());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//加载静态权限链</span></span><br><span class="line"> shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);</span><br><span class="line"> <span class="comment">//加载动态权限链</span></span><br><span class="line"> shiroFilterFactoryBean.setFilterChainDefinitions(<span class="string">""</span>);</span><br><span class="line"> <span class="keyword">return</span> shiroFilterFactoryBean;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>至此问题解决,一开始曾尝试设置 filter 不校验的 url 地址,但是查看源码后发现不支持这么设置,修改源码影响范围较大,遂决定全局关闭 BlockNonAscii 设置</p>
<h3 id="参考文章">参考文章</h3>
<p><a href="https://lequ7.com/guan-yu-springboot-zai-springboot-xia-sheng-ji-shiro-zhi-hou-dao-zhi-zhong-wen-lu-jing-400-fei-fa-de-jie-jue-fang-an.html" title="关于springboot:在SpringBoot下升级shiro之后导致中文路径400非法的解决方案">关于 springboot: 在 SpringBoot 下升级 shiro 之后导致中文路径 400 非法的解决方案</a></p>
<div class="note warning"><p>尝试了上述方法,发现未生效</p>
</div>
<p><a href="https://www.freesion.com/article/89131489103/" title="SHIRO1.6升级到1.7后的问题处理">SHIRO1.6 升级到 1.7 后的问题处理</a></p>
<p>主要参考了上述文章</p>
]]></content>
<categories>
<category>shiro</category>
</categories>
<tags>
<tag>java</tag>
<tag>shiro</tag>
</tags>
</entry>
<entry>
<title>spring 监控平台从零单排【一】</title>
<url>/2023/03/21/springMonitor/</url>
<content><![CDATA[<p>公司之前已经有了简单的服务器监控工具,主要是针对 linux 服务器性能监控和 mysql、redis 数据库的监控,并没有做到对真实服务情况的监控,公司当前后端主要以 java 为主,所以缺少了 jvm 及接口性能的监控。这些可以通过 java 相关开源框架并结合 nginx 的日志来处理。</p>
<p>spring 基本已经是 java 开发的主流框架,spring 也提供了 <strong>Actuator</strong> 开源框架来收集应用的相关指标,如健康检查,审计,指标收集,并且支持 <strong>Prometheus</strong> 整合。</p>
<h3 id="Spring-Boot-Admin">Spring Boot Admin</h3>
<p><strong>Actuator</strong> 提供各种端口来暴露收集的原始数据,并且支持自定义打点收集数据,但是这些并不能直观的展示到平台上,可以借助 <strong>Prometheus</strong> 传递到 <strong>grafana</strong> 上或自行接入,springboot 也提供了一个开发好的 ui 监控平台展示 <strong>Actuator</strong> 的数据,这就是 <strong>Spring Boot Admin</strong>。</p>
<span id="more"></span>
<p>下面简单介绍 <strong>Admin</strong> 的使用姿势</p>
<h4 id="集成">集成</h4>
<p><strong>Admin</strong> 需要一个单独的模块来运行监控平台,即 server;已有的业务模块都会当做收集端,即 client</p>
<h5 id="server">server</h5>
<ol>
<li>
<p>新建模块,直接在 ide 工具中 new 一个新模块即可,创建 <code>application.java</code> 文件和 <code>application.yml</code> 文件</p>
</li>
<li>
<p>新模块 pom 文件中添加依赖</p>
<figure class="highlight xml"><table><tbody><tr><td class="code"><pre><span class="line"><span class="comment"><!-- 支持admin-ui的关键配置 --></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>de.codecentric<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-admin-starter-server<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.5.5<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></tbody></table></figure>
<p><em>我也没搞懂为啥 admin 的组织不在 <code>springboot parent</code> 全家桶里,还需要单独控制版本号</em></p>
<blockquote>
<p>注意 <code>spring-boot-admin-starter-server</code> 的版本,和 <code>spring-boot-starter-parent</code> 保持一致,admin 支持的版本号可以到 maven 官方仓库或一些<a href="https://central.sonatype.com/artifact/de.codecentric/spring-boot-admin-starter-server/3.0.2/versions" title="收集Maven版本号的网站">第三方 maven 仓库</a>查看</p>
</blockquote>
</li>
<li>
<p>新模块 <code>application.java</code> 文件中添加注解 <code>@EnableAdminServer</code></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableAutoConfiguration</span></span><br><span class="line"><span class="meta">@EnableAdminServer</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">App</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span>{</span><br><span class="line"> SpringApplication.run(App.class,args);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>运行新模块查看效果</p>
</li>
</ol>
<p><img src="/2023/03/21/springMonitor/1.png" alt=""></p>
<h5 id="client">client</h5>
<ol>
<li>
<p>修改需要监控的业务模块的 pom 文件</p>
<figure class="highlight xml"><table><tbody><tr><td class="code"><pre><span class="line"><span class="comment"><!-- 支持admin-ui的关键配置 --></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>de.codecentric<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-admin-starter-client<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.5.5<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>修改 <code>application.yml</code> 文件,添加 server 的信息</p>
<figure class="highlight yaml"><table><tbody><tr><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"> <span class="comment">#注册当前服务到admin-ui-server服务中</span></span><br><span class="line"> <span class="attr">boot:</span></span><br><span class="line"> <span class="attr">admin:</span></span><br><span class="line"> <span class="attr">client:</span></span><br><span class="line"> <span class="attr">url:</span> <span class="string">http://localhost:8090</span></span><br><span class="line"><span class="attr">management:</span></span><br><span class="line"> <span class="attr">endpoints:</span></span><br><span class="line"> <span class="attr">web:</span></span><br><span class="line"> <span class="attr">exposure:</span></span><br><span class="line"> <span class="comment">#暴露所有的收集信息给server</span></span><br><span class="line"> <span class="attr">include:</span> <span class="string">"*"</span></span><br><span class="line"> <span class="attr">endpoint:</span></span><br><span class="line"> <span class="attr">health:</span></span><br><span class="line"> <span class="comment">#显示详细健康信息,例如数据库mysql和redis</span></span><br><span class="line"> <span class="attr">show-details:</span> <span class="string">always</span></span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>尤其注意 <code>spring.boot.admin.client.url</code> 的配置,如果 <code>server</code> 配置了 <code>server.servlet.context-path</code>,要注意 url 的地址后也要做相应的拼接</p>
</blockquote>
</li>
<li>
<p>启动业务模块,会自动请求 <code>server</code> 进行注册,<code>server</code> 不需要重启,并且会自动轮询业务模块暴露的收集接口抓取信息</p>
</li>
</ol>
<p><img src="/2023/03/21/springMonitor/2.png" alt=""></p>
<p>至此 <code>Admin</code> 就已经可以初步使用了</p>
<div class="note "><p>可供参考的<a href="https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started" title="官方手册">官方手册</a></p>
</div>
<h3 id="后记">后记</h3>
<p>接入过程中遇到了两个坑,分别记录下</p>
<h4 id="client注册到server报错">client 注册到 server 报错</h4>
<p>报错信息如下</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line">com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `de.codecentric.boot.admin.server.domain.values.Registration` (no Creators, like <span class="keyword">default</span> constructor, exist): cannot deserialize from Object <span class="title function_">value</span> <span class="params">(no delegate- or property-based Creator)</span></span><br></pre></td></tr></tbody></table></figure>
<p>原因是 <code>application.java</code> 启动类中没有正确添加注解,如果使用了 <code>@SpringBootApplication</code>,会因为 <code>@ComponentScan</code> 的特性以当前类所在包来查找 bean,导致不能正确注册 <code>Admin</code> 相关的组件</p>
<div class="note warning"><p>直接 baidu 的解决方法对代码侵入性过高,需要重写 spring 容器中注册的序列化方式类,最后查看 <code>Admin</code> 官网发现了华点</p>
</div>
<h4 id="在client中获取容器当前所有注册路径报错">在 client 中获取容器当前所有注册路径报错</h4>
<p>当使用如下代码时,会提示有重复类型的组件在容器中</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="type">RequestMappingHandlerMapping</span> <span class="variable">mapping</span> <span class="operator">=</span> applicationContext.getBean(RequestMappingHandlerMapping.class);</span><br></pre></td></tr></tbody></table></figure>
<p>报错信息为</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line">Method xssFilterRegistration in com.decent.WebApplication required a single bean, but <span class="number">2</span> were found:</span><br><span class="line"> - requestMappingHandlerMapping: defined by method <span class="string">'requestMappingHandlerMapping'</span> in <span class="keyword">class</span> <span class="title class_">path</span> resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class]</span><br><span class="line"> - controllerEndpointHandlerMapping: defined by method <span class="string">'controllerEndpointHandlerMapping'</span> in <span class="keyword">class</span> <span class="title class_">path</span> resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.class]</span><br></pre></td></tr></tbody></table></figure>
<p><code>Admin</code> 也会注册一个 <code>RequestMappingHandlerMapping</code> 的子类 <code>ControllerEndpointHandlerMapping</code> 到容器中,需要使用根据 bean 名称来获取的 bean 的形式</p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="type">RequestMappingHandlerMapping</span> <span class="variable">mapping</span> <span class="operator">=</span> applicationContext.getBean(<span class="string">"requestMappingHandlerMapping"</span>, RequestMappingHandlerMapping.class);</span><br></pre></td></tr></tbody></table></figure>]]></content>
<categories>
<category>monitor</category>
</categories>
<tags>
<tag>java</tag>
<tag>spring</tag>
<tag>monitor</tag>
<tag>Admin</tag>
</tags>
</entry>
<entry>
<title>spotbugs 使用指南</title>
<url>/2022/10/10/spotbugs/</url>
<content><![CDATA[<h2 id="前言">前言</h2>
<p>代码质量主要依靠代码检查来进行,但当前代码检查占用较多工时。现推荐一个比较成熟的代码检查插件,要求 JAVA 开发每次提交代码前都应使用插件自检,可以一定程度上减少代码检查所用的时间,帮助开发人员规范编码习惯,提高编码水平,本插件不能替代阿里巴巴开发规范插件,只是对其的补充。</p>
<h2 id="SpotBugs使用说明">SpotBugs 使用说明</h2>
<h3 id="安装方法">安装方法</h3>
<p>在 IDEA 中,点击工具类 <strong>File->Settings->Plugins</strong> 后,切换到 <strong>Marketplace</strong> 标签,在输入框中输入 spotbugs,查询到 <strong>SpotBugs</strong> 插件,点击 <strong>install</strong> 按钮安装插件,如图所示:</p>
<p><img src="/2022/10/10/spotbugs/SpotBugsInstall.png" alt=""></p>
<center>
SpotBugs 安装
</center>
<span id="more"></span>
<h3 id="使用手册">使用手册</h3>
<h4 id="使用步骤">使用步骤</h4>
<p>SpotBugs 的使用方式是通过插件扫描代码的方式来检查出代码中隐含的 bug,首先切换 idea 任务栏到 SpotBugs 下(截图红字 3)。SpotBugs 主要有两种扫描方式,可全项目扫描或单模块扫描:</p>
<ul>
<li>全项目扫描使用截图红字 2 中第三个按钮</li>
<li>单模块扫描首先选择需要扫描的模块(截图红字 1),然后点击截图红字 2 中第一个按钮</li>
</ul>
<blockquote>
<p>说明:截图红字 3 中三个按钮依次为:扫描选中模块不包含 TEST 类;扫描选中模块包含 TEST 类;扫描全项目</p>
</blockquote>
<p><img src="/2022/10/10/spotbugs/SpotBugsUseStepOne.png" alt=""></p>
<center>
SpotBugs 使用步骤
</center>
<h4 id="BUG说明">BUG 说明</h4>
<p>SpotBugs 扫描出的问题主要分为几个大类,每个大类中包含多个具体问题,具体问题有自己对应的处理优先级,分为高中低三级。现针对具体问题做解释说明,方便修改错误代码。本文档并未描述 SpotBugs 支持的全部问题,只针对每个大类列举有特点且较难理解的问题,未在本文档中出现但是被 SpotBugs 扫描出的问题,开发者也应自行查找处理,附 <a href="https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html" title="SpotBugs官方校验规则文档">SpotBugs 官方校验规则文档</a></p>
<h5 id="高危(强制修改)">高危(强制修改)</h5>
<h6 id="1-1-No-relationship-between-generic-parameter-and-method-argument">1.1 No relationship between generic parameter and method argument</h6>
<p>方法参数和传入参数存在泛型不一致的情况<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line">List<String> strList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line">strList.add(<span class="string">"1"</span>);</span><br><span class="line">List<Long> idList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line">idList.add(<span class="number">1L</span>);</span><br><span class="line">idList.add(<span class="number">2L</span>);</span><br><span class="line">idList.add(<span class="number">3L</span>);</span><br><span class="line">idList.removeAll(strList);</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-2-Call-to-equals-comparing-different-types">1.2 Call to equals() comparing different types</h6>
<p>equals 方法比较的双方不是相同类型<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line">cn.common.enums.OrderStatusEnum.REFUNDED.equals(cn.common.kentucky.enums.OrderStatusEnum.REFUNDED)</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-3-Nullcheck-of-value-previously-dereferenced">1.3 Nullcheck of value previously dereferenced</h6>
<p>空值检查顺序错误<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> OrderDao.getOrder(dto.getId());</span><br><span class="line"><span class="keyword">if</span> (BreakUpType.CHILD.equals(order.getBreakUpType())) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (order == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SimpleMessage</span>(ErrorCodeEnum.NO, <span class="string">"订单不存在"</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-4-A-parameter-is-dead-upon-entry-to-a-method-but-overwritten">1.4 A parameter is dead upon entry to a method but overwritten</h6>
<p>错误的认为对参数的覆盖会回传给调用者<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">partialH5Refund</span><span class="params">(Long id, String refundNo, String content, OrderRefund orderRefund)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// String类型采用值传递,在方法中修改参数并不会影响原参数</span></span><br><span class="line"> content = String.format(<span class="string">"系统同步部分退款, 退款交易流水号[%s]"</span>, refundNo);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><span><font color="#D2B48C">说明:</font>JAVA 中当传入参数是对象时,修改对象属性是可以生效的,但是直接赋值覆盖对象的操作,是不会影响原参数的;String 类在 JAVA 中是值传递,对其的修改不会影响原参数。具体有关 JAVA 值传递和引用传递的相关介绍可参考文章 <a href="https://zhuanlan.zhihu.com/p/388486387" title="JAVA值传递和引用传递">JAVA 值传递和引用传递</a></span></p>
<h6 id="1-5-Self-assignment-of-clearInterval-rather-than-assigned-to-field">1.5 Self assignment of clearInterval rather than assigned to field</h6>
<p>在 setValue 中错误的将传参的值赋值给传参<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setClearInterval</span><span class="params">(Long clearInterval)</span> {</span><br><span class="line"> <span class="comment">// 应该使用this引用</span></span><br><span class="line"> clearInterval = clearInterval;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-6-Class-names-shouldn’t-shadow-simple-name-of-superclass">1.6 Class names shouldn’t shadow simple name of superclass</h6>
<p>子类名称不应和父类相同<br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderConfig</span> <span class="keyword">extends</span> <span class="title class_">cn</span>.kentucky.order.config.OrderConfig</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-7-Random-object-created-and-used-only-once">1.7 Random object created and used only once</h6>
<p>推荐使用 java.security.SecureRandom 代替 Random</p>
<h6 id="1-8-Boxing-unboxing-to-parse-a-primitive">1.8 Boxing/unboxing to parse a primitive</h6>
<p>使用 Integer.parse 代替 Integer.valueOf;Integer.valueOf 会涉及多次拆箱装箱操作,经历了 String - int - Integer - int 的过程</p>
<h6 id="1-9-Reliance-on-default-encoding">1.9 Reliance on default encoding</h6>
<p>使用 new String、 getBytes 等方法需要指定编码格式,默认编码格式会因服务所在平台发生改变,这会产生意想不到的问题<br>
<br><br>
<font color="#006666">正例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="type">byte</span>[] data = plainText.getBytes(StandardCharsets.UTF_8);</span><br></pre></td></tr></tbody></table></figure>
<h6 id="1-10-HTTP-Response-splitting-vulnerability">1.10 HTTP Response splitting vulnerability</h6>
<p>禁止将用户提交的参数直接放置到 response header 上或作为重定向参数,这可能会出现安全漏洞,具体描述参考 <a href="https://blog.csdn.net/qq_35976271/article/details/103276682" title="HTTP Response Splitting">HTTP Response Splitting</a><br>
<br><br>
<font color="#FF0000">反例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">referer</span> <span class="operator">=</span> request.getHeader(<span class="string">"REFERER"</span>);</span><br><span class="line">response.sendRedirect(referer);</span><br></pre></td></tr></tbody></table></figure>
<h5 id="中等(强制修改)">中等(强制修改)</h5>
<h6 id="2-1-equals-method-overrides-equals-in-superclass-and-may-not-be-symmetric">2.1 equals method overrides equals in superclass and may not be symmetric</h6>
<p>JAVA 中当子类使用 lombok 插件的 @Data 注解时,必须配上 @EqualsAndHashCode (callSuper = true)<br>
<br><br>
<font color="#D2B48C">说明:</font>@Data 默认使用 @EqualsAndHashCode (callSuper = false),当在子类中使用此属性时,调用 equals 方法只会比较子类的属性,不会比较继承自父类的属性,极易产生不符合预期的结果(网上提示,此注解同时会产生父子类互相调用 equals 方法结果不相同的问题,即(a.equals (b) == b.equals (a)),但是实测不会产生本问题,因 lombok 同时生成了 canEqual 方法)</p>
<p><font color="#006666">正例:</font></p>
<figure class="highlight java"><table><tbody><tr><td class="code"><pre><span class="line"><span class="meta">@EqualsAndHashCode(callSuper = true)</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">McOrderVO</span> <span class="keyword">extends</span> <span class="title class_">McOrder</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span></span><br></pre></td></tr></tbody></table></figure>
<h6 id="2-2-Possible-null-pointer-dereference-in-method-on-exception-path">2.2 Possible null pointer dereference in method on exception path</h6>