-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
805 lines (743 loc) · 97.1 KB
/
Copy pathsearch.xml
File metadata and controls
805 lines (743 loc) · 97.1 KB
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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>消息通知系统</title>
<url>/2026/03/07/notification-system/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>最近在给自己的博客系统补一些互动功能,比如点赞、收藏、关注之类的。<br>做着做着就发现一个问题:用户其实不知道别人对他做了什么。</p>
<p>比如:</p>
<ul>
<li>有人点赞了你的文章</li>
<li>有人收藏了你的文章</li>
<li>有人关注了你</li>
<li>或者有人给你发了私信</li>
</ul>
<p>如果没有通知系统,这些行为基本就是“悄悄发生”,用户完全感知不到。</p>
<p>所以就顺手把消息通知模块也做了一遍。这篇文章主要记录一下整个实现过程,以及中间踩的一些坑。</p>
<h2 id="一、先想清楚通知是什么"><a href="#一、先想清楚通知是什么" class="headerlink" title="一、先想清楚通知是什么"></a>一、先想清楚通知是什么</h2><p>一开始我其实想过几种设计,比如:</p>
<ul>
<li>点赞通知一张表</li>
<li>评论通知一张表</li>
<li>关注通知一张表</li>
</ul>
<p>但很快发现这样会非常麻烦。</p>
<p>更简单的方式其实是把通知抽象成一条行为记录:</p>
<p>某个用户对另一个用户做了一件事情。</p>
<p>于是通知结构其实只需要几个关键字段:</p>
<ul>
<li>通知接收者</li>
<li>操作发起者</li>
<li>操作类型</li>
<li>是否已读</li>
<li>创建时间</li>
<li>(如果涉及文章)文章 ID</li>
</ul>
<p>于是最后设计了一张 <code>notifications</code> 表。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> notifications (</span><br><span class="line"> id <span class="type">BIGINT</span> <span class="keyword">PRIMARY KEY</span> AUTO_INCREMENT,</span><br><span class="line"> user_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line"> actor_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line"> type <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line"> article_id <span class="type">BIGINT</span>,</span><br><span class="line"> message_content <span class="type">VARCHAR</span>(<span class="number">1000</span>),</span><br><span class="line"> is_read <span class="type">BOOLEAN</span> <span class="keyword">DEFAULT</span> <span class="literal">FALSE</span>,</span><br><span class="line"> created_at <span class="type">TIMESTAMP</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>这里有两个字段比较关键。</p>
<p><code>user_id</code><br>表示通知接收者。</p>
<p>比如:</p>
<p>“A 给 B 点赞”</p>
<p>这里 <code>user_id = B</code>。</p>
<p><code>actor_id</code><br>表示触发行为的人。</p>
<p>同样这个例子里:</p>
<p><code>actor_id = A</code>。</p>
<p><code>type</code><br>通知类型,用来区分不同通知。</p>
<p>目前我的系统里大概有几种:</p>
<ul>
<li><code>like</code></li>
<li><code>favorite</code></li>
<li><code>comment</code></li>
<li><code>follow</code></li>
<li><code>private_message</code></li>
</ul>
<p>这种设计的好处是以后扩展会很方便,比如以后想加:</p>
<ul>
<li>系统通知</li>
<li>管理员警告</li>
<li>评论回复提醒</li>
</ul>
<p>都可以直接扩展 <code>type</code>。</p>
<h2 id="二、通知是在哪生成的"><a href="#二、通知是在哪生成的" class="headerlink" title="二、通知是在哪生成的"></a>二、通知是在哪生成的</h2><p>通知本身其实只是一个附属功能,它一定是伴随着其他业务产生的。</p>
<p>比如:</p>
<ul>
<li>点赞成功</li>
<li>评论成功</li>
<li>关注成功</li>
</ul>
<p>所以我的做法是把通知逻辑统一封装在一个 <code>NotificationService</code> 里。</p>
<p>各个业务模块只需要在操作成功之后调用它。</p>
<p>比如点赞文章的时候:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">notificationService.createNotification(</span><br><span class="line"> article.getAuthor().getId(),</span><br><span class="line"> userId,</span><br><span class="line"> <span class="string">"like"</span>,</span><br><span class="line"> articleId</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>收藏、关注基本也是同样的逻辑。</p>
<p>这样做的好处是:</p>
<ul>
<li>通知逻辑和业务逻辑是解耦的</li>
<li>以后如果要改通知系统,只需要改一个地方</li>
</ul>
<h2 id="三、未读通知计数"><a href="#三、未读通知计数" class="headerlink" title="三、未读通知计数"></a>三、未读通知计数</h2><p>通知列表其实很好做,分页查数据库就行。</p>
<p>真正麻烦的是未读通知数。</p>
<p>因为很多地方都会显示它,比如:</p>
<ul>
<li>页面右上角的小红点</li>
<li>消息中心</li>
<li>移动端提示</li>
</ul>
<p>如果每次都查数据库:</p>
<p><code>countByUserIdAndIsReadFalse(userId)</code></p>
<p>在用户量大的情况下其实挺浪费资源的。</p>
<p>所以我给未读计数加了一层 Redis 缓存。</p>
<p>逻辑很简单:</p>
<ul>
<li>先查 Redis</li>
<li>没命中再查数据库</li>
<li>查到结果写回 Redis</li>
</ul>
<p>代码大概是这样:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">long</span> <span class="title function_">getUnreadCount</span><span class="params">(Long userId)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">"notification:unread:count:"</span> + userId;</span><br><span class="line"></span><br><span class="line"> <span class="type">Object</span> <span class="variable">cached</span> <span class="operator">=</span> redisUtil.get(key);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (cached != <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> Long.parseLong(cached.toString());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">long</span> <span class="variable">count</span> <span class="operator">=</span> notificationRepository</span><br><span class="line"> .countByUserIdAndIsReadFalse(userId);</span><br><span class="line"></span><br><span class="line"> redisUtil.set(key, count, <span class="number">1800</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> count;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这样大部分请求都会直接命中缓存。</p>
<h2 id="四、缓存更新策略"><a href="#四、缓存更新策略" class="headerlink" title="四、缓存更新策略"></a>四、缓存更新策略</h2><p>缓存加上去之后,还要解决一个问题:什么时候更新 Redis?</p>
<p>我的策略是:</p>
<p>新通知产生的时候:</p>
<p>Redis <code>count +1</code>,但前提是缓存已经存在。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (redisUtil.hasKey(countKey)) {</span><br><span class="line"> redisUtil.incr(countKey, <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>如果缓存不存在就不管,因为下次查询会重新建立缓存。</p>
<p>而当通知被标记为已读时,我一开始的想法是:</p>
<p><code>count -1</code></p>
<p>但后来发现这个方案在并发情况下其实容易出问题。</p>
<p>比如:</p>
<ul>
<li>用户同时标记多条通知</li>
<li>或者新通知和标记已读同时发生</li>
</ul>
<p>最后可能导致计数不准确。</p>
<p>所以最后我选择了一个更简单的方法:</p>
<p>直接删除缓存。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">redisUtil.delete(countKey);</span><br></pre></td></tr></table></figure>
<p>这样下次查询时会重新统计数据库。</p>
<p>虽然多查了一次数据库,但逻辑会简单很多,也更安全。</p>
<h2 id="五、数据库索引"><a href="#五、数据库索引" class="headerlink" title="五、数据库索引"></a>五、数据库索引</h2><p>通知列表是一个比较典型的查询:</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">WHERE</span> user_id <span class="operator">=</span> ?</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> created_at <span class="keyword">DESC</span></span><br></pre></td></tr></table></figure>
<p>另外一个高频查询是:</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">WHERE</span> user_id <span class="operator">=</span> ?</span><br><span class="line"><span class="keyword">AND</span> is_read <span class="operator">=</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure>
<p>所以我给表加了一个联合索引:</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">INDEX idx_user_read (user_id, is_read, created_at <span class="keyword">DESC</span>)</span><br></pre></td></tr></table></figure>
<p>这个索引基本可以覆盖:</p>
<ul>
<li>通知列表查询</li>
<li>未读通知查询</li>
<li>未读数量统计</li>
</ul>
<p>效果还是挺明显的。</p>
<h2 id="六、一个小坑"><a href="#六、一个小坑" class="headerlink" title="六、一个小坑"></a>六、一个小坑</h2><p>功能写完之后测试的时候,我发现一个很奇怪的现象:</p>
<p>我给自己的文章点赞,系统也给我发了一条通知。</p>
<p>技术上没问题,但体验非常奇怪。</p>
<p>后来就在创建通知的时候加了一句判断:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (userId.equals(actorId)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这样就不会出现自己通知自己的情况了。</p>
<h2 id="七、如果以后要升级"><a href="#七、如果以后要升级" class="headerlink" title="七、如果以后要升级"></a>七、如果以后要升级</h2><p>现在这个通知系统其实是一个比较基础的实现,如果继续完善的话还有不少可以做的。</p>
<p>比如:</p>
<h3 id="1-实时通知"><a href="#1-实时通知" class="headerlink" title="1. 实时通知"></a>1. 实时通知</h3><p>现在前端是每隔一段时间轮询接口。</p>
<p>如果用 WebSocket,其实可以做到真正的实时推送。</p>
<h3 id="2-消息队列"><a href="#2-消息队列" class="headerlink" title="2. 消息队列"></a>2. 消息队列</h3><p>现在通知是同步创建的。</p>
<p>如果以后并发比较高,可以把通知放到 MQ 里异步处理。</p>
<p>流程会变成:</p>
<p>用户操作<br>↓<br>发送通知事件<br>↓<br>消息队列<br>↓<br>通知服务消费<br>↓<br>写入数据库</p>
<p>这样可以减少主业务的延迟。</p>
]]></content>
<categories>
<category>项目总结</category>
</categories>
<tags>
<tag>Spring Boot</tag>
<tag>Redis</tag>
<tag>数据库设计</tag>
<tag>系统设计</tag>
<tag>通知系统</tag>
</tags>
</entry>
<entry>
<title>设计模式面试复习笔记</title>
<url>/2026/03/16/design-pattern-interview-notes/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>在准备后端面试时,设计模式几乎是绕不开的话题。<br>很多框架(例如 Spring)内部大量使用了设计模式,因此理解这些模式不仅有助于写业务代码,也能更好地理解框架源码。</p>
<p>设计模式本质上是:</p>
<blockquote>
<p>针对常见软件设计问题的通用解决方案。</p>
</blockquote>
<p>经典设计模式共有 23 种,通常分为三大类:</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>作用</th>
</tr>
</thead>
<tbody><tr>
<td>创建型模式</td>
<td>解决对象创建问题</td>
</tr>
<tr>
<td>结构型模式</td>
<td>解决类或对象组合问题</td>
</tr>
<tr>
<td>行为型模式</td>
<td>解决对象之间交互问题</td>
</tr>
</tbody></table>
<p>面试中最常问的其实只有几种,下面整理几个高频模式。</p>
<h2 id="一、单例模式(Singleton)"><a href="#一、单例模式(Singleton)" class="headerlink" title="一、单例模式(Singleton)"></a>一、单例模式(Singleton)</h2><h3 id="1-解决的问题"><a href="#1-解决的问题" class="headerlink" title="1. 解决的问题"></a>1. 解决的问题</h3><p>确保某个类在系统中只有一个实例,并提供全局访问方式。</p>
<p>典型场景:</p>
<ul>
<li>配置管理</li>
<li>日志对象</li>
<li>数据库连接池</li>
<li>Spring Bean 默认单例</li>
</ul>
<h3 id="2-基本实现(线程不安全)"><a href="#2-基本实现(线程不安全)" class="headerlink" title="2. 基本实现(线程不安全)"></a>2. 基本实现(线程不安全)</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> Singleton instance;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Singleton</span><span class="params">()</span> {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getInstance</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="literal">null</span>) {</span><br><span class="line"> instance = <span class="keyword">new</span> <span class="title class_">Singleton</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> instance;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这个版本在多线程环境下不安全。</p>
<h3 id="3-面试常问:线程安全实现(DCL)"><a href="#3-面试常问:线程安全实现(DCL)" class="headerlink" title="3. 面试常问:线程安全实现(DCL)"></a>3. 面试常问:线程安全实现(DCL)</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">volatile</span> Singleton instance;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Singleton</span><span class="params">()</span> {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getInstance</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">synchronized</span> (Singleton.class) {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="literal">null</span>) {</span><br><span class="line"> instance = <span class="keyword">new</span> <span class="title class_">Singleton</span>();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> instance;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>关键点:</p>
<ul>
<li><code>volatile</code> 防止指令重排</li>
<li>双重检查减少锁开销</li>
</ul>
<h3 id="4-更简单写法(推荐)"><a href="#4-更简单写法(推荐)" class="headerlink" title="4. 更简单写法(推荐)"></a>4. 更简单写法(推荐)</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</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">Singleton</span> <span class="variable">INSTANCE</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Singleton</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Singleton</span><span class="params">()</span> {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getInstance</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> INSTANCE;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>利用类加载机制保证线程安全,写法也更简洁。</p>
<h2 id="二、工厂模式(Factory)"><a href="#二、工厂模式(Factory)" class="headerlink" title="二、工厂模式(Factory)"></a>二、工厂模式(Factory)</h2><h3 id="1-解决的问题-1"><a href="#1-解决的问题-1" class="headerlink" title="1. 解决的问题"></a>1. 解决的问题</h3><p>当对象创建逻辑复杂时,将对象创建过程封装起来。</p>
<p>核心思想:</p>
<blockquote>
<p>让对象的创建和使用分离。</p>
</blockquote>
<h3 id="2-简单工厂示例"><a href="#2-简单工厂示例" class="headerlink" title="2. 简单工厂示例"></a>2. 简单工厂示例</h3><p>假设系统支持多种消息发送方式:邮件、短信、推送。</p>
<p>统一接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">MessageSender</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">send</span><span class="params">(String message)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>实现类:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmailSender</span> <span class="keyword">implements</span> <span class="title class_">MessageSender</span> {</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_">send</span><span class="params">(String message)</span> {</span><br><span class="line"> System.out.println(<span class="string">"发送邮件:"</span> + message);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SmsSender</span> <span class="keyword">implements</span> <span class="title class_">MessageSender</span> {</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_">send</span><span class="params">(String message)</span> {</span><br><span class="line"> System.out.println(<span class="string">"发送短信:"</span> + message);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>工厂类:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MessageFactory</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> MessageSender <span class="title function_">create</span><span class="params">(String type)</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"email"</span>.equals(type)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">EmailSender</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"sms"</span>.equals(type)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SmsSender</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">"unsupported type: "</span> + type);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>使用方式:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">MessageSender</span> <span class="variable">sender</span> <span class="operator">=</span> MessageFactory.create(<span class="string">"email"</span>);</span><br><span class="line">sender.send(<span class="string">"hello"</span>);</span><br></pre></td></tr></table></figure>
<h3 id="3-面试常问"><a href="#3-面试常问" class="headerlink" title="3. 面试常问"></a>3. 面试常问</h3><p>简单工厂 vs 工厂方法:</p>
<table>
<thead>
<tr>
<th>模式</th>
<th>特点</th>
</tr>
</thead>
<tbody><tr>
<td>简单工厂</td>
<td>一个工厂类创建所有对象</td>
</tr>
<tr>
<td>工厂方法</td>
<td>每种产品对应一个工厂</td>
</tr>
</tbody></table>
<p>工厂方法接口示例:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Factory</span> {</span><br><span class="line"> MessageSender <span class="title function_">create</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="三、策略模式(Strategy)"><a href="#三、策略模式(Strategy)" class="headerlink" title="三、策略模式(Strategy)"></a>三、策略模式(Strategy)</h2><h3 id="1-解决的问题-2"><a href="#1-解决的问题-2" class="headerlink" title="1. 解决的问题"></a>1. 解决的问题</h3><p>当一个功能有多种实现方式时,可以在运行时动态选择算法。</p>
<p>核心思想:</p>
<blockquote>
<p>将不同算法封装为独立策略。</p>
</blockquote>
<h3 id="2-示例:支付系统"><a href="#2-示例:支付系统" class="headerlink" title="2. 示例:支付系统"></a>2. 示例:支付系统</h3><p>支付策略接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">PayStrategy</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">pay</span><span class="params">(<span class="type">int</span> amount)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>支付宝策略:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AlipayStrategy</span> <span class="keyword">implements</span> <span class="title class_">PayStrategy</span> {</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_">pay</span><span class="params">(<span class="type">int</span> amount)</span> {</span><br><span class="line"> System.out.println(<span class="string">"支付宝支付:"</span> + amount);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>微信策略:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WechatPayStrategy</span> <span class="keyword">implements</span> <span class="title class_">PayStrategy</span> {</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_">pay</span><span class="params">(<span class="type">int</span> amount)</span> {</span><br><span class="line"> System.out.println(<span class="string">"微信支付:"</span> + amount);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>使用方式:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">PayStrategy</span> <span class="variable">strategy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AlipayStrategy</span>();</span><br><span class="line">strategy.pay(<span class="number">100</span>);</span><br></pre></td></tr></table></figure>
<h3 id="3-面试延伸"><a href="#3-面试延伸" class="headerlink" title="3. 面试延伸"></a>3. 面试延伸</h3><p>Spring 中大量使用策略模式,例如:</p>
<ul>
<li><code>HandlerMapping</code></li>
<li><code>HandlerAdapter</code></li>
<li><code>ViewResolver</code></li>
</ul>
<p>通过接口定义统一行为,不同实现类提供不同策略。</p>
<h2 id="四、观察者模式(Observer)"><a href="#四、观察者模式(Observer)" class="headerlink" title="四、观察者模式(Observer)"></a>四、观察者模式(Observer)</h2><h3 id="1-解决的问题-3"><a href="#1-解决的问题-3" class="headerlink" title="1. 解决的问题"></a>1. 解决的问题</h3><p>当一个对象状态变化时,自动通知其他对象。</p>
<p>核心思想:</p>
<blockquote>
<p>发布-订阅机制。</p>
</blockquote>
<h3 id="2-示例"><a href="#2-示例" class="headerlink" title="2. 示例"></a>2. 示例</h3><p>观察者接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Observer</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">(String message)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>具体观察者:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserObserver</span> <span class="keyword">implements</span> <span class="title class_">Observer</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">UserObserver</span><span class="params">(String name)</span> {</span><br><span class="line"> <span class="built_in">this</span>.name = name;</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">public</span> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">(String message)</span> {</span><br><span class="line"> System.out.println(name + <span class="string">" 收到通知:"</span> + message);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>被观察对象:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.ArrayList;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">NotificationService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<Observer> observers = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addObserver</span><span class="params">(Observer observer)</span> {</span><br><span class="line"> observers.add(observer);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notifyObservers</span><span class="params">(String message)</span> {</span><br><span class="line"> <span class="keyword">for</span> (Observer observer : observers) {</span><br><span class="line"> observer.update(message);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="3-实际应用"><a href="#3-实际应用" class="headerlink" title="3. 实际应用"></a>3. 实际应用</h3><p>典型场景:</p>
<ul>
<li>Spring 事件机制</li>
<li>消息订阅</li>
<li>GUI 事件监听</li>
<li>MQ 消费者模型</li>
</ul>
<h2 id="五、代理模式(Proxy)"><a href="#五、代理模式(Proxy)" class="headerlink" title="五、代理模式(Proxy)"></a>五、代理模式(Proxy)</h2><h3 id="1-解决的问题-4"><a href="#1-解决的问题-4" class="headerlink" title="1. 解决的问题"></a>1. 解决的问题</h3><p>为对象提供一个代理对象,控制对原对象的访问。</p>
<p>常见用途:</p>
<ul>
<li>权限控制</li>
<li>日志记录</li>
<li>延迟加载</li>
<li>AOP</li>
</ul>
<h3 id="2-示例-1"><a href="#2-示例-1" class="headerlink" title="2. 示例"></a>2. 示例</h3><p>接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Service</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>真实对象:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RealService</span> <span class="keyword">implements</span> <span class="title class_">Service</span> {</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_">execute</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"执行真实业务"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>代理对象:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServiceProxy</span> <span class="keyword">implements</span> <span class="title class_">Service</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Service realService;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">ServiceProxy</span><span class="params">(Service realService)</span> {</span><br><span class="line"> <span class="built_in">this</span>.realService = realService;</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">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"权限检查"</span>);</span><br><span class="line"> realService.execute();</span><br><span class="line"> System.out.println(<span class="string">"记录日志"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="3-面试重点"><a href="#3-面试重点" class="headerlink" title="3. 面试重点"></a>3. 面试重点</h3><p>Spring AOP 本质就是代理模式:</p>
<ul>
<li>JDK 动态代理</li>
<li>CGLIB 动态代理</li>
</ul>
<h2 id="六、面试中常见问题"><a href="#六、面试中常见问题" class="headerlink" title="六、面试中常见问题"></a>六、面试中常见问题</h2><h3 id="1-Spring-用到了哪些设计模式?"><a href="#1-Spring-用到了哪些设计模式?" class="headerlink" title="1. Spring 用到了哪些设计模式?"></a>1. Spring 用到了哪些设计模式?</h3><p>常见答案:</p>
<ul>
<li>单例模式(Bean 默认单例)</li>
<li>工厂模式(<code>BeanFactory</code>)</li>
<li>策略模式(<code>HandlerMapping</code>)</li>
<li>代理模式(AOP)</li>
<li>模板方法模式(<code>JdbcTemplate</code>)</li>
<li>观察者模式(Spring Event)</li>
</ul>
<h3 id="2-单例模式为什么需要-volatile?"><a href="#2-单例模式为什么需要-volatile?" class="headerlink" title="2. 单例模式为什么需要 volatile?"></a>2. 单例模式为什么需要 <code>volatile</code>?</h3><p>因为 JVM 可能发生指令重排。</p>
<p>对象创建步骤本应是:</p>
<ol>
<li>分配内存</li>
<li>初始化对象</li>
<li>指向对象</li>
</ol>
<p>但可能重排为:</p>
<ol>
<li>分配内存</li>
<li>指向对象</li>
<li>初始化对象</li>
</ol>
<p>这样会导致其他线程拿到“未初始化完成”的对象。</p>
<p><code>volatile</code> 可以禁止这类重排。</p>
<h3 id="3-策略模式和工厂模式区别"><a href="#3-策略模式和工厂模式区别" class="headerlink" title="3. 策略模式和工厂模式区别"></a>3. 策略模式和工厂模式区别</h3><table>
<thead>
<tr>
<th>模式</th>
<th>关注点</th>
</tr>
</thead>
<tbody><tr>
<td>工厂模式</td>
<td>对象创建</td>
</tr>
<tr>
<td>策略模式</td>
<td>算法选择</td>
</tr>
</tbody></table>
<p>很多时候这两个模式会结合使用:先由工厂创建策略对象,再在运行时切换策略。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>面试中复习设计模式,不需要死记 23 种模式的所有细节。<br>更重要的是把握这三点:</p>
<ul>
<li>这个模式解决了什么问题</li>
<li>在项目中哪里能用</li>
<li>在 Spring 或常见框架中有什么对应落地</li>
</ul>
<p>如果你能结合业务场景讲清楚“为什么用、怎么用、有哪些权衡”,通常会比只背定义更有说服力。</p>
]]></content>
<categories>
<category>项目总结</category>
</categories>
<tags>
<tag>Java</tag>
<tag>设计模式</tag>
<tag>Spring</tag>
<tag>面试复习</tag>
</tags>
</entry>
<entry>
<title>为 Hexo 博客添加 Giscus 评论系统</title>
<url>/2026/02/08/add-giscus-comments/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>为博客添加评论功能是增强互动性的重要一步。这篇文章记录我为 Hexo + Butterfly 博客集成 Giscus 评论系统的完整过程。</p>
<h2 id="为什么选择-Giscus?"><a href="#为什么选择-Giscus?" class="headerlink" title="为什么选择 Giscus?"></a>为什么选择 Giscus?</h2><p>在众多评论系统中(Disqus、Gitalk、Valine 等),我选择 Giscus 的原因:</p>
<ul>
<li><strong>无需数据库</strong>:不需要额外的后端服务</li>
<li><strong>支持 Markdown</strong>:评论可以使用完整的 Markdown 语法</li>
<li><strong>主题适配</strong>:自动适配网站的明暗主题</li>
<li><strong>完全免费</strong>:无任何使用限制</li>
<li><strong>隐私友好</strong>:不会追踪用户数据</li>
</ul>
<h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><h3 id="1-创建评论存储仓库"><a href="#1-创建评论存储仓库" class="headerlink" title="1. 创建评论存储仓库"></a>1. 创建评论存储仓库</h3><p>在 GitHub 上创建一个公开仓库用于存储评论数据。</p>
<p><strong>关键设置:</strong></p>
<ul>
<li>仓库必须是 <strong>public</strong>(公开)</li>
<li>启用 <strong>Discussions</strong> 功能:进入仓库 Settings → Features → 勾选 Discussions</li>
</ul>
<h3 id="2-安装-Giscus-App"><a href="#2-安装-Giscus-App" class="headerlink" title="2. 安装 Giscus App"></a>2. 安装 Giscus App</h3><p>访问 <a href="https://github.com/apps/giscus">Giscus App</a> 并安装到你的评论仓库:</p>
<ol>
<li>点击 “Install”</li>
<li>选择 “Only select repositories”</li>
<li>选中刚才创建的仓库</li>
<li>授权安装</li>
</ol>
<h2 id="配置-Giscus"><a href="#配置-Giscus" class="headerlink" title="配置 Giscus"></a>配置 Giscus</h2><h3 id="3-获取配置参数"><a href="#3-获取配置参数" class="headerlink" title="3. 获取配置参数"></a>3. 获取配置参数</h3><p>访问 <a href="https://giscus.app/zh-CN">Giscus 官网</a> 进行配置:</p>
<p><strong>① 仓库信息</strong></p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">仓库:用户名/仓库名</span><br></pre></td></tr></table></figure>
<p><strong>② Discussion 分类</strong></p>
<p>选择 “Announcements” 类型(推荐),这样只有仓库维护者可以创建新讨论,但所有人都能评论。</p>
<p><strong>③ 映射方式</strong></p>
<p>选择 “pathname”(路径名),这样每篇文章会根据 URL 路径自动创建对应的 Discussion。</p>
<p><strong>④ 主题</strong></p>
<p>选择 “preferred_color_scheme”(跟随系统),自动适配明暗模式。</p>
<p><strong>⑤ 获取关键 ID</strong></p>
<p>配置完成后,Giscus 会生成一段代码,从中提取两个关键参数:</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">src</span>=<span class="string">"https://giscus.app/client.js"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">data-repo</span>=<span class="string">"用户名/仓库名"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">data-repo-id</span>=<span class="string">"XXXXXXXXXXX"</span> <!<span class="attr">--</span> <span class="attr">这是</span> <span class="attr">repo_id</span> <span class="attr">--</span>></span><span class="language-handlebars"><span class="language-xml"></span></span></span><br><span class="line"><span class="language-xml"><span class="language-handlebars"> data-category="Announcements"</span></span></span><br><span class="line"><span class="language-xml"><span class="language-handlebars"> data-category-id="XXXXXXXXXXXX" <span class="comment"><!-- 这是 category_id --></span></span></span></span><br><span class="line"><span class="language-xml"><span class="language-handlebars"> ...></span></span></span><br><span class="line"><span class="language-xml"><span class="language-handlebars"></span></span><span class="tag"></<span class="name">script</span>></span></span><br></pre></td></tr></table></figure>
<p>记下 <code>data-repo-id</code> 和 <code>data-category-id</code> 的值。</p>
<h2 id="在-Butterfly-主题中配置"><a href="#在-Butterfly-主题中配置" class="headerlink" title="在 Butterfly 主题中配置"></a>在 Butterfly 主题中配置</h2><h3 id="4-修改主题配置文件"><a href="#4-修改主题配置文件" class="headerlink" title="4. 修改主题配置文件"></a>4. 修改主题配置文件</h3><p>打开 <code>themes/butterfly/_config.yml</code>,找到 <code>comments</code> 部分:</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">comments:</span></span><br><span class="line"> <span class="comment"># 选择评论系统</span></span><br><span class="line"> <span class="attr">use:</span> <span class="string">giscus</span></span><br><span class="line"> <span class="attr">text:</span> <span class="literal">true</span></span><br><span class="line"> <span class="comment"># lazyload: 懒加载评论</span></span><br><span class="line"> <span class="attr">lazyload:</span> <span class="literal">false</span></span><br><span class="line"> <span class="comment"># count: 显示评论数</span></span><br><span class="line"> <span class="attr">count:</span> <span class="literal">true</span></span><br><span class="line"> <span class="comment"># card_post_count: 在卡片上显示评论数</span></span><br><span class="line"> <span class="attr">card_post_count:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure>
<p>然后找到 <code>giscus</code> 配置段,填入参数:</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">giscus:</span></span><br><span class="line"> <span class="attr">repo:</span> <span class="string">用户名/仓库名</span></span><br><span class="line"> <span class="attr">repo_id:</span> <span class="string">XXXXXXXXXXXXXXXXXX</span></span><br><span class="line"> <span class="attr">category_id:</span> <span class="string">XXXXXXXXXXXXXXXXXX</span></span><br><span class="line"> <span class="attr">theme:</span></span><br><span class="line"> <span class="attr">light:</span> <span class="string">light</span></span><br><span class="line"> <span class="attr">dark:</span> <span class="string">dark</span></span><br><span class="line"> <span class="attr">option:</span></span><br><span class="line"> <span class="attr">data-mapping:</span> <span class="string">pathname</span></span><br><span class="line"> <span class="attr">data-strict:</span> <span class="number">0</span></span><br><span class="line"> <span class="attr">data-reactions-enabled:</span> <span class="number">1</span></span><br><span class="line"> <span class="attr">data-emit-metadata:</span> <span class="number">0</span></span><br><span class="line"> <span class="attr">data-input-position:</span> <span class="string">bottom</span></span><br><span class="line"> <span class="attr">data-lang:</span> <span class="string">zh-CN</span></span><br><span class="line"> <span class="attr">data-loading:</span> <span class="string">lazy</span></span><br></pre></td></tr></table></figure>
<p><strong>参数说明:</strong></p>
<ul>
<li><code>repo</code>: 你的评论仓库(格式:用户名/仓库名)</li>
<li><code>repo_id</code>: 从 Giscus 获取的仓库 ID</li>
<li><code>category_id</code>: 从 Giscus 获取的分类 ID</li>
<li><code>data-mapping: pathname</code>: 使用文章路径作为映射</li>
<li><code>data-lang: zh-CN</code>: 界面语言设为中文</li>
<li><code>data-reactions-enabled: 1</code>: 启用表情回应功能</li>
</ul>
<h3 id="5-重新生成并部署"><a href="#5-重新生成并部署" class="headerlink" title="5. 重新生成并部署"></a>5. 重新生成并部署</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">hexo clean</span><br><span class="line">hexo generate</span><br><span class="line">hexo deploy</span><br></pre></td></tr></table></figure>
<h2 id="效果验证"><a href="#效果验证" class="headerlink" title="效果验证"></a>效果验证</h2><p>部署完成后,访问任意文章页面,应该能看到:</p>
<ul>
<li>文章底部出现 Giscus 评论框</li>
<li>需要登录 GitHub 才能评论</li>
<li>评论会自动同步到仓库的 Discussions 中</li>
<li>支持 Markdown 格式、代码高亮、表情回应</li>
</ul>
<h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ol>
<li><strong>仓库必须公开</strong>:私有仓库无法使用 Giscus</li>
<li><strong>确保安装了 Giscus App</strong>:否则评论无法加载</li>
<li><strong>Discussions 必须启用</strong>:在仓库设置中开启</li>
<li><strong>首次评论会自动创建 Discussion</strong>:按文章路径命名</li>
<li><strong>可以在 GitHub 仓库中管理评论</strong>:删除、编辑、置顶等</li>
</ol>
]]></content>
<categories>
<category>博客搭建</category>
</categories>
<tags>
<tag>Hexo</tag>
<tag>Giscus</tag>
<tag>评论系统</tag>
</tags>
</entry>
<entry>
<title>评论系统设计与实现</title>
<url>/2026/02/11/comment-system/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>本文记录我在博客项目中实现评论功能时,涉及的一些细节。</p>
<hr>
<h2 id="一、后端实现"><a href="#一、后端实现" class="headerlink" title="一、后端实现"></a>一、后端实现</h2><h3 id="1-评论实体设计(Comment)"><a href="#1-评论实体设计(Comment)" class="headerlink" title="1. 评论实体设计(Comment)"></a>1. 评论实体设计(Comment)</h3><p>评论表的核心在于<strong>自引用关系</strong>:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Comment</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="keyword">private</span> Long id;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">private</span> String content; <span class="comment">// VARCHAR(1000)</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@ManyToOne(fetch = FetchType.EAGER)</span></span><br><span class="line"> <span class="keyword">private</span> Article article; <span class="comment">// 所属文章</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@ManyToOne(fetch = FetchType.EAGER)</span></span><br><span class="line"> <span class="keyword">private</span> User author; <span class="comment">// 评论作者</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@ManyToOne(fetch = FetchType.LAZY)</span></span><br><span class="line"> <span class="keyword">private</span> Comment parentComment; <span class="comment">// 父评论(自引用关系)</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@CreationTimestamp</span></span><br><span class="line"> <span class="keyword">private</span> LocalDateTime createdAt;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="设计要点"><a href="#设计要点" class="headerlink" title="设计要点"></a>设计要点</h4><ul>
<li><code>parentComment</code> 指向自身,实现评论嵌套</li>
<li>父评论使用 <code>LAZY</code>,避免循环加载</li>
<li>作者、文章使用 <code>EAGER</code>,避免访问时频繁触发延迟加载</li>
<li>不在实体中直接维护 children,避免 ORM 复杂度失控</li>
</ul>
<blockquote>
<p>高并发场景应使用join fetch 或DTO投影来精确控制查询行为。</p>
</blockquote>
<hr>
<h3 id="2-数据访问层(Repository)"><a href="#2-数据访问层(Repository)" class="headerlink" title="2. 数据访问层(Repository)"></a>2. 数据访问层(Repository)</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">CommentRepository</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><Comment, Long> {</span><br><span class="line"> <span class="comment">// 按文章ID查询,升序排列(老评论在前)</span></span><br><span class="line"> Page<Comment> <span class="title function_">findByArticleIdOrderByCreatedAtAsc</span><span class="params">(Long articleId, Pageable pageable)</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 按作者ID查询(用于个人主页)</span></span><br><span class="line"> Page<Comment> <span class="title function_">findByAuthorId</span><span class="params">(Long authorId, Pageable pageable)</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 查找子评论(用于级联删除)</span></span><br><span class="line"> List<Comment> <span class="title function_">findByParentCommentId</span><span class="params">(Long parentId)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p> 👉 <strong>递归删除时依赖 <code>findByParentCommentId</code> 查找子节点</strong></p>
<blockquote>
<p>评论属于高频读写表,后续应在数据库层添加必要索引。</p>
</blockquote>
<hr>
<h3 id="3-评论创建逻辑(Service)"><a href="#3-评论创建逻辑(Service)" class="headerlink" title="3. 评论创建逻辑(Service)"></a>3. 评论创建逻辑(Service)</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> CommentDTO <span class="title function_">createComment</span><span class="params">(Long articleId, Long userId, String content, Long parentId)</span> {</span><br><span class="line"> <span class="comment">// 验证文章和用户存在</span></span><br><span class="line"> <span class="type">Article</span> <span class="variable">article</span> <span class="operator">=</span> articleRepository.findById(articleId)...</span><br><span class="line"> <span class="type">User</span> <span class="variable">author</span> <span class="operator">=</span> userRepository.findById(userId)...</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 验证内容非空</span></span><br><span class="line"> <span class="keyword">if</span> (content == <span class="literal">null</span> || content.trim().isEmpty()) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"评论内容不能为空"</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 如果是回复,验证父评论</span></span><br><span class="line"> <span class="keyword">if</span> (parentId != <span class="literal">null</span>) {</span><br><span class="line"> <span class="type">Comment</span> <span class="variable">parent</span> <span class="operator">=</span> commentRepository.findById(parentId)...</span><br><span class="line"> <span class="comment">// 确保父评论属于同一篇文章</span></span><br><span class="line"> <span class="keyword">if</span> (!parent.getArticle().getId().equals(articleId)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"父评论与文章不匹配"</span>);</span><br><span class="line"> }</span><br><span class="line"> comment.setParentComment(parent);<span class="comment">// 设置parentcomment</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 保存并转换为DTO</span></span><br><span class="line"> comment = commentRepository.save(comment);</span><br><span class="line"> <span class="keyword">return</span> convertToDTO(comment, userId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="核心约束点"><a href="#核心约束点" class="headerlink" title="核心约束点"></a>核心约束点</h4><ul>
<li>回复的父评论必须属于同一篇文章</li>
<li>所有评论统一通过 DTO 返回,避免实体泄露</li>
<li>创建逻辑不做层级限制,展示逻辑交给前端</li>
</ul>
<hr>
<h3 id="4-递归级联删除"><a href="#4-递归级联删除" class="headerlink" title="4. 递归级联删除"></a>4. 递归级联删除</h3><p>删除一条评论时,必须同时删除:</p>
<ol>
<li>所有子评论(递归)</li>
<li>所有关联的点赞记录</li>
<li>保证外键约束不报错</li>
<li>保证操作原子性</li>
</ol>
<h4 id="实现方案"><a href="#实现方案" class="headerlink" title="实现方案"></a>实现方案</h4><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deleteComment</span><span class="params">(Long commentId, Long userId)</span> {</span><br><span class="line"> <span class="type">Comment</span> <span class="variable">comment</span> <span class="operator">=</span> commentRepository.findById(commentId)...</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 权限检查:只能删除自己的评论</span></span><br><span class="line"> <span class="keyword">if</span> (!comment.getAuthor().getId().equals(userId)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"只能删除自己的评论"</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> deleteCommentWithChildren(comment);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 递归删除子评论</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">deleteCommentWithChildren</span><span class="params">(Comment comment)</span> {</span><br><span class="line"> <span class="comment">// 递归删除所有子评论</span></span><br><span class="line"> List<Comment> children = commentRepository.findByParentCommentId(comment.getId());</span><br><span class="line"> <span class="keyword">for</span> (Comment child : children) {</span><br><span class="line"> deleteCommentWithChildren(child); <span class="comment">// 递归调用</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 先删除该评论的所有点赞(解决FK约束)</span></span><br><span class="line"> commentLikeRepository.deleteByCommentId(comment.getId());</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 最后删除评论本身</span></span><br><span class="line"> commentRepository.deleteById(comment.getId());</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="设计要点-1"><a href="#设计要点-1" class="headerlink" title="设计要点"></a>设计要点</h4><ul>
<li>深度优先遍历(DFS)</li>
<li>时间复杂度 O(n)</li>
<li>使用事务保证一致性</li>
<li>显式控制删除顺序,避免外键(FK)约束问题</li>
</ul>
<blockquote>
<p>为避免复杂递归以及保留上下文,可考虑采用逻辑删除<code>private boolean deleted;</code>。</p>
</blockquote>
<hr>
<h2 id="二、前端实现"><a href="#二、前端实现" class="headerlink" title="二、前端实现"></a>二、前端实现</h2><h3 id="1-数据结构设计"><a href="#1-数据结构设计" class="headerlink" title="1. 数据结构设计"></a>1. 数据结构设计</h3><p>前端接收的是<strong>平铺数组</strong>,自行构建树结构:</p>
<figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">data</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">comments</span>: [], <span class="comment">// 平铺的评论列表(从API获取)</span></span><br><span class="line"> <span class="attr">commentTree</span>: [], <span class="comment">// 树型结构(用于渲染)</span></span><br><span class="line"> <span class="attr">newCommentContent</span>: <span class="string">''</span>, <span class="comment">// 新评论内容</span></span><br><span class="line"> <span class="attr">replyingTo</span>: <span class="literal">null</span>, <span class="comment">// 当前回复的评论对象</span></span><br><span class="line"> <span class="attr">replyContent</span>: <span class="string">''</span> <span class="comment">// 回复内容</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<blockquote>
<p>选择前端构建树结构而不是后端直接递归组装,是为了保持接口简单、避免后端递归查询带来的复杂度,同时让前端掌控展示策略。</p>
</blockquote>
<hr>
<h3 id="2-评论树构建与展平"><a href="#2-评论树构建与展平" class="headerlink" title="2. 评论树构建与展平"></a>2. 评论树构建与展平</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">buildCommentTree</span>(<span class="params">comments</span>) {</span><br><span class="line"> <span class="keyword">const</span> map = <span class="keyword">new</span> <span class="title class_">Map</span>();</span><br><span class="line"> <span class="keyword">const</span> roots = [];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 1. 创建映射表,每个评论作为一个节点</span></span><br><span class="line"> comments.<span class="title function_">forEach</span>(<span class="function"><span class="params">comment</span> =></span> {</span><br><span class="line"> map.<span class="title function_">set</span>(comment.<span class="property">id</span>, { ...comment, <span class="attr">children</span>: [] });</span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 2. 构建父子关系</span></span><br><span class="line"> comments.<span class="title function_">forEach</span>(<span class="function"><span class="params">comment</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> node = map.<span class="title function_">get</span>(comment.<span class="property">id</span>);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 顶级评论(没有父评论)</span></span><br><span class="line"> <span class="keyword">if</span> (!comment.<span class="property">parentId</span>) {</span><br><span class="line"> roots.<span class="title function_">push</span>(node);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 找到最顶层的父评论(展平多层嵌套)</span></span><br><span class="line"> <span class="keyword">let</span> parent = map.<span class="title function_">get</span>(comment.<span class="property">parentId</span>);</span><br><span class="line"> <span class="keyword">while</span> (parent && parent.<span class="property">parentId</span>) {</span><br><span class="line"> parent = map.<span class="title function_">get</span>(parent.<span class="property">parentId</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 将当前评论添加到顶层父评论的children中</span></span><br><span class="line"> <span class="keyword">if</span> (parent) {</span><br><span class="line"> parent.<span class="property">children</span>.<span class="title function_">push</span>(node);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> roots.<span class="title function_">push</span>(node); <span class="comment">// 孤儿节点作为顶级评论</span></span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> roots;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="设计要点-2"><a href="#设计要点-2" class="headerlink" title="设计要点"></a>设计要点</h4><ul>
<li>支持任意层级回复</li>
<li>UI 只展示两层</li>
<li>不需要后端额外接口</li>
<li>构建复杂度 O(n)</li>
</ul>
<blockquote>
<p>前端只展示两层结构,避免前端无限递归渲染</p>
</blockquote>
]]></content>
<categories>
<category>项目总结</category>
</categories>
<tags>
<tag>Spring Boot</tag>
<tag>数据库设计</tag>
<tag>评论系统</tag>
</tags>
</entry>
<entry>
<title>使用 Hexo + Butterfly 搭建我的个人博客</title>
<url>/2026/02/05/hexo-butterfly-blog-setup/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>很早之前就想搭一个属于自己的博客,但一直停留在“想做”的阶段。 </p>
<p>这篇文章主要记录我使用 <strong>Hexo + Butterfly</strong> 搭建博客的过程,一方面作为备忘,另一方面也算是这个博客的起点。</p>
<hr>
<h2 id="关于技术选型"><a href="#关于技术选型" class="headerlink" title="关于技术选型"></a>关于技术选型</h2><p>一开始我也考虑过很多方案,比如 WordPress,或者干脆用前端框架自己写一套。<br>但综合考虑之后,还是选择了 Hexo。</p>
<p>原因其实很简单:<br>Hexo 是静态博客,结构清晰,写文章就是写 Markdown,不需要关心数据库和后端服务。对目前的我来说,这种“专注写作本身”的方式更合适。</p>
<p>在主题方面,我选择了 Butterfly。这个主题整体风格简洁,配置项比较全,文档也比较完善,适合长期维护。</p>
<hr>
<h2 id="Hexo-的安装与初始化"><a href="#Hexo-的安装与初始化" class="headerlink" title="Hexo 的安装与初始化"></a>Hexo 的安装与初始化</h2><p>本地环境准备好 Node.js 和 Git 之后,就可以直接安装 Hexo CLI。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g hexo-cli</span><br></pre></td></tr></table></figure>
<p>接着初始化博客目录:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">hexo init blog</span><br><span class="line"><span class="built_in">cd</span> blog</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure>
<p>随后在 themes 目录下克隆主题仓库:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/jerryc127/hexo-theme-butterfly.git</span><br></pre></td></tr></table></figure>
<p>然后在站点配置文件 _config.yml 中,把主题改成:</p>
<figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="attr">theme:</span> <span class="string">butterfly</span></span><br></pre></td></tr></table></figure>
<p>接下来便是完善一些基础配置和页面。</p>
<hr>
<h2 id="添加-Live2D-小人"><a href="#添加-Live2D-小人" class="headerlink" title="添加 Live2D 小人"></a>添加 Live2D 小人</h2><p>为了让博客更有趣一些,我在右下角添加了一个 Live2D 小人。Butterfly 主题支持通过代码注入的方式快速集成。</p>
<h3 id="配置方法"><a href="#配置方法" class="headerlink" title="配置方法"></a>配置方法</h3><p>打开 <code>themes/butterfly/_config.yml</code>,找到 <code>inject</code> 配置段,在 <code>bottom</code> 部分添加以下代码:</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line"> <span class="attr">head:</span></span><br><span class="line"> <span class="comment"># ... 其他配置</span></span><br><span class="line"> <span class="attr">bottom:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">'<script src="https://cdn.jsdelivr.net/npm/live2d-widget@3.1.4/lib/L2Dwidget.min.js"></script>'</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">'<script>L2Dwidget.init({ model: { jsonPath: "https://cdn.jsdelivr.net/npm/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json" }, display: { position: "right", width: 150, height: 300, hOffset: 30, vOffset: -10 }, mobile: { show: true, scale: 0.6 }, react: { opacityDefault: 0.8, opacityOnHover: 0.2 } });</script>'</span></span><br></pre></td></tr></table></figure>
<h3 id="参数说明"><a href="#参数说明" class="headerlink" title="参数说明"></a>参数说明</h3><ul>
<li><strong>model.jsonPath</strong>:角色模型的 CDN 地址,这里使用的是 Shizuku(时雨)模型</li>
<li><strong>display.position</strong>:显示位置(left/right),这里设为右下角</li>
<li><strong>display.width/height</strong>:模型宽高(像素)</li>
<li><strong>display.hOffset/vOffset</strong>:水平和垂直偏移量</li>
<li><strong>mobile.show</strong>:是否在移动端显示</li>
<li><strong>mobile.scale</strong>:移动端缩放比例</li>
<li><strong>react.opacityDefault</strong>:默认透明度</li>
<li><strong>react.opacityOnHover</strong>:鼠标悬停时的透明度(0.2 表示鼠标移上去会变淡)</li>
</ul>
<h3 id="可选模型"><a href="#可选模型" class="headerlink" title="可选模型"></a>可选模型</h3><p>除了 Shizuku,还可以选择其他模型:</p>
<figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Koharu(小春)</span></span><br><span class="line"><span class="string">"https://cdn.jsdelivr.net/npm/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Hibiki(响)</span></span><br><span class="line"><span class="string">"https://cdn.jsdelivr.net/npm/live2d-widget-model-hibiki@1.0.5/assets/hibiki.model.json"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Miku(初音未来)</span></span><br><span class="line"><span class="string">"https://cdn.jsdelivr.net/npm/live2d-widget-model-miku@1.0.5/assets/miku.model.json"</span></span><br></pre></td></tr></table></figure>
<p>只需要替换 <code>jsonPath</code> 的值即可切换角色。</p>
]]></content>
<categories>
<category>博客搭建</category>
</categories>
<tags>
<tag>Hexo</tag>
<tag>Butterfly</tag>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title>一次理解访问者模式</title>
<url>/2026/03/15/visitor-pattern-when-operations-change/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>在学习设计模式时,访问者模式经常被认为是比较难理解的一种模式。</p>
<p>原因也很简单:它不像单例、工厂那样直观,而且在日常项目里也不是最高频出现的模式。</p>
<p>不过如果你接触过编译器、AST、复杂数据结构遍历,你很可能已经见过它的影子。</p>
<p>简单来说,访问者模式解决的问题是:</p>
<blockquote>
<p>当对象结构稳定,但对对象的操作经常变化时,如何避免频繁修改类结构。</p>
</blockquote>
<h2 id="一、先看一个直观例子"><a href="#一、先看一个直观例子" class="headerlink" title="一、先看一个直观例子"></a>一、先看一个直观例子</h2><p>假设我们有一个系统,表示不同类型的文件:</p>
<ul>
<li>Word 文档</li>
<li>PDF 文件</li>
<li>Excel 表格</li>
</ul>
<p>先定义统一接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">File</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">accept</span><span class="params">(Visitor visitor)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>不同文件类型只负责“接待”访问者:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">WordFile</span> <span class="keyword">implements</span> <span class="title class_">File</span> {</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_">accept</span><span class="params">(Visitor visitor)</span> {</span><br><span class="line"> visitor.visit(<span class="built_in">this</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PdfFile</span> <span class="keyword">implements</span> <span class="title class_">File</span> {</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_">accept</span><span class="params">(Visitor visitor)</span> {</span><br><span class="line"> visitor.visit(<span class="built_in">this</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ExcelFile</span> <span class="keyword">implements</span> <span class="title class_">File</span> {</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_">accept</span><span class="params">(Visitor visitor)</span> {</span><br><span class="line"> visitor.visit(<span class="built_in">this</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这些类本质上是对象结构(数据/类型)的表达。</p>
<h2 id="二、不用访问者会发生什么"><a href="#二、不用访问者会发生什么" class="headerlink" title="二、不用访问者会发生什么"></a>二、不用访问者会发生什么</h2><p>假设系统现在要支持这些操作:</p>
<ul>
<li>打开文件</li>
<li>导出文件</li>
<li>统计文件信息</li>
</ul>
<p>最直接的写法通常是把行为塞进每个类:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">WordFile</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">open</span><span class="params">()</span> {}</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">export</span><span class="params">()</span> {}</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">statistics</span><span class="params">()</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>问题很快出现:如果以后要新增一个功能,比如“打印文件”,就必须改动:</p>
<ul>
<li><code>WordFile</code></li>
<li><code>PdfFile</code></li>
<li><code>ExcelFile</code></li>
</ul>
<p>每个类都得加 <code>print()</code> 方法。</p>
<p>这显然违背了开闭原则:</p>
<blockquote>
<p>对扩展开放,对修改关闭。</p>
</blockquote>
<h2 id="三、引入访问者模式"><a href="#三、引入访问者模式" class="headerlink" title="三、引入访问者模式"></a>三、引入访问者模式</h2><p>访问者模式的核心思想是:</p>
<blockquote>
<p>把“操作”从对象结构中抽离出来。</p>
</blockquote>
<p>对象只负责提供结构,操作交给访问者。</p>
<p>先定义访问者接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">Visitor</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(WordFile file)</span>;</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(PdfFile file)</span>;</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(ExcelFile file)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>实现一个“打开文件”访问者:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">OpenVisitor</span> <span class="keyword">implements</span> <span class="title class_">Visitor</span> {</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_">visit</span><span class="params">(WordFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"打开 Word 文件"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(PdfFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"打开 PDF 文件"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(ExcelFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"打开 Excel 文件"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>再实现一个“导出文件”访问者:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ExportVisitor</span> <span class="keyword">implements</span> <span class="title class_">Visitor</span> {</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_">visit</span><span class="params">(WordFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"导出 Word 文件"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(PdfFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"导出 PDF 文件"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(ExcelFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"导出 Excel 文件"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="四、怎么使用"><a href="#四、怎么使用" class="headerlink" title="四、怎么使用"></a>四、怎么使用</h2><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">File</span> <span class="variable">file</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">WordFile</span>();</span><br><span class="line"><span class="type">Visitor</span> <span class="variable">visitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OpenVisitor</span>();</span><br><span class="line"></span><br><span class="line">file.accept(visitor);</span><br></pre></td></tr></table></figure>
<p>执行流程是:</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">file.accept(visitor)</span><br><span class="line"> ↓</span><br><span class="line">visitor.visit(file)</span><br></pre></td></tr></table></figure>
<p>这个过程本质上就是双重分派(double dispatch)。</p>
<h2 id="五、为什么这样设计"><a href="#五、为什么这样设计" class="headerlink" title="五、为什么这样设计"></a>五、为什么这样设计</h2><p>访问者模式最大的优势是:</p>
<blockquote>
<p>新增操作非常容易。</p>
</blockquote>
<p>比如现在想增加“统计文件大小”,只需要新增一个访问者:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SizeVisitor</span> <span class="keyword">implements</span> <span class="title class_">Visitor</span> {</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_">visit</span><span class="params">(WordFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"统计 Word 文件大小"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(PdfFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"统计 PDF 文件大小"</span>);</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">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(ExcelFile file)</span> {</span><br><span class="line"> System.out.println(<span class="string">"统计 Excel 文件大小"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>原来的文件类完全不用动。</p>
<h2 id="六、典型适用场景"><a href="#六、典型适用场景" class="headerlink" title="六、典型适用场景"></a>六、典型适用场景</h2><p>访问者模式不是每个项目都会用,但在下面场景非常合适:</p>
<h3 id="1-编译器-AST"><a href="#1-编译器-AST" class="headerlink" title="1. 编译器 AST"></a>1. 编译器 AST</h3><p>抽象语法树节点(如 <code>Expression</code>、<code>Statement</code>、<code>IfStatement</code>)结构相对稳定,<br>但操作很多:</p>
<ul>
<li>代码生成</li>
<li>语义分析</li>
<li>优化</li>
</ul>
<h3 id="2-复杂对象结构遍历"><a href="#2-复杂对象结构遍历" class="headerlink" title="2. 复杂对象结构遍历"></a>2. 复杂对象结构遍历</h3><p>例如:</p>
<ul>
<li>文件系统</li>
<li>UI 组件树</li>
<li>文档结构</li>
</ul>
<p>针对同一结构,可以有不同访问者:</p>
<ul>
<li>渲染</li>
<li>统计</li>
<li>导出</li>
</ul>
<h3 id="3-报表-数据分析"><a href="#3-报表-数据分析" class="headerlink" title="3. 报表 / 数据分析"></a>3. 报表 / 数据分析</h3><p>同一组数据不断增加新输出方式:</p>
<ul>
<li>生成 PDF</li>
<li>生成 Excel</li>
<li>生成图表</li>
</ul>
<h2 id="七、缺点"><a href="#七、缺点" class="headerlink" title="七、缺点"></a>七、缺点</h2><p>访问者模式最大缺点是:</p>
<blockquote>
<p>一旦对象结构变化,改动成本很高。</p>
</blockquote>
<p>例如新增一个文件类型 <code>ImageFile</code>,那所有访问者都要补对应方法:</p>
<ul>
<li><code>OpenVisitor</code></li>
<li><code>ExportVisitor</code></li>
<li><code>SizeVisitor</code></li>
<li>以及其他所有 <code>Visitor</code> 实现</li>
</ul>
<p>所以它更适合:</p>
<blockquote>
<p>对象结构稳定,但操作经常变化的系统。</p>
</blockquote>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>访问者模式可以用一句话概括:</p>
<blockquote>
<p>对象负责结构,访问者负责行为。</p>
</blockquote>
<p>它的核心价值是把操作和数据结构分离。</p>
<p>当系统满足以下条件时,访问者模式通常很合适:</p>
<ul>
<li>数据结构稳定</li>
<li>操作经常增加</li>
<li>需要对同一组对象执行多种不同操作</li>
</ul>
]]></content>
<categories>
<category>项目总结</category>
</categories>
<tags>
<tag>系统设计</tag>
<tag>Java</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title>页面刷新时的 Load 动画是如何实现的</title>
<url>/2026/04/23/refresh-load-animation-implementation/</url>
<content><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>之前在看自己博客刷新的时候,发现页面总会先出来一个短暂的 Load 动画:有时是全屏遮罩,有时是进度条,有时只是中间一个在转的小图标。</p>
<p>一开始我以为这是浏览器或者 Hexo 默认带的效果,后来翻了一下 Butterfly 的源码,才发现它其实是主题自己加的一层“过渡界面”。这篇文章就顺着这条线,记录一下它到底是怎么实现的。</p>
<h2 id="它从哪里进入页面"><a href="#它从哪里进入页面" class="headerlink" title="它从哪里进入页面"></a>它从哪里进入页面</h2><p>Butterfly 会在全局布局里直接注入加载层。换句话说,页面刚开始渲染的时候,<code>body</code> 里面就已经先放进去了这层结构。</p>
<p>关键入口在布局文件里:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">body</span><br><span class="line"> !=partial('includes/loading/index', {}, {cache: true})</span><br></pre></td></tr></table></figure>
<p>这个 <code>index</code> 入口会再根据配置选择不同的加载方案:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">if theme.preloader.enable</span><br><span class="line"> if theme.preloader.source === 1</span><br><span class="line"> include ./fullpage-loading.pug</span><br><span class="line"> else</span><br><span class="line"> include ./pace.pug</span><br></pre></td></tr></table></figure>
<p>所以我现在看到的刷新动画,其实就两种形态:</p>
<ul>
<li><code>source: 1</code> 时是全屏 preloader</li>
<li><code>source: 2</code> 时是 pace.js 进度条</li>
</ul>
<h2 id="全屏-Load-动画怎么工作"><a href="#全屏-Load-动画怎么工作" class="headerlink" title="全屏 Load 动画怎么工作"></a>全屏 Load 动画怎么工作</h2><p>如果用的是全屏 preloader,页面会先渲染出一个固定定位的遮罩层:左右两块背景,加中间那个 spinner。效果很简单,但第一眼挺有存在感。</p>
<p>对应的核心结构如下:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">#loading-box</span><br><span class="line"> .loading-left-bg</span><br><span class="line"> .loading-right-bg</span><br><span class="line"> .spinner-box</span><br><span class="line"> .configure-border-1</span><br><span class="line"> .configure-core</span><br><span class="line"> .configure-border-2</span><br><span class="line"> .configure-core</span><br><span class="line"> .loading-word= _p('loading')</span><br></pre></td></tr></table></figure>
<p>这层样式通过固定定位把整个视口盖住,保证页面还没准备好之前,用户看到的不是半成品内容,而是一个完整的加载状态。</p>
<p>CSS 的关键点有三个:</p>
<ul>
<li><code>#loading-box</code> 以及左右背景块都是 <code>position: fixed</code></li>
<li><code>spinner-box</code> 覆盖在正中央,展示旋转动画</li>
<li>当加上 <code>.loaded</code> 类后,左右背景会向两侧平移并退出视口,spinner 也会被隐藏</li>
</ul>
<p>看到这里我就基本明白了:它并不是靠复杂的 JS 一帧一帧去画动画,而是切换 class,让 CSS 的 transition 和 keyframes 自己跑起来。</p>
<h2 id="什么时候结束加载"><a href="#什么时候结束加载" class="headerlink" title="什么时候结束加载"></a>什么时候结束加载</h2><p>真正决定动画什么时候消失的,是一段内联脚本。</p>
<p>它做了两件事:</p>
<h3 id="1-先进入加载状态"><a href="#1-先进入加载状态" class="headerlink" title="1. 先进入加载状态"></a>1. 先进入加载状态</h3><p>脚本一开始会执行 <code>initLoading()</code>:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="attr">initLoading</span>: <span class="function">() =></span> {</span><br><span class="line"> $body.<span class="property">style</span>.<span class="property">overflow</span> = <span class="string">'hidden'</span></span><br><span class="line"> $loadingBox.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">'loaded'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这里会先把 <code>body</code> 的滚动锁住,避免页面还没加载完的时候,用户就已经开始乱滚导致视觉闪动。</p>
<h3 id="2-再在合适时机结束加载"><a href="#2-再在合适时机结束加载" class="headerlink" title="2. 再在合适时机结束加载"></a>2. 再在合适时机结束加载</h3><p>随后它会根据页面状态决定什么时候移除加载层:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="variable language_">document</span>.<span class="property">readyState</span> === <span class="string">'complete'</span>) {</span><br><span class="line"> preloader.<span class="title function_">endLoading</span>()</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">'load'</span>, preloader.<span class="property">endLoading</span>)</span><br><span class="line"> <span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">'DOMContentLoaded'</span>, preloader.<span class="property">endLoading</span>)</span><br><span class="line"> <span class="built_in">setTimeout</span>(preloader.<span class="property">endLoading</span>, <span class="number">7000</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这段逻辑其实挺典型,我自己看下来就是:</p>
<ul>
<li><code>DOMContentLoaded</code> 负责在 DOM 结构可用时尽快结束</li>
<li><code>load</code> 负责等图片、样式、脚本这些资源真正加载完</li>
<li><code>setTimeout</code> 是兜底,防止某些资源出问题后加载层一直挂着不消失</li>
</ul>
<p>最终执行 <code>endLoading()</code> 时,会恢复滚动并给加载层加上 <code>.loaded</code>:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="attr">endLoading</span>: <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> ($loadingBox.<span class="property">classList</span>.<span class="title function_">contains</span>(<span class="string">'loaded'</span>)) <span class="keyword">return</span></span><br><span class="line"> $body.<span class="property">style</span>.<span class="property">overflow</span> = <span class="string">''</span></span><br><span class="line"> $loadingBox.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">'loaded'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>等 <code>loaded</code> 类加上去之后,CSS 动画开始生效,左右背景滑出,整个 Load 动画也就结束了。这个过程很干脆,没有多余的逻辑。</p>
<h2 id="为什么刷新时特别明显"><a href="#为什么刷新时特别明显" class="headerlink" title="为什么刷新时特别明显"></a>为什么刷新时特别明显</h2><p>“刷新页面”这个场景和“站内切页”不太一样。</p>
<p>刷新时浏览器会重新请求整页,主题的加载层也会从最初就出现在 HTML 里,所以用户通常会先看到一个完整的过渡界面,等页面就绪后再被移除。</p>
<p>如果主题启用了 PJAX,站内跳转又会多一层处理:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">btf.<span class="title function_">addGlobalFn</span>(<span class="string">'pjaxSend'</span>, preloader.<span class="property">initLoading</span>, <span class="string">'preloader_init'</span>)</span><br><span class="line">btf.<span class="title function_">addGlobalFn</span>(<span class="string">'pjaxComplete'</span>, preloader.<span class="property">endLoading</span>, <span class="string">'preloader_end'</span>)</span><br></pre></td></tr></table></figure>
<p>这也意味着,不只是首次刷新会出现动画,后续的局部切页同样会沿用这一套加载逻辑。</p>
<h2 id="如果想改成进度条"><a href="#如果想改成进度条" class="headerlink" title="如果想改成进度条"></a>如果想改成进度条</h2><p>Butterfly 还预留了 pace.js 方案。它不会显示全屏遮罩,而是显示一条页面顶部的进度条。</p>
<p>对应逻辑更简单:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">script.</span><br><span class="line"> window.paceOptions = {</span><br><span class="line"> restartOnPushState: false</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> btf.addGlobalFn('pjaxSend', () => {</span><br><span class="line"> Pace.restart()</span><br><span class="line"> }, 'pace_restart')</span><br><span class="line"></span><br><span class="line">link(rel="stylesheet", href=url_for(theme.preloader.pace_css_url || theme.asset.pace_default_css))</span><br><span class="line">script(src=url_for(theme.asset.pace_js))</span><br></pre></td></tr></table></figure>
<p>如果你更偏好轻量、克制一点的视觉效果,我觉得把 <code>preloader.source</code> 切到 <code>2</code> 会更舒服一点。</p>
<h2 id="配置入口"><a href="#配置入口" class="headerlink" title="配置入口"></a>配置入口</h2><p>这部分配置在主题配置文件里:</p>
<figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="attr">preloader:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">false</span></span><br><span class="line"> <span class="comment"># 1. fullpage-loading</span></span><br><span class="line"> <span class="comment"># 2. pace (progress bar)</span></span><br><span class="line"> <span class="attr">source:</span> <span class="number">1</span></span><br><span class="line"> <span class="attr">pace_css_url:</span></span><br></pre></td></tr></table></figure>
<p>如果要自己改配置,我觉得最需要关注的还是这两个点:</p>
<ul>
<li><code>enable</code> 控制是否启用加载动画</li>
<li><code>source</code> 控制是全屏遮罩还是进度条</li>
</ul>
]]></content>
<categories>
<category>博客搭建</category>
</categories>
<tags>
<tag>Hexo</tag>
<tag>Butterfly</tag>
<tag>页面加载</tag>
<tag>前端</tag>
</tags>
</entry>
</search>