-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
1754 lines (844 loc) · 968 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>電商 RESTFul API + Spring Security (3) UnitTest</title>
<link href="/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-3-UnitTest/"/>
<url>/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-3-UnitTest/</url>
<content type="html"><![CDATA[<h2 id="商品-Service-UnitTest"><a href="#商品-Service-UnitTest" class="headerlink" title="商品 Service UnitTest"></a>商品 Service UnitTest</h2><p>這邊針對商品部分 Service 寫一些單元測試,下面先列出預計測試的名稱,主要根據實際方法內會出現判斷的條件去設計,嘗試讓每個 Service 單元測試都 100%。</p><p>先注入並且初始化我們需要用到的 Bean,這邊主要測試 <code>ProductService</code> ,所以加上 <code>@InjectMocks</code> 註解,其他應用到的 Dao 都用 <code>@Mock</code> 標住用 mock 產生虛擬元件注入有標住 <code>@InjectMocks</code> 的 ProductService,@BeforeEach 先初始化待會會測試用到的一些物件資料。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ExtendWith(MockitoExtension.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductServiceTest</span> {</span><br><span class="line"> <span class="meta">@Mock</span></span><br><span class="line"> <span class="keyword">private</span> ProductDao productDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@InjectMocks</span></span><br><span class="line"> <span class="keyword">private</span> ProductService productService;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Product mockProduct1;</span><br><span class="line"> <span class="keyword">private</span> Product mockProduct2;</span><br><span class="line"> <span class="keyword">private</span> ProductRequest mockProductRequest;</span><br><span class="line"> <span class="keyword">private</span> List<Product> mockProducts;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BeforeEach</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> {</span><br><span class="line"> mockProduct1 = <span class="keyword">new</span> <span class="title class_">Product</span>();</span><br><span class="line"> mockProduct1.setId(<span class="number">1</span>);</span><br><span class="line"> mockProduct1.setProductName(<span class="string">"Test Product"</span>);</span><br><span class="line"> mockProduct1.setUnitPrice(<span class="number">10.0</span>);</span><br><span class="line"> mockProduct1.setUnitsInStock(<span class="number">100</span>);</span><br><span class="line"> mockProduct1.setDiscontinued(<span class="literal">false</span>);</span><br><span class="line"> <span class="type">Supplier</span> <span class="variable">supplier1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Supplier</span>();</span><br><span class="line"> supplier1.setId(<span class="number">1</span>);</span><br><span class="line"> mockProduct1.setSupplier(supplier1);</span><br><span class="line"></span><br><span class="line"> mockProduct2 = <span class="keyword">new</span> <span class="title class_">Product</span>();</span><br><span class="line"> mockProduct2.setId(<span class="number">2</span>);</span><br><span class="line"> mockProduct2.setProductName(<span class="string">"Test Wireless Mouse"</span>);</span><br><span class="line"> mockProduct2.setDiscontinued(<span class="literal">false</span>);</span><br><span class="line"> <span class="type">Supplier</span> <span class="variable">supplier2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Supplier</span>();</span><br><span class="line"> supplier2.setId(<span class="number">2</span>);</span><br><span class="line"> mockProduct2.setSupplier(supplier2);</span><br><span class="line"> mockProduct2.setUnitPrice(<span class="number">24.99</span>);</span><br><span class="line"> mockProduct2.setUnitsInStock(<span class="number">103</span>);</span><br><span class="line"></span><br><span class="line"> mockProductRequest = <span class="keyword">new</span> <span class="title class_">ProductRequest</span>();</span><br><span class="line"> mockProductRequest.setProductName(<span class="string">"New/Update Product"</span>);</span><br><span class="line"> mockProductRequest.setUnitPrice(<span class="number">15.0</span>);</span><br><span class="line"> mockProductRequest.setUnitsInStock(<span class="number">50</span>);</span><br><span class="line"> mockProductRequest.setDiscontinued(<span class="literal">false</span>);</span><br><span class="line"> mockProductRequest.setSupplier(supplier1);</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="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGetAllProducts</span><span class="params">()</span> {}</span><br><span class="line"><span class="comment">// 取得特定 id 商品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGetProductById</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 取得特定 id 商品,找不到該 id 之商品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testGetProductById_NotFound</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 創建商品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateProduct</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 更新商品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testUpdateProduct</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 更新商品,找不到該 id 之產品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testUpdateProduct_NotFound</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 刪除商品</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testDeleteProduct</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 搜尋產品並排序</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testSearchAndSortProducts</span><span class="params">()</span> {}</span><br><span class="line"> <span class="comment">// 搜尋產品並排序,傳入商品名參數為空</span></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testSearchAndSortProducts_NullOrEmptyProductName</span><span class="params">()</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>關於基本查詢相關</p><ul><li><code>testGetAllProducts</code><ul><li>模擬回傳 mockProduct1, mockProduct2</li><li>驗證執行後回傳的第 1 個 product 資訊和 mockProduct1 相同</li><li>驗證執行後回傳的第 2 個 product 資訊和 mockProduct2 相同</li><li>驗證回傳數量</li><li>驗證調用 findAll() 1 次</li></ul></li><li><code>testGetProductById</code><ul><li>模擬查尋 id = 1 回傳 mockProduct1</li><li>驗證執行後回傳的 product 資訊和 mockProduct1 相同</li><li>驗證調用 findById() 1 次</li></ul></li><li><code>testGetProductById_NotFound</code><ul><li>模擬查尋 id = 3 回傳空</li><li>驗證執行後 product 是否存在為 false</li><li>驗證調用 findById() 1 次</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testSearchAndSortProducts</span><span class="params">()</span> {</span><br><span class="line"> mockProducts = Arrays.asList(mockProduct2, mockProduct1);</span><br><span class="line"> Page<Product> productPage = <span class="keyword">new</span> <span class="title class_">PageImpl</span><>(mockProducts);</span><br><span class="line"></span><br><span class="line"> when(productDao.findByProductNameContainingIgnoreCase(eq(<span class="string">"Test"</span>), any(PageRequest.class)))</span><br><span class="line"> .thenReturn(productPage);</span><br><span class="line"></span><br><span class="line"> List<Product> result = productService.searchAndSortProducts(<span class="string">"Test"</span>, <span class="string">"id"</span>, <span class="string">"desc"</span>, <span class="number">0</span>, <span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"> assertEquals(<span class="number">2</span>, result.size());</span><br><span class="line"> assertEquals(mockProduct2, result.get(<span class="number">0</span>));</span><br><span class="line"> assertEquals(mockProduct1, result.get(<span class="number">1</span>));</span><br><span class="line"> verify(productDao).findByProductNameContainingIgnoreCase(eq(<span class="string">"Test"</span>), any(PageRequest.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testSearchAndSortProducts_NullOrEmptyProductName</span><span class="params">()</span> {</span><br><span class="line"> mockProducts = Arrays.asList(mockProduct1, mockProduct2);</span><br><span class="line"> Page<Product> productPage = <span class="keyword">new</span> <span class="title class_">PageImpl</span><>(mockProducts);</span><br><span class="line"> when(productDao.findAll(any(PageRequest.class))).thenReturn(productPage);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// null product name</span></span><br><span class="line"> List<Product> resultNull = productService.searchAndSortProducts(<span class="literal">null</span>, <span class="string">"id"</span>, <span class="string">"asc"</span>, <span class="number">0</span>, <span class="number">10</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, resultNull.size());</span><br><span class="line"> assertEquals(mockProduct1, resultNull.get(<span class="number">0</span>));</span><br><span class="line"> assertEquals(mockProduct2, resultNull.get(<span class="number">1</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// empty product name</span></span><br><span class="line"> List<Product> resultEmpty = productService.searchAndSortProducts(<span class="string">""</span>, <span class="string">"id"</span>, <span class="string">"asc"</span>, <span class="number">0</span>, <span class="number">10</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, resultEmpty.size());</span><br><span class="line"> assertEquals(mockProduct1, resultEmpty.get(<span class="number">0</span>));</span><br><span class="line"> assertEquals(mockProduct2, resultNull.get(<span class="number">1</span>));</span><br><span class="line"></span><br><span class="line"> verify(productDao, times(<span class="number">2</span>)).findAll(any(PageRequest.class));</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>關於新增、刪除、修改相關</p><ul><li><code>testCreateProduct</code><ul><li>根據初始化的 mockProductRequest 模擬儲存,回傳 mockProductRequest 轉成的 newProduct</li><li>驗證回傳不為空</li><li>驗證回傳 product 和 newProduct 相同</li><li>驗證調用 save() 1 次</li></ul></li><li><code>testUpdateProduct</code><ul><li>根據初始化的 mockProductRequest 模擬更新,回傳 mockProductRequest 轉成的 updateProduct,模擬查詢 id =1 然後更新成 updateProduct</li><li>驗證回傳 product 和 updateProduct 相同</li><li>驗證調用 findById() 1 次</li><li>驗證調用 save() 1 次</li></ul></li><li><code>testUpdateProduct_NotFound</code><ul><li>模擬查詢 id = 3 商品不存在回傳空</li><li>驗證調用 findById() 1 次</li><li>驗證不調用到 save()</li></ul></li><li><code>testDeleteProduct</code><ul><li>模擬刪除 id =1 商品不回傳值</li><li>驗證調用 deleteById 1 次</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">newMockProduct</span> <span class="operator">=</span> productService.convertToModel(mockProductRequest);</span><br><span class="line"></span><br><span class="line"> when(productDao.save(any(Product.class))).thenReturn(newMockProduct);</span><br><span class="line"></span><br><span class="line"> <span class="type">Product</span> <span class="variable">createdProduct</span> <span class="operator">=</span> productService.createProduct(mockProductRequest);</span><br><span class="line"></span><br><span class="line"> assertNotNull(createdProduct);</span><br><span class="line"> assertEquals(<span class="string">"New/Update Product"</span>, createdProduct.getProductName());</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).save(any(Product.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testUpdateProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">updateMcokProduct</span> <span class="operator">=</span> productService.convertToModel(mockProductRequest);</span><br><span class="line"></span><br><span class="line"> when(productDao.findById(<span class="number">1</span>)).thenReturn(Optional.of(mockProduct1));</span><br><span class="line"> when(productDao.save(any(Product.class))).thenReturn(updateMcokProduct);</span><br><span class="line"></span><br><span class="line"> <span class="type">Product</span> <span class="variable">updatedProduct</span> <span class="operator">=</span> productService.updateProduct(<span class="number">1</span>, mockProductRequest);</span><br><span class="line"></span><br><span class="line"> assertNotNull(updatedProduct);</span><br><span class="line"> assertEquals(<span class="string">"New/Update Product"</span>, updatedProduct.getProductName());</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).findById(<span class="number">1</span>);</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).save(any(Product.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testUpdateProduct_NotFound</span><span class="params">()</span> {</span><br><span class="line"> when(productDao.findById(<span class="number">3</span>)).thenReturn(Optional.empty());</span><br><span class="line"></span><br><span class="line"> <span class="type">Product</span> <span class="variable">updatedProduct</span> <span class="operator">=</span> productService.updateProduct(<span class="number">3</span>, mockProductRequest);</span><br><span class="line"></span><br><span class="line"> assertNull(updatedProduct);</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).findById(<span class="number">3</span>);</span><br><span class="line"> verify(productDao, never()).save(any(Product.class));</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testDeleteProduct</span><span class="params">()</span> {</span><br><span class="line"> doNothing().when(productDao).deleteById(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> productService.deleteProductById(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).deleteById(<span class="number">1</span>);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>搜尋相關</p><ul><li><code>testSearchAndSortProducts</code><ul><li>模擬查詢 ‘Test’ 回傳 mockProduct1, mockProduct2</li><li>驗證回傳 2 筆數</li><li>驗證執行後為降序排列</li><li>驗證調用查詢 1 次</li></ul></li><li><code>testSearchAndSortProducts_NullOrEmptyProductName</code><ul><li>模擬不帶任何 productName 查詢仍回傳 mockProduct1, mockProduct2</li><li>product == null,驗證回傳 mockProduct1, mockProduct2 兩筆</li><li>product == “”,驗證回傳 mockProduct1, mockProduct2 兩筆</li><li>驗證調用查詢 2 次 (上面兩種條件各一次)</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGetAllProducts</span><span class="params">()</span> {</span><br><span class="line"> mockProducts = Arrays.asList(mockProduct1, mockProduct2);</span><br><span class="line"> when(productDao.findAll()).thenReturn(mockProducts);</span><br><span class="line"></span><br><span class="line"> List<Product> products = productService.getAllProducts();</span><br><span class="line"></span><br><span class="line"> assertEquals(products.get(<span class="number">0</span>).getProductName(), <span class="string">"Test Product"</span>);</span><br><span class="line"> assertEquals(products.get(<span class="number">1</span>).getProductName(), <span class="string">"Test Wireless Mouse"</span>);</span><br><span class="line"> assertTrue(products.size() == <span class="number">2</span>);</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).findAll();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGetProductById</span><span class="params">()</span> {</span><br><span class="line"> when(productDao.findById(<span class="number">1</span>)).thenReturn(Optional.of(mockProduct1));</span><br><span class="line"></span><br><span class="line"> Optional<Product> product = productService.getProductById(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> assertTrue(product.isPresent());</span><br><span class="line"> assertEquals(product.get().getProductName(), <span class="string">"Test Product"</span>);</span><br><span class="line"> assertEquals(product.get().getUnitPrice(), <span class="number">10.0</span>);</span><br><span class="line"> assertEquals(product.get().getUnitsInStock(), <span class="number">100</span>);</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).findById(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testGetProductById_NotFound</span><span class="params">()</span> {</span><br><span class="line"> when(productDao.findById(<span class="number">3</span>)).thenReturn(Optional.empty());</span><br><span class="line"></span><br><span class="line"> Optional<Product> product = productService.getProductById(<span class="number">3</span>);</span><br><span class="line"></span><br><span class="line"> assertFalse(product.isPresent());</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).findById(<span class="number">3</span>);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h2 id="訂單-Service-UnitTest"><a href="#訂單-Service-UnitTest" class="headerlink" title="訂單 Service UnitTest"></a>訂單 Service UnitTest</h2><p>再來針對新建訂單部分 Service 的單元測試</p><p>盡量讓內部邏輯可以都被測驗到,每個方法裡的判斷都有走過一遍,這樣測試覆蓋率高也能確保程式運作正常。</p><p>先注入並且初始化我們需要用到的 Bean,這邊主要測試 <code>OrderService</code>,所以加上 <code>@InjectMocks</code> 註解,其他應用到的 Dao 都用 <code>@Mock</code> 標住用 mock 產生虛擬元件注入有標住 <code>@InjectMocks</code> 的 OrderService</p><p>預計可以拆成下面這些項目:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ExtendWith(MockitoExtension.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderServiceTest</span> {</span><br><span class="line"> <span class="meta">@InjectMocks</span></span><br><span class="line"> <span class="keyword">private</span> OrderService orderService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Mock</span></span><br><span class="line"> <span class="keyword">private</span> OrderInfoDao orderInfoDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Mock</span></span><br><span class="line"> <span class="keyword">private</span> OrderItemDao orderItemDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Mock</span></span><br><span class="line"> <span class="keyword">private</span> ProductDao productDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateOrder_Success</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 正確創建訂單</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateOrder_NotFoundProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 訂單內商品不存在</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateOrder_InsufficientStock</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 訂單內商品目前庫存不足</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><code>createOrder_Success</code>: 測試正確創建訂單<ul><li>模擬每個有被呼叫到的 dao 都回應我們預期的結果</li><li>驗證最後有回傳 response</li><li>驗證庫存確實有被更新</li><li>呼叫到的 dao 調用次數正確</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testCreateOrder_Success</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// Arrange</span></span><br><span class="line"> <span class="type">Integer</span> <span class="variable">userId</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> <span class="type">CreateOrderInfoRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CreateOrderInfoRequest</span>();</span><br><span class="line"> <span class="type">BuyItem</span> <span class="variable">buyItem</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BuyItem</span>();</span><br><span class="line"> buyItem.setProductId(<span class="number">1</span>);</span><br><span class="line"> buyItem.setQuantity(<span class="number">2</span>);</span><br><span class="line"> request.setBuyItemList(Arrays.asList(buyItem));</span><br><span class="line"></span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Product</span>();</span><br><span class="line"> product.setId(<span class="number">1</span>);</span><br><span class="line"> product.setProductName(<span class="string">"Test Product"</span>);</span><br><span class="line"> product.setUnitPrice(<span class="number">10.0</span>);</span><br><span class="line"> product.setUnitsInStock(<span class="number">5</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">OrderInfo</span> <span class="variable">orderInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OrderInfo</span>();</span><br><span class="line"> orderInfo.setId(<span class="number">1</span>);</span><br><span class="line"> orderInfo.setUserId(userId);</span><br><span class="line"> orderInfo.setTotalAmount(<span class="number">20.0</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">OrderItem</span> <span class="variable">orderItem</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OrderItem</span>();</span><br><span class="line"> orderItem.setId(<span class="number">1</span>);</span><br><span class="line"> orderItem.setOrderInfoId(<span class="number">1</span>);</span><br><span class="line"> orderItem.setProductId(<span class="number">1</span>);</span><br><span class="line"> orderItem.setQuantity(<span class="number">2</span>);</span><br><span class="line"> orderItem.setAmount(<span class="number">10.0</span>);</span><br><span class="line"></span><br><span class="line"> when(productDao.findById(<span class="number">1</span>)).thenReturn(Optional.of(product));</span><br><span class="line"> when(orderInfoDao.save(any())).thenReturn(orderInfo);</span><br><span class="line"> when(orderItemDao.saveAll(any())).thenReturn(Arrays.asList(orderItem));</span><br><span class="line"></span><br><span class="line"> <span class="type">CreateOrderResponse</span> <span class="variable">response</span> <span class="operator">=</span> orderService.createOrder(userId, request);</span><br><span class="line"></span><br><span class="line"> assertNotNull(response);</span><br><span class="line"> verify(productDao, times(<span class="number">1</span>)).save(any());</span><br><span class="line"> verify(productDao).save(argThat(savedProduct -></span><br><span class="line"> savedProduct.getId().equals(<span class="number">1</span>) && savedProduct.getUnitsInStock() == <span class="number">3</span></span><br><span class="line"> ));</span><br><span class="line"></span><br><span class="line"> verify(orderInfoDao, times(<span class="number">1</span>)).save(any());</span><br><span class="line"> verify(orderItemDao, times(<span class="number">1</span>)).saveAll(any());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><code>createOrder_ProductNotFound</code>: 測試訂單內商品不存在<ul><li>模擬 <code>ProductDao</code> 返回空的 <code>Optional</code></li><li>驗證是否拋出了正確的異常</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateOrder_ProductNotFound</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">userId</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> <span class="type">CreateOrderInfoRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CreateOrderInfoRequest</span>();</span><br><span class="line"> <span class="type">BuyItem</span> <span class="variable">buyItem</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BuyItem</span>();</span><br><span class="line"> buyItem.setProductId(<span class="number">1</span>);</span><br><span class="line"> buyItem.setQuantity(<span class="number">2</span>);</span><br><span class="line"> request.setBuyItemList(Arrays.asList(buyItem));</span><br><span class="line"></span><br><span class="line"> when(productDao.findById(<span class="number">1</span>)).thenReturn(Optional.empty());</span><br><span class="line"> assertThrows(ResponseStatusException.class, () -> orderService.createOrder(userId, request));</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ul><li><code>createOrder_InsufficientStock</code>: 測試訂單內商品目前庫存不足<ul><li>模擬產品庫存量小於請求的數量</li><li>驗證是否拋出 ResponseStatusException ,並檢查異常的狀態碼</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testCreateOrder_InsufficientStock</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">userId</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> <span class="type">CreateOrderInfoRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CreateOrderInfoRequest</span>();</span><br><span class="line"> <span class="type">BuyItem</span> <span class="variable">buyItem</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BuyItem</span>();</span><br><span class="line"> buyItem.setProductId(<span class="number">1</span>);</span><br><span class="line"> buyItem.setQuantity(<span class="number">10</span>);</span><br><span class="line"> request.setBuyItemList(Arrays.asList(buyItem));</span><br><span class="line"></span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Product</span>();</span><br><span class="line"> product.setId(<span class="number">1</span>);</span><br><span class="line"> product.setProductName(<span class="string">"Test Product"</span>);</span><br><span class="line"> product.setUnitPrice(<span class="number">10.0</span>);</span><br><span class="line"> product.setUnitsInStock(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> when(productDao.findById(<span class="number">1</span>)).thenReturn(Optional.of(product));</span><br><span class="line"></span><br><span class="line"> <span class="type">ResponseStatusException</span> <span class="variable">exception</span> <span class="operator">=</span> assertThrows(ResponseStatusException.class,</span><br><span class="line"> () -> orderService.createOrder(userId, request));</span><br><span class="line"> assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode());</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>目前訂單部分測試分享到這邊,針對商品部分因為和先前介紹單元測試那邊類似就沒有多去寫,但開發上可以盡量把重要的功能都涵蓋是最好的。</p><hr>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
</entry>
<entry>
<title>電商 RESTFul API + Spring Security (2) 訂單功能</title>
<link href="/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-2-%E8%A8%82%E5%96%AE%E5%8A%9F%E8%83%BD/"/>
<url>/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-2-%E8%A8%82%E5%96%AE%E5%8A%9F%E8%83%BD/</url>
<content type="html"><![CDATA[<p>這篇繼續擴充訂單功能,可以回顧一下先前畫的架構圖,我們要有一張中間的表來記錄購物車內有哪些商品資訊,這部分建立訂單可以想像完成選擇要購買的商品之後送出訂單所產生的資料紀錄。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241009/20150977I3N7C01CDw.png" alt="https://ithelp.ithome.com.tw/upload/images/20241009/20150977I3N7C01CDw.png"></p><h2 id="訂單功能建立"><a href="#訂單功能建立" class="headerlink" title="訂單功能建立"></a>訂單功能建立</h2><p>order_info 訂單資訊,用來記錄我們整筆訂單的彙整資訊,包含關聯哪位 user, 總金額多少</p><p>order_item 訂單項目資訊,用來記錄整筆訂單中各別項目關聯哪個 product (product_id)、購買數量是多少(quantity)、價錢(amount),從中也可以知道屬於哪筆訂單(order_info_id)。</p><h3 id="基本資料建立"><a href="#基本資料建立" class="headerlink" title="基本資料建立"></a>基本資料建立</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> order_info</span><br><span class="line">(</span><br><span class="line"> id <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">PRIMARY</span> KEY AUTO_INCREMENT,</span><br><span class="line"> user_id <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> total_amount <span class="type">DECIMAL</span>(<span class="number">10</span>, <span class="number">2</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>, <span class="comment">-- 訂單總花費</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"> updated_at <span class="type">TIMESTAMP</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="built_in">CURRENT_TIMESTAMP</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> order_item</span><br><span class="line">(</span><br><span class="line"> id <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">PRIMARY</span> KEY AUTO_INCREMENT,</span><br><span class="line"> order_info_id <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> product_id <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> quantity <span class="type">INT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> amount <span class="type">DECIMAL</span>(<span class="number">10</span>, <span class="number">2</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>Entity</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// OrderInfo.class</span></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "order_info")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderInfo</span> <span class="keyword">extends</span> <span class="title class_">BaseEntity</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer userId;</span><br><span class="line"> <span class="keyword">private</span> Double totalAmount;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// OrderItem.class</span></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "order_item")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderItem</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</span></span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> Integer orderInfoId;</span><br><span class="line"> <span class="keyword">private</span> Integer productId;</span><br><span class="line"> <span class="keyword">private</span> Integer quantity;</span><br><span class="line"> <span class="keyword">private</span> Double amount;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>DTO (CreateOrderInfoRequest.class, CreateOrderResponse.class)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 建立訂單請求</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_">CreateOrderInfoRequest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotEmpty</span></span><br><span class="line"> <span class="keyword">private</span> List<BuyItem> buyItemList;</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="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CreateOrderResponse</span> {</span><br><span class="line"> <span class="keyword">private</span> OrderInfo orderInfo;</span><br><span class="line"> <span class="keyword">private</span> List<HashMap<String, Object>> orderItemList;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="新增訂單"><a href="#新增訂單" class="headerlink" title="新增訂單"></a>新增訂單</h3><p>當勾選好要哪些商品要送出訂單時就會呼叫這隻 API 建立訂單</p><p>Controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderInfoController</span> {</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">log</span> <span class="operator">=</span> LoggerFactory.getLogger(OrderInfoController.class);</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> OrderService orderService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping("/users/{userId}/orders")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<?> createOrder(<span class="meta">@PathVariable</span> Integer userId,</span><br><span class="line"> <span class="meta">@RequestBody</span> <span class="meta">@Valid</span> CreateOrderInfoRequest createOrderInfoRequest) {</span><br><span class="line"></span><br><span class="line"> Optional<User> user = userService.getUserByID(userId);</span><br><span class="line"> <span class="keyword">if</span> (!user.isPresent()) {</span><br><span class="line"> log.warn(<span class="string">"UserId: {} is not found"</span>, userId);</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ResponseStatusException</span>(HttpStatus.BAD_REQUEST);</span><br><span class="line"> }</span><br><span class="line"> <span class="type">CreateOrderResponse</span> <span class="variable">createOrderResponse</span> <span class="operator">=</span> orderService.createOrder(userId, createOrderInfoRequest);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.CREATED).body(createOrderResponse);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Service</p><p>這邊 createOrder 的運作流程:</p><ol><li>取出訂單內各項商品,確認庫存</li><li>計算總價錢</li><li>扣庫存</li><li>建立訂單 orderInfo</li><li>orderItem 建立組裝 Response ())</li><li>組裝 Response (CreateOrderResponse)</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> {</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">log</span> <span class="operator">=</span> LoggerFactory.getLogger(OrderService.class);</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> OrderInfoDao orderInfoDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> OrderItemDao orderItemDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductDao productDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> CreateOrderResponse <span class="title function_">createOrder</span><span class="params">(Integer userId, CreateOrderInfoRequest createOrderInfoRequest)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">Double</span> <span class="variable">totalAmount</span> <span class="operator">=</span> <span class="number">0.0</span>;</span><br><span class="line"> List<OrderItem> orderItemList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (BuyItem buyItem : createOrderInfoRequest.getBuyItemList()) {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> productDao.findById(buyItem.getProductId())</span><br><span class="line"> .orElseThrow(() -> {</span><br><span class="line"> log.warn(<span class="string">"productId: {} not found"</span>, buyItem.getProductId());</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ResponseStatusException</span>(HttpStatus.BAD_REQUEST);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (product.getUnitsInStock() < buyItem.getQuantity()) {</span><br><span class="line"> log.warn(<span class="string">"productId: {} stock is not enough, remaining stock is {}, requested quantity is {}"</span>, buyItem.getProductId(), product.getUnitsInStock(), buyItem.getQuantity());</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ResponseStatusException</span>(HttpStatus.BAD_REQUEST);</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="type">double</span> <span class="variable">amount</span> <span class="operator">=</span> (buyItem.getQuantity() * product.getUnitPrice());</span><br><span class="line"> totalAmount += amount;</span><br><span class="line"></span><br><span class="line"> <span class="type">OrderItem</span> <span class="variable">orderItem</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OrderItem</span>();</span><br><span class="line"> orderItem.setProductId(buyItem.getProductId());</span><br><span class="line"> orderItem.setQuantity(buyItem.getQuantity());</span><br><span class="line"> orderItem.setAmount(amount);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 扣庫存</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">updateStock</span> <span class="operator">=</span> product.getUnitsInStock() - orderItem.getQuantity();</span><br><span class="line"> product.setUnitsInStock(updateStock);</span><br><span class="line"> productDao.save(product);</span><br><span class="line"></span><br><span class="line"> orderItemList.add(orderItem);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 存 orderInfo</span></span><br><span class="line"> <span class="type">OrderInfo</span> <span class="variable">orderInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OrderInfo</span>();</span><br><span class="line"> orderInfo.setUserId(userId);</span><br><span class="line"> orderInfo.setTotalAmount(totalAmount);</span><br><span class="line"> <span class="type">OrderInfo</span> <span class="variable">orderInfoSaved</span> <span class="operator">=</span> orderInfoDao.save(orderInfo);</span><br><span class="line"> <span class="type">int</span> <span class="variable">orderInfoId</span> <span class="operator">=</span> orderInfoSaved.getId();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 存多筆 orderItem</span></span><br><span class="line"> orderItemList.forEach(item -> item.setOrderInfoId(orderInfoId));</span><br><span class="line"> orderItemDao.saveAll(orderItemList);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// response</span></span><br><span class="line"> List<HashMap<String, Object>> responseOrderItemList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"> <span class="keyword">for</span> (OrderItem orderItem : orderItemList) {</span><br><span class="line"> HashMap<String, Object> map = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> productDao.findById(orderItem.getProductId()).get();</span><br><span class="line"></span><br><span class="line"> map.put(<span class="string">"orderItemId"</span>, orderItem.getId());</span><br><span class="line"> map.put(<span class="string">"orderInfoId"</span>, orderItem.getOrderInfoId());</span><br><span class="line"> map.put(<span class="string">"productId"</span>, orderItem.getProductId());</span><br><span class="line"> map.put(<span class="string">"quantity"</span>, orderItem.getQuantity());</span><br><span class="line"> map.put(<span class="string">"amount"</span>, orderItem.getAmount());</span><br><span class="line"> map.put(<span class="string">"productName"</span>, product.getProductName());</span><br><span class="line"> responseOrderItemList.add(map);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">CreateOrderResponse</span> <span class="variable">createOrderResponse</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CreateOrderResponse</span>();</span><br><span class="line"> createOrderResponse.setOrderInfo(orderInfoSaved);</span><br><span class="line"> createOrderResponse.setOrderItemList(responseOrderItemList);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> createOrderResponse;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>dao</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">OrderInfoDao</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><OrderInfo, Integer> {</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">OrderItemDao</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><OrderItem, Integer> {</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="權限建立"><a href="#權限建立" class="headerlink" title="權限建立"></a>權限建立</h3><p>這邊希望可以針對特定路徑 (<code>/api/users/{userId}/orders</code> )去判斷,只有自己能夠建立自己訂單如果創建訂單 userId 沒有對應路徑上的 userId 就會不給與權限。</p><p>針對 SecurityConfig 設置,有特別的寫法可以讓進行請求時進入特定方法來驗證,這邊加上一個 checkUserIdAndRole 方法,裡面用到 Authentication 物件來確認登入者資訊,因為登入取得 JWT 後,這些資訊就會存入 Security Context ,讓你可以取出來進行驗證。</p><p>我這邊有特別開放讓 Admin 可以不受限制建立所有路徑下的訂單,主要是要設計讓 ROLE_BUYER 才可以針對自己路徑的訂單建立,符合 Restful 的風格。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> httpSecurity</span><br><span class="line"> .csrf(customizer -> customizer.disable())</span><br><span class="line"> .authorizeHttpRequests((registry) -> registry</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/register"</span>, <span class="string">"/login"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/error"</span>, <span class="string">"/api/products/**"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/checkAuthentication"</span>).hasAnyAuthority(<span class="string">"ROLE_BUYER"</span>, <span class="string">"ROLE_SELLER"</span>, <span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(HttpMethod.DELETE, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(<span class="string">"/api/users/*"</span>).hasAuthority(<span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> <span class="comment">// 透過 checkUserIdAndRole 來確認權限</span></span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/api/users/{userId}/orders"</span>).access(<span class="built_in">this</span>::checkUserIdAndRole)</span><br><span class="line"> .anyRequest().authenticated()</span><br><span class="line"> )</span><br><span class="line"> .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))</span><br><span class="line"> .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)</span><br><span class="line"> .build();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> AuthorizationDecision <span class="title function_">checkUserIdAndRole</span><span class="params">(Supplier<Authentication> authentication, RequestAuthorizationContext context)</span> {</span><br><span class="line"> <span class="type">int</span> <span class="variable">userId</span> <span class="operator">=</span> Integer.parseInt(context.getVariables().get(<span class="string">"userId"</span>));</span><br><span class="line"> <span class="type">Authentication</span> <span class="variable">auth</span> <span class="operator">=</span> authentication.get();</span><br><span class="line"> <span class="comment">// 轉型問題處理</span></span><br><span class="line"> <span class="keyword">if</span> (!(auth.getPrincipal() <span class="keyword">instanceof</span> UserPrincipal userPrincipal)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">AuthorizationDecision</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="variable">hasAccess</span> <span class="operator">=</span> auth.getAuthorities().stream()</span><br><span class="line"> .anyMatch(a -> a.getAuthority().equals(<span class="string">"ROLE_ADMIN"</span>)) ||</span><br><span class="line"> <span class="comment">// 當前 userId 對應路徑 userId 才允許授權</span></span><br><span class="line"> (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals(<span class="string">"ROLE_BUYER"</span>)) && userPrincipal.getId().equals(userId));</span><br><span class="line"></span><br><span class="line"> log.info(<span class="string">"checkUserIdAndRole access permit: {}"</span>, hasAccess);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">AuthorizationDecision</span>(hasAccess);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>以上大致就完成建立訂單的創建跟權限設定。</p><h3 id="進行測試"><a href="#進行測試" class="headerlink" title="進行測試"></a>進行測試</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"buyItemList"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"productId"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"quantity"</span><span class="punctuation">:</span> <span class="number">1</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"productId"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"quantity"</span><span class="punctuation">:</span> <span class="number">1</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><ul><li>確定 buyer 可以創建 Order_Info,這邊使用 userId = 2 的使用者測試,先登入<br><img src="https://ithelp.ithome.com.tw/upload/images/20241007/2015097721Ty335gbt.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/2015097721Ty335gbt.png"></li></ul><p>對應其 id = 2 路徑就可以創建</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977IM6fHcx3g0.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977IM6fHcx3g0.png"></p><p>如果用其他 id 路徑就會被禁止</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977zZPCXw6Obt.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977zZPCXw6Obt.png"></p><p>以上大概是這次介紹這些電商的一些功能設計,還有加入 Security 權限控管的一些實作,實際上還有很多東西都可以細部去設計,這邊提供一些實務上的應用給大家參考。</p><p>下一篇就簡單寫一些測試也應用到之前的介紹,讓大家可以比較了解應用 Spring Boot 實務的開發部分。</p><hr>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
</entry>
<entry>
<title>電商 RESTFul API + Spring Security (1) 商品功能</title>
<link href="/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-1-%E5%95%86%E5%93%81%E5%8A%9F%E8%83%BD/"/>
<url>/2024/10/19/%E9%9B%BB%E5%95%86-RESTFul-API-Spring-Security-1-%E5%95%86%E5%93%81%E5%8A%9F%E8%83%BD/</url>
<content type="html"><![CDATA[<p>這系列文章會總結先前包含 JPA 和 Security 的應用,詳細可以回顧 Spring Security (1 ~ 4),整合成一個小電商專案 side project,針對後端 API 和認證的部分,內容因為前面大量的內容會比較長,大家有興趣的可以好好來了解一下。</p><h2 id="資料庫結構"><a href="#資料庫結構" class="headerlink" title="資料庫結構"></a>資料庫結構</h2><p>如果先回顧前面 Security 部分建構的資料庫結構 UML</p><p>會有 users, roles, user_roles 三個表去紀錄關於使用者的資訊,也可以從多對多的關聯中找出使用者的角色權限</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241007/201509779TY3BNpxrk.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/201509779TY3BNpxrk.png"></p><p>現在要來加入電商主要的資料就是商品部分,預計會規畫成下面這樣的關係<br><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977aFtoNqUZwj.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977aFtoNqUZwj.png"></p><h2 id="需求規格"><a href="#需求規格" class="headerlink" title="需求規格"></a>需求規格</h2><p>會拆分成兩階段來建構:</p><ol><li>products 建構,先建立 products, suppliers 1 : 1 關係</li><li>order_info 建構,order_info 記錄總價和購買使用者 id,再由 order_item 分別關聯 products 和 order_info,可以關聯出購物車內容,所有需要購買的商品數量和同品項總金額,也關聯出屬於哪張 order_info</li></ol><p>大致清楚架構之後接下來預計會實作的一些功能也列出來:</p><p>商品相關</p><ul><li>product 的 CRUD</li><li>product 查詢功能分類及呈現<ul><li>模糊搜尋商品名稱</li><li>根據分類</li><li>升冪降冪排序</li><li>分頁資料呈現(一頁幾筆,顯示第幾頁資料)</li></ul></li><li>order_info, order_item 資訊讀取 (購物車資訊)</li></ul><p>使用者權限</p><ul><li>分成 buyer, seller, admin</li><li>商品部分 Read 部分不設權限,操作資料部分 Create, Update, Delete 需要對應創建商家才可以</li><li>buyer 可建立訂單</li></ul><p>使用者</p><ul><li>註冊</li><li>登入</li></ul><p>這樣就有基本的電商運作模式需要的一些功能了。</p><p>那先前 Security 的介紹部分已經把使用者註冊、登入和權限的設置都已經先建置好,就可以直接來接著進行商品部分功能建立</p><h2 id="商品功能建立"><a href="#商品功能建立" class="headerlink" title="商品功能建立"></a>商品功能建立</h2><h3 id="基本資料建置"><a href="#基本資料建置" class="headerlink" title="基本資料建置"></a>基本資料建置</h3><p>商品相關資料表,刻意多加入一個 supplier 關聯表 可以對應多對一關聯</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> suppliers</span><br><span class="line">(</span><br><span class="line"> id <span class="type">INT</span> AUTO_INCREMENT <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line"> supplier_name <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</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"> updated_at <span class="type">TIMESTAMP</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="built_in">CURRENT_TIMESTAMP</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> products</span><br><span class="line">(</span><br><span class="line"> id <span class="type">INT</span> AUTO_INCREMENT <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line"> product_name <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> unit_price <span class="type">DECIMAL</span>(<span class="number">10</span>, <span class="number">2</span>),</span><br><span class="line"> units_in_stock <span class="type">INT</span>,</span><br><span class="line"> discontinued <span class="type">BOOLEAN</span> <span class="keyword">DEFAULT</span> <span class="literal">FALSE</span>,</span><br><span class="line"> supplier_id <span class="type">INT</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"> updated_at <span class="type">TIMESTAMP</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line"> <span class="keyword">FOREIGN</span> KEY (supplier_id) <span class="keyword">REFERENCES</span> suppliers (id)</span><br><span class="line">);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>先建立基本 Entity</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> <span class="keyword">extends</span> <span class="title class_">BaseEntity</span> {</span><br><span class="line"> <span class="keyword">private</span> String productName;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Double unitPrice;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Integer unitsInStock;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="type">Boolean</span> <span class="variable">discontinued</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ManyToOne</span></span><br><span class="line"> <span class="keyword">private</span> Supplier supplier;</span><br></pre></td></tr></table></figure><p>建立時需要的 dto,順便透過 Validation 相關註解來驗證傳入欄位的資訊</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><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_">ProductRequest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotBlank(message = "ProductName cannot be blank")</span></span><br><span class="line"> <span class="keyword">private</span> String productName;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotNull</span></span><br><span class="line"> <span class="meta">@PositiveOrZero(message = "UnitPrice must be zero or positive")</span></span><br><span class="line"> <span class="keyword">private</span> Double unitPrice;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotNull</span></span><br><span class="line"> <span class="meta">@PositiveOrZero(message = "UnitInStock must be zero or positive")</span></span><br><span class="line"> <span class="keyword">private</span> Integer unitsInStock;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotNull</span></span><br><span class="line"> <span class="keyword">private</span> Boolean discontinued;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotNull</span></span><br><span class="line"> <span class="keyword">private</span> Supplier supplier;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> LocalDateTime createAt;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> LocalDateTime updatedAt;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="增刪查改"><a href="#增刪查改" class="headerlink" title="增刪查改"></a>增刪查改</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api/products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductService productService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Product> <span class="title function_">createProduct</span><span class="params">(<span class="meta">@RequestBody</span> <span class="meta">@Valid</span> ProductRequest productRequest)</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">createdProduct</span> <span class="operator">=</span> productService.createProduct(productRequest);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<List<Product>> <span class="title function_">getProducts</span><span class="params">()</span> {</span><br><span class="line"> List<Product> productList = productService.getAllProducts();</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(productList);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Optional<Product>> <span class="title function_">getProduct</span><span class="params">(<span class="meta">@PathVariable</span> Integer id)</span> {</span><br><span class="line"> Optional<Product> product = productService.getProductById(id);</span><br><span class="line"> <span class="keyword">if</span> (product.isPresent()) {</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(product);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.NOT_FOUND).build();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PutMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Product> <span class="title function_">updateProduct</span><span class="params">(<span class="meta">@PathVariable</span> Integer id, <span class="meta">@Valid</span> <span class="meta">@RequestBody</span> ProductRequest productRequest)</span> {</span><br><span class="line"> Optional<Product> product = productService.getProductById(id);</span><br><span class="line"> <span class="keyword">if</span> (!product.isPresent()) {</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.NOT_FOUND).build();</span><br><span class="line"> }</span><br><span class="line"> <span class="type">Product</span> <span class="variable">updatedProduct</span> <span class="operator">=</span> productService.updateProduct(id, productRequest);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(updatedProduct);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@DeleteMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<?> deleteProduct(<span class="meta">@PathVariable</span> Integer id) {</span><br><span class="line"> Optional<Product> product = productService.getProductById(id);</span><br><span class="line"> <span class="keyword">if</span> (product.isPresent()) {</span><br><span class="line"> productService.deleteProductById(id);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.NO_CONTENT).build();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.NOT_FOUND).build();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Service</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductDao productDao;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> List<Product> <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> productDao.findAll();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Optional<Product> <span class="title function_">getProductById</span><span class="params">(<span class="type">int</span> id)</span> {</span><br><span class="line"> <span class="keyword">return</span> productDao.findById(id);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">updateProduct</span><span class="params">(Integer id, ProductRequest productRequest)</span> {</span><br><span class="line"> Optional<Product> product = getProductById(id);</span><br><span class="line"> <span class="keyword">if</span> (product.isPresent()) {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">updatedProduct</span> <span class="operator">=</span> convertToModel(productRequest);</span><br><span class="line"> updatedProduct.setId(id);</span><br><span class="line"> <span class="keyword">return</span> productDao.save(updatedProduct);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">createProduct</span><span class="params">(ProductRequest productRequest)</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> convertToModel(productRequest);</span><br><span class="line"> <span class="keyword">return</span> productDao.save(product);</span><br><span class="line"> }</span><br><span class="line"></span><br><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_">deleteProductById</span><span class="params">(Integer id)</span> {</span><br><span class="line"> productDao.deleteById(id);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> List<Product> <span class="title function_">searchProducts</span><span class="params">(String productName)</span> {</span><br><span class="line"> <span class="keyword">return</span> productDao.findByProductName(productName);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">convertToModel</span><span class="params">(ProductRequest productRequest)</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Product</span>();</span><br><span class="line"> product.setProductName(productRequest.getProductName());</span><br><span class="line"> product.setUnitPrice(productRequest.getUnitPrice());</span><br><span class="line"> product.setUnitsInStock(productRequest.getUnitsInStock());</span><br><span class="line"> product.setDiscontinued(productRequest.getDiscontinued());</span><br><span class="line"> product.setSupplier(productRequest.getSupplier());</span><br><span class="line"> <span class="keyword">return</span> product;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="篩選-x2F-查詢"><a href="#篩選-x2F-查詢" class="headerlink" title="篩選/查詢"></a>篩選/查詢</h3><p>開一個相關端口接收對應參數,可以對應倒前端搜尋頁面的操作</p><p>sortBy 篩選欄位</p><p>sortOrder 降冪或升冪排序</p><p>page 呈現第幾頁資料</p><p>limit 一頁顯示幾筆</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api/products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductService productService;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/search")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<List<Product>> <span class="title function_">searchProducts</span><span class="params">(</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(required = false)</span> String productName,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(defaultValue = "id", required = false)</span> String sortBy,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(defaultValue = "asc", required = false)</span> String sortOrder,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(defaultValue = "0", required = false)</span> <span class="type">int</span> page,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(defaultValue = "5", required = false)</span> <span class="type">int</span> limit</span></span><br><span class="line"><span class="params"> )</span> {</span><br><span class="line"> List<Product> productList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"> productList = productService.searchAndSortProducts(productName, sortBy, sortOrder, page, limit);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(productList);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以透過 Sort/Pageable 物件進行篩選及資料分頁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductDao productDao;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> List<Product> <span class="title function_">searchAndSortProducts</span><span class="params">(String productName, String sortBy, String sortOrder, <span class="type">int</span> page, <span class="type">int</span> limit)</span> {</span><br><span class="line"> <span class="type">Sort</span> <span class="variable">sort</span> <span class="operator">=</span> sortOrder.equalsIgnoreCase(<span class="string">"asc"</span>) ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending();</span><br><span class="line"> <span class="type">Pageable</span> <span class="variable">pageable</span> <span class="operator">=</span> PageRequest.of(page, limit, sort);</span><br><span class="line"></span><br><span class="line"> Page<Product> productPage = <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">if</span> (productName == <span class="literal">null</span> || productName.isEmpty()) {</span><br><span class="line"> productPage = productDao.findAll(pageable);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> productPage = productDao.findByProductNameContainingIgnoreCase(productName, pageable);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> List<Product> products = productPage.getContent();</span><br><span class="line"> <span class="keyword">return</span> products;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="權限建立"><a href="#權限建立" class="headerlink" title="權限建立"></a>權限建立</h3><p>讓 seller 才能建立商品資料,需要再 SecurityConfig 那邊新增相關路徑對應可以操作的權限</p><p>也可以同步在端口上加上 <code>@PreAuthorize("hasRole('SELLER')")</code> ,這部分可以不用,兩種選一個即可達成,但如果需要標示清楚易閱讀可以選擇兩邊都加入,但管理上就要注意是否重工,可能調整時兩邊都要設定,兩種都用 <code>@PreAuthorize</code> 可以幫忙切得更精細一點。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> httpSecurity</span><br><span class="line"> .csrf(customizer -> customizer.disable())</span><br><span class="line"> .authorizeHttpRequests((registry) -> registry</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/register"</span>, <span class="string">"/login"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/error"</span>, <span class="string">"/api/products/**"</span>, <span class="string">"/who-am-i"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/checkAuthentication"</span>).hasAnyAuthority(<span class="string">"ROLE_BUYER"</span>, <span class="string">"ROLE_SELLER"</span>, <span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> <span class="comment">// 需要 ROLE_SELLER 才能修改跟刪除</span></span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(HttpMethod.DELETE, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(<span class="string">"/api/users/*"</span>).hasAuthority(<span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .anyRequest().authenticated()</span><br><span class="line"> )</span><br><span class="line"> .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))</span><br><span class="line"> .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)</span><br><span class="line"> .build();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="進行測試"><a href="#進行測試" class="headerlink" title="進行測試"></a>進行測試</h3><p>這邊提供一些資料先塞進去測試</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> suppliers (id, supplier_name)</span><br><span class="line"><span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">'TechSupply Inc.'</span>),</span><br><span class="line"> (<span class="number">2</span>, <span class="string">'GadgetWorld Ltd.'</span>),</span><br><span class="line"> (<span class="number">3</span>, <span class="string">'ElectroGoods Co.'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> products (product_name, supplier_id, unit_price, units_in_stock, discontinued)</span><br><span class="line"><span class="keyword">VALUES</span> (<span class="string">'Wireless Mouse'</span>, <span class="number">1</span>, <span class="number">24.99</span>, <span class="number">150</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'Bluetooth Keyboard'</span>, <span class="number">2</span>, <span class="number">49.99</span>, <span class="number">75</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'USB-C Charger'</span>, <span class="number">3</span>, <span class="number">19.99</span>, <span class="number">200</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'Gaming Headset'</span>, <span class="number">1</span>, <span class="number">79.99</span>, <span class="number">50</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'4K Monitor'</span>, <span class="number">2</span>, <span class="number">299.99</span>, <span class="number">40</span>, <span class="literal">TRUE</span>),</span><br><span class="line"> (<span class="string">'External Hard Drive'</span>, <span class="number">1</span>, <span class="number">89.99</span>, <span class="number">120</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'Mechanical Keyboard'</span>, <span class="number">3</span>, <span class="number">129.99</span>, <span class="number">30</span>, <span class="literal">TRUE</span>),</span><br><span class="line"> (<span class="string">'Portable SSD'</span>, <span class="number">3</span>, <span class="number">159.99</span>, <span class="number">60</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'Wireless Earbuds'</span>, <span class="number">1</span>, <span class="number">199.99</span>, <span class="number">80</span>, <span class="literal">FALSE</span>),</span><br><span class="line"> (<span class="string">'Laptop Stand'</span>, <span class="number">3</span>, <span class="number">39.99</span>, <span class="number">100</span>, <span class="literal">FALSE</span>);</span><br></pre></td></tr></table></figure><ul><li>先確認未登入任何人都可以直接進行商品資訊</li></ul><p><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977yD9tZ572Ky.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977yD9tZ572Ky.png"></p><ul><li>確認登入一個 seller 才可以進行商品創建<br><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977HEMk1uLBf7.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977HEMk1uLBf7.png"></li></ul><p>輸入 JWT<br><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977oAnJy0dnoV.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977oAnJy0dnoV.png"></p><p>輸入新增電文,下面提供簡單電文給大家參考</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"productName"</span><span class="punctuation">:</span> <span class="string">"Good Mouse"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"unitPrice"</span><span class="punctuation">:</span> <span class="number">110.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"unitsInStock"</span><span class="punctuation">:</span> <span class="number">100</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"discontinued"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"supplier"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p><img src="https://ithelp.ithome.com.tw/upload/images/20241007/20150977fJU2g7vMFG.png" alt="https://ithelp.ithome.com.tw/upload/images/20241007/20150977fJU2g7vMFG.png"></p><ul><li>確認 seller 修改商品、刪除商品</li></ul><p>與前面新增相同,確認這些端口有正常運作。</p><p>以上大概是先介紹商品功能的擴充介紹,下一篇來加入訂單功能。</p><hr>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
</entry>
<entry>
<title>Spring Security (4) - JWT 驗證及結合 FilterChain</title>
<link href="/2024/10/15/Spring-Security-4-JWT-%E9%A9%97%E8%AD%89%E5%8F%8A%E7%B5%90%E5%90%88-FilterChain/"/>
<url>/2024/10/15/Spring-Security-4-JWT-%E9%A9%97%E8%AD%89%E5%8F%8A%E7%B5%90%E5%90%88-FilterChain/</url>
<content type="html"><![CDATA[<p>接續上一篇我們已經成功產生 JWT 回傳,所以後續使用者需要攜帶 JWT 至 Header 內然後發送請求到我們後端,我們需要驗證 JWT 是否有效然後決定使用者是否可以進入瀏覽或是使用功能,而這相對應的驗證流程,其實 Spring Security 有一套 FilterChain 的機制可以協助我們,這其中包含我們前面實作的帳號密碼驗證等等都是包含在裡面,那我們需要做的就是把 JWT 的驗證放入 FilterChain 裡面。</p><h2 id="解析-Token-取出-Claims"><a href="#解析-Token-取出-Claims" class="headerlink" title="解析 Token 取出 Claims"></a>解析 Token 取出 Claims</h2><p>把之前拿來產生 Token 的 密鑰( getKey() 產生的) 拿來解析 Token,extractAllClaims() 帶入 token 可以取出其中的 Claims 也就是 Payload 的部分。</p><p>因為 Payload 裡面有很多屬性,下面另外抽出各別取 username 使用者名稱, expiration 到期時間等等的方法,方便後續驗證時可以引用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</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_">JwtService</span> {</span><br><span class="line"> <span class="comment">// ......略</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Claims <span class="title function_">extractAllClaims</span><span class="params">(String token)</span> <span class="keyword">throws</span> JwtException {</span><br><span class="line"> <span class="keyword">return</span> Jwts.parser()</span><br><span class="line"> <span class="comment">// 把之前 getKey() 取得的密鑰帶入來解析</span></span><br><span class="line"> .setSigningKey(getKey())</span><br><span class="line"> .build().parseClaimsJws(token).getBody();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Claims <span class="title function_">extractAllClaims</span><span class="params">(String token)</span> <span class="keyword">throws</span> JwtException {</span><br><span class="line"> <span class="keyword">return</span> Jwts.parser()</span><br><span class="line"> .setSigningKey(getKey())</span><br><span class="line"> .build().parseClaimsJws(token).getBody();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <T> T <span class="title function_">extractClaim</span><span class="params">(String token, Function<Claims, T> claimResolver)</span> {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> extractAllClaims(token);</span><br><span class="line"> <span class="keyword">return</span> claimResolver.apply(claims);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">extractUserName</span><span class="params">(String token)</span> {</span><br><span class="line"> <span class="comment">// extract the username from jwt token</span></span><br><span class="line"> <span class="keyword">return</span> extractClaim(token, Claims::getSubject);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">validateToken</span><span class="params">(String token, UserDetails userDetails)</span> {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">userName</span> <span class="operator">=</span> extractUserName(token);</span><br><span class="line"> <span class="keyword">return</span> (userName.equals(userDetails.getUsername()) && !isTokenExpired(token));</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_">isTokenExpired</span><span class="params">(String token)</span> {</span><br><span class="line"> <span class="keyword">return</span> extractExpiration(token).before(<span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Date <span class="title function_">extractExpiration</span><span class="params">(String token)</span> {</span><br><span class="line"> <span class="keyword">return</span> extractClaim(token, Claims::getExpiration);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>先寫個 Controller 確認一下解析內容 ,JWT 會放在 Header 下面 Authorization 的屬性內,@RequestHeader 可以幫忙取得 Header 的內容,然後需要切割掉前面的部分,他會是呈現這樣的格式 <code>Bearer {JWT}</code> 所以需要切掉前面 Bearer 和一個空格,就是 7 個字元,切好之後就是 token 部分直接帶入給我們前面的方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/extractJwt")</span></span><br><span class="line"><span class="keyword">public</span> Map<String, Object> <span class="title function_">extractJwt</span><span class="params">(<span class="meta">@RequestHeader(HttpHeaders.AUTHORIZATION)</span> String authorization)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> authorization.substring(<span class="number">7</span>);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> jwtService.extractAllClaims(token);</span><br><span class="line"> } <span class="keyword">catch</span> (JwtException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BadCredentialsException</span>(e.getMessage(), e);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>實際用 postman 操作,用先前資料 sean 登入取得 JWT,然後用 postman 選擇 Authoriztion ⇒ Auth Type 選 Bearer Token 然後帶入 JWT 就可以看到回傳結果正確。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">!https://ithelp.ithome.com.tw/upload/images/20240913/20150977UVI1FHxUjC.png</span><br></pre></td></tr></table></figure><p>!<a href="https://ithelp.ithome.com.tw/upload/images/20240913/20150977UVI1FHxUjC.png">https://ithelp.ithome.com.tw/upload/images/20240913/20150977UVI1FHxUjC.png</a></p><h2 id="完成-JwtFilter"><a href="#完成-JwtFilter" class="headerlink" title="完成 JwtFilter"></a>完成 JwtFilter</h2><p>確定我們解析 token 沒問題就實作 JwtFilter 然後設置到 config 內。</p><p>這邊需要繼承 <code>OncePerRequestFilter</code> 這個 filter,他會協助每當請求來時只會進行一次驗證,否則 Spring Security 執行第一次後,因為該 Filter 剛好是個元件(bean),於是 Spring Boot 又執行第二次。</p><p>然後我們要 Override <code>doFilterInternal()</code> 這個方法,他會過濾請求,然後我們就照先前的解析方式,分別拿到 token 和 username 然後進行驗證,最後目標確認沒問題就會將其轉成 Authentication 物件,存入 Security Context,讓 Spring Security 的 FilterChain 進行驗證時可以取出確認。</p><p>下面的一些判斷都是確定請求內有帶對應的 header 才會動作</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtFilter</span> <span class="keyword">extends</span> <span class="title class_">OncePerRequestFilter</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> JwtService jwtService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> ApplicationContext context;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> MyUserDetailsService myUserDetailsService;</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> <span class="keyword">void</span> <span class="title function_">doFilterInternal</span><span class="params">(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="type">String</span> <span class="variable">authHeader</span> <span class="operator">=</span> request.getHeader(<span class="string">"Authorization"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (authHeader != <span class="literal">null</span> && authHeader.startsWith(<span class="string">"Bearer "</span>)) {</span><br><span class="line"> token = authHeader.substring(<span class="number">7</span>);</span><br><span class="line"> username = jwtService.extractUserName(token);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// context 裡面沒東西才驗證 token,</span></span><br><span class="line"> <span class="keyword">if</span> (username != <span class="literal">null</span> && SecurityContextHolder.getContext().getAuthentication() == <span class="literal">null</span>) {</span><br><span class="line"> <span class="type">UserDetails</span> <span class="variable">userDetails</span> <span class="operator">=</span> myUserDetailsService.loadUserByUsername(username);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 檢查 userDetails 和 token 解析出來資訊相同且未過期</span></span><br><span class="line"> <span class="keyword">if</span> (jwtService.validateToken(token, userDetails)) {</span><br><span class="line"> <span class="type">UsernamePasswordAuthenticationToken</span> <span class="variable">authtoken</span></span><br><span class="line"> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(userDetails, <span class="literal">null</span>, userDetails.getAuthorities());</span><br><span class="line"> SecurityContextHolder.getContext().setAuthentication(authtoken);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (JwtException ex) {</span><br><span class="line"> response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);</span><br><span class="line"> }</span><br><span class="line"> filterChain.doFilter(request, response);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面驗證 validateToken 部分會確認是否有過期,還有 token 解開的使用者名稱及 userDetails 內是相同,確認 JWT 是有效,然後才寫入 SecurityContext</p><h2 id="config-加入-filter"><a href="#config-加入-filter" class="headerlink" title="config 加入 filter"></a>config 加入 filter</h2><p>前面實作完成,要加到 Spring Security 的 filter chain 中,認證的效果才能生效。</p><p>Spring Security 的 filter chain 會比其他 Filter 還優先執行。而裡頭負責進行的 Filter 叫做 UsernamePasswordAuthenticationFilter,正是負責帳號密碼認證。會選擇放在這個之前是因為,</p><p>JWT 認證通常是無狀態的,不依賴使用者名稱和密碼,如果 JWT 有效,就不需要再進行使用者名稱密碼認證。所以會加入在這個 filter 之前執行我們的 JwtFilter。</p><p>補充底下也添加 sessionManagement 選擇為無狀態,因為採用 Jwt 通常就不會採用 sesstion 來進行管理所以加入到配置中 Security 就不會幫我們產生 Session。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserDetailsService userDetailsService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> JwtFilter jwtFilter;</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> AuthenticationProvider <span class="title function_">authProvider</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">DaoAuthenticationProvider</span> <span class="variable">provider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DaoAuthenticationProvider</span>();</span><br><span class="line"> provider.setUserDetailsService(userDetailsService);</span><br><span class="line"> provider.setPasswordEncoder(<span class="keyword">new</span> <span class="title class_">BCryptPasswordEncoder</span>(<span class="number">12</span>));</span><br><span class="line"> <span class="keyword">return</span> provider;</span><br><span class="line"> }</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> AuthenticationManager <span class="title function_">authenticationManager</span><span class="params">(AuthenticationConfiguration config)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> config.getAuthenticationManager();</span><br><span class="line"> }</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> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> httpSecurity</span><br><span class="line"> .csrf(customizer -> customizer.disable())</span><br><span class="line"> .authorizeHttpRequests((registry) -> registry</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/register"</span>, <span class="string">"/login"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/error"</span>, <span class="string">"/api/products/**"</span>, <span class="string">"/who-am-i"</span>).permitAll() <span class="comment">//指定路徑允許所有用戶訪問,不需身份驗證</span></span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/checkAuthentication"</span>).hasAnyAuthority(<span class="string">"ROLE_BUYER"</span>, <span class="string">"ROLE_SELLER"</span>, <span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(HttpMethod.DELETE, <span class="string">"/api/products"</span>).hasAuthority(<span class="string">"ROLE_SELLER"</span>)</span><br><span class="line"> .requestMatchers(<span class="string">"/api/users/**"</span>).hasAuthority(<span class="string">"ROLE_ADMIN"</span>) <span class="comment">// 任何 /api/users 開頭的,且所有方法都算</span></span><br><span class="line"> <span class="comment">// .requestMatchers(HttpMethod.GET, "/api/users/?*").hasAuthority("ROLE_ADMIN") // 只有 /api/users/{id} 才算</span></span><br><span class="line"> .anyRequest().authenticated()<span class="comment">//其他尚未匹配到的路徑都需要身份驗證</span></span><br><span class="line"> )</span><br><span class="line"> .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))</span><br><span class="line"> .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)</span><br><span class="line"> .build();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>最後進行測試,同樣拿 getUsers() 的方法測試看看用 Jwt 可否取得,這邊用 sean 登入,然後把 jwt 帶入去進行請求,可以故意把 Jwt 填錯看看,上面有特別包一個 try catch 來捕獲 JwtException 會回 403。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">!https://ithelp.ithome.com.tw/upload/images/20240913/201509778ZlqzHGsY0.png</span><br></pre></td></tr></table></figure><p>!<a href="https://ithelp.ithome.com.tw/upload/images/20240913/201509778ZlqzHGsY0.png">https://ithelp.ithome.com.tw/upload/images/20240913/201509778ZlqzHGsY0.png</a></p><p>如果正確可以成功獲得資料</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">!https://ithelp.ithome.com.tw/upload/images/20240913/20150977KtSpIJXCar.png</span><br></pre></td></tr></table></figure><p>!<a href="https://ithelp.ithome.com.tw/upload/images/20240913/20150977KtSpIJXCar.png">https://ithelp.ithome.com.tw/upload/images/20240913/20150977KtSpIJXCar.png</a></p><hr><p>參考資料:</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2021/06/spring-boot-security-implement-authentication-filter-with-jwt.html">第 17.6 課-實作 Spring Security 的認證 Filter(以 JWT 為例)</a></li><li>Udemy - Java Spring Framework 6 with Spring Boot 3</li><li><a href="%5Bhttps://medium.com/@heather_programming/spring-security-jwt-%E9%A9%97%E8%AD%89-d51dc4eb5fe5%5D(https://medium.com/@heather_programming/spring-security-jwt-%E9%A9%97%E8%AD%89-d51dc4eb5fe5)">【Spring Security】JWT 驗證</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Spring Security </category>
</categories>
</entry>
<entry>
<title>Spring Security (3) - JWT 介紹及導入</title>
<link href="/2024/10/15/Spring-Security-3-JWT-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%B0%8E%E5%85%A5/"/>
<url>/2024/10/15/Spring-Security-3-JWT-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%B0%8E%E5%85%A5/</url>
<content type="html"><![CDATA[<h1 id="JWT"><a href="#JWT" class="headerlink" title="JWT"></a>JWT</h1><p>JSON Web Token (JWT),是一種根據將 JSON 格式資料進行編碼的的開放標準(RFC 7519),用於雙方之間安全將訊息作為 JSON 物件進行傳輸,訊息是經過數位簽章 (Digital Signature) 加密,因此可以被驗證及信任。</p><p>目前也成為各大網站之間進行資源獲取時所採用證明身份的訊息格式。</p><h2 id="為什麼要用"><a href="#為什麼要用" class="headerlink" title="為什麼要用?"></a>為什麼要用?</h2><ol><li>授權 (Authorization):先前提過,授權就是指根據使用者身分給予特定的瀏覽或是存取權限,這也是 JWT 常用的情境,例如使用者登入後,點選特定的功能或區域,發送請求時,需夾帶 JWT,Server 端就可根據該 JWT 所帶的資訊驗證是否可以開放使用該功能。像是單一路口登錄 (Single Sign On) 實作上也是廣泛應用到 JWT 。</li><li>訊息交換 (Information Exchange):JWT 可以透過公鑰/私鑰來做簽章,讓我們可知道是誰發送這個 JWT,此外,由於簽章是使用 header 和 payload 計算的,因此可驗證內容是否遭到篡改。</li></ol><h2 id="JWT-結構"><a href="#JWT-結構" class="headerlink" title="JWT 結構"></a>JWT 結構</h2><p>可以透過這個網站來測試產生的 jwt 解開後的內容</p><p><a href="https://jwt.io/">https://jwt.io/</a></p><h3 id="Head"><a href="#Head" class="headerlink" title="Head"></a>Head</h3><p>標頭來顯示使用演算法及 token 類型</p><ul><li>alg: token 被加密的演算法,如<strong>HMAC</strong>、<strong>SHA256</strong>、<strong>RSA</strong></li><li>typ:token 類型,基本上都是 JWT</li></ul><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"alg"</span><span class="punctuation">:</span> <span class="string">"HS256"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"typ"</span><span class="punctuation">:</span> <span class="string">"JWT"</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><h3 id="Payload"><a href="#Payload" class="headerlink" title="Payload"></a>Payload</h3><p>放入要攜帶的資訊或聲明 (claims) 的內容。</p><p>通常有 sub, iat, exp 這些資訊。這邊補充後面導入 JWT 套件可以設定的資訊大寫名稱,等號後面也有指出對應的 jwt 內代表名稱</p><ul><li>ISSUER = “iss” 簽發者</li><li>SUBJECT = “sub” 主旨,通常作為識別主要內容 (username)</li><li>AUDIENCE = “aud” 接收方</li><li>EXPIRATION = “exp” 到期時間,需大於簽發時間</li><li>NOT_BEFORE = “nbf” 定義某個時間前不可使用</li><li>ISSUED_AT = “iat” 簽發時間</li><li>ID = “jti” 識別身分不重複 id</li></ul><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"sub"</span><span class="punctuation">:</span> <span class="string">"sean"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"iat"</span><span class="punctuation">:</span> <span class="number">1725950094</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"exp"</span><span class="punctuation">:</span> <span class="number">1725950274</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><h3 id="Signature"><a href="#Signature" class="headerlink" title="Signature"></a>Signature</h3><p>簽章會是根據 Header 和 Payload 分別都用 base64UrlEncode ,再加上密鑰 (secret) 進行雜湊演算,產生一組不可反解的亂數,當成簽章,用來驗證 JWT 是否經過篡改。</p><p>特別注意 Header, Payload 會經過 Base64 編碼的處理。是可以被還原請不要放敏感資訊。</p><h1 id="導入前準備"><a href="#導入前準備" class="headerlink" title="導入前準備"></a>導入前準備</h1><p>pom 導入目前 jjwt 新板 0.12.6</p><p><a href="https://github.com/jwtk/jjwt">https://github.com/jwtk/jjwt</a></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><dependency></span><br><span class="line"> <groupId>io.jsonwebtoken</groupId></span><br><span class="line"> <artifactId>jjwt-api</artifactId></span><br><span class="line"> <version><span class="number">0.12</span><span class="number">.6</span></version></span><br><span class="line"></dependency></span><br><span class="line"><dependency></span><br><span class="line"> <groupId>io.jsonwebtoken</groupId></span><br><span class="line"> <artifactId>jjwt-impl</artifactId></span><br><span class="line"> <version><span class="number">0.12</span><span class="number">.6</span></version></span><br><span class="line"> <scope>runtime</scope></span><br><span class="line"></dependency></span><br><span class="line"><dependency></span><br><span class="line"> <groupId>io.jsonwebtoken</groupId></span><br><span class="line"> <artifactId>jjwt-jackson</artifactId></span><br><span class="line"> <version><span class="number">0.12</span><span class="number">.6</span></version></span><br><span class="line"> <scope>runtime</scope></span><br><span class="line"></dependency></span><br></pre></td></tr></table></figure><h1 id="登入驗證"><a href="#登入驗證" class="headerlink" title="登入驗證"></a>登入驗證</h1><p>導入相關參數至 application.properties</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 隨意設置</span></span><br><span class="line">jwt.secret-key= <span class="punctuation">{</span>yourKey<span class="punctuation">}</span></span><br><span class="line"><span class="comment">// 有效秒數</span></span><br><span class="line">jwt.valid-seconds=<span class="number">60</span></span><br></pre></td></tr></table></figure><h2 id="JwtService"><a href="#JwtService" class="headerlink" title="JwtService"></a>JwtService</h2><p>我們將實際要產生 token 和驗證的方法放入 JwtService。</p><p>這邊提供兩種產生鑰匙的方式,可以透過 application.properties 環境參數帶入,但我測試時使用幾次如果自己隨便輸入可能會因為格式不符就會被認定不安全,所以另外提供 KeyGenerator 來協助產生的方法,寫在 generateSecretKey(),再由 getKey() 將密鑰類型從 String 轉成 Key ,這樣每次程式重新啟動都會產生一組密鑰,這邊也會印在 console 可以拿來回頭到前面提供的網站確認是否可以成功驗證。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtService</span> {</span><br><span class="line"> <span class="meta">@Value("${jwt.secret-key}")</span></span><br><span class="line"> <span class="keyword">private</span> String JWT_SECRET_KEY_STR;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Value("${jwt.valid-seconds}")</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> EXPIRATION_TIME;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> String secretKey;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">JwtService</span><span class="params">()</span> {</span><br><span class="line"> secretKey = generateSecretKey();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">generateSecretKey</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="type">KeyGenerator</span> <span class="variable">keyGen</span> <span class="operator">=</span> KeyGenerator.getInstance(<span class="string">"HmacSHA256"</span>);</span><br><span class="line"> <span class="type">SecretKey</span> <span class="variable">secretKey</span> <span class="operator">=</span> keyGen.generateKey();</span><br><span class="line"> System.out.println(<span class="string">"Secret Key : "</span> + secretKey.toString());</span><br><span class="line"> <span class="keyword">return</span> Base64.getEncoder().encodeToString(secretKey.getEncoded());</span><br><span class="line"> } <span class="keyword">catch</span> (NoSuchAlgorithmException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"Error generating secret key"</span>, e);</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> Key <span class="title function_">getKey</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">byte</span>[] keyBytes = Decoders.BASE64.decode(secretKey);</span><br><span class="line"> <span class="keyword">return</span> Keys.hmacShaKeyFor(keyBytes);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="產生-Token"><a href="#產生-Token" class="headerlink" title="產生 Token"></a>產生 Token</h3><p>使用 jjwt 套件提供的產生 token 方法來進行設置,把使用名稱 setSubject 設到 sub、有效時間 setExpiration 設到 exp,還有帶入上面取得的 key</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> String <span class="title function_">generateToken</span><span class="params">(Authentication authentication)</span> {</span><br><span class="line"> Map<String, Object> claims = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Jwts.builder()</span><br><span class="line"> .setClaims(claims)</span><br><span class="line"> .setHeaderParam(<span class="string">"typ"</span>, <span class="string">"JWT"</span>)</span><br><span class="line"> .setSubject(authentication.getName())</span><br><span class="line"> .setIssuedAt(<span class="keyword">new</span> <span class="title class_">Date</span>(System.currentTimeMillis()))</span><br><span class="line"> .setExpiration(<span class="keyword">new</span> <span class="title class_">Date</span>(System.currentTimeMillis() + <span class="number">1000</span> * EXPIRATION_TIME * <span class="number">3</span>))</span><br><span class="line"> .signWith(getKey(), SignatureAlgorithm.HS256).compact();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="寫個-controller-測試一下"><a href="#寫個-controller-測試一下" class="headerlink" title="寫個 controller 測試一下"></a>寫個 controller 測試一下</h3><p>這邊有多加入一個 Security 內提供的驗證,透過 AuthenticationManager 來產生 Authentication 物件協助驗證。</p><p>先回到 SecurityConfig 註冊 AuthenticationManager 的 Bean</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</span> {</span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> AuthenticationManager <span class="title function_">authenticationManager</span><span class="params">(AuthenticationConfiguration config)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> config.getAuthenticationManager();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 略</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>引入 Controller 使用,只要我們傳遞 UsernamePasswordAuthenticationToken 這個物件,參數把 username, password 給他就可以回傳給我們 Authentication,裡面會包含是否通過帳號密碼驗證還有使用者相關的資訊 (UserDetails, Authorities…),我們可透過取得裡面的 authenticated 判斷是否有通過,這物件也提供 isAuthenticated() 方法來判斷。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> AuthenticationManager authenticationManager;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> JwtService jwtService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> MyUserDetailsService userDetailsService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping("/login")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> UserLoginRequest userRequest)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="type">Authentication</span> <span class="variable">authentication</span> <span class="operator">=</span> authenticationManager</span><br><span class="line"> .authenticate(<span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(userRequest.getUsername(), userRequest.getPassword()));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (authentication.isAuthenticated()) {</span><br><span class="line"> <span class="type">String</span> <span class="variable">jwtToken</span> <span class="operator">=</span> jwtService.generateToken(authentication);</span><br><span class="line"> <span class="keyword">return</span> jwtToken;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Login failed"</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>成功就可以看到回傳的 JWT</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240910/20150977SLO7hERqjh.png" alt="https://ithelp.ithome.com.tw/upload/images/20240910/20150977SLO7hERqjh.png"></p><hr><p>參考資料:</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2021/06/spring-boot-security-authentication-and-authorization.html">新手工程師的程式教室 -【Spring Boot】第 17.1 課-初探 Spring Security 的認證與授權</a></li><li>Udemy - Java Spring Framework 6 with Spring Boot 3</li><li><a href="https://zatalk.xyz/jwt-introduction/">JWT 介紹</a></li><li><a href="https://5xcampus.com/posts/what-is-jwt">是誰在敲打我窗?什麼是 JWT ?</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Spring Security </category>
</categories>
</entry>
<entry>
<title>Spring Security (2) - 串接個人資料庫</title>
<link href="/2024/10/08/Spring-Security-2-%E4%B8%B2%E6%8E%A5%E5%80%8B%E4%BA%BA%E8%B3%87%E6%96%99%E5%BA%AB/"/>
<url>/2024/10/08/Spring-Security-2-%E4%B8%B2%E6%8E%A5%E5%80%8B%E4%BA%BA%E8%B3%87%E6%96%99%E5%BA%AB/</url>
<content type="html"><![CDATA[<p>實際要來看一下我們要使用個人的使用者資訊資料表要如何串接,因為 Security 提供很多客製化的介面所以需要實作許多特殊的物件就會讓流程蠻複雜的,整體來說可以先有個概念,我們原本需要透過 dao 來和 DB 溝通的部分需要改成 Security 框架的模式,原本可以由 Spring Data JPA 直接取得 User 的物件回傳,但因為 Security 將使用者資訊會封裝成 <code>UserDetails</code> 這個物件來回傳,必須要透過 <code>UserDetailsService</code> 這邊來取得這個物件,所以會想辦法把這些東西去實做出來。</p><p>資料庫是串接 MySQL ,相關配置可以參考 <a href="https://oseanchen.github.io/2024/09/24/SpringBoot/Spring-Data-JPA-%EF%BC%881%EF%BC%89%E5%9F%BA%E7%A4%8E%E6%87%89%E7%94%A8%E6%9E%B6%E6%A7%8B/">Spring Data JPA (1)基礎應用架構</a> 引入相關 Dependency</p><h1 id="資料庫架構-Database-Schema"><a href="#資料庫架構-Database-Schema" class="headerlink" title="資料庫架構 Database Schema"></a>資料庫架構 Database Schema</h1><p>提供 SQL 給大家參考</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> users</span><br><span class="line">(</span><br><span class="line"> id <span class="type">BIGINT</span> <span class="keyword">PRIMARY</span> KEY AUTO_INCREMENT,</span><br><span class="line"> username <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">UNIQUE</span>,</span><br><span class="line"> email <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">UNIQUE</span>,</span><br><span class="line"> password <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> created_at DATETIME <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line"> updated_at DATETIME <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="built_in">CURRENT_TIMESTAMP</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> roles</span><br><span class="line">(</span><br><span class="line"> id <span class="type">BIGINT</span> <span class="keyword">PRIMARY</span> KEY AUTO_INCREMENT,</span><br><span class="line"> role_name <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">UNIQUE</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> user_roles</span><br><span class="line">(</span><br><span class="line"> user_id <span class="type">BIGINT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> role_id <span class="type">BIGINT</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> <span class="keyword">PRIMARY</span> KEY (user_id, role_id),</span><br><span class="line"> <span class="keyword">FOREIGN</span> KEY (user_id) <span class="keyword">REFERENCES</span> users (id),</span><br><span class="line"> <span class="keyword">FOREIGN</span> KEY (role_id) <span class="keyword">REFERENCES</span> roles (id)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="Config-配置"><a href="#Config-配置" class="headerlink" title="Config 配置"></a>Config 配置</h2><p>延續前面文章的配置</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</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> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> httpSecurity</span><br><span class="line"> .csrf(customizer -> customizer.disable())</span><br><span class="line"> .authorizeHttpRequests((registry) -> registry</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/register"</span>, <span class="string">"/login"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/error"</span>, <span class="string">"/api/products/**"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/checkAuthentication"</span>).hasAnyAuthority(<span class="string">"ROLE_BUYER"</span>, <span class="string">"ROLE_SELLER"</span>, <span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .requestMatchers(<span class="string">"/api/users/**"</span>).hasAuthority(<span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .anyRequest().authenticated()</span><br><span class="line"> )</span><br><span class="line"> .formLogin(Customizer.withDefaults())</span><br><span class="line"> .build();</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h2 id="建-User-Role-UserRole-UserRepository"><a href="#建-User-Role-UserRole-UserRepository" class="headerlink" title="建 User, Role , UserRole, UserRepository"></a>建 User, Role , UserRole, UserRepository</h2><p>權限部分需要在 users 表裡面多一個 role 的欄位來進行多對多關聯,並且建立 roles 表和 user_roles 表</p><p>User Entity</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "users")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">extends</span> <span class="title class_">BaseEntity</span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Column(nullable = false)</span></span><br><span class="line"> <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Column(nullable = false)</span></span><br><span class="line"> <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Column(nullable = false)</span></span><br><span class="line"> <span class="keyword">private</span> String email;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ManyToMany(fetch = FetchType.EAGER)</span></span><br><span class="line"> <span class="meta">@JoinTable(</span></span><br><span class="line"><span class="meta"> name = "user_roles",</span></span><br><span class="line"><span class="meta"> joinColumns = @JoinColumn(name = "user_id"),</span></span><br><span class="line"><span class="meta"> inverseJoinColumns = @JoinColumn(name = "role_id"),</span></span><br><span class="line"><span class="meta"> uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role_id"})</span></span><br><span class="line"><span class="meta"> )</span></span><br><span class="line"> <span class="keyword">private</span> Set<Role> roles = <span class="keyword">new</span> <span class="title class_">HashSet</span><>();</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Role Entity</p><p>roles 表內 roleName 的欄位用 Enum 作為資料類型,所以另外有 UserRole 的 Enum</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table(name = "roles")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Role</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</span></span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Enumerated(EnumType.STRING)</span></span><br><span class="line"> <span class="meta">@Column(nullable = false, unique = true)</span></span><br><span class="line"> <span class="keyword">private</span> UserRole roleName;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>UserRole Enum</p><p>三種權限分別是 ADMIN, BUYER, SELLER 分別是管理者、買家、商家。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.oseanchen.crudproject.model;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">UserRole</span> {</span><br><span class="line"> ROLE_ADMIN,</span><br><span class="line"> ROLE_BUYER,</span><br><span class="line"> ROLE_SELLER</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="UserPricipal-實作-Custom-UserDetails"><a href="#UserPricipal-實作-Custom-UserDetails" class="headerlink" title="UserPricipal 實作 (Custom UserDetails)"></a>UserPricipal 實作 (Custom UserDetails)</h2><p>啟動程式時,Spring Security 會檢查專案中是否有 UserDetailsService 。如果沒有,則自動建立一個前一篇有提到的預設管理元件 InMemoryUserDetailsManager ,並提供預設 user 然後把密碼印在 console 。如果我們自行提供 UserDetailsService ,則 Security 就會自動採用。</p><p>把我們 User Entity 轉成 Security 的 UserDetails 介面,我們取名 UserPrincipal 來稱呼你也可以自訂自己的 UserDetails 名稱,由於是介面所以它所提供的方法都要實做一遍。</p><p>裡面有幾個主要的就是 <code>getUsername</code>, <code>getPassword</code>, <code>getAuthorities</code> ,讓我們可以拿到我們要的 User 資訊,其他的設置主要關於帳號是否到期、鎖定、密碼過期、啟用等等,可以依據你的需求配置紀錄相關資訊,讓 Spring Security 可以進行驗證,就算帳號密碼正確也不一定可以通過認證。這邊先進行簡單的設計都預設為 true 通過驗證</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserPrincipal</span> <span class="keyword">implements</span> <span class="title class_">UserDetails</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> User user;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">UserPrincipal</span><span class="params">(User user)</span> {</span><br><span class="line"> <span class="built_in">this</span>.user = user;</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> Collection<? <span class="keyword">extends</span> <span class="title class_">GrantedAuthority</span>> getAuthorities() {</span><br><span class="line"></span><br><span class="line"> Set<GrantedAuthority> authorities = user.getRoles().stream()</span><br><span class="line"> .map(role -> <span class="keyword">new</span> <span class="title class_">SimpleGrantedAuthority</span>(role.getRoleName().name()))</span><br><span class="line"> .collect(Collectors.toSet());</span><br><span class="line"> <span class="keyword">return</span> authorities;</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> String <span class="title function_">getPassword</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> user.getPassword();</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> String <span class="title function_">getUsername</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> user.getUsername();</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="type">boolean</span> <span class="title function_">isAccountNonExpired</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</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="type">boolean</span> <span class="title function_">isAccountNonLocked</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</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="type">boolean</span> <span class="title function_">isCredentialsNonExpired</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</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="type">boolean</span> <span class="title function_">isEnabled</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="UserDetailService-實作-Custom-UserDetailService"><a href="#UserDetailService-實作-Custom-UserDetailService" class="headerlink" title="UserDetailService 實作 (Custom UserDetailService)"></a>UserDetailService 實作 (Custom UserDetailService)</h2><p>接著需要加入管理的部分, 先前有提到原本預設管理元件 InMemoryUserDetailsManager 它其實就會去實作 UserDetailService,裡面有一個方法是 loadUserByUsername 需要實作,這邊需要實作用 username 來找出 User 資料,然後把 User 封裝成 UserDetails</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyUserDetailsService</span> <span class="keyword">implements</span> <span class="title class_">UserDetailsService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserDao repo;</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> UserDetails <span class="title function_">loadUserByUsername</span><span class="params">(String username)</span> <span class="keyword">throws</span> UsernameNotFoundException {</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> repo.findByUsername(username);</span><br><span class="line"> <span class="keyword">if</span> (user == <span class="literal">null</span>) {</span><br><span class="line"> System.out.println(<span class="string">"username = "</span> + username + <span class="string">" not found"</span>);</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UsernameNotFoundException</span>(<span class="string">"user not found"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">UserPrincipal</span>(user);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="UserController-UserService-實作確認可創建及撈出資料"><a href="#UserController-UserService-實作確認可創建及撈出資料" class="headerlink" title="UserController, UserService 實作確認可創建及撈出資料"></a>UserController, UserService 實作確認可創建及撈出資料</h2><p>建立 UserController 的幾個主要端口讓 user 可以先創建跟查詢,這邊我們透過 userRequest 的 DTO 來傳遞註冊時的帳號密碼等資訊</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/users")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<List<User>> <span class="title function_">getUsers</span><span class="params">()</span> {</span><br><span class="line"> List<User> userList = userService.getUsers();</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(userList);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/users/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Optional<User>> <span class="title function_">getUser</span><span class="params">(<span class="meta">@PathVariable</span> Integer id)</span> {</span><br><span class="line"> Optional<User> user = userService.getUserByID(id);</span><br><span class="line"> <span class="keyword">if</span> (user.isPresent()) {</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.OK).body(user);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.NOT_FOUND).build();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping("/register")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<User> <span class="title function_">Register</span><span class="params">(<span class="meta">@RequestBody</span> <span class="meta">@Valid</span> UserRequest userRequest)</span> {</span><br><span class="line"> <span class="type">User</span> <span class="variable">createdUser</span> <span class="operator">=</span> userService.createUser(userRequest);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.status(HttpStatus.CREATED).body(createdUser);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Service 這邊主要處理資料轉換,註冊的部分要 save user 預設先用一個 set<Role> 裝入預設的角色,我們是設定一般辦好會有買家跟賣家的權限,所以從 UserRole Enum 裡面取得放入。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserService</span> {</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserDao userDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> RoleDao roleDao;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> List<User> <span class="title function_">getUsers</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> userDao.findAll();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Optional<User> <span class="title function_">getUserByID</span><span class="params">(Integer id)</span> {</span><br><span class="line"> <span class="keyword">return</span> userDao.findById(id);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> User <span class="title function_">createUser</span><span class="params">(UserRequest userRequest)</span> {</span><br><span class="line"> Set<Role> roles = <span class="keyword">new</span> <span class="title class_">HashSet</span><>();</span><br><span class="line"> List<String> roleList = Arrays.asList(UserRole.ROLE_BUYER.name(), UserRole.ROLE_SELLER.name());</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> convertToModel(userRequest);</span><br><span class="line"> user.getRoles().addAll(roles);</span><br><span class="line"> <span class="keyword">return</span> userDao.save(user);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> User <span class="title function_">convertToModel</span><span class="params">(UserRequest userRequest)</span> {</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line"> user.setUsername(userRequest.getUsername());</span><br><span class="line"> user.setEmail(userRequest.getEmail());</span><br><span class="line"> user.setPassword(userRequest.getPassword());</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>UserRequest DTO</p><p>DTO 內應用 @Valid 相關的驗證註解設定一些對應的參數規格</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.oseanchen.crudproject.dto;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> jakarta.validation.constraints.*;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"></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_">UserRequest</span> {</span><br><span class="line"> <span class="meta">@NotBlank(message = "username can not be blank")</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotBlank(message = "password can not be blank")</span></span><br><span class="line"> <span class="meta">@Size(min = 6, message = "password length at least 6 characters")</span></span><br><span class="line"> <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@NotBlank(message = "email can not be blank")</span></span><br><span class="line"> <span class="meta">@Email(message = "invalid email format")</span></span><br><span class="line"> <span class="keyword">private</span> String email;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="建立-PasswordEncoder-提升資安規格"><a href="#建立-PasswordEncoder-提升資安規格" class="headerlink" title="建立 PasswordEncoder 提升資安規格"></a>建立 PasswordEncoder 提升資安規格</h2><p>使用 Bcrypt 加密方式,比起 MD5 或是 SHA 的雜湊演算更安全,因為加入隨機生成的鹽值 salt 可以防止駭客使用彩虹表來進行破解。</p><h3 id="Service-引入-BCryptPasswordEncoder"><a href="#Service-引入-BCryptPasswordEncoder" class="headerlink" title="Service 引入 BCryptPasswordEncoder"></a>Service 引入 BCryptPasswordEncoder</h3><p>引入後將準備儲存的密碼進行加密。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">BCryptPasswordEncoder</span> <span class="variable">encoder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BCryptPasswordEncoder</span>(<span class="number">12</span>);</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> User <span class="title function_">createUser</span><span class="params">(UserRequest userRequest)</span> {</span><br><span class="line"> Set<Role> roles = <span class="keyword">new</span> <span class="title class_">HashSet</span><>();</span><br><span class="line"> List<UserRole> roleList = Arrays.asList(UserRole.ROLE_BUYER, UserRole.ROLE_SELLER);</span><br><span class="line"> <span class="comment">// 改加入 encoder</span></span><br><span class="line"> userRequest.setPassword(encoder.encode(userRequest.getPassword()));</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> convertToModel(userRequest);</span><br><span class="line"> user.getRoles().addAll(roles);</span><br><span class="line"> <span class="keyword">return</span> userDao.save(user);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>最後可以測試目前儲存的資料,假設我們放入下面註冊的資料。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"username"</span> <span class="punctuation">:</span> <span class="string">"sean"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"password"</span> <span class="punctuation">:</span> <span class="string">"sean123"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"email"</span><span class="punctuation">:</span><span class="string">"[email protected]"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"username"</span> <span class="punctuation">:</span> <span class="string">"tom"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"password"</span> <span class="punctuation">:</span> <span class="string">"tom123"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"email"</span><span class="punctuation">:</span><span class="string">"[email protected]"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>資料庫儲存時應該看到的資料像是下面這樣,密碼會是加密過的。<br><img src="https://ithelp.ithome.com.tw/upload/images/20240912/20150977gPv8Bk0TLP.png" alt="https://ithelp.ithome.com.tw/upload/images/20240912/20150977gPv8Bk0TLP.png"></p><h3 id="Config-改變-AuthenticationProvider-Encoder-方式"><a href="#Config-改變-AuthenticationProvider-Encoder-方式" class="headerlink" title="Config 改變 AuthenticationProvider Encoder 方式"></a>Config 改變 AuthenticationProvider Encoder 方式</h3><p>這邊會透過 DaoAuthenticationProvider 重新設定 AuthenticationProvider 所使用的 userDetailsService, 還有 passwordEncoder</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</span> {</span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserDetailsService userDetailsService;</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> AuthenticationProvider <span class="title function_">authProvider</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">DaoAuthenticationProvider</span> <span class="variable">provider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DaoAuthenticationProvider</span>();</span><br><span class="line"> provider.setUserDetailsService(userDetailsService);</span><br><span class="line"> provider.setPasswordEncoder(<span class="keyword">new</span> <span class="title class_">BCryptPasswordEncoder</span>(<span class="number">12</span>));</span><br><span class="line"> <span class="keyword">return</span> provider;</span><br><span class="line"> }</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> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"><span class="comment">//...略</span></span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h3 id="實現登入"><a href="#實現登入" class="headerlink" title="實現登入"></a>實現登入</h3><p>最後直接啟動程式連到 <a href="http://localhost:8080/login">http://localhost:8080/login</a> 確認可以正常登入,且登入之後可以根據我們 config 配置的權限進行瀏覽。</p><p>例如我上面 Config 配置只讓 ADMIN 的使用者可以查詢所有 user 的資料,如果是具有該權限的就可以成功瀏覽,但是其他使用者查詢就會是 403 Forbidden</p><p>先使用 sean 登入<br><img src="https://ithelp.ithome.com.tw/upload/images/20240913/20150977Vqw5hWttRR.png" alt="https://ithelp.ithome.com.tw/upload/images/20240913/20150977Vqw5hWttRR.png"></p><p>再使用 tom 登入<br><img src="https://ithelp.ithome.com.tw/upload/images/20240913/20150977H4P40dESR1.png" alt="https://ithelp.ithome.com.tw/upload/images/20240913/20150977H4P40dESR1.png"></p><h2 id="總結:"><a href="#總結:" class="headerlink" title="總結:"></a>總結:</h2><p>以上就完成串接自己資料庫並且實現 Security 基本驗證的框架示範,總結一下這部分因為太多步驟要處理:</p><ol><li>建立資料庫 schema, Entity、串連 Spring Data JPA 和資料庫</li><li>UserDetails 介面實作</li><li>UserDetailsService 介面實作撈出 user 並且封裝成 UserDetails 回傳</li><li>建置 Controller 及 Service 測試</li><li>加入 BCryptPasswordEncoder 實作,引入註冊帳號加密密碼後儲存、Config 重設 AuthenticationProvider 加入對應的 BCryptPasswordEncoder</li></ol><p>希望大家對於 Spring Security 有更多的認識,也可以自己建立起保護 server 的環境喔! 下一篇會引入現今業界常使用到的 JWT 方式來進行登入及相關權限的管控。</p><hr><p>參考資料:</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2023/11/spring-boot-security-authentication-integrating-with-mongodb-database.html">【Spring Boot】第 17.2 課-在 Spring Security 整合資料庫進行認證</a></li><li>Udemy - Java Spring Framework 6 with Spring Boot 3</li><li><a href="https://juejin.cn/post/7187020239182725181">https://juejin.cn/post/7187020239182725181</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Spring Security </category>
</categories>
</entry>
<entry>
<title>Spring Security (1) - 介紹及應用</title>
<link href="/2024/10/08/Spring-Security-1-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E6%87%89%E7%94%A8/"/>
<url>/2024/10/08/Spring-Security-1-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E6%87%89%E7%94%A8/</url>
<content type="html"><![CDATA[<p>相信大家應該作為軟體工程師都知道資訊安全的重要性,如果你寫好一個系統,沒有受到好的資安控管,容易成為駭客下手的目標。特別是許多網頁或應用程式都建立在會員系統的基礎之下,這些資訊對於認證(authentication)與授權(authorization)控管相當重要,認證就相當於帳號登入,對系統表示自己是個合法的使用者。而授權則是系統允許該使用者存取某服務,看到特定頁面或是功能等等,也就是存取 API 許可。Spring Security 就提供了這部分的支援讓我們能夠輕鬆整合到 Spring Boot 專案中。</p><h2 id="介紹"><a href="#介紹" class="headerlink" title="介紹"></a>介紹</h2><p>Spring Security 是 Spring Boot 框架中的提供的一個資訊安全模組,能夠靈活的配置來有效保護 Web 應用程序免受各種安全威脅,主要有下面這些特色:</p><ul><li><strong>快速整合</strong>:Spring Boot Security 可以通過簡單的配置快速整合到 Spring Boot 應用中。</li><li><strong>預設安全性</strong>:即使沒有進行任何配置,它也能為應用程序提供基本的安全防護,包括預設的登入頁面和認證方法 。</li><li><strong>靈活配置</strong>:開發者可以根據需求自定義安全設置,如用戶認證、授權規則等 。</li><li><strong>角色權限訪問控制(RBAC)</strong>:允許根據用戶角色設置不同的訪問權限。</li><li>**過濾器鏈(FilterChain)**:使用一系列過濾器來處理 HTTP 請求,實現各種安全功能 ,像是 JWT。</li></ul><h2 id="引入-SpringBoot-Security"><a href="#引入-SpringBoot-Security" class="headerlink" title="引入 SpringBoot Security"></a>引入 SpringBoot Security</h2><p>在 pom.xml 加入</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-security<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><h2 id="簡單測試預設配置"><a href="#簡單測試預設配置" class="headerlink" title="簡單測試預設配置"></a>簡單測試預設配置</h2><p>可以啟動看一下目前 Security 幫我們做了什麼事情,你會發現什麼都還沒配置就可以將你的 API 加上一層防護,並且給你一個預設的登入頁面跟預設的相關認證登入的配置。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240819/20150977uIs7tM8r9R.png"></p><p>如果要測試目前登入及認證就用他預設的帳號密碼,預設帳號為 user ,密碼會顯示在 console,可以使用來登入看看。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240819/20150977NPv3OsujeD.png"></p><p>你可以自己建立一個 API 接口測試看看,成功登入後就可以順利獲取各端口的資源,我這邊規劃設計方向是電商,可以有買家賣家的可獲取的頁面。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WelcomeController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">home</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Authentication</span> <span class="variable">authentication</span> <span class="operator">=</span> SecurityContextHolder.getContext().getAuthentication();</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Welcome Security Demo, "</span> + authentication.getName();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/register")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">register</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"註冊頁面"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/products")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">products</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"商品頁面"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/addProducts")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">addProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"新增商品頁面"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/users")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getUsers</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"管理員觀看使用者頁面"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>這邊有放一個小彩蛋,就是 Authentication 這個物件,只要成功認證就可以透過這個物件取得使用者的相關資>訊,我就拿來印出 username,之後的文章會講到更多。</p></blockquote><h2 id="自定義配置"><a href="#自定義配置" class="headerlink" title="自定義配置"></a>自定義配置</h2><h3 id="Config"><a href="#Config" class="headerlink" title="Config"></a>Config</h3><p>預設配置通常也不是我們希望使用的,我們可以建立一個配置 class (名稱自訂),我就叫 SecurityConfig 並且套上 <code>@EnableWebSecurity</code>, <code>@Configuration</code>,這樣就可以讓 Spring Security 認定這個配置,並幫我們套用我們自定義的配置。</p><h3 id="InMemoryUserDetailsManager"><a href="#InMemoryUserDetailsManager" class="headerlink" title="InMemoryUserDetailsManager"></a>InMemoryUserDetailsManager</h3><p>第一件事情我們可以建立幾個預存的使用者,並且設置權限,後續會再實際連結自己的資料庫。</p><p>大家先有個概念 <strong><code>UserDetails</code></strong> 這個主要是 Security 用來針對帳號密碼驗證需要的重要物件,其中 Security 預設有提供一個 <code>InMemoryUserDetailsManager</code> 介面實作 <code>UserDetailsService</code> 及 <code>UserDetailsManager</code> ,可以用來管理 <code>UserDetails</code> 並預存在記憶體中提供我們可以直接使用。</p><p>我們可以自己設定需要用的 user 帳號密碼還有權限等等</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</span> {</span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> UserDetailsService <span class="title function_">initUsers</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">UserDetails</span> <span class="variable">buyer</span> <span class="operator">=</span> User.withDefaultPasswordEncoder()</span><br><span class="line"> .username(<span class="string">"buyer"</span>)</span><br><span class="line"> .password(<span class="string">"buyer"</span>)</span><br><span class="line"> .authorities(<span class="string">"BUYER"</span>)</span><br><span class="line"> .build();</span><br><span class="line"></span><br><span class="line"> <span class="type">UserDetails</span> <span class="variable">seller</span> <span class="operator">=</span> User</span><br><span class="line"> .withUsername(<span class="string">"seller"</span>)</span><br><span class="line"> .password(<span class="string">"{noop}seller"</span>)</span><br><span class="line"> .authorities(<span class="string">"SELLER"</span>)</span><br><span class="line"> .build();</span><br><span class="line"></span><br><span class="line"> <span class="type">UserDetails</span> <span class="variable">admin</span> <span class="operator">=</span> User</span><br><span class="line"> .withUsername(<span class="string">"admin"</span>)</span><br><span class="line"> .password(<span class="string">"{bcrypt}$2a$12$Z/TYK22AUhfoswp6/24GWOcyspUMGX9lYht6nytvzrDrbRDOzk6BC"</span>)</span><br><span class="line"> .authorities(<span class="string">"ADMIN"</span>)</span><br><span class="line"> .build();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">InMemoryUserDetailsManager</span>(buyer, seller, admin);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因為是自己練習,所以是可以用 <code>withDefaultPasswordEncoder</code>是不使用任何 PasswordEncoder,但目前官方不推薦,畢竟一般設計密碼是不可以原文儲存的,這邊也提供建議的寫法,可判斷加密類型,參考上面 seller 和 admin 的寫法,需要在密碼前面加上前綴,Security 會自動判斷要使用哪種演算法來驗證。</p><table><thead><tr><th>前綴</th><th>演算法</th><th>密碼原文</th><th>security 參數寫法</th></tr></thead><tbody><tr><td>{noop}</td><td>不加密</td><td>seller</td><td>{noop}seller</td></tr><tr><td>{bcrypt}</td><td>BCrypt</td><td>admin</td><td>{bcrypt}$2a$12$Z/TYK22AUhfoswp6/24GWOcyspUMGX9lYht6nytvzrDrbRDOzk6BC</td></tr><tr><td>{sha256}</td><td>SHA256</td><td>password</td><td>{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0</td></tr></tbody></table><blockquote><p>{noop} 的意思其實是 No Operation 和 <code>withDefaultPasswordEncoder</code> 是一樣都不加密</p></blockquote><h3 id="SecurityFilterChain-配置授權規則"><a href="#SecurityFilterChain-配置授權規則" class="headerlink" title="SecurityFilterChain 配置授權規則"></a>SecurityFilterChain 配置授權規則</h3><p>利用 SecurityFilterChain 來配置每個資源可以請求訪問的對象,也就可以控管特定權限的人能去哪,</p><p>authorizeHttpRequests 方法,用來設定進入 SecurityFilterChain 的 Request 如何授權。</p><p>requestMatchers({http method}, “url”) 這個方法可傳入 API 路徑與 HTTP 方法,並且用<code>permitAll</code> ,<code>hasAnyAuthority</code> 或 <code>hasAuthority</code> 選擇可以訪問的權限 (設定範圍請見下方表格)。</p><p>由於這個自定義開啟後,預設的登入頁面也就不會有,所以要重新配置 formLogin 方法,是啟用先前的登入畫面,注意需要禁用 csrf 才可以讓 Postman 作用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><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_">SecurityConfig</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> SecurityFilterChain <span class="title function_">filterChain</span><span class="params">(HttpSecurity httpSecurity)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="keyword">return</span> httpSecurity</span><br><span class="line"> .csrf(customizer -> customizer.disable())</span><br><span class="line"> .authorizeHttpRequests((registry) -> registry</span><br><span class="line"> .requestMatchers(HttpMethod.POST, <span class="string">"/register"</span>, <span class="string">"/login"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/error"</span>, <span class="string">"/api/products/**"</span>).permitAll()</span><br><span class="line"> .requestMatchers(HttpMethod.GET, <span class="string">"/checkAuthentication"</span>).hasAnyAuthority(<span class="string">"ROLE_BUYER"</span>, <span class="string">"ROLE_SELLER"</span>, <span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .requestMatchers(<span class="string">"/api/users/**"</span>).hasAuthority(<span class="string">"ROLE_ADMIN"</span>)</span><br><span class="line"> .anyRequest().authenticated()</span><br><span class="line"> )</span><br><span class="line"> .formLogin(Customizer.withDefaults())</span><br><span class="line"> .build();</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>訪問權限方法作用範圍</p><table><thead><tr><th>方法名稱</th><th>作用</th></tr></thead><tbody><tr><td>permitAll</td><td>任何狀態都可以存取</td></tr><tr><td>hasAuthority</td><td>需登入且具備某個特定權限才能存取。</td></tr><tr><td>hasAnyAuthority</td><td>需登入且具備任一個權限就能存取。</td></tr><tr><td>authenticated</td><td>需登入才能存取。</td></tr></tbody></table><p>路徑匹配寫法</p><table><thead><tr><th>萬用字元</th><th>意義</th><th>範例寫法</th><th>適用</th><th>不適用</th></tr></thead><tbody><tr><td>*</td><td>0 到多個字元</td><td>/products/*</td><td>/products、/products/123</td><td>/products/123/draft</td></tr><tr><td>**</td><td>0 到多個階層</td><td>/products/**</td><td>任何「/products」開頭的路徑</td><td>-</td></tr><tr><td>?</td><td>1 個字元</td><td>/products/?</td><td>/products/1</td><td>/products/123、/products</td></tr><tr><td>?_ 或 _?</td><td>1 到多個字元</td><td>/products/?*</td><td>/products/1、/products/123</td><td>/products</td></tr></tbody></table><p>突然冒出一堆物件要配是跟方法要熟悉 😱,當初我入門的時候也是很混亂 (Security 其實就是以他底層難懂著名的…) 如果有興趣瞭解比較詳細 Security 對於登入認證的 FilterChain 運作流程可以去 <a href="https://oseanchen.github.io/2024/09/14/SpringBoot/Spring-Security-FilterChain-%E5%A6%82%E4%BD%95%E9%80%B2%E8%A1%8C%E7%99%BB%E5%85%A5%E8%AA%8D%E8%AD%89/?highlight=sec">Spring Security FilterChain 如何進行登入認證</a> 有詳細說明,也比較好了解後面的一些建置流程。</p><p>不過這些配置很多都是起初建立好之後基本上就比較不會再去調整,但是大家對於底層的運作還是要有基本的概念。下一篇會介紹如何接上自己資料庫,不去使用 <code>InMemoryUserDetailsManager</code> 預設使用者,我們也可以建立註冊和登入功能讓我們能記錄自己的使用者。</p><hr><p>參考資料:</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2021/06/spring-boot-security-authentication-and-authorization.html">新手工程師的程式教室 -【Spring Boot】第 17.1 課-初探 Spring Security 的認證與授權</a></li><li>Udemy - Java Spring Framework 6 with Spring Boot 3</li><li><a href="https://docs.spring.io/spring-security/reference/">Spring.io docs</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Spring Security </category>
</categories>
</entry>
<entry>
<title>UnitTest(5)- Mock Test</title>
<link href="/2024/10/07/UnitTest-5-Mock-Test/"/>
<url>/2024/10/07/UnitTest-5-Mock-Test/</url>
<content type="html"><![CDATA[<p>接續前一篇進行 Service 的測試,我們接序同一個情境針對 Product 的 CRUD,但是應用不同的寫法, 這邊會運用到 Mockito 這個套件來幫助我們應用 Mock 的虛構物件進行測試。</p><h2 id="關於-Mock"><a href="#關於-Mock" class="headerlink" title="關於 Mock"></a>關於 Mock</h2><ul><li>顧名思義 Mock 有模擬、假的意思,將這樣模擬概念應用到測試中,就是希望程式原先依賴的對象都可以透過 mock 方式建立一個假的對象去模擬真實行為。</li><li>先前介紹單元測試時,有提到特性是應該各單元測試互相獨立,不依賴其他外部系統,Mock Test 是一種單元測試方法,能專注於要測試的程式本身的運作中,避免測試一個方法卻要建構整個 bean 相關的依賴架構。</li></ul><p>一個系統架構會有多個類別之間互相依賴,且在 Spring Boot 中會都註冊成為 Bean,如下圖這樣多個 Class 互相依賴,Class A 需要依賴 B 和 C</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241001/201509778lUjZPHQvZ.png" alt="https://ithelp.ithome.com.tw/upload/images/20241001/201509778lUjZPHQvZ.png"></p><p>引入 mock 測試時,就可以創建假的對象類,替換掉真實的 Class B 和 C,並可以自己設定這個 mock 對象的參數和期望結果,讓我們可以專注在測試當前的 Class A 應該要得到的回傳或是要 Pass 的結果,不受其他的外部服務影響,就能提高測試效率並專注當前測試 Class 的運作。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241001/20150977LbyWkqvCCD.png" alt="https://ithelp.ithome.com.tw/upload/images/20241001/20150977LbyWkqvCCD.png"></p><h2 id="Mockito-套件"><a href="#Mockito-套件" class="headerlink" title="Mockito 套件"></a>Mockito 套件</h2><p>Java 的 mock 測試框架, Java 中目前主流的 mock 測試工具有 Mockito、JMock、EasyMock..等, SpringBoot 目前內建的是使用 Mockito 框架。和先前 Junit 一樣,只要引用 spring-boot -starter- test 這個 dependency 就包含在裡面。</p><h2 id="測試重點過程及用法"><a href="#測試重點過程及用法" class="headerlink" title="測試重點過程及用法"></a>測試重點過程及用法</h2><ul><li>建立模擬物件:Mockito 可以建立模擬類別,並且標示模擬類被注入位置</li><li>控制行為:可定義模擬物件行為,如方法返回值、拋出異常等,且都是我們預期的行為,不須真實運作,可以避免實體運作帶來的影響(資料庫操作資料或實體當下無法運作)</li><li>驗證互動:可以驗證模擬物件是如我們的預期進行控制行為,驗證互動次數、觸發順序等等。</li></ul><h3 id="模擬物件建立"><a href="#模擬物件建立" class="headerlink" title="模擬物件建立"></a>模擬物件建立</h3><p><code>@Mock</code> :標註建立一個模擬物件,通常為資料庫溝通區塊(Dao)或是目前不希望調用真實實體的物件</p><p><code>@InjectMocks</code> :標註模擬物件注入位置,如果今天要測試 Service 會應用到某個 Dao 來進行資料溝通,就是註記在該 Service 類別上,就會把 <code>@Mock</code> 的物件注入</p><h3 id="控制行為"><a href="#控制行為" class="headerlink" title="控制行為"></a>控制行為</h3><p>使用 when() 這個方法可以控制當特定方法觸發時回傳我們要的結果。</p><p>下面是幾種常用的寫法:</p><ul><li>一般驗證:</li></ul><p><code>when(方法).thenReturn(自訂回傳結果)</code></p><ul><li>拋出 Exception 驗證:</li></ul><p><code>when(方法).thenThrow(Exception Class)</code></p><ul><li>沒有回傳驗證:</li></ul><p><code>doNothing().when(方法)</code></p><h3 id="驗證互動"><a href="#驗證互動" class="headerlink" title="驗證互動"></a>驗證互動</h3><p>使用 verify() 來驗證模擬對象的方法是否按照預期被調用。</p><p>下面是幾種常用的寫法:<br><code>verify(被呼叫類, times(呼叫次數)).被呼叫類使用方法()</code></p><p>如果不寫 times 就是預設<strong>驗證被呼叫類呼叫使用方法一次</strong></p><p>上面控制和驗證如果需要限制為特定值或是特定類別</p><p><code>when(productRepository.save(any(Product.class))).thenReturn(product);</code></p><p>如果 mock 會被呼叫到的方法有順序驗證,可使用 inOrder 物件來讓 mockito 依照順序驗證</p><p>先指定需要驗證的 mock 物件裝入 inOrder,再透過 inOrder.verify() 來進行,類似作法如下</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">InOrder</span> <span class="variable">inOrder</span> <span class="operator">=</span> inOrder(productTestDao);</span><br><span class="line"></span><br><span class="line">inOrder.verify(productTestDao, times(<span class="number">1</span>)).findById(id);</span><br><span class="line">inOrder.verify(productTestDao, times(<span class="number">1</span>)).save(any(ProductTest.class));</span><br></pre></td></tr></table></figure><h3 id="Mockito-使用注意"><a href="#Mockito-使用注意" class="headerlink" title="Mockito 使用注意"></a><strong>Mockito 使用注意</strong></h3><ul><li>不能 mock 靜態方法</li><li>不能 mock private 方法</li><li>不能 mock final class</li></ul><h2 id="Mock-測試應用"><a href="#Mock-測試應用" class="headerlink" title="Mock 測試應用"></a>Mock 測試應用</h2><p>如果以先前我們測試 ProductService 的部分來畫一個概念圖,原本要測試需要@Autowired ProductDao,並且會直接更動資料庫,如果應用 Mock 將 ProductDao 模擬成 MockProductDao 來模擬和資料庫溝通的操作或回傳,就可以來協助我們進行這些行為的測試。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20241001/20150977WKRi57l9ip.png" alt="https://ithelp.ithome.com.tw/upload/images/20241001/20150977WKRi57l9ip.png"></p><p>Mockito 引入測試有兩種寫法:</p><ol><li>測試類要加上 <code>@ExtendWith(MockitoExtension.class)</code></li><li>每個測試類都要初始化,所以可以用 @BeforeEach 配合初始化 <code>MockitoAnnotations.*openMocks*(this)</code></li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用 @ExtendWith 或 @BeforeEach 其中之一來初始化 MockitoExtension</span></span><br><span class="line"><span class="comment">// @ExtendWith(MockitoExtension.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductTestServiceTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Mock</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestDao productTestDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@InjectMocks</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestService productTestService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BeforeEach</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setup</span><span class="params">()</span> {</span><br><span class="line"> MockitoAnnotations.openMocks(<span class="built_in">this</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testReadMockProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// mockProduct</span></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">mockProduct</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProductTest</span>();</span><br><span class="line"> mockProduct.setId(<span class="number">1L</span>);</span><br><span class="line"> mockProduct.setName(<span class="string">"Switch 2"</span>);</span><br><span class="line"> mockProduct.setPrice(<span class="number">11999.99</span>);</span><br><span class="line"> mockProduct.setDescription(<span class="string">"任天堂熱門的手持遊戲機"</span>);</span><br><span class="line"></span><br><span class="line"> when(productTestDao.findById(<span class="number">1L</span>)).thenReturn(Optional.of(mockProduct));</span><br><span class="line"></span><br><span class="line"> Optional<ProductTest> product = productTestService.getProductById(<span class="number">1L</span>);</span><br><span class="line"> assertTrue(product.isPresent());</span><br><span class="line"> assertEquals(<span class="number">11999.99</span>, product.get().getPrice());</span><br><span class="line"> assertEquals(<span class="string">"任天堂熱門的手持遊戲機"</span>, product.get().getDescription());</span><br><span class="line"> verify(productTestDao, times(<span class="number">1</span>)).findById(<span class="number">1L</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testUpdateMockProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> <span class="number">2L</span>;</span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">mockProduct</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProductTest</span>();</span><br><span class="line"> mockProduct.setId(id);</span><br><span class="line"> mockProduct.setName(<span class="string">"PlayStation 5 pro discount"</span>);</span><br><span class="line"> mockProduct.setPrice(<span class="number">21999.99</span>);</span><br><span class="line"> mockProduct.setDescription(<span class="string">"索尼的次世代遊戲主機 - 降價促銷"</span>);</span><br><span class="line"></span><br><span class="line"> when(productTestDao.findById(id)).thenReturn(Optional.of(mockProduct));</span><br><span class="line"> when(productTestDao.save(any(ProductTest.class))).thenReturn(mockProduct);</span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">updatedMockProduct</span> <span class="operator">=</span> productTestService.updateProductById(id, mockProduct);</span><br><span class="line"></span><br><span class="line"> assertNotNull(updatedMockProduct);</span><br><span class="line"> assertEquals(id, updatedMockProduct.getId());</span><br><span class="line"> assertEquals(<span class="string">"PlayStation 5 pro discount"</span>, updatedMockProduct.getName());</span><br><span class="line"> assertEquals(<span class="number">21999.99</span>, updatedMockProduct.getPrice());</span><br><span class="line"></span><br><span class="line"> <span class="type">InOrder</span> <span class="variable">inOrder</span> <span class="operator">=</span> inOrder(productTestDao);</span><br><span class="line"></span><br><span class="line"> inOrder.verify(productTestDao, times(<span class="number">1</span>)).findById(id);</span><br><span class="line"> inOrder.verify(productTestDao, times(<span class="number">1</span>)).save(any(ProductTest.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testDeleteMockProduct</span><span class="params">()</span> {</span><br><span class="line"> doNothing().when(productTestDao).deleteById(<span class="number">1L</span>);</span><br><span class="line"> productTestService.deleteProductById(<span class="number">1L</span>);</span><br><span class="line"> verify(productTestDao, times(<span class="number">1</span>)).deleteById(<span class="number">1L</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCreateMockProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 新增一個新的產品</span></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">mockProduct</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProductTest</span>();</span><br><span class="line"> mockProduct.setId(<span class="number">6L</span>);</span><br><span class="line"> mockProduct.setName(<span class="string">"三星 Galaxy S22"</span>);</span><br><span class="line"> mockProduct.setPrice(<span class="number">20999.99</span>);</span><br><span class="line"> mockProduct.setDescription(<span class="string">"三星旗艦智慧型手機"</span>);</span><br><span class="line"></span><br><span class="line"> when(productTestDao.save(mockProduct)).thenReturn(mockProduct);</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">savedProduct</span> <span class="operator">=</span> productTestService.saveProduct(mockProduct);</span><br><span class="line"></span><br><span class="line"> assertNotNull(savedProduct);</span><br><span class="line"> assertNotNull(savedProduct.getId());</span><br><span class="line"> assertEquals(<span class="string">"三星 Galaxy S22"</span>, savedProduct.getName());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p>參考資料:</p><ul><li><a href="https://kucw.io/blog/2020/2/spring-unit-test-mockito/">SpringBoot - 單元測試工具 Mockito</a></li><li><a href="https://blog.csdn.net/footless_bird/article/details/130690055">mockito(模擬測試)框架基本使用指南</a></li><li><a href="%5Bhttps://vivifish.medium.com/java-%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6%E5%B7%A5%E5%85%B7-mockito-e5f0ce93579d%5D(https://vivifish.medium.com/java-%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6%E5%B7%A5%E5%85%B7-mockito-e5f0ce93579d)">Java Unit Test — Mockito</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Unit Test </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> JUnit 5 </tag>
<tag> Mockito </tag>
</tags>
</entry>
<entry>
<title>UnitTest(4)- Service/Dao 層測試撰寫</title>
<link href="/2024/10/07/UnitTest-4-Service-Dao-%E5%B1%A4%E6%B8%AC%E8%A9%A6%E6%92%B0%E5%AF%AB/"/>
<url>/2024/10/07/UnitTest-4-Service-Dao-%E5%B1%A4%E6%B8%AC%E8%A9%A6%E6%92%B0%E5%AF%AB/</url>
<content type="html"><![CDATA[<p>這一篇我們來針對 Service / Dao 層進行測試的撰寫吧,以 MVC 架構下算是 Model 的部分會比較常進行測試程式的撰寫,因為牽涉到主要業務邏輯的運作,所以會比起 Controller 和 View 來說比較常需要進行測試,以 MVC 架構之下我認為個別需要測試的頻率及重點表整理如下</p><table><thead><tr><th></th><th>Model(Service/Dao)</th><th>Controller</th><th>View</th></tr></thead><tbody><tr><td>測試頻率</td><td>高</td><td>中</td><td>低</td></tr><tr><td>重點</td><td>業務邏輯</td><td></td><td></td></tr></tbody></table><p>數據驗證<br>狀態管理 | 請求/錯誤處理驗證<br>rounting 邏輯<br>數據轉換(DTO 轉換) | 數據綁定<br>模板渲染 |</p><p>所以這篇來主要針對最高頻率進行測試部分作介紹,Controller 層會應用到的寫法又會不太一樣,之後有機會可以再補充。</p><h2 id="單元測試準備"><a href="#單元測試準備" class="headerlink" title="單元測試準備"></a>單元測試準備</h2><p>簡單建立一個之前介紹 Jpa 關聯時使用的類似欄位架構</p><p>db schema and data</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> product (</span><br><span class="line"> id <span class="type">BIGINT</span> AUTO_INCREMENT <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line"> name <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> price <span class="type">DECIMAL</span>(<span class="number">10</span>, <span class="number">2</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"> description <span class="type">VARCHAR</span>(<span class="number">255</span>)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> product (name, price, description) <span class="keyword">VALUES</span></span><br><span class="line">(<span class="string">'Switch 2'</span>, <span class="number">11999.99</span>, <span class="string">'任天堂熱門的手持遊戲機'</span>),</span><br><span class="line">(<span class="string">'PlayStation 5 pro'</span>, <span class="number">24999.99</span>, <span class="string">'索尼的次世代遊戲主機'</span>),</span><br><span class="line">(<span class="string">'Xbox Series X'</span>, <span class="number">10999.99</span>, <span class="string">'微軟高效能的遊戲主機'</span>),</span><br><span class="line">(<span class="string">'iPhone 16'</span>, <span class="number">26999.99</span>, <span class="string">'蘋果最新型的智慧型手機'</span>),</span><br><span class="line">(<span class="string">'Dell XPS 13'</span>, <span class="number">35999.99</span>, <span class="string">'戴爾輕薄高效能的筆記型電腦'</span>);</span><br></pre></td></tr></table></figure><p>ProductTest Entity</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table(name = "product")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</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 name;</span><br><span class="line"> <span class="keyword">private</span> Double price;</span><br><span class="line"> <span class="keyword">private</span> String description;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Getters and Setters</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>ProductTestDao</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ProductTestDao</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><ProductTest, Long> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Optional<ProductTest> <span class="title function_">findByName</span><span class="params">(String name)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>ProductTestService</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductTestService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestDao productTestDao;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> ProductTest <span class="title function_">saveProduct</span><span class="params">(ProductTest productTest)</span> {</span><br><span class="line"> <span class="keyword">return</span> productTestDao.save(productTest);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Optional<ProductTest> <span class="title function_">getProductById</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="keyword">return</span> productTestDao.findById(id);</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_">deleteProduct</span><span class="params">(Long id)</span> {</span><br><span class="line"> productTestDao.deleteById(id);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="進行-Service-測試撰寫"><a href="#進行-Service-測試撰寫" class="headerlink" title="進行 Service 測試撰寫"></a>進行 Service 測試撰寫</h2><p>先加入測試讀取的方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="comment">//@DataJpaTest</span></span><br><span class="line"><span class="comment">//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 使用實際的資料庫</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductTestServiceTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestDao productTestDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestService productTestService;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testReadExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// id = 1 , name = "Switch 2"</span></span><br><span class="line"> Optional<ProductTest> product = productTestService.getProductById(<span class="number">1L</span>);</span><br><span class="line"> assertTrue(product.isPresent());</span><br><span class="line"> assertEquals(<span class="number">11999.99</span>, product.get().getPrice());</span><br><span class="line"> assertEquals(<span class="string">"任天堂熱門的手持遊戲機"</span>, product.get().getDescription());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>仔細看和先前的測試範例不同,我們需要在測試 Class 上面加入 <code>@SpringBootTest</code> 因為先前是直接針對同一包程式內 Class 進行測試,沒有引用到被 Spring Boot 管理的 Bean 進行,但現在要測試的 Service 由於已經註冊為 Bean 交由 SpringBoot 管理,所以需要這個註解告知 SpringBoot 我們才能使用 @Autowired 將 TestProductService 引用進來</p><p>下面再加入新增、修改、刪除的測試</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><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_">testUpdateExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// id = 2, name = "PlayStation 5 pro"</span></span><br><span class="line"> Optional<ProductTest> productOptional = productTestService.getProductById(<span class="number">2L</span>);</span><br><span class="line"> assertTrue(productOptional.isPresent());</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> productOptional.get();</span><br><span class="line"> productTest.setPrice(<span class="number">23999.99</span>);</span><br><span class="line"> productTest.setDescription(<span class="string">"索尼的次世代遊戲主機 - 降價促銷"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">updatedProductTest</span> <span class="operator">=</span> productTestDao.save(productTest);</span><br><span class="line"></span><br><span class="line"> assertEquals(<span class="number">23999.99</span>, updatedProductTest.getPrice());</span><br><span class="line"> assertEquals(<span class="string">"索尼的次世代遊戲主機 - 降價促銷"</span>, updatedProductTest.getDescription());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testDeleteExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// id = 5, name = "Dell XPS 13"</span></span><br><span class="line"> Optional<ProductTest> productOptional = productTestService.getProductById(<span class="number">5L</span>);</span><br><span class="line"> assertTrue(productOptional.isPresent());</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> productOptional.get();</span><br><span class="line"> <span class="type">Long</span> <span class="variable">productId</span> <span class="operator">=</span> productTest.getId();</span><br><span class="line"></span><br><span class="line"> productTestDao.deleteById(productId);</span><br><span class="line"></span><br><span class="line"> Optional<ProductTest> deletedProduct = productTestService.getProductById(productId);</span><br><span class="line"> assertFalse(deletedProduct.isPresent());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testCreateNewProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 新增一個新的產品</span></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProductTest</span>();</span><br><span class="line"> productTest.setName(<span class="string">"三星 Galaxy S22"</span>);</span><br><span class="line"> productTest.setPrice(<span class="number">20999.99</span>);</span><br><span class="line"> productTest.setDescription(<span class="string">"三星旗艦智慧型手機"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">savedProductTest</span> <span class="operator">=</span> productTestService.saveProduct(productTest);</span><br><span class="line"></span><br><span class="line"> assertNotNull(savedProductTest);</span><br><span class="line"> assertNotNull(savedProductTest.getId());</span><br><span class="line"> assertEquals(<span class="string">"三星 Galaxy S22"</span>, savedProductTest.getName());</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>這邊需要注意有使用到 <code>@Transactional</code> 註解來幫我們進行事務管理,這個註解會在我們進行完該項測試後將所有資料庫有進行的操作都 rollback 恢復至原本測試前,可以避免影響資料庫內的資料。</p><h2 id="補充-DataJpaTest"><a href="#補充-DataJpaTest" class="headerlink" title="補充 @DataJpaTest"></a>補充 @DataJpaTest</h2><p>因為剛好在查一些測試相關資料找到這個測試註解的應用,之前撰寫時沒有用過,如果你的測試只是針對 Jpa 的操作(CRUD)就可以使用,可以輕量化測試 JPA 的功能,只加載相關的資源來進行測試,如果沒有接資料庫也會提供嵌入式資料庫(H2),如果有需要注入 Service, Controller 或是其他非 JPA Repository 層的測試就沒辦法執行。</p><p>這邊也提供一個使用這個註解的版本,因為我有加入自己的資料庫,所以需要多使用 @AutoConfigureTestDatabase 這個註解來告知使用自己的資料庫</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@DataJpaTest</span></span><br><span class="line"><span class="meta">@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)</span> <span class="comment">// 使用實際的資料庫</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductTestServiceTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductTestDao productTestDao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testReadExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 假設資料庫已經有 "Switch 2" 這筆資料</span></span><br><span class="line"> Optional<ProductTest> retrievedProduct = productTestDao.findByName(<span class="string">"Switch 2"</span>);</span><br><span class="line"> assertTrue(retrievedProduct.isPresent());</span><br><span class="line"> assertEquals(<span class="number">11999.99</span>, retrievedProduct.get().getPrice());</span><br><span class="line"> assertEquals(<span class="string">"任天堂熱門的手持遊戲機"</span>, retrievedProduct.get().getDescription());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testUpdateExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 假設資料庫已經有 "PlayStation 5 pro" 這筆資料</span></span><br><span class="line"> Optional<ProductTest> productOptional = productTestDao.findByName(<span class="string">"PlayStation 5 pro"</span>);</span><br><span class="line"> assertTrue(productOptional.isPresent());</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> productOptional.get();</span><br><span class="line"> productTest.setPrice(<span class="number">23999.99</span>);</span><br><span class="line"> productTest.setDescription(<span class="string">"索尼的次世代遊戲主機 - 降價促銷"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">updatedProductTest</span> <span class="operator">=</span> productTestDao.save(productTest);</span><br><span class="line"></span><br><span class="line"> assertEquals(<span class="number">23999.99</span>, updatedProductTest.getPrice());</span><br><span class="line"> assertEquals(<span class="string">"索尼的次世代遊戲主機 - 降價促銷"</span>, updatedProductTest.getDescription());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testDeleteExistingProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 假設資料庫已經有 "Dell XPS 13" 這筆資料</span></span><br><span class="line"> Optional<ProductTest> productOptional = productTestDao.findByName(<span class="string">"Dell XPS 13"</span>);</span><br><span class="line"> assertTrue(productOptional.isPresent());</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> productOptional.get();</span><br><span class="line"> <span class="type">Long</span> <span class="variable">productId</span> <span class="operator">=</span> productTest.getId();</span><br><span class="line"></span><br><span class="line"> productTestDao.deleteById(productId);</span><br><span class="line"></span><br><span class="line"> Optional<ProductTest> deletedProduct = productTestDao.findById(productId);</span><br><span class="line"> assertFalse(deletedProduct.isPresent());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><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_">testCreateNewProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 新增一個新的產品</span></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">productTest</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProductTest</span>();</span><br><span class="line"> productTest.setName(<span class="string">"三星 Galaxy S22"</span>);</span><br><span class="line"> productTest.setPrice(<span class="number">20999.99</span>);</span><br><span class="line"> productTest.setDescription(<span class="string">"三星旗艦智慧型手機"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">ProductTest</span> <span class="variable">savedProductTest</span> <span class="operator">=</span> productTestDao.save(productTest);</span><br><span class="line"></span><br><span class="line"> assertNotNull(savedProductTest);</span><br><span class="line"> assertNotNull(savedProductTest.getId());</span><br><span class="line"> assertEquals(<span class="string">"三星 Galaxy S22"</span>, savedProductTest.getName());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>這邊大致展示基本要進行 Service 層測試要注意的東西,不過這樣的測試寫法會需要依賴到資料庫操作,以及所有該 Service 需要依賴的其他物件。有時候我們可能還沒完成 dao 或是其他依賴的區域時會沒辦法進行測試,下一篇會介紹到 Mock 就可以解決這樣的情形。</p><hr><p>參考資料:</p><ul><li>Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)</li><li><a href="https://junit.org/junit5/">JUnit5</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Unit Test </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> JUnit 5 </tag>
</tags>
</entry>
<entry>
<title>UnitTest(3)- Junit 5 常用註解</title>
<link href="/2024/10/06/UnitTest-3-Junit-5-%E5%B8%B8%E7%94%A8%E8%A8%BB%E8%A7%A3/"/>
<url>/2024/10/06/UnitTest-3-Junit-5-%E5%B8%B8%E7%94%A8%E8%A8%BB%E8%A7%A3/</url>
<content type="html"><![CDATA[<h2 id="Junit-5-常用註解"><a href="#Junit-5-常用註解" class="headerlink" title="Junit 5 常用註解"></a>Junit 5 常用註解</h2><ul><li>@Test:標註方法為測試程試</li><li>@BeforeEach:每項測試項目開始前都會執行一次</li><li>@AfterEach:每項測試項目結束都會執行一次</li><li>@BeforeAll:測試開始之前執行一次</li><li>@AfterAll:測試結束之後執行一次</li><li>@Disabled:不列入測試項目</li><li>@DisplayName:測試結果顯示特定名稱</li></ul><p>延伸先前計算相關的方法測試,我們補齊加減乘除各種方法跟測試</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Calculator</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">return</span> x + y;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">subtract</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">return</span> x - y;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">multiply</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">return</span> x * y;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">divide</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">if</span> (y == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">"Cannot divide by zero"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> x / y;</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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CalculatorTest</span> {</span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testAdd</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.add(<span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSubtract</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.subtract(<span class="number">3</span>, <span class="number">1</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testMultiply</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.multiply(<span class="number">3</span>, <span class="number">2</span>);</span><br><span class="line"> assertEquals(<span class="number">6</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testDivide</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.divide(<span class="number">2</span>, <span class="number">2</span>);</span><br><span class="line"> assertEquals(<span class="number">1</span>, result, <span class="string">"result should be 1"</span>);</span><br><span class="line"> assertThrows(IllegalArgumentException.class, () -> {</span><br><span class="line"> calc.divide(<span class="number">2</span>, <span class="number">0</span>);</span><br><span class="line"> });</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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CalculatorTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Calculator calc;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BeforeEach</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">initCalculator</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"-----每個項目執行前我都執行一次 @BeforeEach-----"</span>);</span><br><span class="line"> calc = <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AfterEach</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">tearDownCalculator</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"-----每個項目執行完我都執行一次 @AfterEach-----"</span>);</span><br><span class="line"> calc = <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BeforeAll</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">beforeAll</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"=====全部開始前是我 @BeforeAll====="</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AfterAll</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">afterAll</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"=====全部結束後是我 @AfterAll====="</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="meta">@DisplayName("加法測試")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testAdd</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"執行 testAdd()"</span>);</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.add(<span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="meta">@DisplayName("減法測試")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSubtract</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"執行 testSubtract()"</span>);</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.subtract(<span class="number">3</span>, <span class="number">1</span>);</span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testMultiply</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"執行 testMultiply()"</span>);</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.multiply(<span class="number">3</span>, <span class="number">2</span>);</span><br><span class="line"> assertEquals(<span class="number">6</span>, result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="meta">@Disabled</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testDivide</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"執行 testDivide()"</span>);</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.divide(<span class="number">2</span>, <span class="number">2</span>);</span><br><span class="line"> assertEquals(<span class="number">1</span>, result, <span class="string">"result should be 1"</span>);</span><br><span class="line"> assertThrows(IllegalArgumentException.class, () -> {</span><br><span class="line"> calc.divide(<span class="number">2</span>, <span class="number">0</span>);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><p>可以觀察到前面的方法因為都需要初始化 Calculator ,所以可以運用到 @BeforeEach 這個註解來幫我們讓程式碼更簡潔。</p></li><li><p>每個測試結束後也可以加入 @AfterEach 來進行特定處理,這邊就假設我們需要釋出空間把 calc 清空</p></li><li><p>依據測試需要也可以用 @BeforeAll @AfterAll 可以在最一開始及最後進行一些處理,不管你執行的是單項測試或是全部一起,這兩種都會最先和最後被執行。</p></li><li><p>@DisplayName 則是可以直接改變執行結果顯示的測試名稱</p></li><li><p>@Disabled 會跳過該項目,不被執行,但還是會在執行結果中顯示有被禁用</p></li><li></li></ul><p>把每個執行都加上 log 之後可以看一下執行結果</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240927/201509779ppBP4lUyD.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/201509779ppBP4lUyD.png"></p><p>可以看到對應的 log 就知道那些執行的順序,是否回想起這就是 AOP 的應用了!! 其實在測試撰寫的時候也都會用到這樣的概念。</p><hr><p>參考資料:</p><ul><li><a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5 User Guide</a></li><li><a href="https://waynecheng.coderbridge.io/2021/02/13/Unit-Test/">單元測試 (Unit Test)</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Unit Test </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> JUnit 5 </tag>
</tags>
</entry>
<entry>
<title>UnitTest(2) - Junit 5 測試方法與斷言應用</title>
<link href="/2024/10/06/UnitTest-2-Junit-5-%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95%E8%88%87%E6%96%B7%E8%A8%80%E6%87%89%E7%94%A8/"/>
<url>/2024/10/06/UnitTest-2-Junit-5-%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95%E8%88%87%E6%96%B7%E8%A8%80%E6%87%89%E7%94%A8/</url>
<content type="html"><![CDATA[<p>上一篇簡單說明基本測試的概念跟要怎麼建立測試程式在 Spring Boot 之後,這邊就來接著提供一些測試的設計原則和一些方法。</p><h2 id="3A-原則-3A-Pattern"><a href="#3A-原則-3A-Pattern" class="headerlink" title="3A 原則 (3A Pattern)"></a>3A 原則 (3A Pattern)</h2><ul><li>Arrange: 初始化物件、要用到的參數</li><li>Act: 呼叫要測試的方法</li><li>Assert: 驗證測試結果</li></ul><p>如果以上一篇的範例來看,我們要測試加法的功能會這樣設計就是應用到 3A 原則</p><ul><li>Arrange:首先就是要先建立好測試的環境,把要用到的物件初始化,建立好相關的參數,也就是我們引入 Calculator 這個物件進入並且建立好要執行加法的參數。</li><li>Act:接著呼叫測試的方法並且帶入我們要的參數</li><li>Assert:利用斷言的方法來驗證,是否符合預期</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CalculatorTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testAdd</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// Arange</span></span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">x</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> <span class="type">int</span> <span class="variable">y</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Act</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.add(x, y);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Assert</span></span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</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><table><thead><tr><th><strong>斷言方法</strong></th><th><strong>說明</strong></th></tr></thead><tbody><tr><td>assertEquals(A, B)</td><td>預期 A 和實際 B 值相等,等同使用 equals() 方法比較,A和B 是基本類型</td></tr><tr><td>assertNotEquals(A, B)</td><td>預期 A 和實際 B 值不相等</td></tr><tr><td>assertTrue(A)</td><td>A 條件判斷為 true</td></tr><tr><td>assertFalse(A)</td><td>A 條件判斷為 false</td></tr><tr><td>assertNull(A)</td><td>A 物件為空值</td></tr><tr><td>assertNotNull(A)</td><td>A 物件不為空值</td></tr><tr><td>assertThrows(exception, method)</td><td>預期拋出 exception,method 為執行的方法,當執行該方法拋出對應的 exception 就會通過</td></tr></tbody></table><p>如果接著我們打算擴充 Calculator 並且進行測試,多一個除法的功能,並且除以0會拋出 IllegalArgumentException</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">divide</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">if</span> (y == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">"不能除以 0"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> x / y;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>這邊就可以應用到那面那些方法,這邊也加入 message 可以協助我們沒通過出錯時會跳出對應的訊息,像是我這邊在 assertEquals, assertNotEquals 後面加入的訊息</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testDivide</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.divide(<span class="number">2</span>,<span class="number">2</span>);</span><br><span class="line"> assertEquals(<span class="number">1</span>, result, <span class="string">"result should be 1"</span>);</span><br><span class="line"> assertNotEquals(<span class="number">0</span>, result, <span class="string">"result should not be 0"</span>);</span><br><span class="line"> assertTrue(result > <span class="number">0</span>);</span><br><span class="line"> assertFalse(result != <span class="number">1</span>);</span><br><span class="line"> assertThrows(IllegalArgumentException.class, () -> {</span><br><span class="line"> calc.divide(<span class="number">2</span>,<span class="number">0</span>);</span><br><span class="line"> });</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>正常上面的 assert 都會通過。<br>如果 assertEquals 有出錯,假設我們把 1 改成 2 <code>assertEquals(2, result, "result should be 1");</code><br>就可以看到 console 有把這段錯誤提示印出來<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977O6veBDLhQF.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977O6veBDLhQF.png"></p><p>特別要說明就是 <code>assertThrows()</code> 這個方法需要放入第1個參數是 Exception.class,第2個用 lamda 表達式,把要執行驗證的方法加入到 <code>( )→{ }</code> 的 { } 裡面。</p><p>大致上基本的設計原則還有 Junit 5 的斷言用法就介紹到這邊,下一篇會介紹進階使用 Mock 的部分。</p><h2 id="補充-Intellij-test-coverage"><a href="#補充-Intellij-test-coverage" class="headerlink" title="補充 Intellij test coverage"></a>補充 Intellij test coverage</h2><p>這邊可以小補充一下 Intellij 有提供一個功能可以知道我們測試的覆蓋程度,這對於撰寫測試時式一個很好的指標參考,如果你的方法裡面有很多判斷或是條件,他會告訴你目前寫的測試大概涵蓋多少,如果把我們的剛剛寫的範例拿來測試。</p><p>在要執行的方法點選右鍵,可以看到用 Run test with Coverage<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977SzhUizxYoa.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977SzhUizxYoa.png"></p><p>可以看到有顯示目前覆蓋的百分比,只有 50 %<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977wriwyLzW9W.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977wriwyLzW9W.png"></p><p>可以直接點擊右方項目跳到被測試的區域,並且給予顏色標記哪些有被測試到。<br>這邊注意到因為我們點 testDivide 所以沒有執行到 testAdd,所以他也標記出 add 方法沒被測。<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/201509779yM3PJCf5U.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/201509779yM3PJCf5U.png"></p><p>解開註解後,包含 Exception 的測試後,確實覆蓋率有提升<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977L8DWvbNT4L.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977L8DWvbNT4L.png"></p><p>剩下沒滿足是因為我們只有執行 testDivide ,如果點選整個 CalculatorTest 的測試就會達到 100%<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977BHV1PYkPLZ.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977BHV1PYkPLZ.png"></p><hr><p>參考資料:</p><ul><li><a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5 User Guide</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Unit Test </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> JUnit 5 </tag>
</tags>
</entry>
<entry>
<title>UnitTest(1) - Junit 5 基本介紹及建立測試程式</title>
<link href="/2024/10/04/UnitTest-1-Junit-5-%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%BB%BA%E7%AB%8B%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F/"/>
<url>/2024/10/04/UnitTest-1-Junit-5-%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%BB%BA%E7%AB%8B%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F/</url>
<content type="html"><![CDATA[<p>相信有許多人知道開發有一個重要的環節就是進行測試,不管是透過直接操作功能、打 API 或是程式內部執行測是程式等等都是測試的一種,今天要介紹的單元測試就是指測試程式碼最小功能單位的運作。</p><h2 id="單元測試目的"><a href="#單元測試目的" class="headerlink" title="單元測試目的"></a>單元測試目的</h2><ul><li>測試功能運作邏輯</li><li>確保不會改壞程式</li></ul><h2 id="單元測試特性"><a href="#單元測試特性" class="headerlink" title="單元測試特性"></a>單元測試特性</h2><ul><li>一次只測一個功能點或一個 api</li><li>可被自動化運行</li><li>各單元測試互相獨立,不依賴</li></ul><h2 id="JUnit-5-介紹"><a href="#JUnit-5-介紹" class="headerlink" title="JUnit 5 介紹"></a>JUnit 5 介紹</h2><p>Java 常見的開源測試框架,目前最新版本為 JUnit5,使用了 Java 8 及更高版本的 Java 語言特性。 進行單元測試的可讀性更強,編寫更容易,且可以輕鬆擴充。</p><h3 id="版本相容注意"><a href="#版本相容注意" class="headerlink" title="版本相容注意"></a>版本相容注意</h3><table><thead><tr><th><strong>Spring Boot 版本</strong></th><th><strong>相容的 JUnit 版本</strong></th></tr></thead><tbody><tr><td>≤ 2.1</td><td>JUnit 4</td></tr><tr><td>2.2, 2.3</td><td>JUnit 4、JUnit 5</td></tr><tr><td>≥ 2.4</td><td>JUnit 5</td></tr></tbody></table><h2 id="Spring-Boot-導入-JUnit"><a href="#Spring-Boot-導入-JUnit" class="headerlink" title="Spring Boot 導入 JUnit"></a>Spring Boot 導入 JUnit</h2><p>通常只要建立一般 web 的專案就會導入 spring-boot-starter-test 裡面就會包含 Junit 測試框架了</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-test<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">scope</span>></span>test<span class="tag"></<span class="name">scope</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><p>導入注意</p><ul><li>測試程式放到 test 資料夾</li><li>class 名稱保持原本 class 加上 Test 為結尾</li><li>測試的 package 結構和原本一致 (下圖紅框及橘框的結構)## 建立一個測試程式</li></ul><p>先建立一個 Calculator 類別並有一個 add 方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Calculator</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">return</span> x + y;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>依照前面導入注意在 test 資料夾下面建構與 main 相同的結構,並建立一個 class 並依照對應的命名規則來建立。</p><p>或是你使用 intellij 可以對這個個類別或是方法按下 Alt + Insert (Windows) 或 command + N (Mac) 來叫出特殊功能選單並選 test 就可以看到編輯器會自動幫我們建立對應的 package 結構至 test 內,並且可以選擇自動產生哪些對應內部 class 有的方法至新的測試程式內。<br><img src="https://ithelp.ithome.com.tw/upload/images/20240926/20150977G3JJ1bKSQt.png" alt="https://ithelp.ithome.com.tw/upload/images/20240926/20150977G3JJ1bKSQt.png"></p><h2 id="建立一個測試程式"><a href="#建立一個測試程式" class="headerlink" title="建立一個測試程式"></a>建立一個測試程式</h2><p>先建立一個 Calculator 類別並有一個 add 方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Calculator</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> {</span><br><span class="line"> <span class="keyword">return</span> x + y;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>依照前面導入注意在 test 資料夾下面建構與 main 相同的結構,並建立一個 class 並依照對應的命名規則來建立。</p><p>或是你使用 intellij 可以對這個個類別或是方法按下 Alt + Insert (Windows) 或 command + N (Mac) 來叫出特殊功能選單並選 test 就可以看到編輯器會自動幫我們建立對應的 package 結構至 test 內,並且可以選擇自動產生哪些對應內部 class 有的方法至新的測試程式內。<br><img src="https://ithelp.ithome.com.tw/upload/images/20240926/20150977bWVt0xm0OU.png" alt="https://ithelp.ithome.com.tw/upload/images/20240926/20150977bWVt0xm0OU.png"></p><p>建立測試程式測試 add 方法,要在方法上面加入 @Test 告訴 Spring Boot 這是測試程式</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CalculatorTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testAdd</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Calculator</span> <span class="variable">calc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"> <span class="type">int</span> <span class="variable">x</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"> <span class="type">int</span> <span class="variable">y</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> calc.add(x, y);</span><br><span class="line"></span><br><span class="line"> assertEquals(<span class="number">2</span>, result);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>assertEquals() 就是斷言方法,可以幫助我們驗證預期結果,這邊就先確定測是可以正常運行,我們放入第 1 個參數是預期值及第 2 個是實際結果,如果帶入的兩個值相等就會通過測試。<br><img src="https://ithelp.ithome.com.tw/upload/images/20240926/20150977L9TZTv5s5U.png" alt="https://ithelp.ithome.com.tw/upload/images/20240926/20150977L9TZTv5s5U.png"></p><p>如果有寫好多個測試方法想要一次跑完,就可以點選 class 左方的圖示一次執行全部的測試<br><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977h7RY71N0Z7.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977h7RY71N0Z7.png"></p><p>這一篇先完成基本的測試方法建立,下一篇會詳細介紹更多的斷言用法。</p><hr><p>Ref:</p><ul><li>Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)</li><li><a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5 User Guide</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
<category> Unit Test </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> JUnit 5 </tag>
</tags>
</entry>
<entry>
<title>Lombok</title>
<link href="/2024/09/30/Lombok/"/>
<url>/2024/09/30/Lombok/</url>
<content type="html"><![CDATA[<p>前面在介紹使用 Spring Data JPA 的時候,在關聯的部分有碰到一些使用 Lombok 的問題,這篇就來介紹一下個別的註解作用,也可以讓大家使用的時候也知道背後運作的邏輯。</p><p>Lombok 是一個套件可以幫助我們定義物件時省去許多重複冗長的 code ,可以大幅提升開發效率,下面是常見的註解。</p><h2 id="套件引入"><a href="#套件引入" class="headerlink" title="套件引入"></a>套件引入</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><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>org.projectlombok<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>lombok<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">optional</span>></span>true<span class="tag"></<span class="name">optional</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><h2 id="常用註解介紹"><a href="#常用註解介紹" class="headerlink" title="常用註解介紹"></a>常用註解介紹</h2><p><img src="https://ithelp.ithome.com.tw/upload/images/20240928/20150977eUFDzWPaBM.png" alt="https://ithelp.ithome.com.tw/upload/images/20240928/20150977eUFDzWPaBM.png"></p><p>小補充: 其實有很多方法如果是用 Intellij 都可以在物件內點選右鍵使用 generate 產生,但是如果可以用一個註解就解決這些還是來得方便很多,但建議剛開始新手可以先用這些 generate 來產生對應方法,也可以藉此了解每個註解背後的原始 code 是怎麼運作。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240928/20150977gPXIq87igK.png" alt="https://ithelp.ithome.com.tw/upload/images/20240928/20150977gPXIq87igK.png"></p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240928/20150977mvcWRtENVH.png" alt="https://ithelp.ithome.com.tw/upload/images/20240928/20150977mvcWRtENVH.png"></p><h3 id="Getter-x2F-Setter"><a href="#Getter-x2F-Setter" class="headerlink" title="@Getter / @Setter"></a>@Getter / @Setter</h3><p>進行物件導向的撰寫必備的方法,用來取得或修改物件屬性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Setter</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(原始定義方法) 效果同上</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Integer <span class="title function_">getId</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> id;</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_">setId</span><span class="params">(Integer id)</span> {</span><br><span class="line"> <span class="built_in">this</span>.id = id;</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_">setName</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="keyword">public</span> String <span class="title function_">getName</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> name;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="ToString"><a href="#ToString" class="headerlink" title="@ToString"></a>@ToString</h3><p>自動 override <code>toString()</code> 方法,使用時會印出所有屬性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ToString</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(註解生效原始碼) 效果同上</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">toString</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Product{"</span> +</span><br><span class="line"> <span class="string">"id="</span> + id +</span><br><span class="line"> <span class="string">", name='"</span> + name + <span class="string">'\''</span> +</span><br><span class="line"> <span class="string">'}'</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="NoArgsConstructor-x2F-AllArgsConstructor-x2F-RequiredArgsConstructor"><a href="#NoArgsConstructor-x2F-AllArgsConstructor-x2F-RequiredArgsConstructor" class="headerlink" title="@NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor"></a><strong>@NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor</strong></h3><p>起始子 (Constructor) 的創建,有三種註解是根據創建時是否包含內部屬性作為參數來決定。</p><p>例如有個 product 有 id, name 的屬性</p><ul><li><strong>@AllArgsConstructo</strong>r 自動帶入當前所有屬性作為可建立時的值</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(註解生效原始碼) 效果同上,使用註解表示我們獲得一個創建物件時可以帶入這些屬性並賦予值的方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Product</span><span class="params">(Integer id, String name)</span> {</span><br><span class="line"> <span class="built_in">this</span>.id = id;</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></pre></td></tr></table></figure><ul><li><strong>@NoArgsConstructor</strong> 自動產生一個空的起始子</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(註解生效原始碼) 效果同上,創建時就只是建立出空的物件,但沒辦法帶入任何屬性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Product</span><span class="params">()</span> {</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><strong>@RequiredArgsConstructor</strong> 可以讓你建立一個包含 “final 修飾屬性” 的起始子。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(註解生效原始碼) 效果同上,使用註解後可以看到只有帶 final 的屬性可以被帶入建立</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">product</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Integer id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Product</span><span class="params">(Integer id)</span> {</span><br><span class="line"> <span class="built_in">this</span>.id = id;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>**補充如果所有的屬性都沒有用 final 修飾的話,那就會生成一個沒有參數的起始子,等同 @NoArgsConstructor</p><h3 id="EqualsAndHashCode"><a href="#EqualsAndHashCode" class="headerlink" title="@EqualsAndHashCode"></a>@EqualsAndHashCode</h3><p>可以產生 <code>equals(Object other)</code> 和 <code>hashcode()</code> 方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EqualsAndHashCode</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> price;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(註解生效原始碼) 效果同上,可以看到這兩個方法會 Override 原本 equals(0 和 hashcode()</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> price;</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="type">boolean</span> <span class="title function_">equals</span><span class="params">(Object o)</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">this</span> == o) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (o == <span class="literal">null</span> || getClass() != o.getClass()) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> (Product) o;</span><br><span class="line"> <span class="keyword">return</span> id == product.id && price == product.price && Objects.equals(name, product.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="type">int</span> <span class="title function_">hashCode</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> Objects.hash(id, name, price);</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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><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(MybatisDemoApplication.class, args);</span><br><span class="line"><span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> Product.builder()</span><br><span class="line">.id(<span class="number">1</span>)</span><br><span class="line">.name(<span class="string">"book"</span>)</span><br><span class="line">.price(<span class="number">100</span>)</span><br><span class="line">.build();</span><br><span class="line"></span><br><span class="line"><span class="type">Product</span> <span class="variable">product1</span> <span class="operator">=</span> Product.builder()</span><br><span class="line">.id(<span class="number">1</span>)</span><br><span class="line">.name(<span class="string">"book"</span>)</span><br><span class="line">.price(<span class="number">100</span>)</span><br><span class="line">.build();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判斷物件是否相等</span></span><br><span class="line">System.out.println(product.equals(product1));</span><br><span class="line">System.out.println(product.hashCode() == product1.hashCode());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>預設的狀況會是兩個都是 False,因為只要新產生一個物件預設就是會有不同的記憶體位置,所以 reference 不同導致這兩個比較都 False,但是我們透過註解改寫上面兩個方法可以根據內部屬性的值是否都相等來判斷這兩個物件是否相等,hashcode 也會將內部的屬性值帶入去比較,就會得到兩個都是 True</p><h3 id="Data"><a href="#Data" class="headerlink" title="@Data"></a><strong>@Data</strong></h3><p>等於同時加了以下注解</p><ul><li>@Getter</li><li>@Setter</li><li>@ToString</li><li>@EqualsAndHashCode</li><li>@RequiredArgsConstructor</li></ul><p>使用上可以讓 code 很簡潔,但須要注意這個註解實做哪些方法,才不會不知道為什麼有些東西不是如你所想的在運作,這註解也會導致一些部分的問題,像是有物件繼承時,@EqualsAndHashCode 這註解預設是不考慮父類別的屬性,可能就會導致你物件比較時會出錯。詳細可以閱讀參考資料<a href="https://blog.cashwu.com/blog/2024/java-lombok-data-annotation-equals-and-hashcode">4</a>。</p><h3 id="Value"><a href="#Value" class="headerlink" title="@Value"></a>@Value</h3><p>等於同時加了以下注解,通常用來宣告給只讀取資料的物件使用,不提供任何 Setter</p><ul><li>@Getter</li><li>@ToString</li><li>@EqualsAndHashCode</li><li>@RequiredArgsConstructor</li></ul><h3 id="Builder"><a href="#Builder" class="headerlink" title="@Builder"></a>@Builder</h3><p>可以讓你使用特殊的流式語法來創建物件,簡化物件的創建過程。用了建造者模式(Builder Pattern),使得創建物件時更具靈活性和可讀性。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Builder</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> id;</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> price;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>流式寫法,可讀性提高,也可以選擇性創建屬性提高靈活性,例如你可以只帶入 name , price 不帶 id 創建。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> Product.builder()</span><br><span class="line">.id(<span class="number">1</span>)</span><br><span class="line">.name(<span class="string">"book"</span>)</span><br><span class="line">.price(<span class="number">100</span>)</span><br><span class="line">.build();</span><br></pre></td></tr></table></figure><p>不過使用這個通常還是會搭配 @Setter 因為有許多時候還是會操作物件去進行 set,所以通常 Builder 會配合 @Data 一起使用。</p><h3 id="Slf4j"><a href="#Slf4j" class="headerlink" title="@Slf4j"></a>@Slf4j</h3><p>自動產生 Log 的物件可以提供當前物件內使用,除了 @Slf4j 之外,lombok 也提供其他日誌框架,像是 @Log、@Log4j…等</p><p>註解使用後就可以直接使用 Slf4j 相關的方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><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_">LogDemo</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"> log.info(<span class="string">"hello"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(原始定義方法)未使用需自行建立 Slf4j 物件</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogDemo</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> java.util.logging.<span class="type">Logger</span> <span class="variable">log</span> <span class="operator">=</span> java.util.logging.Logger.getLogger(LogDemo.class.getName());</span><br><span class="line"></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"> log.info(<span class="string">"hello"</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><p>其實在進一步認識了解之後,發現這個套件其實有蠻多討論針對是否要引用,甚至有許多團隊開發會不建議使用,其中我也是了解了一下這些部分,工作開發上我有碰過有使用也有碰過沒有使用,自己是開發個人小專案基本會使用,但多人開發上其實團隊內需要好好討論是否使用及一些規範需要訂出來,畢竟有許多缺點都是多人協作下會有問題,下面總結一些優缺點:</p><p>優點:</p><ul><li>提高開發效率:透過註解的形式自動生成方法,讓代碼變得簡潔</li><li>減少維護成本:屬性做修改時,也簡化維護這些屬性所生成的 getter/setter 方法等</li></ul><p>缺點:</p><ul><li>侵入性高:如果有一個人使用了 Lombok,那麼其他依賴這個專案的專案或是協作開發者,就須要安裝 IDE 插件或是引入套件。</li><li>技術債提升:如果對於各種註解的底層原理不理解的話,很容易產生問題,像是 @Data 這種簡化多種註解及方法的就需要考量是否該使用。</li><li>影響升級:如需要升級到某個新版本的 JDK 的時候,若在 Lombok 中不支持的話就會受到影響。</li><li>破壞封裝:使用 getter, setter 註解後會讓每個屬性的方法都開放,如果有些部分需要自行控制就會破壞封裝性,可能會讓一些部份不被</li></ul><hr><p>參考資料:</p><ol><li><a href="https://projectlombok.org/">projectlombok</a></li><li><a href="https://kucw.io/blog/2020/3/java-lombok/">五分鐘學會 Lombok 用法</a></li><li><a href="https://www.cnblogs.com/hollischuang/p/12294100.html">新來個技術總監,禁止我們使用 Lombok!</a></li><li><a href="https://blog.cashwu.com/blog/2024/java-lombok-data-annotation-equals-and-hashcode">Lombok @Data 的魔法與陷阱:深入探討 @EqualsAndHashCode</a></li></ol>]]></content>
<categories>
<category> Spring Boot </category>
<category> Lombok </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Spring Data Jpa </tag>
</tags>
</entry>
<entry>
<title>Spring Data JPA (4) 資料庫關聯 1 : N</title>
<link href="/2024/09/27/SpringBoot/Spring-Data-JPA-%EF%BC%884%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E9%97%9C%E8%81%AF-1-N/"/>
<url>/2024/09/27/SpringBoot/Spring-Data-JPA-%EF%BC%884%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E9%97%9C%E8%81%AF-1-N/</url>
<content type="html"><![CDATA[<p><img src="https://ithelp.ithome.com.tw/upload/images/20240904/20150977s7zH4hK4Ja.png" alt="https://ithelp.ithome.com.tw/upload/images/20240904/20150977s7zH4hK4Ja.png"></p><h2 id="一對多-1-N"><a href="#一對多-1-N" class="headerlink" title="一對多 1 : N"></a>一對多 1 : N</h2><p>一對多關聯可以看到,products 和 reviews 這兩張表的關係,一個商品會有多則評論,所以設計上會在 reviews 裡面紀錄 product_id 來關聯回去 products。會應用到 <code>@OneToMany</code> <code>@ManyToOne</code> 這兩種註解來根據你是哪一方進行關聯。</p><p>建立父實體 reviews,標記 @ManyToOne</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "reviews")</span></span><br><span class="line"><span class="meta">@JsonIgnoreProperties({"product"})</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Review</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</span></span><br><span class="line"> <span class="keyword">private</span> Long id;</span><br><span class="line"> <span class="keyword">private</span> String comment;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> rating;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ManyToOne</span></span><br><span class="line"> <span class="meta">@JoinColumn(name = "product_id", referencedColumnName = "id")</span></span><br><span class="line"> <span class="keyword">private</span> Product product;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意 reviews 是多的那邊所以用 <code>@ManyToOne</code> ,和一對一配置相同,如果是父實體就會用上 @JoinColumn 並且標出 FK 的欄位。</p><p>建立子實體 products,標記 @OneToMany</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "products")</span></span><br><span class="line"><span class="comment">//@JsonIgnoreProperties({"productDetail"})</span></span><br><span class="line"><span class="comment">//@JsonIgnoreProperties({"reviews"})</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</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 name;</span><br><span class="line"> <span class="keyword">private</span> Double price;</span><br><span class="line"> <span class="keyword">private</span> String description;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)</span></span><br><span class="line"> <span class="meta">@JoinColumn(name = "product_detail_id", referencedColumnName = "id")</span></span><br><span class="line"> <span class="keyword">private</span> ProductDetails productDetails;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "product")</span></span><br><span class="line"> <span class="keyword">private</span> List<Review> reviews;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>這邊也配置雙向關聯回去,但是要記得使用 @JsonIgnoreProperties 來指定某些屬性不要序列化成 Json 避免雙向循環,假設你是純粹要把關聯用再查 Review 時可以帶出關聯的 Product 就可以不用在 Product 的 Entity 寫 @OneToMany 的部分,但如果要從 Product 關聯回去 Review 就必須兩邊都綁定,因為 Product 在這邊屬於被子實體,要 mappedBy 確定父實體的關聯屬性,沒辦法單方向關聯回去。</p><p>這邊我們讓 Product 可以關聯回去 Review 所以可以查到下面這樣</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"最後生還者"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">59.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"由 Naughty Dog 開發的動作冒險遊戲。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"productDetails"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"developer"</span><span class="punctuation">:</span> <span class="string">"Naughty Dog"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"publisher"</span><span class="punctuation">:</span> <span class="string">"Sony Interactive Entertainment"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"releaseDate"</span><span class="punctuation">:</span> <span class="string">"2013-06-14"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"languageSupport"</span><span class="punctuation">:</span> <span class="string">"English, Japanese, Chinese"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"reviews"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"comment"</span><span class="punctuation">:</span> <span class="string">"這是一款令人驚嘆的遊戲,擁有引人入勝的故事和令人驚嘆的視覺效果!"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rating"</span><span class="punctuation">:</span> <span class="number">5</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">11</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"comment"</span><span class="punctuation">:</span> <span class="string">"遊戲不錯,但希望戰鬥系統能更具挑戰性。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rating"</span><span class="punctuation">:</span> <span class="number">4</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">12</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"comment"</span><span class="punctuation">:</span> <span class="string">"每一秒鐘都充滿樂趣,真的是一款精緻的遊戲!"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rating"</span><span class="punctuation">:</span> <span class="number">5</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"巫師3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">49.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"由 CD Projekt Red 開發的開放世界角色扮演遊戲。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"productDetails"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"developer"</span><span class="punctuation">:</span> <span class="string">"CD Projekt Red"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"publisher"</span><span class="punctuation">:</span> <span class="string">"CD Projekt"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"releaseDate"</span><span class="punctuation">:</span> <span class="string">"2015-05-19"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"languageSupport"</span><span class="punctuation">:</span> <span class="string">"English, Chinese, Polish"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"reviews"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"comment"</span><span class="punctuation">:</span> <span class="string">"很棒的遊戲,但某些部分的節奏感覺有點慢。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rating"</span><span class="punctuation">:</span> <span class="number">4</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">13</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"comment"</span><span class="punctuation">:</span> <span class="string">"畫質很好,但劇情有點單調。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rating"</span><span class="punctuation">:</span> <span class="number">3</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="comment">// ......略</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>關於一對多的設置上其實跟一對一很類似,主要都是要區別出哪個是父實體和子實體,一開始規劃資料表時就可以根據此來配置關聯的欄位,實際撈取資料上也要根據需求決定是否需要雙向關聯,回傳的資料上也可以採用 DTO 的方式可以回傳需要的欄位,中間實作上也要避免雙向循環,這樣就可以好好應用這些關聯方式,下一篇來介紹多對多的部分。</p><h2 id="Ref"><a href="#Ref" class="headerlink" title="Ref:"></a>Ref:</h2><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2024/06/spring-boot-jpa-one-to-many-relationship-and-bidirectional-association.html">使用 JPA 配置資料表關聯(以一對多關聯為例)</a></li><li><a href="https://openhome.cc/Gossip/EJB3Gossip/BidirectionalAssociation.html">雙向關聯</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Spring Data Jpa </tag>
</tags>
</entry>
<entry>
<title>Spring Data JPA (3) 資料庫關聯 1 : 1</title>
<link href="/2024/09/27/SpringBoot/Spring-Data-JPA-%EF%BC%883%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E9%97%9C%E8%81%AF-1-1/"/>
<url>/2024/09/27/SpringBoot/Spring-Data-JPA-%EF%BC%883%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E9%97%9C%E8%81%AF-1-1/</url>
<content type="html"><![CDATA[<p>操作 Spring Data JPA 需要對於資料表之間的關聯與配置有一定的了解,可以幫助你在規劃資料庫及撈取資料上有很大的幫助。</p><p>後續文章會依序介紹常見的幾種關聯操作,若這邊設計一個情境開設一個電商平台,賣電玩的商品,會有一些表紀錄商品的一些資訊,我們可以藉此來探討資料表之間的關聯。主要有下面這幾張表:</p><ul><li>products 商品資訊,記錄名稱 (name)、價錢 (price)、描述 (description)等資訊</li><li>product_details 商品細節,紀錄開發商 (developer)、發行商 (publisher)、發行日 (release_date)、支援語言 (language_support)等資訊</li><li>reviews 評論,紀錄評論者 (reviewer_name)、內容 (comment)、分數 (rating)</li><li>tags 標籤,遊戲類型及各式分類標註 (name)</li><li>product_tags 商品及標籤的關聯表 (多對多需要)</li></ul><p><img src="https://ithelp.ithome.com.tw/upload/images/20240904/201509775cEGhSRpBL.png" alt="https://ithelp.ithome.com.tw/upload/images/20240904/201509775cEGhSRpBL.png"></p><h2 id="關聯配置相關參數介紹"><a href="#關聯配置相關參數介紹" class="headerlink" title="關聯配置相關參數介紹"></a>關聯配置相關參數介紹</h2><p>關聯時必須知道誰是父實體(或稱維護方,具有 FK 可以關聯被維護 PK),誰是子實體(被維護、被參考對象)</p><table><thead><tr><th></th><th>父實體(維護方)</th><th>子實體(被維護方)</th></tr></thead><tbody><tr><td>關聯</td><td>具有 FK</td><td>被關聯 PK 或其他</td></tr><tr><td>註解應用區別</td><td>@JoinColumn()</td><td>關聯註解加入參數 mappedBy</td></tr></tbody></table><h3 id="Cascade"><a href="#Cascade" class="headerlink" title="Cascade"></a>Cascade</h3><p>級聯配置參數,針對父實體的資料操作,子實體會有對應自動操作</p><ul><li>CascadeType.ALL:無論儲存、更新、刷新或移除,一併對被參考物件作出對應動作。</li><li>CascadeType.PERSIST:父實體進行儲存 (repository 的 save) 操作,自動對被關聯子實體進行儲存操作</li><li>CascadeType.MERGE:父實體進行更新操作時,自動對關聯的子實體執行更新操作。</li><li>CascadeType.REFRESH:父實體進行刷新操作時,自動對關聯的子實體執行刷新操作。</li><li>CascadeType.REMOVE:父實體進行刪除操作時,自動對關聯的子實體執行刪除操作。</li></ul><h3 id="Fetch"><a href="#Fetch" class="headerlink" title="Fetch"></a>Fetch</h3><p>資料讀取配置參數,設定讀取的方式是立即或是特定條件才讀</p><ul><li>FetchType.EARGE:馬上加載子實體</li><li>FetchType.LAZY:只有應用到子實體的屬性時才會被加載</li></ul><p>各類關聯註解預設值參考</p><table><thead><tr><th><strong>Relation</strong></th><th><strong>Default</strong></th></tr></thead><tbody><tr><td>@OneToOne</td><td>FetchType.EARGE</td></tr><tr><td>@ManyToOne</td><td>FetchType.EARGE</td></tr><tr><td>@OneToMany</td><td>FetchType.LAZY</td></tr><tr><td>@ManyToMany</td><td>FetchType.LAZY</td></tr></tbody></table><h2 id="一對一-1-1"><a href="#一對一-1-1" class="headerlink" title="一對一 1 : 1"></a>一對一 1 : 1</h2><p>一對一的關聯可以從 products 和 product_details 這兩張表之間的關聯看到,一個商品會對應一個詳細資料,要透過 JPA 會用到 <code>@OneToOne</code> 註解來進行關聯,這些關聯註解都有一些參數可以設置。在這邊的範例就是 products 是父實體,product_details 是子實體。</p><p>定義父實體 products</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "products")</span></span><br><span class="line"><span class="comment">//@JsonIgnoreProperties({"productDetail"}) // 避免循環引用</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</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 name;</span><br><span class="line"> <span class="keyword">private</span> Double price;</span><br><span class="line"> <span class="keyword">private</span> String description;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@OneToOne(cascade = CascadeType.ALL)</span></span><br><span class="line"> <span class="meta">@JoinColumn(name = "product_detail_id", referencedColumnName = "id")</span></span><br><span class="line"> <span class="keyword">private</span> ProductDetails productDetails;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>@JoinColumn 的 name 參數指的是要提供 FK 的欄位名稱,referencedColumnName 是指被關聯方的 關聯欄位(不寫預設是被關聯的主鍵 PK)</p><p>定義子實體 product_details</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Table(name = "product_details")</span></span><br><span class="line"><span class="meta">@JsonIgnoreProperties("product")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductDetails</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</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 developer;</span><br><span class="line"> <span class="keyword">private</span> String publisher;</span><br><span class="line"> <span class="keyword">private</span> String releaseDate;</span><br><span class="line"> <span class="keyword">private</span> String languageSupport;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@OneToOne(mappedBy = "productDetails")</span></span><br><span class="line"> <span class="keyword">private</span> Product product;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>這邊需要注意如果需要雙向關聯,這邊也要在關聯要呈現的屬性上面加上 @OneToOne(mappedBy = “{父方映射關聯的欄位}”),這邊也就是指父實體放 @OneToOne 欄位的變數名 productDetails</p><p>另外補充如果雙向關聯開啟,就會出現無限循環關聯的問題,你可以想像會出現 product 關聯出 product_details 但裡面又關聯回 product 就會一直永無止境導致回傳資料混亂,有幾個方法可以解決:</p><ol><li>改用 DTO 將需要關聯資料裝入回傳</li><li>@JsonIgnore 放在被關聯欄位上面就可以忽略關聯</li><li>@JsonIgnoreProperties(”{關聯欄位變數名}”) 放在實體 class 上面</li></ol><p>實際撈回來的資料</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"最後生還者"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">59.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"由 Naughty Dog 開發的動作冒險遊戲。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"productDetails"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"developer"</span><span class="punctuation">:</span> <span class="string">"Naughty Dog"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"publisher"</span><span class="punctuation">:</span> <span class="string">"Sony Interactive Entertainment"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"releaseDate"</span><span class="punctuation">:</span> <span class="string">"2013-06-14"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"languageSupport"</span><span class="punctuation">:</span> <span class="string">"English, Japanese, Chinese"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"巫師3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">49.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"由 CD Projekt Red 開發的開放世界角色扮演遊戲。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"productDetails"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"developer"</span><span class="punctuation">:</span> <span class="string">"CD Projekt Red"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"publisher"</span><span class="punctuation">:</span> <span class="string">"CD Projekt"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"releaseDate"</span><span class="punctuation">:</span> <span class="string">"2015-05-19"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"languageSupport"</span><span class="punctuation">:</span> <span class="string">"English, Chinese, Polish"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="comment">// 略......</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>以上就是基本一對一的關聯操作,下篇會來介紹一對多 (1 : N) 的操作模式。</p><p>這邊想要補充一下推薦有使用 Intellij 的 database UI ,連接好資料庫之後,對資料庫點右鍵選 Diagrams > Show Diagram 他就可以將各欄位之間的關聯狀況畫出來,還可以匯出圖片,就可以不用自己畫 ER 圖了,真的太強大 !!<br><img src="https://ithelp.ithome.com.tw/upload/images/20240905/20150977RPC1MnpZSG.png" alt="https://ithelp.ithome.com.tw/upload/images/20240905/20150977RPC1MnpZSG.png"></p><hr><h2 id="Ref"><a href="#Ref" class="headerlink" title="Ref:"></a>Ref:</h2><ul><li><a href="https://openhome.cc/Gossip/EJB3Gossip/CascadeTypeFetchType.html">CascadeType 與 FetchType</a></li><li><a href="https://chikuwa-tech-study.blogspot.com/2024/05/spring-boot-jpa-one-to-one-relationship.html">使用 JPA 配置資料表關聯(以一對一關聯為例)</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Spring Data Jpa </tag>
</tags>
</entry>
<entry>
<title>Spring Data JPA (2) 資料庫查詢應用</title>
<link href="/2024/09/25/SpringBoot/Spring-Data-JPA-%EF%BC%882%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E6%9F%A5%E8%A9%A2%E6%87%89%E7%94%A8/"/>
<url>/2024/09/25/SpringBoot/Spring-Data-JPA-%EF%BC%882%EF%BC%89%E8%B3%87%E6%96%99%E5%BA%AB%E6%9F%A5%E8%A9%A2%E6%87%89%E7%94%A8/</url>
<content type="html"><![CDATA[<p>這邊來認識一些我們可能會用到的一些資料庫查詢操作</p><p>根據前面的 products 結構來插入一些資料熟悉一些操作的用法吧。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> products (name, price, description) <span class="keyword">VALUES</span></span><br><span class="line">(<span class="string">'薩爾達傳說:曠野之息'</span>, <span class="number">59.99</span>, <span class="string">'設定在幻想世界中的開放世界動作冒險遊戲。'</span>),</span><br><span class="line">(<span class="string">'超級瑪利歐:奧德賽'</span>, <span class="number">49.99</span>, <span class="string">'以瑪利歐為主角的平臺遊戲,探索各種世界。'</span>),</span><br><span class="line">(<span class="string">'集合啦!動物森友會'</span>, <span class="number">54.99</span>, <span class="string">'在無人島上進行發展的生活模擬遊戲。'</span>),</span><br><span class="line">(<span class="string">'最終幻想 VII 重製版'</span>, <span class="number">69.99</span>, <span class="string">'經典 RPG 的重製版,具有更新的圖像和遊戲玩法。'</span>),</span><br><span class="line">(<span class="string">'巫師3:狂獵'</span>, <span class="number">39.99</span>, <span class="string">'開放世界 RPG,玩家扮演狩魔獵人傑洛特。'</span>),</span><br><span class="line">(<span class="string">'電馭叛客2077'</span>, <span class="number">59.99</span>, <span class="string">'設定在反烏托邦未來世界的開放世界 RPG。'</span>),</span><br><span class="line">(<span class="string">'對馬戰鬼'</span>, <span class="number">59.99</span>, <span class="string">'設定在日本封建時代的動作冒險遊戲。'</span>),</span><br><span class="line">(<span class="string">'碧血狂殺2'</span>, <span class="number">49.99</span>, <span class="string">'以西部為主題的開放世界動作冒險遊戲。'</span>),</span><br><span class="line">(<span class="string">'黑帝斯'</span>, <span class="number">24.99</span>, <span class="string">'在地下城中進行逃脫的類 Roguelike 遊戲。'</span>),</span><br><span class="line">(<span class="string">'我們之中'</span>, <span class="number">4.99</span>, <span class="string">'在太空船上進行的多人派對遊戲,涉及團隊合作與背叛。'</span>);</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="操作查詢方法"><a href="#操作查詢方法" class="headerlink" title="操作查詢方法"></a>操作查詢方法</h2><h3 id="一-根據欄位值的相等條件"><a href="#一-根據欄位值的相等條件" class="headerlink" title="(一) 根據欄位值的相等條件"></a><strong>(一) 根據欄位值的相等條件</strong></h3><p>前面一篇有提到可以直接透過欄位名稱來查找,JPA 會自動幫我們對應,最常用到的就是 findById,如果想要查找其他欄位就可以直接改成像是 findByName, findByPrice 等等都可以讓 JPA 幫我們匹配。</p><p>這邊根據 findByName 我們可以透過名稱找到對應商品</p><p>controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/q")</span></span><br><span class="line"><span class="keyword">public</span> Product <span class="title function_">getProductsByName</span><span class="params">(<span class="meta">@RequestParam</span> String name)</span> {</span><br><span class="line"> <span class="keyword">return</span> productService.getProductByName(name);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>service</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductService</span> {</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductRepository productRepository;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">getProductByName</span><span class="params">(String name)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findByName(name);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>repository</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ProductRepository</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><Product, Integer> {</span><br><span class="line"> Product <span class="title function_">findByName</span><span class="params">(String name)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="https://ithelp.ithome.com.tw/upload/images/20240902/20150977BLTg7Zm4IW.png" alt="https://ithelp.ithome.com.tw/upload/images/20240902/20150977BLTg7Zm4IW.png"></p><h3 id="二-範圍條件"><a href="#二-範圍條件" class="headerlink" title="(二) 範圍條件"></a><strong>(二) 範圍條件</strong></h3><p>可以根據數值的範圍來進行查詢,像是這邊可以用價錢來進行示範</p><p>他的格式會像 findBy{判斷欄位}GreaterThanEqual()</p><ul><li>大於 <code>findByPriceGreaterThan()</code></li><li>大於等於 <code>findByPriceGreaterThanEqual()</code></li><li>小於 <code>findByPriceLessThan()</code></li><li>小於等於 <code>findByPriceLessThanEqual()</code></li></ul><p>這邊用大於等於</p><p>controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/priceGreaterThanEqual")</span></span><br><span class="line"><span class="keyword">public</span> List<Product> <span class="title function_">getProductsByPriceGreaterThanEqual</span><span class="params">(<span class="meta">@RequestParam</span> <span class="type">int</span> range)</span> {</span><br><span class="line"> <span class="keyword">return</span> productService.getProductGreaterThanEqual(range);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>service</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductService</span> {</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductRepository productRepository;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> List<Product> <span class="title function_">getProductGreaterThanEqual</span><span class="params">(<span class="type">int</span> range)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findByPriceGreaterThanEqual(range);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>repository</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ProductRepository</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><Product, Integer> {</span><br><span class="line"> Product <span class="title function_">findByName</span><span class="params">(String name)</span>;</span><br><span class="line"> List<Product> <span class="title function_">findByPriceGreaterThanEqual</span><span class="params">(<span class="type">int</span> range)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查詢大於 60 元剛好只有 69.99 元的最終幻想符合<br><img src="https://ithelp.ithome.com.tw/upload/images/20240902/20150977HBj3KmiXkx.png" alt="https://ithelp.ithome.com.tw/upload/images/20240902/20150977HBj3KmiXkx.png"></p><h3 id="多個條件"><a href="#多個條件" class="headerlink" title="多個條件"></a><strong>多個條件</strong></h3><p>可透過 And 或 Or 組合</p><p>controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("/nameOrPrice")</span></span><br><span class="line"><span class="keyword">public</span> List<Product> <span class="title function_">getProductsByNameOrPrice</span><span class="params">(<span class="meta">@RequestParam</span> String name, <span class="meta">@RequestParam</span> <span class="type">double</span> price)</span> {</span><br><span class="line"> <span class="keyword">return</span> productService.getProductByNameOrPrice(name, price);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>service</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> List<Product> <span class="title function_">getProductByNameOrPrice</span><span class="params">(String name, <span class="type">double</span> price)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findByNameOrPrice(name, price);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>repository</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ProductRepository</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><Product, Integer> {</span><br><span class="line"> List<Product> <span class="title function_">findByNameOrPrice</span><span class="params">(String name, <span class="type">double</span> price)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="https://ithelp.ithome.com.tw/upload/images/20240902/20150977UIMYJWNpeV.png" alt="https://ithelp.ithome.com.tw/upload/images/20240902/20150977UIMYJWNpeV.png"><br>or 的話只要其中一項條件符合就會執行,像是下面有搜出 name = 黑帝斯 也有搜出 price = 49.99</p><h3 id="原生-SQL"><a href="#原生-SQL" class="headerlink" title="原生 SQL"></a>原生 SQL</h3><p>使用 JPA 如果不夠彈性你也可使用原生的語法 <code>@Query</code></p><p>@Query 裡面要第一個參數註明是 <code>nativeQuery = true</code> , 第二個參數是 vlaue = “{sql 語法}” ,也可以把 value 省略,直接用 SQL 字串放第二個參數,注意這邊 SQL 語法結尾請不要加分號 “;”</p><p>參數的帶入也可以用 :param 的方式,這邊其實寫法有很多種,大家就找到自己喜歡或是團隊易懂的方式來撰寫。</p><p>*如果 nativeQuery 沒有開啟就是使用 JPQL</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 用 """ """ 可以不用顧慮換行問題</span></span><br><span class="line"><span class="comment">// @Query(</span></span><br><span class="line"><span class="comment">// nativeQuery = true,</span></span><br><span class="line"><span class="comment">// value = """</span></span><br><span class="line"><span class="comment">// SELECT *</span></span><br><span class="line"><span class="comment">// FROM `products`</span></span><br><span class="line"><span class="comment">// WHERE `name` = ?1 OR `price` = ?2</span></span><br><span class="line"><span class="comment">// """</span></span><br><span class="line"><span class="comment">// )</span></span><br><span class="line"><span class="comment">// JPQL 須注意如果語法長中間可能換行及空格導致語法錯誤</span></span><br><span class="line"><span class="meta">@Query("SELECT p FROM Product p WHERE p.name = :name OR p.price = :price")</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Native SQL 須注意如果語法長中間可能換行及空格導致語法錯誤</span></span><br><span class="line"> <span class="meta">@Query(</span></span><br><span class="line"><span class="meta"> nativeQuery = true,</span></span><br><span class="line"><span class="meta"> value = "SELECT * FROM products WHERE name = ?1 OR price = ?2"</span></span><br><span class="line"><span class="meta"> )</span></span><br><span class="line"> List<Product> <span class="title function_">findByNameOrPriceNative</span><span class="params">(String name, <span class="type">double</span> price)</span>;</span><br></pre></td></tr></table></figure><h2 id="排序-Sorting-與分頁-Pagination"><a href="#排序-Sorting-與分頁-Pagination" class="headerlink" title="排序(Sorting)與分頁(Pagination)"></a>排序(Sorting)與分頁(Pagination)</h2><h3 id="Sort-物件"><a href="#Sort-物件" class="headerlink" title="Sort 物件"></a>Sort 物件</h3><p>利用 Sort 物件可以做到排序,看是要升序(ASC)或是降序(DESC),這個物件是可以直接帶入 repository 的方法中告知 JPA 要進行排序</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// asc</span></span><br><span class="line"><span class="type">Sort</span> <span class="variable">sort</span> <span class="operator">=</span> Sort.by({欄位}).ascending()</span><br><span class="line"><span class="comment">// desc</span></span><br><span class="line"><span class="type">Sort</span> <span class="variable">sort</span> <span class="operator">=</span> Sort.by({欄位}).descending();</span><br></pre></td></tr></table></figure><p>以撈出全部資料 Service 方法為例,根據價錢進行升序</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> List<Product> <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Sort</span> <span class="variable">sort</span> <span class="operator">=</span> Sort.by(<span class="string">"price"</span>).ascending();</span><br><span class="line"> <span class="keyword">return</span> productRepository.findAll(sort);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到顯示資料就照價錢由小到大排了</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240903/20150977FBBLSm2UcJ.png" alt="https://ithelp.ithome.com.tw/upload/images/20240903/20150977FBBLSm2UcJ.png"><br>Sort 也可以整合下面分頁物件一起做到排序及分頁</p><h3 id="Pageable-物件"><a href="#Pageable-物件" class="headerlink" title="Pageable 物件"></a>Pageable 物件</h3><p>Pageabe 是一個介面,通常會放入三個參數來透過 PageRequest 方法實作</p><ul><li>第一個參數 page 表示第幾頁(需要特別注意第一頁是 0)</li><li>第二個參數 limit 表示回傳資料筆數上限</li><li>第三個參數 sort 表示排序物件</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Pageable</span> <span class="variable">pageable</span> <span class="operator">=</span> PageRequest.of(page, limit, sort);</span><br></pre></td></tr></table></figure><p>把上面的 Sort 物件一起帶進去,並且告知我要取得第一頁的 3 筆資料</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Page<Product> <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Sort</span> <span class="variable">sort</span> <span class="operator">=</span> Sort.by(<span class="string">"price"</span>).ascending();</span><br><span class="line"></span><br><span class="line"> <span class="type">Pageable</span> <span class="variable">pageable</span> <span class="operator">=</span> PageRequest.of(<span class="number">0</span>,<span class="number">3</span>, sort);</span><br><span class="line"></span><br><span class="line"> Page<Product> productPage = productRepository.findAll(pageable);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> productPage;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>回傳的資料可以看到確實有排序,還有提供分頁的相關資訊,也只顯示出第一頁的 3 筆資料</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"content"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">4</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"最終幻想 VII 重製版"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">69.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"經典 RPG 的重製版,具有更新的圖像和遊戲玩法。"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"薩爾達傳說:曠野之息"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">59.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"設定在幻想世界中的開放世界動作冒險遊戲。"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"id"</span><span class="punctuation">:</span> <span class="number">6</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"電馭叛客2077"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"price"</span><span class="punctuation">:</span> <span class="number">59.99</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"設定在反烏托邦未來世界的開放世界 RPG。"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"pageable"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"pageNumber"</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"pageSize"</span><span class="punctuation">:</span> <span class="number">3</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"sort"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"empty"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"sorted"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"unsorted"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"offset"</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"paged"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"unpaged"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"totalPages"</span><span class="punctuation">:</span> <span class="number">4</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"totalElements"</span><span class="punctuation">:</span> <span class="number">10</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"last"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"size"</span><span class="punctuation">:</span> <span class="number">3</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"number"</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"sort"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"empty"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"sorted"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"unsorted"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"numberOfElements"</span><span class="punctuation">:</span> <span class="number">3</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"first"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"empty"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>以上就是基本的一些查詢跟應用,對於許多網站的搜尋及資料的查找都是相當基本的用法,Spring Data Jpa 也提供給我們許多方便的用法,還有一定的彈性可以自行撰寫原生 SQL。那下篇來講一些關於資料庫關聯的一些操作介紹吧。</p><hr><p>參考資料:</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2024/05/spring-boot-mysql-using-jpa-repository.html">使用 JPA Repository 存取 MySQL 資料庫</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Spring Data Jpa </tag>
</tags>
</entry>
<entry>
<title>Spring Data JPA(1)基礎應用架構</title>
<link href="/2024/09/24/SpringBoot/Spring-Data-JPA-%EF%BC%881%EF%BC%89%E5%9F%BA%E7%A4%8E%E6%87%89%E7%94%A8%E6%9E%B6%E6%A7%8B/"/>
<url>/2024/09/24/SpringBoot/Spring-Data-JPA-%EF%BC%881%EF%BC%89%E5%9F%BA%E7%A4%8E%E6%87%89%E7%94%A8%E6%9E%B6%E6%A7%8B/</url>
<content type="html"><![CDATA[<p>前面已經介紹過各類資料操作框架的特色之後,這邊來介紹其中我比較熟悉也蠻多人使用的 Spring Data Jpa,雖然比較不用寫 SQL 所以可能會對於 SQL 掌握比較少,但我覺得透過物件的方式來操作資料也是本身 Java 的特色之一,搭配 Spring Boot 能夠整合在框架中使用是很方便的</p><h2 id="使用前概念"><a href="#使用前概念" class="headerlink" title="使用前概念"></a>使用前概念</h2><p>根據常用開發架構的 MVC 架構,資料庫的部分主要是由 Model 層部分進行處理,這邊對應在 Spring Boot 的開發上會放在 Dao 和 Service,Dao 主要是定義我們與資料庫的溝通連結,在 Spring Data Jpa 主要就是繼承 JpaRepository 去處理資料的存取,最後將拿到得資料返回給 Service 進行業務邏輯的操作或是資料的組裝,最後回給 Controller。</p><h2 id="引入套件"><a href="#引入套件" class="headerlink" title="引入套件"></a>引入套件</h2><p>pom.xml 加入下面套件,spring-boot-starter-data-jpa 就是我們要使用的 JPA 套件,另外一個則是 mysql 連線用的套件,可以根據使用不同資料庫做更換</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-data-jpa<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></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>com.mysql<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>mysql-connector-j<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">scope</span>></span>runtime<span class="tag"></<span class="name">scope</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><h2 id="配置-application-properties"><a href="#配置-application-properties" class="headerlink" title="配置 application properties"></a>配置 application properties</h2><p>這部分根據你使用的資料庫不同會有一些差異,以下拿 Mysql 為例,主要需要配置概念都差不多,就是配置你的資料庫 Driver, 資料來源, 使用者名稱, 密碼等等</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 基本來源配置</span></span><br><span class="line">spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver</span><br><span class="line">spring.datasource.url=jdbc:mysql://localhost:3306/{databaseName}</span><br><span class="line">spring.datasource.username={username}</span><br><span class="line">spring.datasource.password={userPassword}</span><br><span class="line"></span><br><span class="line"><span class="comment"># 額外配置</span></span><br><span class="line"><span class="comment"># 資料表初始化及自動更新</span></span><br><span class="line">spring.jpa.hibernate.ddl-auto=update</span><br><span class="line"></span><br><span class="line"><span class="comment"># 是否在 console 印出 SQL 指令並對其格式化</span></span><br><span class="line">spring.jpa.properties.hibernate.show_sql=<span class="literal">true</span></span><br><span class="line">spring.jpa.properties.hibernate.format_sql=<span class="literal">true</span></span><br></pre></td></tr></table></figure><p>假設我們想要設計產品 <code>Product</code> 的資料操作,會定義他的實體類別 (Entity),來對應到資料庫的 <code>products</code> 表,當我們進行資料都是用 Product 這個物件來操作資料的存取,查詢就將資料轉成我們定義好的物件回給我們,儲存、修改、刪除等等的操作也是由此物件去進行。</p><h3 id="資料庫建立及資料表建立"><a href="#資料庫建立及資料表建立" class="headerlink" title="資料庫建立及資料表建立"></a>資料庫建立及資料表建立</h3><p>資料庫建立部分大家可以自行找自己所使用的用法,這邊就不說明,提供一個簡單通用的 SQL 例子方便沒使用過的可以有環境進行操作</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE <span class="title function_">products</span> <span class="params">(</span></span><br><span class="line"><span class="params"> id BIGINT AUTO_INCREMENT PRIMARY KEY,</span></span><br><span class="line"><span class="params"> name VARCHAR(<span class="number">255</span>)</span> NOT NULL,</span><br><span class="line"> price <span class="title function_">DECIMAL</span><span class="params">(<span class="number">10</span>, <span class="number">2</span>)</span> NOT NULL,</span><br><span class="line"> description TEXT,</span><br><span class="line"> created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,</span><br><span class="line"> updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="Entity"><a href="#Entity" class="headerlink" title="Entity"></a>Entity</h2><p>新增一個 class,定義我們要作為資料庫操作物件,會對應到資料表的個別欄位</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> jakarta.persistence.Entity;</span><br><span class="line"><span class="keyword">import</span> jakarta.persistence.GeneratedValue;</span><br><span class="line"><span class="keyword">import</span> jakarta.persistence.GenerationType;</span><br><span class="line"><span class="keyword">import</span> jakarta.persistence.Id;</span><br><span class="line"><span class="keyword">import</span> jakarta.persistence.Table;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table(name = "products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> {</span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.IDENTITY)</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 name;</span><br><span class="line"> <span class="keyword">private</span> Double price;</span><br><span class="line"> <span class="keyword">private</span> String description;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Getter and Setter...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>@Entity 告訴 Jpa 它是要儲存到資料庫的實體類別</p><p>@Table 則設定它在資料庫中,要對應到叫做「products」的 table 。</p><p>@Id 表示 table 中的主鍵(Primary Key,PK)</p><p>@GernatedValue() 定義了主鍵值的產生方式。設定的方式 strategy = GenerationType.IDENTITY 就是表示主鍵會自動增長。</p><blockquote><p>後面章節會講到一些 Lombok 註解的應用,可以讓 Spring Data Jpa 使用上更加簡潔方便,但希望各位使用時還是要清楚知道每個註解的由來跟原始作法</p></blockquote><h2 id="Dao-層繼承-JpaRepository"><a href="#Dao-層繼承-JpaRepository" class="headerlink" title="Dao 層繼承 JpaRepository"></a>Dao 層繼承 JpaRepository</h2><p>新增 Repository 來做為資料庫溝通介面</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.data.jpa.repository.JpaRepository;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Repository;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ProductRepository</span> <span class="keyword">extends</span> <span class="title class_">JpaRepository</span><Product, Long> {</span><br><span class="line"> Product <span class="title function_">findByName</span><span class="params">(String name)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Respository 就是屬於 Dao 的部分,這邊會新增一個介面( interface) 來繼承 JpaRepository,就可以將 JPA 對於物件實體的映射包含資料庫的連線等等實現,後面<> 內需要放入你要操作的物件實體及他內部主鍵型別 (id 的型別) 最後會是像這樣 JpaRepository<Entity, 主鍵型別>,下面可以加入任何你想要使用的方法來操作資料庫,通常預設已經有基本可以針對 id 搜尋單筆,或是直接查詢整筆,就不用宣告出來,透過繼承就可以後續直接再 Service 層中使用。</p><h2 id="Service-層中用慣例優於配置來進行-CRUD-操作"><a href="#Service-層中用慣例優於配置來進行-CRUD-操作" class="headerlink" title="Service 層中用慣例優於配置來進行 CRUD 操作"></a>Service 層中用慣例優於配置來進行 CRUD 操作</h2><p>新增 Service 層應用 JPA Repository 定義好的或是預設的方法操作資料</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"><span class="keyword">import</span> java.util.Optional;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductRepository productRepository;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 新增產品</span></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">createProduct</span><span class="params">(Product product)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.save(product);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 根據 id 查詢單一產品</span></span><br><span class="line"> <span class="keyword">public</span> Optional<Product> <span class="title function_">getProductById</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findById(id);</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">public</span> Product <span class="title function_">getProductByName</span><span class="params">(String name)</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findByName(name);</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">public</span> List<Product> <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> productRepository.findAll();</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">public</span> Product <span class="title function_">updateProduct</span><span class="params">(Long id, Product productDetails)</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> productRepository.findById(id)</span><br><span class="line"> .orElseThrow(() -> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"Product not found with id: "</span> + id));</span><br><span class="line"></span><br><span class="line"> product.setName(productDetails.getName());</span><br><span class="line"> product.setPrice(productDetails.getPrice());</span><br><span class="line"> product.setDescription(productDetails.getDescription());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> productRepository.save(product);</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">public</span> <span class="keyword">void</span> <span class="title function_">deleteProduct</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> productRepository.findById(id)</span><br><span class="line"> .orElseThrow(() -> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"Product not found with id: "</span> + id));</span><br><span class="line"></span><br><span class="line"> productRepository.delete(product);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>這邊就可以看到我們使用 JPA 所帶來的好處,已經直接提供給我們很多 ORM 包裝好的方法,像是 findById(), findAll(), save(), delete(),這些本該是自己要撰寫 Sql 才能和資料庫溝通的,但我們在 Repository 沒有定義任何方法就可以使用,除了一個 findByName 這個是我們自己定義的,但你也會發現這方法也沒進行任何處理,只是一個簡單明瞭的名稱就可以做到查詢,其實都是 JPA 幫我們處理好,他會自行匹配你定義好在 Entity 的個別欄位,所以當你寫 findBy{欄位名} 他就會自行匹配對應的 sql ( <code>SELECT * FROM prodcuts WHERE {欄位名} = ‘{值}’</code>; ),確實都不用寫到任何 SQL 就可以完成基本的操作。</p><p>這些慣例優於配置的用法其實還有很多,是你想要進行複雜查詢要自己寫的話要怎麼使用,後面會有進一步介紹。</p><h3 id="Controller-建立接口接收請求及回傳結果"><a href="#Controller-建立接口接收請求及回傳結果" class="headerlink" title="Controller 建立接口接收請求及回傳結果"></a>Controller 建立接口接收請求及回傳結果</h3><p>最後就是開端口接收請求及回傳 Service 的結果</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.ResponseEntity;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"><span class="keyword">import</span> java.util.Optional;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api/products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> ProductService productService;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 新增產品</span></span><br><span class="line"> <span class="meta">@PostMapping</span></span><br><span class="line"> <span class="keyword">public</span> Product <span class="title function_">createProduct</span><span class="params">(<span class="meta">@RequestBody</span> Product product)</span> {</span><br><span class="line"> <span class="keyword">return</span> productService.createProduct(product);</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="meta">@GetMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Product> <span class="title function_">getProductById</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span> {</span><br><span class="line"> Optional<Product> product = productService.getProductById(id);</span><br><span class="line"> <span class="keyword">return</span> product.map(ResponseEntity::ok)</span><br><span class="line"> .orElseGet(() -> ResponseEntity.notFound().build());</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="meta">@GetMapping</span></span><br><span class="line"> <span class="keyword">public</span> List<Product> <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> productService.getAllProducts();</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="meta">@PutMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Product> <span class="title function_">updateProduct</span><span class="params">(<span class="meta">@PathVariable</span> Long id, <span class="meta">@RequestBody</span> Product productDetails)</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="type">Product</span> <span class="variable">updatedProduct</span> <span class="operator">=</span> productService.updateProduct(id, productDetails);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.ok(updatedProduct);</span><br><span class="line"> } <span class="keyword">catch</span> (RuntimeException e) {</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.notFound().build();</span><br><span class="line"> }</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="meta">@DeleteMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<Void> <span class="title function_">deleteProduct</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> productService.deleteProduct(id);</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.noContent().build();</span><br><span class="line"> } <span class="keyword">catch</span> (RuntimeException e) {</span><br><span class="line"> <span class="keyword">return</span> ResponseEntity.notFound().build();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>看完你會對於 Spring Data JPA 有進一步的了解了,也大概知道其中的特色在哪,有太多太多細節再透過後面的章節慢慢介紹。</p><hr><p>參考資料 :</p><ul><li><a href="https://chikuwa-tech-study.blogspot.com/2024/05/spring-boot-setup-mysql-and-introduce-jpa.html"><strong>準備 MySQL 資料庫與認識 Spring Data JPA</strong></a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Spring Data Jpa </tag>
</tags>
</entry>
<entry>
<title>Spring Data JPA, JDBC Template, Mybatis 比較</title>
<link href="/2024/09/23/SpringBoot/Spring-Data-JPA-JDBC-Template-Mybatis-%E6%AF%94%E8%BC%83/"/>
<url>/2024/09/23/SpringBoot/Spring-Data-JPA-JDBC-Template-Mybatis-%E6%AF%94%E8%BC%83/</url>
<content type="html"><![CDATA[<p>今天就來認識一下使用 Spring Boot 的時候可能會碰到的三種資料存取框架 Spring Data JPA、JDBC Template 和 MyBatis ,他們都各有其特色和適合的應用場景,當你需要碰到資料庫的處理,肯定需要來了解一下。</p><h2 id="Spring-Data-JPA"><a href="#Spring-Data-JPA" class="headerlink" title="Spring Data JPA"></a>Spring Data <strong>JPA</strong></h2><h3 id="特點:"><a href="#特點:" class="headerlink" title="特點:"></a><strong>特點:</strong></h3><ul><li><strong>ORM 的框架:</strong>將資料庫表格映射到我們定義的 Java 物件上面,並提供自動化的資料庫操作。資料操作可以用物件的概念還進行操作,程式碼具物件特性和可讀性。</li><li><strong>簡化資料存取:</strong>透過 JPA(<strong>(Java Persistence API</strong>),可自動根據操作的慣例來減少手動撰寫 SQL 的需求,例如 findById, findAll 等等的直觀方法可以直接進行操作,不需要另外定義。</li><li><strong>支援 JPQL (Java Persistence Query Language):</strong> 提供類似 SQL 的查詢語言,但更強調物件導向,使用的是物件屬性而非資料庫欄位。</li><li><strong>與 Spring 整合:</strong>由於是 Spring Framework 的一部分,與 Spring 框架可自然引入整合。</li></ul><h3 id="優點:"><a href="#優點:" class="headerlink" title="優點:"></a><strong>優點:</strong></h3><ul><li>減少 SQL 操作的細節,並專注於物件導向設計時。</li><li>資料庫結構相對穩定,不需要頻繁修改時操作方便。</li></ul><h3 id="缺點:"><a href="#缺點:" class="headerlink" title="缺點:"></a><strong>缺點:</strong></h3><ul><li>對複雜查詢支持不夠靈活。 雖提供 JPQL 但對於複雜的操作上較侷限,可能仍需使用 Native Query 使用原生 SQL 進行。</li><li>在高效能需求下,可能因自動生成的 SQL 語句效能不佳。</li></ul><h2 id="JDBC-Template"><a href="#JDBC-Template" class="headerlink" title="JDBC Template"></a><strong>JDBC Template</strong></h2><h3 id="特點:-1"><a href="#特點:-1" class="headerlink" title="特點:"></a><strong>特點:</strong></h3><ul><li><strong>簡化原生 JDBC 操作:</strong>JDBC Template 封裝了原生 JDBC API,簡化了連接、資源管理和錯誤處理等繁瑣細節。</li><li><strong>高度控制 SQL 語句:</strong>需要開發者手動撰寫 SQL 語句,讓開發者完全掌握 SQL 的細節和效能。</li><li><strong>簡單易用:</strong>支援查詢單一資料、多筆資料、更新、插入和批次操作,容易學習和掌握。</li><li><strong>與 Spring 整合:</strong> 由於是 Spring Framework 的一部分,與 Spring 框架可自然引入整合。</li></ul><h3 id="優點:-1"><a href="#優點:-1" class="headerlink" title="優點:"></a><strong>優點:</strong></h3><ul><li>靈活撰寫 SQL</li><li>可應對資料庫結構不斷變動或查詢需求複雜的系統。</li></ul><h3 id="缺點:-1"><a href="#缺點:-1" class="headerlink" title="缺點:"></a><strong>缺點:</strong></h3><ul><li>需要手動撰寫和維護 SQL 語句,增加代碼量。</li><li>不如 JPA 那樣自動處理關聯或物件映射。</li></ul><h2 id="MyBatis"><a href="#MyBatis" class="headerlink" title="MyBatis"></a><strong>MyBatis</strong></h2><h3 id="特點:-2"><a href="#特點:-2" class="headerlink" title="特點:"></a><strong>特點:</strong></h3><ul><li><strong>類 ORM:</strong>因為 ORM 的設計概念就是要減少 SQL 的使用,自動透過語法去產生 SQL,MyBatis 支援將 SQL 查詢結果自動映射到 Java 物件這樣類似 ORM 的概念,但 SQL 需要開發者自行撰寫,所以不算 ORM。</li><li><strong>高靈活性:</strong>開發者可以完全掌控 SQL 語句,適合處理複雜查詢或動態 SQL 的場景,例如可以在 SQL 中加入特定條件判斷,選擇性的執行語句。</li><li><strong>XML 和註解配置:</strong> 可以使用 XML 或註解配置 SQL 語句,適合團隊協作和代碼重用。也將 SQL 結果自動映射到 Java 類別,減少手動處理的麻煩。</li></ul><h3 id="適用場景:"><a href="#適用場景:" class="headerlink" title="適用場景:"></a><strong>適用場景:</strong></h3><ul><li>靈活撰寫 SQL</li><li>可應對資料庫結構不斷變動或查詢需求複雜的系統。</li></ul><h3 id="缺點:-2"><a href="#缺點:-2" class="headerlink" title="缺點:"></a><strong>缺點:</strong></h3><ul><li>XML 配置可能過於冗長,增加代碼複雜度。</li><li>需要維護大量自訂 SQL,若不加以管理,可能導致代碼難以維護。</li></ul><h3 id="JPA、JDBC-Template-和-MyBatis-的比較與選擇指引"><a href="#JPA、JDBC-Template-和-MyBatis-的比較與選擇指引" class="headerlink" title="JPA、JDBC Template 和 MyBatis 的比較與選擇指引"></a><strong>JPA、JDBC Template 和 MyBatis 的比較與選擇指引</strong></h3><table><thead><tr><th><strong>特性</strong></th><th><strong>JPA</strong></th><th><strong>JDBC Template</strong></th><th><strong>MyBatis</strong></th></tr></thead><tbody><tr><td><strong>SQL 控制</strong></td><td>低(自動生成)</td><td>高(手動撰寫)</td><td>高(手動撰寫)</td></tr><tr><td><strong>資料庫操作簡易度</strong></td><td>高(自動化映射與操作)</td><td>中(簡化資源管理與操作)</td><td>中(映射支援,但 SQL 需自行處理)</td></tr><tr><td><strong>ORM 支援</strong></td><td>完整支援</td><td>無(需要手動處理)</td><td>半自動化(SQL 自訂,結果自動映射)</td></tr><tr><td><strong>複雜查詢</strong></td><td>一般(使用 JPQL,靈活性有限)</td><td>高(完全自訂 SQL)</td><td>高(動態 SQL 支援)</td></tr><tr><td><strong>學習曲線</strong></td><td>中(需理解 ORM 概念)</td><td>低(適合熟悉 SQL 的開發者)</td><td>中(需學習 XML 或註解配置)</td></tr><tr><td><strong>效能</strong></td><td>中(依賴 ORM 實現)</td><td>高(手動優化 SQL)</td><td>高(可完全控制 SQL)</td></tr></tbody></table><h2 id="使用心得分享:"><a href="#使用心得分享:" class="headerlink" title="使用心得分享:"></a>使用心得分享:</h2><p>目前我工作上有碰過的就是 JPA 和 MyBatis 這兩種,自己使用的心得會覺得,如果資料庫簡單,多半都是進行比較單純的 CRUD 操作時,因為物件關係明確時,JPA 會比較方便去操作,像是自己做一些小專案可以很快地建立起操作資料的連結或;當資料庫比較複雜需要較多 SQL 操作,並且對 SQL 熟悉,有高度掌控需求時,MyBatis 會更適合,JDBC 雖然我沒用過,但看過一些用法也是著重原生 SQL 的特性,所以也會比較接近 MyBatis 的應用場景。</p><h2 id="小總結:"><a href="#小總結:" class="headerlink" title="小總結:"></a>小總結:</h2><ul><li>若開發專案重視物件導向設計與簡化 CRUD 操作,並且不太需要控制 SQL 細節,選擇 JPA。</li><li>若專案需要靈活控制 SQL 並希望減少手動管理 JDBC 資源的複雜度,選擇 JDBC Template</li><li>若專案需要高度靈活的 SQL 控制,並且涉及大量複雜查詢和動態 SQL,選擇 MyBatis</li></ul><hr><p>參考資料:</p><ul><li><a href="https://medium.com/@zzzzzYang/spring-data-jpa-vs-mybatis-8611acff00ec"><strong>Spring Data JPA vs MyBatis</strong></a></li><li><a href="https://juejin.cn/post/7221800875742773307"><strong>MyBatis、Spring JDBC 和 Spring Data JPA:選擇哪種持久框架?</strong></a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> ORM </tag>
<tag> Database </tag>
</tags>
</entry>
<entry>
<title>Spring MVC - Thymeleaf</title>
<link href="/2024/09/22/SpringBoot/Spring-MVC-Thymeleaf/"/>
<url>/2024/09/22/SpringBoot/Spring-MVC-Thymeleaf/</url>
<content type="html"><![CDATA[<p>這篇來補充一下一些 Spring MVC 的 View 的部分吧,這部分其實就是前端的處理,雖然業界許多已經前後端分離,所以後端只需要著重在 Spring Boot 本身的後端 Server 上進行資料的處理,但其實過去前後端整合 FullStack 的部分也是有許多公司仍在使用, Spring Boot 其實也有提供和許多前端模板整合的方式來進行全端的處理,所以這部分如果碰到也是需要</p><h2 id="Thymeleaf"><a href="#Thymeleaf" class="headerlink" title="Thymeleaf"></a>Thymeleaf</h2><ul><li>SpringBoot 推薦使用的 web 前端模板引擎</li><li>能夠輕鬆實現 MVC 架構並配合後端資料動態渲染前端頁面</li><li>可在 Html 中嵌入動態內容</li><li>其他類似的模板引擎: JSP, Freemarker, Groovy, Velocity 等</li></ul><h2 id="引入套件及配置"><a href="#引入套件及配置" class="headerlink" title="引入套件及配置"></a>引入套件及配置</h2><p>pom.xml</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-thymeleaf<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><p>application.properties</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 表示是否啟用Thymeleaf 的快取,開發階段建議關閉</span><br><span class="line">spring.thymeleaf.cache=<span class="literal">false</span></span><br><span class="line"># 模板前綴設置,預設即為classpath:/templates/</span><br><span class="line">spring.thymeleaf.prefix=classpath:/templates/</span><br><span class="line"># 模板後綴設置,預設為.html</span><br><span class="line">spring.thymeleaf.suffix=.html</span><br></pre></td></tr></table></figure><p>項目結構</p><p>主要需要注意的是模板放的位置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">src/</span><br><span class="line">├── main/</span><br><span class="line">│ ├── java/</span><br><span class="line">│ │ └── com/example/demo/</span><br><span class="line">│ │ ├── DemoApplication.java</span><br><span class="line">│ │ └── HomeController.java</span><br><span class="line">│ ├── resources/</span><br><span class="line">│ │ ├── application.properties</span><br><span class="line">│ │ └── templates/</span><br><span class="line">│ │ └── home.html</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="後端資料處理及傳遞"><a href="#後端資料處理及傳遞" class="headerlink" title="後端資料處理及傳遞"></a>後端資料處理及傳遞</h2><p>假設我們在後端建立一個 HomeController</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Controller</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HelloController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/home")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">home</span><span class="params">(Model model)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">//模擬 Service 回傳的資料</span></span><br><span class="line"> <span class="type">Student</span> <span class="variable">student</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>();</span><br><span class="line"> student.setId(<span class="number">1</span>);</span><br><span class="line"> student.setName(<span class="string">"Sean"</span>);</span><br><span class="line"></span><br><span class="line"> model.addAttribute(<span class="string">"message"</span>, <span class="string">"Welcome to Thymeleaf!"</span>);</span><br><span class="line"> model.addAttribute(<span class="string">"student"</span>, student);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"home"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以用到前面的設計 URL 方法來設計,注意的是這邊就需要用到一個重要的註解 <code>@Controller</code> 這個註解表示不是直接傳遞一個 Json 或 Xml 格式的資料,而是包含前端渲染的模板,當使用這個註解之後 Spring Boot 就會幫我們自動匹配 /resources/templates 下面的模板配合我們所傳遞的資料回傳給前端渲染。</p><p>也要特別注意到前端要接收的資料需要裝在一個 <code>Model</code> 的物件內,我們可以用 <code>addAttribute(”key”, “value”)</code> 來裝進去,key 就會是前端需要取值時候帶入的,然後就會呈現 value 的值,value 可以是任意類型可以是字串、數值或是物件。</p><p>最後回傳的資料都是用 String 格式,且回傳的值就是要呈現的模板名稱,像是這邊我們要呈現在 home.html 就是回傳 “home”,很直觀就可以聯想把剛剛我們設定好的資料丟到這個模板呈現。</p><h2 id="前端渲染接收到資料"><a href="#前端渲染接收到資料" class="headerlink" title="前端渲染接收到資料"></a>前端渲染接收到資料</h2><p>創建 home.html 頁面至 templates,並且設定好需要 thymeleaf 渲染的資料,使用 <code><tag th:text = “${key}”><tag></code> 的語法來呈現,前面 tag 可以是任意 html 標籤,要呈現的值放在中間並用 “${key}” 來抓出Model 帶來要呈現的資料,如果是物件可以用 “${object.key}” 來抓到內部的個別屬性</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">html</span>></span></span><br><span class="line"><span class="tag"><<span class="name">html</span> <span class="attr">xmlns:th</span>=<span class="string">"http://www.thymeleaf.org"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>Thymeleaf Example<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">body</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">h1</span> <span class="attr">th:text</span>=<span class="string">"${message}"</span>></span>Default message<span class="tag"></<span class="name">h1</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span>></span>ID<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.id}"</span>></span>Default id<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span>></span>NAME<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.name}"</span>></span>Default name<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">body</span>></span></span><br><span class="line"><span class="tag"></<span class="name">html</span>></span></span><br></pre></td></tr></table></figure><p><img src="https://ithelp.ithome.com.tw/upload/images/20240828/20150977r8OaaGDF2k.png" alt="https://ithelp.ithome.com.tw/upload/images/20240828/20150977r8OaaGDF2k.png"></p><h2 id="如果有多筆資料要呈現"><a href="#如果有多筆資料要呈現" class="headerlink" title="如果有多筆資料要呈現"></a>如果有多筆資料要呈現</h2><p>多筆學生資料可以用 List 來裝,也同樣存進去 model</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Controller</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HelloController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping("/home")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">home</span><span class="params">(Model model)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">//模擬 Service, dao 回傳的資料</span></span><br><span class="line"> List<Student> studentList = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"> <span class="type">Student</span> <span class="variable">student1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>();</span><br><span class="line"> student1.setId(<span class="number">1</span>);</span><br><span class="line"> student1.setName(<span class="string">"Sean"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">Student</span> <span class="variable">student2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>();</span><br><span class="line"> student2.setId(<span class="number">2</span>);</span><br><span class="line"> student2.setName(<span class="string">"Ken"</span>);</span><br><span class="line"></span><br><span class="line"> studentList.add(student1);</span><br><span class="line"> studentList.add(student2);</span><br><span class="line"></span><br><span class="line"> model.addAttribute(<span class="string">"message"</span>, <span class="string">"Welcome to Thymeleaf!"</span>);</span><br><span class="line"> model.addAttribute(<span class="string">"student"</span>, studentList);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"home"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>前端改用 Thymeleaf For 迴圈語法,<code><tag th:each=”item : ${itemList}”></code> 同樣 tag 可以用在各種類型的標籤上面,後面的用法概念都是一樣。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">html</span>></span></span><br><span class="line"><span class="tag"><<span class="name">html</span> <span class="attr">xmlns:th</span>=<span class="string">"http://www.thymeleaf.org"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>Thymeleaf Example<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">body</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">h1</span> <span class="attr">th:text</span>=<span class="string">"${message}"</span>></span>Default message<span class="tag"></<span class="name">h1</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">thead</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">th</span>></span>ID<span class="tag"></<span class="name">th</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">th</span>></span>Name<span class="tag"></<span class="name">th</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">thead</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tbody</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span> <span class="attr">th:each</span>=<span class="string">"student : ${student}"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.id}"</span>></span>ID<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.name}"</span>></span>Name<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tbody</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">body</span>></span></span><br><span class="line"><span class="tag"></<span class="name">html</span>></span></span><br></pre></td></tr></table></figure><p>可以呈現多筆的資料</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240828/20150977RFIX55tolo.png" alt="https://ithelp.ithome.com.tw/upload/images/20240828/20150977RFIX55tolo.png"></p><h2 id="頁面抽為片段模組化"><a href="#頁面抽為片段模組化" class="headerlink" title="頁面抽為片段模組化"></a>頁面抽為片段模組化</h2><p>首先,將前面 home.html 表格的片段抽取到一個單獨的模板中,這個模板也放在 <code>src/main/resources/templates/</code> 目錄下,並命名為 <code>fragments.html</code>。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">html</span>></span></span><br><span class="line"><span class="tag"><<span class="name">html</span> <span class="attr">xmlns:th</span>=<span class="string">"http://www.thymeleaf.org"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>Fragments<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">body</span>></span></span><br><span class="line"> <span class="comment"><!-- 定義一個 fragment,名為 studentTable --></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">th:fragment</span>=<span class="string">"studentTable"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">table</span> <span class="attr">border</span>=<span class="string">"1"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">thead</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">th</span>></span>ID<span class="tag"></<span class="name">th</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">th</span>></span>Name<span class="tag"></<span class="name">th</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">thead</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tbody</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">tr</span> <span class="attr">th:each</span>=<span class="string">"student : ${student}"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.id}"</span>></span>1<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">td</span> <span class="attr">th:text</span>=<span class="string">"${student.name}"</span>></span>Name<span class="tag"></<span class="name">td</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tr</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">tbody</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">body</span>></span></span><br><span class="line"><span class="tag"></<span class="name">html</span>></span></span><br></pre></td></tr></table></figure><p>在要插入的主模板 <code>home.html</code> 中引入這個 fragment,並將 student 列表傳遞進去。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">html</span>></span></span><br><span class="line"><span class="tag"><<span class="name">html</span> <span class="attr">xmlns:th</span>=<span class="string">"http://www.thymeleaf.org"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>Student List<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">head</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">body</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">h1</span> <span class="attr">th:text</span>=<span class="string">"${message}"</span>></span>Welcome to Thymeleaf!<span class="tag"></<span class="name">h1</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="comment"><!-- 引入顯示 fragment --></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">th:include</span>=<span class="string">"fragments :: studentTable"</span>></span><span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">body</span>></span></span><br><span class="line"><span class="tag"></<span class="name">html</span>></span></span><br></pre></td></tr></table></figure><p>controller 可以不用更動,但這樣抽成片段可以讓你嵌入任何你想要顯示的模板內。</p><p>以上就是一些基本的用法,這樣方便的整合真的是很適合後端工程師做一些簡單專案,如果需要一些前端處理可以透過這樣的框架輕鬆呈現,降低學習成本,不過這樣也是有一些優缺點,面對不同開發環境或是專案類型大家還是可以評估是否需要前後端分離,這邊也整理分享一下我認為一些前後端分離跟合併的優缺點</p><h2 id="前後端分離和合併的特色整理"><a href="#前後端分離和合併的特色整理" class="headerlink" title="前後端分離和合併的特色整理"></a>前後端分離和合併的特色整理</h2><table><thead><tr><th><strong>方面</strong></th><th><strong>前後端分離</strong></th><th><strong>前後端合併</strong></th></tr></thead><tbody><tr><td><strong>定義</strong></td><td>前端與後端開發為獨立系統,前端使用框架如 React/Vue/Angular 等,後端提供 API</td><td>前端與後端統一開發,通常使用如 Thymeleaf、JSP 等服務端渲染技術</td></tr><tr><td><strong>技術選擇自由度</strong></td><td>高,前後端可以選擇不同的技術</td><td>低,前後端技術需統一,受限於後端技術</td></tr><tr><td><strong>團隊合作效率</strong></td><td>高,前後端團隊可平行開發,互不影響</td><td>低,前後端開發耦合較高,需要更多協作</td></tr><tr><td><strong>可重用性</strong></td><td>高,後端 API 可被多個前端應用共享</td><td>低,前端與後端綁定,難以重用其他應用</td></tr><tr><td><strong>可維護性</strong></td><td>好,前後端獨立,修改不會相互影響</td><td>差,前後端修改需同步進行,耦合度高</td></tr><tr><td><strong>前端性能優化</strong></td><td>易,前端完全控制資源加載與渲染,可進行各種性能優化</td><td>難,受限於後端技術,性能優化空間有限</td></tr><tr><td><strong>開發初期成本</strong></td><td>高,需要額外搭建 API 與前端構建工具等基礎設施</td><td>低,整合開發環境,開發初期更為簡單</td></tr><tr><td><strong>部署與運維</strong></td><td>複雜,前後端獨立部署,需處理更多部署與運維細節</td><td>簡單,單一應用部署與運維更容易</td></tr><tr><td><strong>SEO 優勢</strong></td><td>低,前端渲染的應用對 SEO 不友好,需要額外處理</td><td>高,服務端渲染的應用對 SEO 更友好,爬蟲易於抓取</td></tr><tr><td><strong>應用場景</strong></td><td>- 大型或組織分工明確團隊,可同步開發,提升效率。(社交平台、電商)</td><td></td></tr></tbody></table><ul><li>有多平台需要通用 API。(如 Web、行動裝置、桌面應用)</li><li>迭代更動快速、可前後端分開的微服務系統。(SaaS、SaaP) | - 中小型團隊,簡化部屬及初期開發成本、降低偏後端人員的學習曲線</li><li>SEO 要求高 (新聞網站、Blog)</li><li>依賴後端邏輯,前端基本展示,前後端整合會更直觀。 (如 ERP、CRM) |</li></ul><hr><p>參考資料:</p><ul><li><a href="https://www.thymeleaf.org/">thymeleaf</a></li><li><a href="https://blog.csdn.net/weixin_44814196/article/details/135288104"><strong>前後端分離架構的特性以及優缺點</strong></a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Thymeleaf </tag>
</tags>
</entry>
<entry>
<title>Spring MVC - 基本介紹</title>
<link href="/2024/09/22/SpringBoot/Spring-MVC-%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%B4%B9/"/>
<url>/2024/09/22/SpringBoot/Spring-MVC-%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%B4%B9/</url>
<content type="html"><![CDATA[<h2 id="Spring-MVC"><a href="#Spring-MVC" class="headerlink" title="Spring MVC"></a>Spring MVC</h2><p>Spring MVC 是基於 Servlet API 建構的網頁開發框架,根據 MVC 架構的原則,提供了的合適的開發流程和功能,也是 Spring 框架的一部分,可與 Spring 生態系統無缝整合。</p><h2 id="Spring-MVC-中的-MVC-架構"><a href="#Spring-MVC-中的-MVC-架構" class="headerlink" title="Spring MVC 中的 MVC 架構"></a>Spring MVC 中的 MVC 架構</h2><p>一般我們常說到的 MVC 架構,就是 Model, View, Controller 這三層,在 Spring Boot 中則是定義成 Dao, Service, Controller, Template(View),前端部分就是 View 層的處理,而 Spring Boot 則是針對後端資料部分進行處理剩下的 Controller、Model。</p><h3 id="Controller"><a href="#Controller" class="headerlink" title="Controller"></a>Controller</h3><ul><li>Controller: 針對請求首先進入的位置,就是一個 API 或是 APP 的進入點,通常只負責將請求處理,驗證,並讓符合格式的請求進入到後面 Service 進行後續資料處理</li></ul><h3 id="Model"><a href="#Model" class="headerlink" title="Model"></a>Model</h3><ul><li>Service: 業務邏輯處理,根據請求與資料庫溝通,拿出對應的資料進行操作</li><li>Dao: 負責與資料庫溝通的介面,早期透過 JPA 來處理,現多半透過資料庫操作的框架輔助像是 Spring Data JPA, JDBC Template, My batis 等</li></ul><h2 id="引入套件-Spring-Web"><a href="#引入套件-Spring-Web" class="headerlink" title="引入套件 Spring Web"></a>引入套件 Spring Web</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-web<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><p>Spring Web (spring-boot-starter-web)套件就是 Spring MVC 的核心,引入之後就可以透過瀏覽器和我們啟動的程式有所連結,給與指定的 URL 就可以連結到我們程式的 Controller。</p><h2 id="URL-Routing"><a href="#URL-Routing" class="headerlink" title="URL Routing"></a>URL Routing</h2><p>我們要能夠使用 Spring MVC 的框架來完成一個請求,首先要把我們的端口和 URL 可以配對上,也就是 Routing,要完成這部分的配置就必須熟悉這套件提供給我們的一些註解的使用,可以參考先前 <a href="https://www.notion.so/Day-3-Bean-5b205fe4fa2e44758f9306a7906585e6?pvs=21">Day 3 - Bean 介紹及常見應用</a> 的 Web 與 RESTful API 部分,還有需要對於基本 HTTP 協定及方法要有一定了解, 底下就不做詳細介紹。</p><p>以下列出一些基本常見的用法舉例</p><p><code>@RestController</code> 和 <code>@Controller</code> 用來標記類別成為端口,標記後你就可以設定底下的方法成為端口。而 <code>@RestController</code> 會自動將返回的物件轉成 Json 格式, <code>@Controller</code> 通常是搭配前後端整合把 View 層的處理也整合進 Spring Boot,後面介紹到 Thymeleaf 這個前端模組就會講到。</p><p>假設我們需要來做一些基本的 CRUD 端口</p><table><thead><tr><th></th><th>Http Method</th><th>URL</th></tr></thead><tbody><tr><td>查詢商品</td><td>GET</td><td>/api/products/{id}</td></tr><tr><td>新增商品</td><td>POST</td><td>/api/products</td></tr><tr><td>更新商品</td><td>PUT</td><td>/api/products/{id}</td></tr><tr><td>刪除商品</td><td>DELETE</td><td>/api/products/{id}</td></tr></tbody></table><p>設定端口就會需要配置接受的 http 方法參數等等,所以就要透過下面的註解來實現:</p><ul><li><code>@RequestMapping</code>,可以放在主類別表示進入這個地方的主要 URL path,如果根據上面 Restful 的設計,前面共同的區域 <code>/api/products</code> 就可以放在這裡去設定。也可以放在方法上定義每個方法的 URL path,但需要多帶入參數設定來配置端口的限制(http method, content type…),使用上會建議方法可以用下面的,除非需要做很精細的配置或條件,選擇下面的方式比較簡潔明瞭。</li><li><code>@GetMapping</code> <code>@PostMapping</code> <code>@DeleteMapping</code> 等可以標記使用的 Http 方,這邊定義的路徑會接續上面 <code>@RequestMapping</code> 定義的後面</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api/products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductController</span> {</span><br><span class="line"> <span class="comment">// 新增產品</span></span><br><span class="line"> <span class="meta">@PostMapping</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">createProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@GetMapping("/{id}")</span></span><br><span class="line"> <span class="comment">// @RequestMapping(value = "/{id}", method = RequestMethod.GET)</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@PutMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">updateProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@DeleteMapping("/{id}")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">deleteProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"成功刪除產品"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>都改成使用 <code>@RequestMapping</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("/api/products")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 新增產品</span></span><br><span class="line"> <span class="meta">@RequestMapping(method = RequestMethod.POST)</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">createProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@RequestMapping(value = "/{id}", method = RequestMethod.GET)</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getAllProducts</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@RequestMapping(value = "/{id}", method = RequestMethod.PUT)</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">updateProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</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="meta">@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">deleteProduct</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"成功刪除產品"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>大家可以自行用 Postman 或是 Talent Api Tester 這個 chrome 插件 (<a href="https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=zh-TW&pli=1">https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=zh-TW&pli=1</a>) 來測試一下上面的 api 回傳結果,通常啟動程式後預設 port 都是 8080,所以 server 的位置就是 <a href="http://localhost:8080,我們上面定義的路徑就是接著這個後面。">http://localhost:8080,我們上面定義的路徑就是接著這個後面。</a></p><p><code>http://localhost:8080/api/products</code> 對這個位置用 GET 請求就可以成功拿到回傳”成功查詢商品”<br><img src="https://ithelp.ithome.com.tw/upload/images/20240827/20150977kdSdqmj3nI.png" alt="https://ithelp.ithome.com.tw/upload/images/20240827/20150977kdSdqmj3nI.png"></p><p><a href="http://localhost:8080/api/products">http://localhost:8080/api/products</a> 如果這路徑用 PUT 方法請求會失敗,表示不允許的方法<br><img src="https://ithelp.ithome.com.tw/upload/images/20240827/20150977zexFWzs9Ad.png" alt="https://ithelp.ithome.com.tw/upload/images/20240827/20150977zexFWzs9Ad.png"></p><p>但如果用<code>http://localhost:8080/api/products/1</code> 就可以成功,因為我們上面定義的路徑要多帶 id<br><img src="https://ithelp.ithome.com.tw/upload/images/20240827/20150977rRUEEebHIu.png" alt="https://ithelp.ithome.com.tw/upload/images/20240827/20150977rRUEEebHIu.png"></p><hr><p>參考資料:</p><ul><li>[Spring MVC 介紹及使用]<a href="https://blog.csdn.net/m0_64338546/article/details/132071506">(https://blog.csdn.net/zzuhkp/article/details/120685952</a>)</li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
</tags>
</entry>
<entry>
<title>專案管理 Maven</title>
<link href="/2024/09/21/SpringBoot/%E5%B0%88%E6%A1%88%E7%AE%A1%E7%90%86-Maven/"/>
<url>/2024/09/21/SpringBoot/%E5%B0%88%E6%A1%88%E7%AE%A1%E7%90%86-Maven/</url>
<content type="html"><![CDATA[<h2 id="Maven"><a href="#Maven" class="headerlink" title="Maven"></a>Maven</h2><p>Maven 是一個<strong>專案管理與自動化構建的工具</strong>,主要用於 Java 的專案。 目前由 Apache 軟體基金會管理。另外一個常見的工具 Gradle,這邊就不多介紹,主要的用途都類似。</p><p>是根據專案物件模型(Project Object Model, POM)概念設計,在 pom.xml 檔案中描述專案的基本資訊、與外部模組的依賴關係、建構順序、目錄與外掛程式,並執行 Maven 提供的構建方法就可快速完成編譯、打包等等。剛進入透過 Maven 管理的專案會自動從遠端的程式庫 (<a href="https://mvnrepository.com/">mvn repository</a>)下載依賴</p><h2 id="專案結構"><a href="#專案結構" class="headerlink" title="專案結構"></a>專案結構</h2><p>當我們剛創建好一個由 Maven 構建的 Spring Boot 專案,他的結構會像是下面這樣</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line">my-spring-boot-app</span><br><span class="line">│ pom.xml</span><br><span class="line">└───src</span><br><span class="line"> └───main</span><br><span class="line"> │ ├───java</span><br><span class="line"> │ │ └───com</span><br><span class="line"> │ │ └───example</span><br><span class="line"> │ │ └───demo</span><br><span class="line"> │ │ └───DemoApplication.java</span><br><span class="line"> │ └───resources</span><br><span class="line"> │ ├───application.properties</span><br><span class="line"> │ └───static</span><br><span class="line"> │ └───templates</span><br><span class="line"> └───test</span><br><span class="line"> └───java</span><br><span class="line"> └───com</span><br><span class="line"> └───example</span><br><span class="line"> └───demo</span><br><span class="line"> └───DemoApplicationTests.java</span><br><span class="line"></span><br><span class="line">![https://ithelp.ithome.com.tw/upload/images/20240821/201509775QKIvymNt5.png](https://ithelp.ithome.com.tw/upload/images/20240821/201509775QKIvymNt5.png)</span><br><span class="line"></span><br><span class="line">## pom.xml</span><br><span class="line"></span><br><span class="line">可以點開 pom.xml</span><br><span class="line"></span><br><span class="line">```xml</span><br><span class="line"><?xml version="1.0" encoding="UTF-8"?></span><br><span class="line"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"</span><br><span class="line">xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"></span><br><span class="line"><modelVersion>4.0.0</modelVersion></span><br><span class="line"><parent></span><br><span class="line"><groupId>org.springframework.boot</groupId></span><br><span class="line"><artifactId>spring-boot-starter-parent</artifactId></span><br><span class="line"><version>3.3.2</version></span><br><span class="line"><relativePath/> <!-- lookup parent from repository --></span><br><span class="line"></parent></span><br><span class="line"><groupId>com.oseanchen</groupId></span><br><span class="line"><artifactId>demoproj</artifactId></span><br><span class="line"><version>0.0.1-SNAPSHOT</version></span><br><span class="line"><name>demoproj</name></span><br><span class="line"><description>demoproj</description></span><br><span class="line"><url/></span><br><span class="line"><licenses></span><br><span class="line"><license/></span><br><span class="line"></licenses></span><br><span class="line"><developers></span><br><span class="line"><developer/></span><br><span class="line"></developers></span><br><span class="line"><scm></span><br><span class="line"><connection/></span><br><span class="line"><developerConnection/></span><br><span class="line"><tag/></span><br><span class="line"><url/></span><br><span class="line"></scm></span><br><span class="line"><properties></span><br><span class="line"><java.version>17</java.version></span><br><span class="line"></properties></span><br><span class="line"><dependencies></span><br><span class="line"><dependency></span><br><span class="line"><groupId>org.springframework.boot</groupId></span><br><span class="line"><artifactId>spring-boot-starter-web</artifactId></span><br><span class="line"></dependency></span><br><span class="line"></span><br><span class="line"><dependency></span><br><span class="line"><groupId>org.springframework.boot</groupId></span><br><span class="line"><artifactId>spring-boot-starter-test</artifactId></span><br><span class="line"><scope>test</scope></span><br><span class="line"></dependency></span><br><span class="line"></dependencies></span><br><span class="line"></span><br><span class="line"><build></span><br><span class="line"><plugins></span><br><span class="line"><plugin></span><br><span class="line"><groupId>org.springframework.boot</groupId></span><br><span class="line"><artifactId>spring-boot-maven-plugin</artifactId></span><br><span class="line"></plugin></span><br><span class="line"></plugins></span><br><span class="line"></build></span><br><span class="line"></span><br><span class="line"></project></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>這個檔案紀錄目前該專案所有重要構建資訊,</p><p>只要你對這份文件上面有進行任何修改 Maven 都會自動根據你的需求重新構建</p><p>一些重要資訊說明</p><h3 id="SpringBoot-相關資訊"><a href="#SpringBoot-相關資訊" class="headerlink" title="SpringBoot 相關資訊"></a>SpringBoot 相關資訊</h3><p>可以知道目前運行版本,如果直接改動 reload 後會自動轉換成不同版</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">parent</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<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-starter-parent<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">version</span>></span>3.3.2<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">parent</span>></span></span><br></pre></td></tr></table></figure><h3 id="專案資訊"><a href="#專案資訊" class="headerlink" title="專案資訊"></a>專案資訊</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">groupId</span>></span>com.oseanchen<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>demoproj<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">version</span>></span>0.0.1-SNAPSHOT<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"><<span class="name">name</span>></span>demoproj<span class="tag"></<span class="name">name</span>></span></span><br><span class="line"><span class="tag"><<span class="name">description</span>></span>demoproj<span class="tag"></<span class="name">description</span>></span></span><br></pre></td></tr></table></figure><p>groupId : 通常預設建議是公司或個人名稱及網域</p><p>artifactId: groupid 下一層 package,通常也是專案名稱</p><blockquote><p>仔細看一下專案結構會發現啟動類就放在 com.oseanchen.demoproj 下面,這也是根據當初建立專案時設定這些內容所產生</p></blockquote><p>version: 專案版本</p><p>name: 專案名稱</p><p>decription: 專案描述</p><h3 id="Java-版本"><a href="#Java-版本" class="headerlink" title="Java 版本"></a>Java 版本</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">properties</span>></span></span><br><span class="line"><span class="tag"><<span class="name">java.version</span>></span>17<span class="tag"></<span class="name">java.version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">properties</span>></span></span><br></pre></td></tr></table></figure><h3 id="依賴-dependencies"><a href="#依賴-dependencies" class="headerlink" title="依賴(dependencies)"></a>依賴(dependencies)</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependencies</span>></span></span><br><span class="line"></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>org.springframework.boot<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-starter-web<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></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>org.springframework.boot<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-starter-test<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">scope</span>></span>test<span class="tag"></<span class="name">scope</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">dependencies</span>></span></span><br></pre></td></tr></table></figure><p>這邊可以根據我們需要加入,如果你不確定對應的 gorupId, artifacId 可到 mvn repository 上面搜尋對應的套件名稱及版本(下圖紅框) 就可以根據同樣的格式引入,下面是只有引入 mysql,你可以選擇你要的版本進到最後資訊頁下面有引入的 xml (下圖綠框) 可以直接複製貼到 pom.xml 裡面</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240822/20150977E1uQPtwuBw.png" alt="https://ithelp.ithome.com.tw/upload/images/20240822/20150977E1uQPtwuBw.png"></p><h3 id="Maven-Lifecycle"><a href="#Maven-Lifecycle" class="headerlink" title="Maven Lifecycle"></a>Maven Lifecycle</h3><p>再來通常 IDE 都會整合 maven 的構建功能直接讓我們可以使用,也可以透過指令,但要另外安裝 maven 至本機,我這邊以 Intellij 提供 maven 整合的插件做說明,可以直接點選右邊 maven 圖示(我有改過 icon,所以原本應該是個 m 的圖示),點開後選擇專案的 package 下面會有一個 life cycle,就可以看到整個專案構件的生命週期分成哪些步驟。</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240822/201509771qUmVDYZa0.png" alt="https://ithelp.ithome.com.tw/upload/images/20240822/201509771qUmVDYZa0.png"></p><h3 id="三種基本的-build-life-cycle"><a href="#三種基本的-build-life-cycle" class="headerlink" title="三種基本的 build life cycle:"></a>三種基本的 build life cycle:</h3><ul><li><strong>Clean Lifecycle</strong> — 清理專案的生命週期 (共 3 個 Phase) 用於清除先前建置或打包專案時所產生的檔案。通常在專案建置時,所產生的那些檔案都會被放置於 target 的資料夾底下,因此當你執行<code>clean</code> 的生命週期時,也就會把 target 資料夾給刪除。</li><li><strong>Default Lifecycle</strong> — 建構專案的預設生命週期 (共 23 個 Phase) 流程中,包含幾個重要階段下面會依照順序執行,也是 Intellij maven pulgin 右側選單可以看到的。<ol><li>validate:檢查專案是否所有必需的資訊都有符合或都為正確資訊。</li><li>compile:編譯專案的原始程式碼。</li><li>test:透過一些單元測試的框架來執行測試。</li><li>package:將編譯完成後的內容打包成被指定的格式,例如 .jar 或 .war,放入 target 資料夾內,顯示的名稱會是 pom.xml 專案資訊部分 artifactId + version。</li><li>verify:檢查測試結果,以確保專案的品質</li><li>install:將打包後的結果安裝在本地端的 maven repository,供其他本地端的專案導入使用。</li><li>deploy:在本地端(建構環境)中完成建構確認沒有錯誤發生後,就會將打包後的結果上傳到遠端的 maven repository 供全世界的人使用。</li></ol></li><li><strong>Site Lifecycle</strong> — 建立專案站點的生命週期(共 4 個 Phase)用於產生一些專案說明文件的網站,會將生成 html 的頁面作為網站的說明文件。</li></ul><p>當你執行了任何一個 Phase,則 maven 會從第一個 Phase 開始執行,直到你所指定的 Phase 執行完成後才會結束動作,除非中間有遇到問題,例如選擇 package 就會依序 validate → compile → test → package ,如果 test 有未通過就會中斷,測試部分是可以自己配置是否要略過。</p><p>實務上這些過程都會根據自身需求調整配置,像是多半現在許多專案都會配合 docker build , push 上雲等等,也可以加入 docker plugin 進行,不一定走完完整的 life cycle,這些就是基本 Maven 的構件 SpringBoot 專案的</p><p>Ref:</p><ul><li><a href="https://maven.apache.org/"><a href="http://maven.apache.org/">maven.apache.org</a></a></li><li><a href="https://ithelp.ithome.com.tw/articles/10303335">Maven 簡介</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Spring Boot </tag>
<tag> Maven </tag>
</tags>
</entry>
<entry>
<title>Spring Boot Bean 介紹及常見應用</title>
<link href="/2024/09/21/SpringBoot/Spring-Boot-Bean-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%B8%B8%E8%A6%8B%E6%87%89%E7%94%A8/"/>
<url>/2024/09/21/SpringBoot/Spring-Boot-Bean-%E4%BB%8B%E7%B4%B9%E5%8F%8A%E5%B8%B8%E8%A6%8B%E6%87%89%E7%94%A8/</url>
<content type="html"><![CDATA[<h1 id="Spring-Bean-註解-Annotation"><a href="#Spring-Bean-註解-Annotation" class="headerlink" title="Spring Bean (註解 Annotation)"></a>Spring Bean (註解 Annotation)</h1><p>由 Spring IoC 容器管理的對象稱為 Bean 。Bean 是由 Spring IoC 容器實例化、組裝和管理的對象。應用 Annotation 可以將一個物件註冊成為 Bean。</p><p>使用 Annotation 的好處:</p><ul><li>只要在任意的 class 開頭定義好,Spring 啟動後就會自動去找,不需要一個一個匯入</li><li>名稱就可以對應該 Class 要做甚麼或是被引入甚麼套件</li><li>Spring 會自動根據 Annotation 去將該 class 設定成特定的用途或是引入特定套件。</li></ul><h2 id="依照不同應用層面分類"><a href="#依照不同應用層面分類" class="headerlink" title="依照不同應用層面分類:"></a>依照不同應用層面分類:</h2><h3 id="1-配置與啟動"><a href="#1-配置與啟動" class="headerlink" title="1. 配置與啟動"></a>1. <strong>配置與啟動</strong></h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li><code>@SpringBootApplication</code>:<ul><li>結合了 <code>@Configuration</code>、<code>@EnableAutoConfiguration</code> 和 <code>@ComponentScan</code>,是啟動 Spring Boot 應用的核心註解。</li></ul></li><li><code>@Configuration</code>:<ul><li>標記一個類為配置類,通常用來定義 <code>@Bean</code> 方法。</li></ul></li><li><code>@ComponentScan</code>:<ul><li>自動掃描並註冊被 <code>@Component</code>、<code>@Service</code>、<code>@Repository</code> 等註解的類別,通常在 <code>@SpringBootApplication</code> 中已隱式配置。</li></ul></li><li><code>@EnableAutoConfiguration</code>:<ul><li>讓 Spring Boot 根據加入的依賴自動配置 Spring 應用程式,通常已被包含在 <code>@SpringBootApplication</code> 中。</li></ul></li></ul><h3 id="2-依賴注入-DI"><a href="#2-依賴注入-DI" class="headerlink" title="2. 依賴注入 (DI)"></a>2. <strong>依賴注入 (DI)</strong></h3><ul><li><code>@Component</code>:<ul><li>標記一個類別為 Spring 管理的組件(Bean),會自動加入到 Spring 容器中。</li></ul></li><li><code>@Service</code>:<ul><li><code>@Component</code>的一種,表示 Service 業務邏輯層的類別。</li></ul></li><li><code>@Repository</code>:<ul><li><code>@Component</code>的一種,表示 Dao 資料存取層的類別,並且會自動處理資料庫例外。</li></ul></li><li><code>@Controller</code>:<ul><li><code>@Component</code>的一種, 表示 Controller 端口控制層的類別,包含資料取得或是配合前端模板 Thymeleaf 等前端頁面呈現。</li></ul></li><li><code>@Autowired</code>:<ul><li>自動注入 Bean,各類別內部要使用已經註冊的 bean 就可以注入使用</li></ul></li><li><code>@Bean</code><ul><li>針對方法類進行 bean 注冊</li></ul></li><li><code>@Value</code>:<ul><li>用來注入配置檔中的值,通常從 <code>application.properties</code> 或 <code>application.yml</code> 中獲取。</li></ul></li></ul><h3 id="3-Web-與-RESTful-API"><a href="#3-Web-與-RESTful-API" class="headerlink" title="3. Web 與 RESTful API"></a>3. <strong>Web 與 RESTful API</strong></h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-web<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li><code>@RestController</code>:<ul><li>結合了 <code>@Controller</code> 和 <code>@ResponseBody</code>,表示這個類別是 RESTful Controller,方法返回的資料會直接轉換成 JSON 或 XML,用於純資料取得的 API 端口。</li></ul></li><li><code>@RequestMapping</code>:<ul><li>定義 Controller 中的 URL 規則,可以用於類別或方法上。也可以用更具體的註解如 <code>@GetMapping</code>, <code>@PostMapping</code>, <code>@PutMapping</code>, <code>@DeleteMapping</code> 來指定 HTTP 方法。</li></ul></li><li><code>@PathVariable</code>:<ul><li>用來擷取 URL 路徑中的參數,通常用在動態路徑中。</li></ul></li><li><code>@RequestParam</code>:<ul><li>用來擷取 URL 中的查詢參數或表單資料。</li></ul></li><li><code>@RequestBody</code>:<ul><li>用來將 HTTP 請求的 JSON 或 XML 資料直接轉換為 Java 物件,通常用於 POST/PUT 請求。</li></ul></li><li><code>@ResponseBody</code>:<ul><li>將方法的返回值直接轉換為 JSON 或 XML 並返回給前端,適合 RESTful API。</li></ul></li></ul><h3 id="4-ORM-資料庫操作"><a href="#4-ORM-資料庫操作" class="headerlink" title="4. ORM, 資料庫操作"></a>4. ORM, <strong>資料庫操作</strong></h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-data-jpa<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li><code>@Entity</code>:<ul><li>標記一個類別為 JPA 實體,代表資料庫中的一個表格。</li></ul></li><li><code>@Table</code>:<ul><li>用來指定實體對應的資料表名稱,若不指定,預設會使用類名作為表名。</li></ul></li><li><code>@Id</code>:<ul><li>指定實體的主鍵欄位。</li></ul></li><li><code>@GeneratedValue</code>:<ul><li>指定主鍵的生成策略,常見的有 <code>IDENTITY</code>、<code>SEQUENCE</code> 等。</li></ul></li><li><code>@Column</code>:<ul><li>用來定義實體中屬性對應的資料庫欄位,包含名稱、長度等設定。</li></ul></li><li><code>@Repository</code>:<ul><li>用於標記資料存取層,搭配 Spring Data JPA 時,可以自動生成 CRUD 操作。</li></ul></li><li><code>@Query</code>:<ul><li>自定義查詢語句,適合需要複雜查詢的情境。</li></ul></li></ul><h3 id="5-AOP-與異常處理"><a href="#5-AOP-與異常處理" class="headerlink" title="5. AOP 與異常處理"></a>5. <strong>AOP 與異常處理</strong></h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li><code>@Aspect</code>:<ul><li>定義切面類別,用來實現橫切關注點。</li></ul></li><li><code>@Before</code>、<code>@After</code>、<code>@Around</code>:<ul><li>用來在方法執行的前、後或周圍插入額外的邏輯。</li></ul></li><li><code>@ExceptionHandler</code>:<ul><li>用於定義控制器中的異常處理方法。</li></ul></li></ul><h3 id="6-其他常用註解"><a href="#6-其他常用註解" class="headerlink" title="6. 其他常用註解"></a>6. <strong>其他常用註解</strong></h3><ul><li><code>@Valid</code>, <code>@Validated</code>:<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><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>org.springframework.boot<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-starter-validation<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>用於啟動 Bean 驗證,可限制參數或內部屬性輸入長度或是格式等等,通常和 <code>@RequestBody</code> 或 <code>@RequestParam</code> 搭配使用。</li></ul></li></ul><p>Ref:</p><ul><li><a href="https://zh.wikipedia.org/wiki/Java%E6%B3%A8%E8%A7%A3">Java 注解</a></li><li><a href="https://medium.com/@maskwork77.dev/prepare-2631b972c1f8">Java — Spring Annotation</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
</tags>
</entry>
<entry>
<title>Spring Boot 介紹</title>
<link href="/2024/09/15/SpringBoot/Spring-Boot-%E4%BB%8B%E7%B4%B9/"/>
<url>/2024/09/15/SpringBoot/Spring-Boot-%E4%BB%8B%E7%B4%B9/</url>
<content type="html"><![CDATA[<h2 id="Spring-Boot"><a href="#Spring-Boot" class="headerlink" title="Spring Boot"></a>Spring Boot</h2><p>是 <strong>Java 最主流的後端開發框架</strong>,<strong>Spring Boot 其實算是早期的框架 Spring 的擴充升級</strong>,其核心的概念都有延續。過往框架 Spring 配置繁瑣,為跟上其他語言的框架,像是 Python (Django), PHP (Laravel) 等主流框架簡化配置的設計,所以才衍伸出 Spring Boot 快速整合管理 Spring 底下的各類模組 (Spring Data, Spring Security, Spring AOP、Spring MVC… ) 來達到快速開發。</p><p>主要框架核心概念就是<strong>控制反轉 (Inversion Of Control)</strong> 及 <strong>依賴注入 (Dependency Injection)</strong> 來達到解耦,減少程式碼之間的依賴性,增加開發彈性,詳細的說明請見 <a href="https://oseanchen.github.io/2024/02/21/SpringBoot/Spring-Boot-IOC-and-DI/">Spring Boot IOC and DI</a>。</p><p>Spring Boot 在啟動時,會執行元件掃描(component scan),找出具有元件標記的類別(如 @RestController、@Service、@Component),建立成元件(bean),並且將這些 bean 放入 Spring 的容器中統一管理。由於元件的物件實體不需要自己寫程式創建 (new) 出來,而是交給框架的容器機制來建立,所以 Spring 融入了 IOC 的精神。</p><h2 id="特色"><a href="#特色" class="headerlink" title="特色"></a>特色</h2><ul><li><strong>約定優於配置</strong>: 簡化開發採用許多預設值來配置應用程式,這些預設值可節省開發人員的時間和精力,改善 Java 傳統 Spring 框架通常需要在配置文件 (xml) 中進行大量的手動配置。</li><li><strong>簡單的開發</strong>:提供快速開發工具,如 Spring Initializr,可快速構建一個基本的 Spring Boot 應用程式,減少模組套件引用及專案建立時的配置,也整合 Maven 或 Grandle 等專案管理套件更加提升開發效率。</li><li><strong>嵌入式伺服器</strong>:提供嵌入式的 Tomcat、Jetty、Undertow、Netty,使開發者可以打包一個 JAR 檔案,直接運行,而不需要外部的應用伺服器。</li><li><strong>開箱即用</strong>:內建許多功能及配置,如資料庫連接、安全性、日誌紀錄和許多第三方庫的支持等。這些功能使得開發人員可以更快速地構建應用程式,而不必花費太多時間來處理底層細節。</li><li><strong>微服務整合</strong>:可輕鬆整合 Spring Cloud 框架來實現服務發現、負載均衡和數據處理等功能。</li></ul><hr><p>Ref:</p><ul><li><a href="https://spring.io/projects/spring-boot">Spring.io</a></li><li><a href="https://kucw.io/doc/springboot/1/">古古的後端筆記 - SpringBoot 簡介</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
</tags>
</entry>
<entry>
<title>Spring Security FilterChain 如何進行登入認證</title>
<link href="/2024/09/14/SpringBoot/Spring-Security-FilterChain-%E5%A6%82%E4%BD%95%E9%80%B2%E8%A1%8C%E7%99%BB%E5%85%A5%E8%AA%8D%E8%AD%89/"/>
<url>/2024/09/14/SpringBoot/Spring-Security-FilterChain-%E5%A6%82%E4%BD%95%E9%80%B2%E8%A1%8C%E7%99%BB%E5%85%A5%E8%AA%8D%E8%AD%89/</url>
<content type="html"><![CDATA[<div class="note simple"><p>進行 Spring Security 的實作時碰到很多底層運行架構不是很了解的狀況,上網查找一些資訊拼湊出一個比較容易理解的流程。主要是針對帳號密碼登入時的授權部分,因為牽涉到很多元件的運作,所以需要整理一下。</p></div><h3 id="帳號密碼認證流程"><a href="#帳號密碼認證流程" class="headerlink" title="帳號密碼認證流程"></a>帳號密碼認證流程</h3><p>認證和授權動作都是在請求到達 Spring MVC 的運作之前,Security 就已經透過一套機制進行一系列的驗證,首先會先碰到的就是</p><ul><li>(1) <strong><code>Filter Chain</code></strong> 的系統,這邊會透過一系列的過濾篩選,只要請求的一些 header 或是任何內容有不符合的就會先被過濾掉,算是攔截器的概念,層層篩選其中關於登入的部分會由 <strong><code>UsernamePasswordAuthenticationFilter</code></strong> 進行處理,接著會被導入</li><li>(2) <strong><code>Authentication Manager</code></strong> 統一針對授權去管理,從一堆 Provider 中找到一個可以成功認證的</li><li>(3) <strong><code>Authentication Provider</code></strong> 來判斷帳號密碼是否正確,密碼的加密及認證等等處理 (配置密碼加密的類型 PasswordEncoder 也是從這邊設置),他會從 <strong><code>UserDetailService</code></strong> 這個介面取出對應的 User 並且結合其他資訊封裝成 <strong><code>UserDetails</code></strong> (使用者相關資訊、權限…) 這個 Security 所使用的使用者資訊界面物件,然後交由</li><li>(4) <strong><code>Authentication Provider</code></strong> 使用進行驗證,</li><li>(5) (6) 如果驗證成功就會產生一個 已驗證的 <strong><code>Authentication</code></strong> 物件存於 <strong><code>Security Context</code></strong> ,這位置預設保存在 Session ,所以 Authentication 會和 Session 綁訂,後續請求也都可以重複取出使用,不必重新驗證。如果失敗就拋出例外回前端。</li><li>成功的話 (7) 接著就會將請求導入後續我們熟悉的 MVC 架構流程。</li></ul><p><img src="https://ithelp.ithome.com.tw/upload/images/20240914/201509773bAvCQLiQT.png" alt="https://ithelp.ithome.com.tw/upload/images/20240914/201509773bAvCQLiQT.png"></p><hr><p>參考資料:</p><ul><li><a href="https://backendstory.com/spring-security-authentication-architecture-explained-in-depth/">Spring Security: Authentication Architecture Explained In Depth</a></li><li><a href="https://medium.com/@heather_programming/spring-security-%E5%B8%B3%E8%99%9F%E5%AF%86%E7%A2%BC%E9%A9%97%E8%AD%89-98a4bfd03abe">Spring Security】帳號密碼驗證</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
</entry>
<entry>
<title>Spring Boot Scheduled</title>
<link href="/2024/05/14/SpringBoot/Spring-Boot-Scheduled/"/>
<url>/2024/05/14/SpringBoot/Spring-Boot-Scheduled/</url>
<content type="html"><![CDATA[<h2 id="排程服務-Cronjob"><a href="#排程服務-Cronjob" class="headerlink" title="排程服務 Cronjob"></a>排程服務 Cronjob</h2><ul><li>排程服務主要應用針對例行的處理,根據設定的時間或是間隔自動啟動 </li><li>多應用 cron 的表達式去設定 </li><li>常見應用的服務如 Cron, Spring Task Scheduling, windows 任務排程器, Airflow, 各雲端服務商對應的 Cronjob 服務</li></ul><h2 id="Spring-Task-Scheduling-用法"><a href="#Spring-Task-Scheduling-用法" class="headerlink" title="Spring Task Scheduling 用法"></a>Spring Task Scheduling 用法</h2><ol><li>要執行排程的 class 加入 <code>@Component</code> 變成 bean</li><li>SpringApplication.run 的啟動類加上 <code>@EnableScheduling</code> 才會啟用 </li></ol>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Cronjob </tag>
</tags>
</entry>
<entry>
<title>Spring AOP</title>
<link href="/2024/04/10/SpringBoot/Spring-AOP/"/>
<url>/2024/04/10/SpringBoot/Spring-AOP/</url>
<content type="html"><![CDATA[<h2 id="AOP-Aspect-Oriented-Programming-切面導向設計"><a href="#AOP-Aspect-Oriented-Programming-切面導向設計" class="headerlink" title="AOP (Aspect Oriented Programming) 切面導向設計"></a>AOP (Aspect Oriented Programming) 切面導向設計</h2><ul><li><p>是一種程式設計的模式,主要理念是<strong>為了減少重複出現的邏輯</strong>,比方說 log 紀錄,登入驗證、資料驗證等重複性事務,如果多個方法都需要執行到這類方法,就可以抽出來透過定義在切面中,減少程式耦合及方便去進行維護,讓開發者可以專注在更重要的業務邏輯中。</p></li><li><p>如同下圖每個箭頭是不同的方法或流程,如每個方法都要加入同樣的處理,就可以當成一個切面抽出來所以像是 log, security 的部分就被稱為 cross-cutting concerns 但都可以抽出共同需要處理的部分作為切面(藍色及綠色)。</p></li></ul><p><img src="https://i.ibb.co/nwbF551/aop.png"></p><ul><li>SpringBoot 本身也提供好用的註解去實踐這樣的設計模式,可以進行切面的操作,你可以根據切入點的執行前中後等等進行設定,以控制我們的方法執行的順序。</li></ul><h2 id="Spring-AOP-使用"><a href="#Spring-AOP-使用" class="headerlink" title="Spring AOP 使用"></a>Spring AOP 使用</h2><h3 id="定義切面"><a href="#定義切面" class="headerlink" title="定義切面"></a>定義切面</h3><ul><li>通常要使用 Spring Boot AOP 需要先將切面定義出來,會在我們需要標記的切面類別上加上 <code>@Aspect</code>,且注意需要加上<code>@Component</code> 將類別標記成為 bean 才可以使用</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogAspect</span> {</span><br><span class="line"> <span class="comment">// 撰寫切面處理方法及控制發動時間..</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>把需要的東西注入及標註哪些方法需要被 <code>@Pointcut</code> 標記為切入點套用切面</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogAspect</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> LoggerFactory.getLogger(<span class="built_in">this</span>.getClass());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// com.example.demoaop.TestController class 內所有的方法(不包含子 package)會生效</span></span><br><span class="line"> <span class="meta">@Pointcut("execution(* com.example.demoaop.TestController.*(..))")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">pointcut</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="Execution-切入點"><a href="#Execution-切入點" class="headerlink" title="Execution 切入點"></a>Execution 切入點</h3><p>關於切入點 execution 表達式:</p><p>切入點為 com.example.demo.Test 底下的 print() 方法</p><p><code>execution(* com.example.demo.Test.print())</code></p><p>切入點為 com.example.demo.Test 底下的所有方法</p><p><code>execution(* com.example.demo.Test.*(..))</code></p><p>切入點為 com.example.demo 這個 package 中的所有 class 的所有方法(不包含子 package)</p><p><code>execution(* com.example.demo.*(..))</code></p><p>切入點為 com.example.demo 這個 package 及其底下所有子 package 中的所有 class 的所有方法</p><p><code>execution(* com.example.demo..*(..))</code></p><h3 id="Advice-切面控制"><a href="#Advice-切面控制" class="headerlink" title="Advice 切面控制"></a>Advice 切面控制</h3><p>控制切面的執行被稱為 Advice ,Spring 有提供 5 種 Advice 可以讓你操作方法需要執行的時間</p><ol><li><code>@Before</code> : 在切入點執行前前執行切面。</li><li><code>@After</code> : 在切入點執行後後執行切面。</li><li><code>@AfterReturning</code> : 在切入點傳回(return)內容後執行,可以對傳回內容進行一些加工處理。</li><li><code>@Around</code> : 在切入點前後執行切面,並配合 proceed 可控制何時執行切入點本身的內容。</li><li><code>@AfterThrowing</code> : 當切入點拋出例外後執行</li></ol><h2 id="範例及應用"><a href="#範例及應用" class="headerlink" title="範例及應用"></a>範例及應用</h2><ul><li>controller</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.demoaop;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestController</span> {</span><br><span class="line"> <span class="meta">@GetMapping("/test/{word}")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">test</span><span class="params">(<span class="meta">@PathVariable</span> String word)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (word.equals(<span class="string">"yes"</span>)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> word;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>AOP 定義類別</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.demoaop;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.aspectj.lang.JoinPoint;</span><br><span class="line"><span class="keyword">import</span> org.aspectj.lang.ProceedingJoinPoint;</span><br><span class="line"><span class="keyword">import</span> org.aspectj.lang.annotation.*;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Arrays;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogAspect</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> LoggerFactory.getLogger(<span class="built_in">this</span>.getClass());</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Pointcut("execution(* com.example.demoaop.TestController.*(..))")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">pointcut</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Before("pointcut()")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">before</span><span class="params">(JoinPoint joinPoint)</span> {</span><br><span class="line"> System.out.println(<span class="string">"*****before advice start*****"</span>);</span><br><span class="line"> logger.info(<span class="string">"do before "</span> + joinPoint.getSignature().getName());</span><br><span class="line"> Arrays.stream(joinPoint.getArgs()).forEach(System.out::println);</span><br><span class="line"> System.out.println(<span class="string">"*****before advice end*****"</span>);</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@After("pointcut()")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">after</span><span class="params">(JoinPoint joinPoint)</span> {</span><br><span class="line"> System.out.println(<span class="string">"-----after advice start-----"</span>);</span><br><span class="line"> logger.info(<span class="string">"do after "</span> + joinPoint.getSignature().getName());</span><br><span class="line"> System.out.println(<span class="string">"-----after advice end-----"</span>);</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Around("pointcut()")</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">around</span><span class="params">(ProceedingJoinPoint joinPoint)</span> <span class="keyword">throws</span> Throwable {</span><br><span class="line"> System.out.println(<span class="string">"=====Around advice starts====="</span>);</span><br><span class="line"></span><br><span class="line"> <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 呼叫proceed() 方法開始執行原方法</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">result</span> <span class="operator">=</span> joinPoint.proceed();</span><br><span class="line"> <span class="type">long</span> <span class="variable">spentTime</span> <span class="operator">=</span> System.currentTimeMillis() - startTime;</span><br><span class="line"> logger.info(joinPoint.getSignature().getName() + <span class="string">" Time spent: "</span> + spentTime);</span><br><span class="line"></span><br><span class="line"> System.out.println(<span class="string">"=====Around advice ends====="</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AfterReturning(pointcut = "pointcut()", returning = "result")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">logReturnResponse</span><span class="params">(Object result)</span> { <span class="comment">// 紀錄 Response</span></span><br><span class="line"> System.out.println(<span class="string">"=====after returning advice starts====="</span>);</span><br><span class="line"> <span class="keyword">if</span> (result != <span class="literal">null</span>) {</span><br><span class="line"> System.out.println(result);</span><br><span class="line"> }</span><br><span class="line"> System.out.println(<span class="string">"=====after returning advice ends====="</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AfterThrowing(pointcut = "pointcut()", throwing = "throwable")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterThrowing</span><span class="params">(JoinPoint joinPoint, Throwable throwable)</span> {</span><br><span class="line"> System.out.println(<span class="string">"=====after throwing advice starts====="</span>);</span><br><span class="line"> System.out.println(joinPoint.getSignature().getName());</span><br><span class="line"> System.out.println(throwable);</span><br><span class="line"> System.out.println(<span class="string">"=====after throwing advice ends====="</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果當我啟動後並且透過 TestController 的 test 方法進行請求並帶入 “no”,就會看到以下 log 顯示</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240927/201509778ELfzgftIm.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/201509778ELfzgftIm.png"></p><blockquote><p>around start -><br>before start -> before method -> before end -><br>after returning start -> after returning method -> after returning end -><br>after start -> after method -> after end -><br>(proceed 控制執行 around 內容) around method -> around end</p></blockquote><p>如果是請求帶 yes 導致中途拋出異常就會變成</p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240927/20150977XJUuxogSCl.png" alt="https://ithelp.ithome.com.tw/upload/images/20240927/20150977XJUuxogSCl.png"></p><blockquote><p>around start -><br>before start -> before method -> before end -><br>(拋出錯誤所以沒有 return 不執行 after returning,改執行 after throwing) after throwing start -> after throwing method -> after throwing end -><br>after start -> after method -> after end -> 拋錯</p></blockquote><p>*after 不管有沒有拋錯都執行</p><p>所以這就是基本 AOP 的用法,大家應該對於相關的切面控制都有一定的了解了,不妨可以嚐試加入自己的專案內,針對一些特定位置重複會進行的方法或是 log 紀錄抽出來作為切面,這樣設計可以讓程式碼更加簡潔。</p><p><strong>參考資料:</strong></p><ol><li><a href="https://chikuwa-tech-study.blogspot.com/2021/06/spring-boot-aop-introduction.html">【Spring Boot】第 20 課-切面導向程式設計(AOP)</a></li><li><a href="https://openhome.cc/Gossip/Spring/SpringAOP.html">使用 Spring AOP</a></li><li><a href="https://ithelp.ithome.com.tw/m/articles/10329009">Spring Boot 零基礎入門 (12) - Spring AOP 的用法</a></li></ol>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> Design Pattern </tag>
</tags>
</entry>
<entry>
<title>Spring Boot IOC and DI</title>
<link href="/2024/02/21/SpringBoot/Spring-Boot-IOC-and-DI/"/>
<url>/2024/02/21/SpringBoot/Spring-Boot-IOC-and-DI/</url>
<content type="html"><![CDATA[<h1 id="Day-2-控制反轉-IOC-vs-依賴注入-DI"><a href="#Day-2-控制反轉-IOC-vs-依賴注入-DI" class="headerlink" title="Day 2 - 控制反轉 (IOC) vs 依賴注入(DI)"></a>Day 2 - 控制反轉 (IOC) vs 依賴注入(DI)</h1><h2 id="控制反轉-Inversion-of-Control-IOC"><a href="#控制反轉-Inversion-of-Control-IOC" class="headerlink" title="控制反轉 Inversion of Control (IOC)"></a>控制反轉 Inversion of Control (IOC)</h2><p>控制反轉是一種程式設計的方式。它的精神在於程式中所<strong>需要的輔助物件,並不是在自己的類別中建立,而是由外部控制的</strong>。建立好後,將其傳遞給主程式,這個動作稱為依賴注入,是控制反轉的實現方式。</p><ul><li>將 Object 的控制權交給外部的 Spring 容器來管理</li></ul><h2 id="依賴注入-dependency-injection-DI"><a href="#依賴注入-dependency-injection-DI" class="headerlink" title="依賴注入 dependency injection (DI)"></a>依賴注入 dependency injection (DI)</h2><p>實現控制反轉的方式,Spring 的設計目標之一是為了解隅,利用依賴抽象而非依賴實例的方式,因此設計了依賴注入(DI)。</p><ul><li>@Component , @RestController @Service … 等註各類解加在 class 上,將 class 交給 Spring 管理,這些也就是在 Spring Boot 中所謂的 Bean (後面章節會有詳細介紹)</li><li>@Autowired 將被管理的 Bean 注入</li></ul><h2 id="應用範例"><a href="#應用範例" class="headerlink" title="應用範例"></a>應用範例</h2><ul><li>舉例來說有個物件專門來處理寄送訊息叫做 Sender ,我們需要使用到這個物件就必須透過 new 方法來建立出來,假設今天原本的 Sender 有問題更改成別種 Sender ,就必須有建立這個物件的地方都需要修改一遍,這時候改成用 DI 的方式就會比較容易維護及更動。</li></ul><p>沒有使用 DI 方式,使用 New 方法來建立物件並引用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Notify</span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="type">Sender</span> <span class="variable">sender</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Sender</span>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果需要更換方法,每個有用到的地方都要改</span></span><br><span class="line"> <span class="comment">// private NewSender sender = new NewSender();</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_">Send</span><span class="params">()</span> {</span><br><span class="line"> sender.print(<span class="string">"Hi! I'm Sean"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="Autowired-注入單一實做物件"><a href="#Autowired-注入單一實做物件" class="headerlink" title="@Autowired 注入單一實做物件"></a>@Autowired 注入單一實做物件</h3><p>將 Sender 交給 Spring 容器管理,使用 @Autowired 注入去使用,等於是透過 Spring 去建立物件出來</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Notify</span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果要更改方法,往上註冊為 bean 的部分修改就可以,注入部分不需要改變</span></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> Sender sender;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">Send</span><span class="params">()</span> {</span><br><span class="line"> sender.print(<span class="string">"Hi! I'm Sean"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="Autowired-注入介面內含多個已實做物件"><a href="#Autowired-注入介面內含多個已實做物件" class="headerlink" title="@Autowired 注入介面內含多個已實做物件"></a>@Autowired 注入介面內含多個已實做物件</h3><p>如果我們可能有多種 Sender 需要去實踐,可以用 interface 來建立一個介面去分成不同方法實踐</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Sender</span> {</span><br><span class="line"><span class="keyword">void</span> <span class="title function_">send</span><span class="params">(String msg)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GoogleMailSender</span> <span class="keyword">implements</span> <span class="title class_">Sender</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 msg)</span> {</span><br><span class="line">System.out.println(<span class="string">"內容: "</span> + msg + <span class="string">" 我用 Google 郵件寄出了!!"</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OutlookMailSender</span> <span class="keyword">implements</span> <span class="title class_">Sender</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 msg)</span> {</span><br><span class="line">System.out.println(<span class="string">"內容: "</span> + msg + <span class="string">" 我用 Outlook 郵件寄出了!!"</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以將 @Component 選擇注入其中一種,Spring 容器會自動幫我們選擇我們有注入的那個,但如果兩種都注入,就會出現錯誤,所以要再選擇用 @Qualifier(”bean 名稱”) 註解來指定要入住哪個</p><ul><li><ul><li>注意注入容器的 bean 名稱開頭是小寫</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Notify</span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="meta">@Qualifer("GoogleMailSender")</span> <span class="comment">// @Qualifer("outlookMailSender")</span></span><br><span class="line"> <span class="keyword">private</span> Sender sender;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">Send</span><span class="params">()</span> {</span><br><span class="line"> sender.print(<span class="string">"Hi! I'm Sean"</span>)</span><br><span class="line"> }</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><p>利用 IOC 和 DI 可以有以下優點:</p><ul><li>鬆耦合</li></ul><p>class 之間的鬆耦合,降低各個 class 之間的關聯性。</p><p>像是假設我們本來得在 Notify 中去指定要使用的是哪家廠牌的 MailSender(可能是 google, yahoo…) 而這就會讓 Notify 和 MailSender 的耦合變大,但使用了 Spring IoC 之後,不需要了解這個 Sender 是哪種,只要確定有被正確注入某種可以使用的 MailSender。</p><ul><li>統一生命週期管理</li></ul><p>交由 Spring 容器來管理這些物件的生命週期,減少不必要的引用。</p><p>因為我們將 MailSender 這個 object 改成是交由 Spring 容器來管理,因此 Spring 就會負責物件的創建、初始化、以及銷毀,所以就不需要我們親自去處理這件事情。</p><ul><li>方便測試程式</li></ul><p>可以方便注入至所需要測試的程式中</p><p>所有的 object 都是由外部的 Spring 容器來做管理,因此我們就可以輕鬆在測試程式內在測試的過程中,將 Spring 容器中的 object 給替換掉,容易的使用像是 Mock 的技術。</p><hr><p>Ref:</p><ul><li><a href="https://kucw.io/doc/springboot/7/">古古的後端筆記 - Bean 的創建和注入 - @Component、@Autowired</a></li><li><a href="notion://www.notion.so/%5B%3Chttps://zh.wikipedia.org/zh-tw/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC%3E%5D(%3Chttps://zh.wikipedia.org/zh-tw/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC%3E)">控制反轉</a></li><li><a href="https://medium.com/javarevisited/spring-beans-in-depth-a6d8b31db8a1">Spring Beans in Depth</a></li><li>Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)</li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Spring Boot </tag>
</tags>
</entry>
<entry>
<title>JPA, JDBC, Hibernate</title>
<link href="/2024/02/16/SpringBoot/JPA,%20JDBC,%20Hibernate/"/>
<url>/2024/02/16/SpringBoot/JPA,%20JDBC,%20Hibernate/</url>
<content type="html"><![CDATA[<h2 id="JPA-Java-Persistance-API"><a href="#JPA-Java-Persistance-API" class="headerlink" title="JPA (Java Persistance API)"></a>JPA (Java Persistance API)</h2><ul><li><p>字面意思就是一個 Java 的持久化 API,將資料「儲存」與「讀取」的過程,就稱為「持久化」也可以說是將資料儲存到資料庫的過程。</p></li><li><p>JPA 其實是一個 Java 標準,定義了一組用於 ORM 的 API。它提供了一個標準的方法,使 Java 開發者能夠透過標準的介面來執行 ORM 操作,而不受特定數據庫供應商或是 ORM 框架的限制。</p></li></ul><h2 id="Hibernate"><a href="#Hibernate" class="headerlink" title="Hibernate"></a>Hibernate</h2><ul><li>Hibernate 是一個開源的 ORM 框架,實現了 JPA 標準,是 Java 中最廣泛使用的 ORM 框架之一。</li></ul><h2 id="JDBC-Java-Database-Connectivity"><a href="#JDBC-Java-Database-Connectivity" class="headerlink" title="JDBC (Java Database Connectivity)"></a>JDBC (Java Database Connectivity)</h2><ul><li>JDBC 是透過 Java 來連接資料庫的一種技術。它提供了一組用於執行 SQL 查詢、更新和管理數據庫連接的 Java 介面。透過 JDBC,Java 可以與各種不同的數據庫系統進行通信。</li></ul><h2 id="Spring-JDBC"><a href="#Spring-JDBC" class="headerlink" title="Spring JDBC"></a>Spring JDBC</h2><ul><li>由 Spring 提供基於 JDBC 的模組,它提供了一個抽象層,用於簡化 JDBC 操作並處理資源管理。允許開發者使用簡單的 API 來執行 SQL 查詢、更新、刪除等操作,<strong>仍需使用原始的 SQL 語法進行資料庫操作</strong>。</li></ul><h2 id="Spring-Data-JPA"><a href="#Spring-Data-JPA" class="headerlink" title="Spring Data JPA"></a>Spring Data JPA</h2><ul><li>Spring Data 項目中的一部分,它是對 Java Persistence API (JPA) 的實現,<strong>應用了 ORM 的概念,並操作 JAVA object 的方式來進行資料庫的操作</strong>,並且可以用於多種數據庫,而不僅僅是關聯式數據庫。</li></ul><hr><p><strong>參考資料:</strong></p><ol><li><p><a href="https://medium.com/learning-from-jhipster/13-%E7%94%9A%E9%BA%BC%E6%98%AF-jdbc-orm-jpa-orm%E6%A1%86%E6%9E%B6-hibernate-c762a8c5e112">甚麼是 JDBC、ORM、 JPA、ORM 框架、Hibernate</a></p></li><li><p><a href="https://www.baeldung.com/learn-jpa-hibernate">Learn JPA & Hibernate</a></p></li></ol>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
</tags>
</entry>
<entry>
<title>編碼(encoding) vs 加解密 vs 雜湊(Hash)</title>
<link href="/2023/08/23/%E7%B7%A8%E7%A2%BC-encoding-vs-%E5%8A%A0%E8%A7%A3%E5%AF%86-vs-%E9%9B%9C%E6%B9%8A-Hash/"/>
<url>/2023/08/23/%E7%B7%A8%E7%A2%BC-encoding-vs-%E5%8A%A0%E8%A7%A3%E5%AF%86-vs-%E9%9B%9C%E6%B9%8A-Hash/</url>
<content type="html"><![CDATA[<h2 id="編碼-Encoding"><a href="#編碼-Encoding" class="headerlink" title="編碼(Encoding)"></a>編碼(Encoding)</h2><p>將原本的資料經過運算轉換成另一組資料,如果要還原可透過反向解碼(decoding)<br><strong>ex: Base64, URl, UTF-8</strong></p><h2 id="加解密-Encrypt-and-Decrypt"><a href="#加解密-Encrypt-and-Decrypt" class="headerlink" title="加解密(Encrypt and Decrypt)"></a>加解密(Encrypt and Decrypt)</h2><p>將資料透過一個 <strong>Key</strong> 來做加密或解密轉換(和編碼最大的差別)<br>可以根據鑰匙是否相同分成<strong>對稱式</strong>與<strong>非對稱</strong></p><h4 id="對稱式加密-Symmetric-Encryption-DES-x2F-Triple-DES、AES"><a href="#對稱式加密-Symmetric-Encryption-DES-x2F-Triple-DES、AES" class="headerlink" title="對稱式加密 (Symmetric Encryption) (DES/Triple DES、AES)"></a>對稱式加密 (Symmetric Encryption) (DES/Triple DES、AES)</h4><p>鑰匙相同,加密及解密都共用</p><p><strong>- AES(Advanced Encryption Standard)</strong><br><strong>- DES(Data Encryption Standard)</strong><br><strong>- DES/Triple</strong></p><p>AES 是目前被廣泛接受和使用的加密方式,因提供了更高的安全性和更快的加密速度。DES 和 DES/Triple DES 由於安全性弱和性能較慢,通常不是首選。</p><h4 id="非對稱式加密-Asymmetric-Encryption-RSA-演算法"><a href="#非對稱式加密-Asymmetric-Encryption-RSA-演算法" class="headerlink" title="非對稱式加密 (Asymmetric Encryption) (RSA 演算法)"></a>非對稱式加密 (Asymmetric Encryption) (RSA 演算法)</h4><p>加密與解密方式為:<strong>公鑰加密,私鑰解密</strong></p><p><strong>ex: RSA、ECC</strong></p><h2 id="雜湊-Hash"><a href="#雜湊-Hash" class="headerlink" title="雜湊(Hash)"></a>雜湊(Hash)</h2><p>SHA256 Hash 幾個特點:</p><ul><li>不管資料量多大經過 SHA256 運算字串長度都是一樣的</li><li>運算完的資料大小一定是 256 bit</li><li>不可逆的算法</li><li>相同的值用 SHA 運算過後值都是一樣的</li><li>資料加密上可以做到防竄改,確認演算前的值要完全一樣,且不可逆</li></ul><p><strong>ex: SHA-256、MD5</strong></p><h2 id="表格總結"><a href="#表格總結" class="headerlink" title="表格總結"></a>表格總結</h2><table><thead><tr><th align="left"></th><th align="center">可逆</th><th align="center">運算後長度</th><th align="center">安全性</th><th>Key</th></tr></thead><tbody><tr><td align="left">雜湊演算</td><td align="center">不可</td><td align="center">一樣</td><td align="center">高</td><td>無</td></tr><tr><td align="left">編碼</td><td align="center">可</td><td align="center">不一樣</td><td align="center">不高</td><td>無</td></tr><tr><td align="left">加解密</td><td align="center">可</td><td align="center">不一樣</td><td align="center">高</td><td>有</td></tr></tbody></table><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ol><li><a href="https://medium.com/@RiverChan/%E5%9F%BA%E7%A4%8E%E5%AF%86%E7%A2%BC%E5%AD%B8-%E5%B0%8D%E7%A8%B1%E5%BC%8F%E8%88%87%E9%9D%9E%E5%B0%8D%E7%A8%B1%E5%BC%8F%E5%8A%A0%E5%AF%86%E6%8A%80%E8%A1%93-de25fd5fa537">基礎密碼學(對稱式與非對稱式加密技術)</a></li><li><a href="https://hackmd.io/@HuangPX/B1boJwRht?utm_source=preview-mode&utm_medium=rec">密碼學</a></li><li><a href="https://blog.darkthread.net/blog/store-pwd-safely/">密碼要怎麼儲存才安全?該加多少鹽?-科普角度</a></li><li><a href="https://hitcon.org/2018/CMT/slide-files/d1_s2_r4.pdf">應用密碼學入門</a></li></ol>]]></content>
<categories>
<category> 密碼學 </category>
</categories>
<tags>
<tag> 編碼 </tag>
<tag> 加密 </tag>
<tag> 雜湊演算 </tag>
</tags>
</entry>
<entry>
<title>.equals() 和 == 差別</title>
<link href="/2023/05/22/Java/equals-%E5%92%8C-%E5%B7%AE%E5%88%A5/"/>
<url>/2023/05/22/Java/equals-%E5%92%8C-%E5%B7%AE%E5%88%A5/</url>
<content type="html"><![CDATA[<h2 id="“-x3D-x3D-”"><a href="#“-x3D-x3D-”" class="headerlink" title="“==”"></a>“==”</h2><ul><li>是用來判斷兩個比較物件是不是有相同的 references。</li><li>每一個物件都有獨立的 reference,假如 reference 不同就表示它們是不同的物件。</li></ul><h2 id="equals"><a href="#equals" class="headerlink" title=".equals()"></a>.equals()</h2><ul><li>是用來比較物件的值(儲存在 heap 上的值)。</li><li>可以判斷兩個有不同 references 的物件是否指向相同的值</li></ul><h2 id="總結:"><a href="#總結:" class="headerlink" title="總結:"></a>總結:</h2><ul><li>== 比較的是兩個對象的引用或基本數據類型的值。</li><li>.equals() 比較的是兩個對象的內容,通常需要被覆寫,並根據自定義的邏輯進行比較。</li></ul><p>在使用時,需要注意以下幾點:</p><blockquote><ul><li>對於基本數據類型,應該使用 == 來進行相等性比較。</li><li>對於引用類型,如果只是想比較兩個對象是否引用同一個 reference,可以使用 ==。如果需要比較兩個對象的內容的值是否相等,應該使用 .equals() 方法。</li><li>在使用 .equals() 方法時,需要注意處理 null 值的情況,以避免 NullPointerException。可以使用 Objects.equals() 方法(Java 7+)或自定義的邏輯來處理 null 值。</li></ul></blockquote>]]></content>
<categories>
<category> Java 基礎 </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>StringUtils 常用判斷字串方法與 Java 內建方法比較</title>
<link href="/2023/05/22/Java/StringUtils-%E5%B8%B8%E7%94%A8%E5%88%A4%E6%96%B7%E5%AD%97%E4%B8%B2%E6%96%B9%E6%B3%95%E8%88%87-Java-%E5%85%A7%E5%BB%BA%E6%96%B9%E6%B3%95%E6%AF%94%E8%BC%83/"/>
<url>/2023/05/22/Java/StringUtils-%E5%B8%B8%E7%94%A8%E5%88%A4%E6%96%B7%E5%AD%97%E4%B8%B2%E6%96%B9%E6%B3%95%E8%88%87-Java-%E5%85%A7%E5%BB%BA%E6%96%B9%E6%B3%95%E6%AF%94%E8%BC%83/</url>
<content type="html"><![CDATA[<h2 id="Java-內建方法"><a href="#Java-內建方法" class="headerlink" title="Java 內建方法"></a>Java 內建方法</h2><p>Java 本身提供幾個關於字串的判斷,會因為 null 產生 exception</p><h4 id="isEmpty"><a href="#isEmpty" class="headerlink" title="isEmpty()"></a><code>isEmpty()</code></h4><p>字串有長度(空白也算)就是 false</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">""</span>.isEmpty(); <span class="comment">//true</span></span><br><span class="line"><span class="string">" "</span>.isEmpty(); <span class="comment">//false</span></span><br><span class="line"><span class="string">"blank"</span>.isEmpty(); <span class="comment">//false</span></span><br><span class="line"><span class="literal">null</span>.isEmpty(); <span class="comment">// NullPointerException</span></span><br></pre></td></tr></table></figure><h4 id="isBlank"><a href="#isBlank" class="headerlink" title="isBlank()"></a><code>isBlank()</code></h4><p>字串有長度沒文字還是 true</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"123"</span>.isBlank(); <span class="comment">//false</span></span><br><span class="line"><span class="string">""</span>.isBlank(); <span class="comment">//true</span></span><br><span class="line"><span class="string">" "</span>.isBlank(); <span class="comment">//true</span></span><br><span class="line"><span class="literal">null</span>.isBlank(); <span class="comment">// NullPointerException</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="StringUtils"><a href="#StringUtils" class="headerlink" title="StringUtils"></a>StringUtils</h2><p>StringUtils 用於處理 String 的,提供了比 Java String 多更多實用方法,且<strong>不會因為 null 而產生 exception</strong>。<br>屬於 Apache (org.apache.commons.lang.StringUtils) 的操作 String 類型數據常用工具套件</p><h4 id="StringUtils-isBlank"><a href="#StringUtils-isBlank" class="headerlink" title="StringUtils.isBlank()"></a><code>StringUtils.isBlank()</code></h4><ul><li>null [o]</li><li>空字串 [o]</li><li>空格 [o]</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">StringUtils.isBlank(<span class="literal">null</span>) <span class="comment">// true</span></span><br><span class="line">StringUtils.isBlank(<span class="string">""</span>) <span class="comment">// true</span></span><br><span class="line">StringUtils.isBlank(<span class="string">" "</span>) <span class="comment">// true</span></span><br><span class="line">StringUtils.isBlank(<span class="string">"blank"</span>) <span class="comment">// false</span></span><br><span class="line">StringUtils.isBlank(<span class="string">" blank "</span>) <span class="comment">// false</span></span><br></pre></td></tr></table></figure><h4 id="StringUtils-isNotBlank"><a href="#StringUtils-isNotBlank" class="headerlink" title="StringUtils.isNotBlank()"></a><code>StringUtils.isNotBlank()</code></h4><ul><li>null [x]</li><li>空字串 [x]</li><li>空格 [x]</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">StringUtils.isNotBlank(<span class="literal">null</span>); <span class="comment">// false</span></span><br><span class="line">StringUtils.isNotBlank(<span class="string">""</span>); <span class="comment">// false</span></span><br><span class="line">StringUtils.isNotBlank(<span class="string">" "</span>); <span class="comment">// false</span></span><br><span class="line">StringUtils.isNotBlank(<span class="string">"not blank"</span>); <span class="comment">// true</span></span><br><span class="line">StringUtils.isNotBlank(<span class="string">" hello "</span>); <span class="comment">// true</span></span><br></pre></td></tr></table></figure><h4 id="StringUtils-isEmpty"><a href="#StringUtils-isEmpty" class="headerlink" title="StringUtils.isEmpty()"></a><code>StringUtils.isEmpty()</code></h4><p>有 length 就不是 empty</p><ul><li>null [o]</li><li>空字串 [o]</li><li>空格 [x] 有 length</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">StringUtils.isEmpty(<span class="literal">null</span>) <span class="comment">// true</span></span><br><span class="line">StringUtils.isEmpty(<span class="string">""</span>) <span class="comment">// true</span></span><br><span class="line">StringUtils.isEmpty(<span class="string">" "</span>) <span class="comment">// false</span></span><br><span class="line">StringUtils.isEmpty(<span class="string">"Empty"</span>) <span class="comment">// false</span></span><br><span class="line">StringUtils.isEmpty(<span class="string">" Empty "</span>) <span class="comment">// false</span></span><br></pre></td></tr></table></figure><h2 id="總結"><a href="#總結" class="headerlink" title="總結"></a>總結</h2><blockquote><p>兩者判斷方式相同,但一些資料的流動難免會有可能讀寫上產生 null 所以 StringUtils 可以藉此不會因為 NullPointerException ,比較建議作為資料驗證判斷使用。</p></blockquote>]]></content>
<categories>
<category> Java 基礎 </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>Spring restTemplate 使用方法</title>
<link href="/2023/05/10/SpringBoot/Spring-restTemplate-%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/"/>
<url>/2023/05/10/SpringBoot/Spring-restTemplate-%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/</url>
<content type="html"><![CDATA[<h1 id="Spring-restTemplate-使用方法"><a href="#Spring-restTemplate-使用方法" class="headerlink" title="Spring restTemplate 使用方法"></a>Spring restTemplate 使用方法</h1><div class="note simple"><p>一般來說很多請求的處理多半是透過前端進行,但也免不了有時候後端需要直接發送請求來處理,像是後端需要去特定 api 拿取資源又或是和其他 server 之間的溝通部分很常需要自己發送請求,Spring Boot 也有提供 RestTemplate 這個物件來輕鬆實踐,早期多半使用 Java 原生的 HttpConnectionUrl 來處理,那這次就介紹一下現在比較常用到的幾個常見 RestTemplate 方法。</p></div><p>這邊提供一個開放的 api 可以進行測試 (<a href="https://jsonplaceholder.typicode.com/">https://jsonplaceholder.typicode.com</a>),裡面有詳細介紹有哪些路徑可以使用,可以直接用 postman 對特定位置來請求看看。下面介紹就會針對這個 api 進行請求。</p><h2 id="getForObject-x2F-postForObject"><a href="#getForObject-x2F-postForObject" class="headerlink" title="getForObject()/postForObject()"></a><strong>getForObject()/postForObject()</strong></h2><p>只想取得回覆的 body,對於其他 header 等資訊不在意,所以 response 裡面只會單純回應對方要回傳的 body</p><p>寫一個 Class 來發送請求至 <a href="https://jsonplaceholder.typicode.com/posts/1%EF%BC%8C%E5%8F%96%E5%BE%97%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%85%A7%E5%AE%B9">https://jsonplaceholder.typicode.com/posts/1,取得第一篇文章內容</a></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RestTemplateDemo</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="type">RestTemplate</span> <span class="variable">restTemplate</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line"> <span class="keyword">static</span> <span class="type">String</span> <span class="variable">urlForGet</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/1"</span>;</span><br><span class="line"> <span class="keyword">static</span> <span class="type">String</span> <span class="variable">UrlForPost</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts"</span>;</span><br><span class="line"></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"> getForObject();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * getForObject</span></span><br><span class="line"><span class="comment"> * GET</span></span><br><span class="line"><span class="comment"> * response 只有回傳 api 回應內容,不帶有 header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getForObject</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do getForObject ---"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">response</span> <span class="operator">=</span> restTemplate.getForObject(urlForGet, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">postForObject</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do postForObject ---"</span>);</span><br><span class="line"> HashMap<String, String> mapPost = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"> mapPost.put(<span class="string">"title"</span>, <span class="string">"新的文章標題"</span>);</span><br><span class="line"> mapPost.put(<span class="string">"body"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">response</span> <span class="operator">=</span> restTemplate.postForObject(UrlForPost, mapPost, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>執行這個方法可以在 terminal 看到回應<br><img src="https://ithelp.ithome.com.tw/upload/images/20240923/20150977ntQvAh9xoW.png" alt="https://ithelp.ithome.com.tw/upload/images/20240923/20150977ntQvAh9xoW.png"></p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240923/20150977kdaUZU8qON.png" alt="https://ithelp.ithome.com.tw/upload/images/20240923/20150977kdaUZU8qON.png"></p><h2 id="getForEntity-x2F-postForEntity"><a href="#getForEntity-x2F-postForEntity" class="headerlink" title="getForEntity()/postForEntity()"></a><strong>getForEntity()/postForEntity()</strong></h2><p>同時需要其他資訊如 header, response code 等等,其返回值是 ResponseEntity 物件,ResponseEntity 是 Spring 對 HTTP 請求的回覆封裝,getForObject 請求內部物件方法,getForEntity 都可以使用。</p><p>使用<code>ResponseEntity<T> responseEntity</code>來接收回覆結果。在配合用特定方法取得封裝物件內容:</p><ul><li>getBody()</li><li>getHeaders()</li><li>getStatusCode()</li></ul><p>寫一個方法來發送請求</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * getForEntity</span></span><br><span class="line"><span class="comment"> * GET</span></span><br><span class="line"><span class="comment"> * response 組成 ResponseEntity ,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">getForEntity</span><span class="params">()</span>{</span><br><span class="line"> ResponseEntity<String> response = restTemplate.getForEntity(urlForGet, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</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="comment"> * postForEntity</span></span><br><span class="line"><span class="comment"> * POST</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">postForEntity</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do postForEntity ---"</span>);</span><br><span class="line"> HashMap<String, String> mapPost = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"> mapPost.put(<span class="string">"title"</span>, <span class="string">"新的文章標題"</span>);</span><br><span class="line"> mapPost.put(<span class="string">"body"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> ResponseEntity<String> response = restTemplate.postForEntity(UrlForPost, mapPost, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>main 下加入這方法且執行後可以看到回應不只有原本<br><img src="https://ithelp.ithome.com.tw/upload/images/20240923/20150977AHjTcG84wq.png" alt="https://ithelp.ithome.com.tw/upload/images/20240923/20150977AHjTcG84wq.png"></p><p><img src="https://ithelp.ithome.com.tw/upload/images/20240923/20150977W9G5wQ2fAH.png" alt="https://ithelp.ithome.com.tw/upload/images/20240923/20150977W9G5wQ2fAH.png"></p><h2 id="exchange"><a href="#exchange" class="headerlink" title="exchange"></a><strong>exchange</strong></h2><p>更完整可以配置請求的相關內容,可以配置 header, HTTP method, Url 等等在送出請求之前,並且加入請求內容或是參數,最後和 get/postForEntity 一樣得到回應為封裝的 ResponseEntity。所以可以從中獲取你需要的各種資訊。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * exchange</span></span><br><span class="line"><span class="comment"> * (GET) 適用任何 http method。</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">exchangeWithGet</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">// 設定 header</span></span><br><span class="line"> <span class="type">HttpHeaders</span> <span class="variable">headers</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpHeaders</span>();</span><br><span class="line"> headers.setContentType(MediaType.APPLICATION_JSON);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 建立 HttpEntity 物件,包含請求頭及請求內容(可以省略)</span></span><br><span class="line"> HttpEntity<String> request = <span class="keyword">new</span> <span class="title class_">HttpEntity</span><>(<span class="string">""</span>, headers);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 執行 GET 請求,並取得回應資料</span></span><br><span class="line"> ResponseEntity<String> response = restTemplate.exchange(urlForGet, HttpMethod.GET, request, String.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 讀取回應內容</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">responseBody</span> <span class="operator">=</span> response.getBody();</span><br><span class="line"> System.out.println(responseBody);</span><br><span class="line"> <span class="keyword">return</span> responseBody;</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="comment"> * exchange</span></span><br><span class="line"><span class="comment"> * (POST) 適用任何 http method。</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">exchangeWithPost</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do exchangeWithPost ---"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 設定 header</span></span><br><span class="line"> <span class="type">HttpHeaders</span> <span class="variable">headers</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpHeaders</span>();</span><br><span class="line"></span><br><span class="line"> headers.setContentType(MediaType.APPLICATION_JSON);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 建立 HttpEntity 物件,包含請求頭及請求內容(可以省略)</span></span><br><span class="line"> <span class="type">Post</span> <span class="variable">post</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Post</span>(<span class="string">"新的文章標題"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> HttpEntity<Post> request = <span class="keyword">new</span> <span class="title class_">HttpEntity</span><>(post, headers);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 執行 GET 請求,並取得回應資料</span></span><br><span class="line"> ResponseEntity<String> response = restTemplate.exchange(UrlForPost, HttpMethod.POST, request, String.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 讀取回應內容</span></span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>回應部分就先省略,因為就如同上面的 getForEntity 和 postForEntity 範例一樣,</p><h2 id="put"><a href="#put" class="headerlink" title="put()"></a>put()</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * put</span></span><br><span class="line"><span class="comment"> * PUT</span></span><br><span class="line"><span class="comment"> * 沒有回傳值</span></span><br><span class="line"><span class="comment"> */</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_">put</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/2"</span>;</span><br><span class="line"> <span class="type">Post</span> <span class="variable">post</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Post</span>(<span class="string">"修改文章標題"</span>, <span class="string">"修改文章內容"</span>);</span><br><span class="line"> restTemplate.put(url, post);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>沒有回傳</p><h2 id="delete"><a href="#delete" class="headerlink" title="delete()"></a>delete()</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * delete</span></span><br><span class="line"><span class="comment"> * DELETE</span></span><br><span class="line"><span class="comment"> * 沒有回傳值</span></span><br><span class="line"><span class="comment"> */</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_">delete</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/3"</span>;</span><br><span class="line"> restTemplate.delete(url);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>沒有回傳</p><h2 id="完整範例"><a href="#完整範例" class="headerlink" title="完整範例"></a>完整範例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.oseanchen.demotest.restTemplate;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.http.*;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RestTemplateDemo</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="type">RestTemplate</span> <span class="variable">restTemplate</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line"> <span class="keyword">static</span> <span class="type">String</span> <span class="variable">urlForGet</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/1"</span>;</span><br><span class="line"> <span class="keyword">static</span> <span class="type">String</span> <span class="variable">UrlForPost</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts"</span>;</span><br><span class="line"></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"><span class="comment">// getForObject();</span></span><br><span class="line"><span class="comment">// getForEntity();</span></span><br><span class="line"><span class="comment">// postForObject();</span></span><br><span class="line"> postForEntity();</span><br><span class="line"><span class="comment">// exchangeWithGet();</span></span><br><span class="line"><span class="comment">// exchangeWithPost();</span></span><br><span class="line"><span class="comment">// put();</span></span><br><span class="line"><span class="comment">// delete();</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * getForObject</span></span><br><span class="line"><span class="comment"> * GET</span></span><br><span class="line"><span class="comment"> * response 只有回傳 api 回應內容,不帶有 header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getForObject</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do getForObject ---"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">response</span> <span class="operator">=</span> restTemplate.getForObject(urlForGet, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">postForObject</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do postForObject ---"</span>);</span><br><span class="line"> HashMap<String, String> mapPost = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"> mapPost.put(<span class="string">"title"</span>, <span class="string">"新的文章標題"</span>);</span><br><span class="line"> mapPost.put(<span class="string">"body"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">response</span> <span class="operator">=</span> restTemplate.postForObject(UrlForPost, mapPost, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</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="comment"> * getForEntity</span></span><br><span class="line"><span class="comment"> * GET</span></span><br><span class="line"><span class="comment"> * response 組成 ResponseEntity ,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">getForEntity</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do getForEntity ---"</span>);</span><br><span class="line"> ResponseEntity<String> response = restTemplate.getForEntity(urlForGet, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</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="comment"> * postForEntity</span></span><br><span class="line"><span class="comment"> * POST</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">postForEntity</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do postForEntity ---"</span>);</span><br><span class="line"> HashMap<String, String> mapPost = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line"> mapPost.put(<span class="string">"title"</span>, <span class="string">"新的文章標題"</span>);</span><br><span class="line"> mapPost.put(<span class="string">"body"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> ResponseEntity<String> response = restTemplate.postForEntity(UrlForPost, mapPost, String.class);</span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</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="comment"> * exchange</span></span><br><span class="line"><span class="comment"> * (GET) 適用任何 http method。</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">exchangeWithGet</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">// 設定 header</span></span><br><span class="line"> <span class="type">HttpHeaders</span> <span class="variable">headers</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpHeaders</span>();</span><br><span class="line"> headers.setContentType(MediaType.APPLICATION_JSON);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 建立 HttpEntity 物件,包含請求頭及請求內容(可以省略)</span></span><br><span class="line"> HttpEntity<String> request = <span class="keyword">new</span> <span class="title class_">HttpEntity</span><>(<span class="string">""</span>, headers);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 執行 GET 請求,並取得回應資料</span></span><br><span class="line"> ResponseEntity<String> response = restTemplate.exchange(urlForGet, HttpMethod.GET, request, String.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 讀取回應內容</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">responseBody</span> <span class="operator">=</span> response.getBody();</span><br><span class="line"> System.out.println(responseBody);</span><br><span class="line"> <span class="keyword">return</span> responseBody;</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="comment"> * exchange</span></span><br><span class="line"><span class="comment"> * (POST) 適用任何 http method。</span></span><br><span class="line"><span class="comment"> * 設定 header, body 等至 HttpEntity , 回傳 response 為 ResponseEntity,內含 body, header, response code...</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity<String> <span class="title function_">exchangeWithPost</span><span class="params">()</span>{</span><br><span class="line"> System.out.println(<span class="string">"--- do exchangeWithPost ---"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 設定 header</span></span><br><span class="line"> <span class="type">HttpHeaders</span> <span class="variable">headers</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpHeaders</span>();</span><br><span class="line"></span><br><span class="line"> headers.setContentType(MediaType.APPLICATION_JSON);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 建立 HttpEntity 物件,包含請求頭及請求內容(可以省略)</span></span><br><span class="line"> <span class="type">Post</span> <span class="variable">post</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Post</span>(<span class="string">"新的文章標題"</span>, <span class="string">"大家好,我是Sean!!"</span>);</span><br><span class="line"> HttpEntity<Post> request = <span class="keyword">new</span> <span class="title class_">HttpEntity</span><>(post, headers);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 執行 GET 請求,並取得回應資料</span></span><br><span class="line"> ResponseEntity<String> response = restTemplate.exchange(UrlForPost, HttpMethod.POST, request, String.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 讀取回應內容</span></span><br><span class="line"> System.out.println(response);</span><br><span class="line"> <span class="keyword">return</span> response;</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="comment"> * put</span></span><br><span class="line"><span class="comment"> * PUT</span></span><br><span class="line"><span class="comment"> * 沒有回傳值</span></span><br><span class="line"><span class="comment"> */</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_">put</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/2"</span>;</span><br><span class="line"> <span class="type">Post</span> <span class="variable">post</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Post</span>(<span class="string">"修改文章標題"</span>, <span class="string">"修改文章內容"</span>);</span><br><span class="line"> restTemplate.put(url, post);</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="comment"> * delete</span></span><br><span class="line"><span class="comment"> * DELETE</span></span><br><span class="line"><span class="comment"> * 沒有回傳值</span></span><br><span class="line"><span class="comment"> */</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_">delete</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">"https://jsonplaceholder.typicode.com/posts/3"</span>;</span><br><span class="line"> restTemplate.delete(url);</span><br><span class="line"> }</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><p>上述各類型的用法其實都很雷同,主要先看是否需要有進一步關於回應的各種資訊。</p><ul><li>如果不需要就可以考慮使用 get/postForObject 純粹獲取回應 body 就好。</li><li>如果需要那些完整回應的資訊就可以考慮用 get/postForEntity 或 Exchange 來進行請求,這樣就可以從中獲取像是 status code, header 等的資訊。</li><li>如果要細部指定 header 或是各類型的方法可以使用 exchange 來進行請求可以根據需求調整,詳細可以參考下表。</li><li>delete, put, patch 等如果不需要回應可以直接使用。</li></ul><table><thead><tr><th></th><th>return type</th><th>set header</th><th>getStatusCode()</th><th>getHeaders()</th><th>getBody()</th></tr></thead><tbody><tr><td>getForObject/postForObject</td><td>String (response body)</td><td>x</td><td>x</td><td>x</td><td>x</td></tr><tr><td>getForEntity/postForEntity</td><td>ResponseEntity<T></td><td>x</td><td>o</td><td>o</td><td>o</td></tr><tr><td>exchange</td><td>ResponseEntity<T></td><td>o</td><td>o</td><td>o</td><td>o</td></tr><tr><td>put/delete</td><td>void</td><td>x</td><td>x</td><td>x</td><td>x</td></tr></tbody></table><hr><p>Ref:</p><ul><li><a href="https://liuyueyi.github.io/hexblog/2018/08/13/180813-Spring%E4%B9%8BRestTemplate%E4%BD%BF%E7%94%A8%E5%B0%8F%E7%BB%93%E4%B8%80/">Spring 之 <strong>Rest</strong>Template 使用小结</a></li><li><a href="https://blog.csdn.net/YCJ_xiyang/article/details/90481832"><strong>rest</strong>Template 常用方法介绍</a></li><li><a href="https://zendei.com/article/96878.html">—上手就會 <strong>Rest</strong>Template 使用指南</a></li></ul>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> http request </tag>
</tags>
</entry>
<entry>
<title>JAVA main()</title>
<link href="/2023/05/03/Java/JAVA-main/"/>
<url>/2023/05/03/Java/JAVA-main/</url>
<content type="html"><![CDATA[<h1 id="Java-main"><a href="#Java-main" class="headerlink" title="Java main()"></a>Java main()</h1><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">HelloWorld</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">System.out.println(<span class="string">"Hello World!!"</span>);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>一個類別只有一個 main 方法並且是整個程式的執行進入點(Entry point)。</li><li>封裝等級必須宣告為 public,且只有主類別可以宣告為 public。(預設為 public,所以 public 可省略)</li><li>主類別名稱必須與主檔名相同(不含副檔名)。</li><li>修飾字的宣告只能宣告為 abstract、final 或省略不寫。</li><li>public static void 是固定不可以改變的</li></ul><h2 id="名詞解釋"><a href="#名詞解釋" class="headerlink" title="名詞解釋"></a>名詞解釋</h2><h4 id="public"><a href="#public" class="headerlink" title="public"></a>public</h4><p>表示方法是公開,可以在這個 class 以外的地方使用</p><h4 id="static"><a href="#static" class="headerlink" title="static"></a>static</h4><p>表示靜態,是描述這個方法的狀態,程式剛開始執行前,就要把這些東西載入到記憶體,main() 方法不需要產生物件(Object)就能被執行,所以它必須是個 “static” 成員</p><h4 id="void"><a href="#void" class="headerlink" title="void"></a>void</h4><p>表示這個方法沒有回傳值,一但 main()方法終止,java 程序也將終止。因此,從 main()方法返回沒有任何意義。</p><h4 id="main-String-args"><a href="#main-String-args" class="headerlink" title="main (String[] args)"></a>main (String[] args)</h4><p>為 java jvm 找尋程式執行的進入點辨識的方法,參數可以接受字串陣列當做參數,這是 JVM 定義好的,String[] 表示字串陣列,args 是為這些字串陣列取的變數名稱,當然也可以寫 main(String[] abc)。<br>args:arguments</p><hr><p><strong>參考資料:</strong></p><ol><li><a href="https://www.geeksforgeeks.org/java-main-method-public-static-void-main-string-args/">https://www.geeksforgeeks.org/java-main-method-public-static-void-main-string-args/</a></li></ol>]]></content>
<categories>
<category> Java 基礎 </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>JAVA Static</title>
<link href="/2023/04/29/Java/JAVA-Static/"/>
<url>/2023/04/29/Java/JAVA-Static/</url>
<content type="html"><![CDATA[<h1 id="JAVA-Static"><a href="#JAVA-Static" class="headerlink" title="JAVA Static"></a>JAVA Static</h1><h2 id="static-靜態"><a href="#static-靜態" class="headerlink" title="static 靜態"></a>static 靜態</h2><h4 id="作用對象"><a href="#作用對象" class="headerlink" title="作用對象"></a>作用對象</h4><p>可用來修飾 (類別的屬性、方法或子類別),不能用 static 修飾最上層 Class</p><h4 id="定義"><a href="#定義" class="headerlink" title="定義"></a>定義</h4><p>靜態的意思是,在程式載入記憶體的時候,跟著程式一起在記憶體中佔有空間,而不是主程式開始執行後才跟記憶體要空間。</p><p>static 可以想像成<code>程式執行時已經載入到記憶體中</code>,有點類似於全域的概念,Java 中沒有全域性變數的概念。但可以透過 static 實現此目的,可以將變數宣告為靜態並將其用作全域性變數。</p><h4 id="範例-1"><a href="#範例-1" class="headerlink" title="範例 1"></a>範例 1</h4><p>如果物件內定義 static 屬性,就可以使用類名稱訪問靜態變數,不需要建立一個物件來呼叫靜態變數。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Student</span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="type">int</span> id;</span><br><span class="line"> <span class="keyword">static</span> String name;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">StaticTesting</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"> Student.id = <span class="number">1</span>;</span><br><span class="line"> Student.name = <span class="string">"Sean"</span>;</span><br><span class="line"> <span class="type">int</span> <span class="variable">StudentId</span> <span class="operator">=</span> Student.id;</span><br><span class="line"> <span class="type">String</span> <span class="variable">StudentName</span> <span class="operator">=</span> Student.name;</span><br><span class="line"> System.out.println(<span class="string">"Id: "</span>+StudentId);</span><br><span class="line"> System.out.println(<span class="string">"Name: "</span>+StudentName);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Output:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Id: 1</span><br><span class="line">Name: Sean</span><br></pre></td></tr></table></figure><h4 id="範例-2"><a href="#範例-2" class="headerlink" title="範例 2"></a>範例 2</h4><p>static 也可以同時用在類別方法中,可以讓有被注入這個類別的程式內都可以使用,類似於使得類別內的變數擁有全域的特性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GlobalVarTest</span> {</span><br><span class="line"> <span class="keyword">static</span> String Global;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">setGlobal</span><span class="params">(String s)</span> {</span><br><span class="line"> Global = s;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getGlobal</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> Global;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">pubic <span class="keyword">class</span> <span class="title class_">TestGlobal1</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">print</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">a</span> <span class="operator">=</span> <span class="string">"hi"</span>;</span><br><span class="line"> Global.setGlobal(a)</span><br><span class="line"> System.out.println(<span class="string">"TestGlobal1 set Global: "</span> + GlobalVarTest.getGlobal());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">pubic <span class="keyword">class</span> <span class="title class_">TestGlobal2</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">print</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">b</span> <span class="operator">=</span> <span class="string">"123"</span>;</span><br><span class="line"> Global.setGlobal(b)</span><br><span class="line"> System.out.println(<span class="string">"TestGlobal2 set Global: "</span> + GlobalVarTest.getGlobal());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Output:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">TestGlobal1 set Global: hi</span><br><span class="line">TestGlobal2 set Global: 123</span><br></pre></td></tr></table></figure>]]></content>
<categories>
<category> Java 基礎 </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>RabbitMQ 介紹</title>
<link href="/2023/04/21/Message_Queue/RabbitMQ-%E4%BB%8B%E7%B4%B9/"/>
<url>/2023/04/21/Message_Queue/RabbitMQ-%E4%BB%8B%E7%B4%B9/</url>
<content type="html"><![CDATA[<h2 id="軟體簡介"><a href="#軟體簡介" class="headerlink" title="軟體簡介"></a>軟體簡介</h2><ul><li>著名應用 message Queue (message brokers) 的開源軟體</li><li>支持 AMQP 0-9-1 等多種訊息傳遞協定</li><li>支援多種語言及插件工具</li><li>圖形化介面管理及監控訊息狀態</li><li>其他類似軟體 EX: Kafka、AWS SQS…</li></ul><h2 id="AMQP-基本架構及名詞解釋"><a href="#AMQP-基本架構及名詞解釋" class="headerlink" title="AMQP 基本架構及名詞解釋"></a>AMQP 基本架構及名詞解釋</h2><p><img src="https://i.imgur.com/vJlh8T5.png"><br>AMQP 0-9-1: AMQP (Advanced Message Queuing Protocol)是一種訊息傳遞的協定,具有各個角色實踐訊息傳遞的中間代理。</p><p>由 Producer 發佈訊息到 exchange,可以想像成郵局或郵箱。然後依照綁定的規則將訊息發到 Queue(Broker) 。然後 consumer 依照期間監聽(訂閱)的 Queue 將其中訊息取出。</p><ul><li><code>Producer</code> : 發送訊息的程式</li><li><code>Queue</code>:儲存訊息等待接收的緩衝區域,具有 FIFO 特性(先進先出)</li><li><code>Consumer</code>:訊息接收的程式</li><li><code>Exchange</code>:決定將訊息推送至指定位置<ul><li>Direct:直接丟給指定的 Queue</li><li>Topic:設定 binding 規則,丟給符合的 Queue</li><li>Fanout:一次丟給全部負責的 Queue</li></ul></li></ul><h2 id="5-種-RabbitMQ-設計模式"><a href="#5-種-RabbitMQ-設計模式" class="headerlink" title="5 種 RabbitMQ 設計模式"></a>5 種 RabbitMQ 設計模式</h2><h3 id="1-Simple"><a href="#1-Simple" class="headerlink" title="1. Simple"></a>1. Simple</h3><p>最基本的模式,只有一個 Producer 負責送 message 給 Queue、只有一個 Consumer 去 Q ueue 裡將 message 接收</p><p><img src="https://i.imgur.com/kphjY7p.png"></p><h3 id="2-Worker"><a href="#2-Worker" class="headerlink" title="2. Worker"></a>2. Worker</h3><p>這模式像是多個工人會共同去完成工作</p><p>同時有多個 Consumer 會去收 Queue 裡的 message,可以增加 message 消化的速率。常用的一種模式,通常是用在大量非同步的作業上,只要連接同一個 Queue,就可以在多台機器上 Consumer 平行處理。</p><p><img src="https://i.imgur.com/IsOedmp.png"></p><h3 id="3-Publish-x2F-Subscibe"><a href="#3-Publish-x2F-Subscibe" class="headerlink" title="3. Publish/Subscibe"></a>3. Publish/Subscibe</h3><p>可以想像成訂閱頻道或是服務一樣,有新的發表或是訊息就會廣播給訂閱者。</p><p>所以需在 Producer、Queue、Consumer 之間多一個 Exchange,Producer 會先把 message 丟給 Exchange (Type = Fanout),Fanout 模式的 Exchange 去決定要把這個 message 丟給綁定(訂閱)的 Queue 上</p><p><img src="https://i.imgur.com/kWcOt87.png"></p><h3 id="4-Routing"><a href="#4-Routing" class="headerlink" title="4. Routing"></a>4. Routing</h3><p>想成貼上標籤(Routing key)並依照標籤指定類型來傳送</p><p>當 Producer 把 message 丟給 Exchange(Type = Direct) 時,同時要在這個 message 上面帶上一個 routing key,而 Exchange 就會根據這個 routing key,將 message 丟到指定的 Queue 上</p><p>就像下圖的例子,Q1 只會收到關於 orange 相關 routing key 的 message、Q2 則是 black 及 green 的 message</p><p><img src="https://i.imgur.com/lPALPn7.png"></p><h3 id="5-Topics"><a href="#5-Topics" class="headerlink" title="5. Topics"></a>5. Topics</h3><p>也是標上特定標籤,可以想成能區分更仔細的多重標籤。</p><p>類似 Routing 模式,同樣有 Exchange (Type = Topic),也透過 routing key 來分流訊息,差別在 topic 的特性能夠模糊綁定非固定的 routing key。 設定模糊 routing key 的格式需以 .(dot) 分隔的字串,*(star) 只能代替一個單詞、#(bash) 可以代替零個或多個單詞。</p><p>根據下圖的狀況,訊息將使用由三個標籤組成的 routing key 發送,第一個標籤是 “速度”,第二個標籤是 “顏色”,第三個標籤是 “動物”:<速度>.<顏色>.<動物>。</p><p>Q1 以 .orange. 綁定,與 Q2 以 <em>.</em>.rabbit 和 lazy.# 綁定,也就是:</p><ul><li>Q1 會收到所有關於 “橘色動物” 的訊息</li><li>Q2 會收到所有關於 “兔子” 的訊息,以及所有關於 “任何顏色且懶惰動物” 的訊息</li></ul><p><img src="https://i.imgur.com/VkBc4i4.png"></p><hr><p>參考資料:</p><ol><li><a href="https://www.rabbitmq.com/tutorials/amqp-concepts.html">AMQP 0-9-1 Model Explained</a></li><li><a href="https://enzochang.com/rabbitmq-introduction/">RabbitMQ 簡介與 5 種設計模式</a></li><li><a href="https://kucw.github.io/blog/2020/11/rabbitmq/">RabbitMQ 基本介紹、安裝教學</a></li></ol>]]></content>
<categories>
<category> RabbitMQ </category>
</categories>
<tags>
<tag> RabbitMQ </tag>
<tag> Message Queue </tag>
<tag> Asynchronous </tag>
</tags>
</entry>
<entry>
<title>Spring MVC Http method 取得請求參數方法</title>
<link href="/2023/03/25/SpringBoot/Spring-MVC-Http-method-%E5%8F%96%E5%BE%97%E8%AB%8B%E6%B1%82%E5%8F%83%E6%95%B8%E6%96%B9%E6%B3%95/"/>
<url>/2023/03/25/SpringBoot/Spring-MVC-Http-method-%E5%8F%96%E5%BE%97%E8%AB%8B%E6%B1%82%E5%8F%83%E6%95%B8%E6%96%B9%E6%B3%95/</url>
<content type="html"><![CDATA[<div class="note simple"><p>Spring Boot 常使用取得請求的方式:</p><ul><li>@RequestParam</li><li>@RequestBody</li><li>@RequestHeader</li><li>@PathVariable</li></ul></div><h2 id="RequestParam"><a href="#RequestParam" class="headerlink" title="@RequestParam"></a>@RequestParam</h2><p>用法:</p><ul><li>GET</li><li>required = true/false (預設 true),如果是 false 就不用代入但可能會因此有 null 報 exception</li><li>defaultValue = xxx,設置預設值,設置後 required 就會被忽略</li></ul><p>用途:取得放在 URL 的參數</p><p><code>http:localhost:8080/test1?id=123</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RequestController</span> {</span><br><span class="line"> <span class="meta">@RequestMapping("/test1")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">test1</span><span class="params">(<span class="meta">@RequestParam</span> Integer id,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(defaultValue = "Sean")</span> String name,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam(value = "nickname", required = false)</span> String nickname</span></span><br><span class="line"><span class="params"> )</span> {</span><br><span class="line"> System.out.println(<span class="string">"id:"</span> + id);</span><br><span class="line"> System.out.println(<span class="string">"name:"</span> + name);</span><br><span class="line"> System.out.println(<span class="string">"name:"</span> + nickname);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"your request id is "</span> + id + <span class="string">" and name is "</span> + name + <span class="string">", or you can called me "</span> + nickname;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>發送 request,帶入 id =1, name = Sean,故意不帶入第 3 個參數 nickname,可以看到最後會回傳 null 但也不會出錯誤,但如果把 required = true 就會出現 400 Bad Request 錯誤。<br><img src="https://i.imgur.com/nEWhpMX.png"></p><p>console 上面顯示<br><img src="https://i.imgur.com/U0AvxlZ.png"></p><h2 id="RequestBody"><a href="#RequestBody" class="headerlink" title="@RequestBody"></a>@RequestBody</h2><p>用法:</p><ul><li>POST, PUT</li><li>required = true/false (預設 true)</li><li>需要先定義對應參數的 java class</li><li>RequestBody 裡面多傳參數也不會報錯,有定義的才會被使用,少傳就會有 null</li></ul><p>用途:取得 Request body 裡面的參數(將 Json 轉為自定義 Java Object)</p><p><code>http:localhost:8080/test2</code></p><p>controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping("/test2")</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">test2</span><span class="params">(<span class="meta">@RequestBody</span> Student student)</span>{</span><br><span class="line"> System.out.println(<span class="string">"student id: "</span> + student.getId() );</span><br><span class="line"> System.out.println(<span class="string">"student name: "</span> + student.getName() );</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"hello test2"</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Student class</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Student</span> {</span><br><span class="line"> Integer id;</span><br><span class="line"> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Integer <span class="title function_">getId</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> id;</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_">setId</span><span class="params">(Integer id)</span> {</span><br><span class="line"> <span class="built_in">this</span>.id = id;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getName</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> name;</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_">setName</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><br></pre></td></tr></table></figure><p>發送 request,帶入{ “id” =1, “name” = “Sean” }的 JSON 物件,如果請求內所帶的物件屬性不是 Student 物件設定的,不管多或少其實都不會有問題,但如果希望參數設定可以驗證避免使用者傳錯參數的話,就必須要透過驗證註解像是<code>@Valid</code>, <code>@NotNull</code>, <code>@NotBlank</code> 等等的部分才可以做到了,這部分後續文章會有進一步說明。<br><img src="https://i.imgur.com/LB9SHzv.png"></p><p><img src="https://i.imgur.com/5otFdmB.png"></p><p>console 上面顯示<br><img src="https://i.imgur.com/avU1pAf.png"></p><h2 id="RequestHeader"><a href="#RequestHeader" class="headerlink" title="@RequestHeader"></a>@RequestHeader</h2><p>用法:</p><ul><li>GET/POST</li><li>只能加在方法參數上</li><li>name(或用 value): 指定 request header 的 header 名字(比 requestparam 的 name 常用)</li><li>required = true/false 同 @RequestParam</li><li>defaultValue: 預設值 同 @RequestParam</li></ul><p>用途:取得放在 requestHeader 的參數,可以用 GET 請求中設定 header; 如果用 POST 一般都會帶入 Content-Type = application/json</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping("/test3")</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">test3</span><span class="params">(<span class="meta">@RequestHeader</span> String info,</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestHeader</span> (name = <span class="string">"gogo"</span>, required = <span class="literal">false</span>)</span> String gogo){</span><br><span class="line"> System.out.println(<span class="string">"header info: "</span> + info);</span><br><span class="line"> System.out.println(<span class="string">"header gogo: "</span> + gogo);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"hello test3"</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>發送 request,header 上面帶入指定的 header 名稱,如果是參數定義的就可以抓到<br><img src="https://i.imgur.com/gqsy8e0.png"></p><p>console 上面顯示<br><img src="https://i.imgur.com/v2NLbfl.png"></p><h2 id="PathVariable"><a href="#PathVariable" class="headerlink" title="@PathVariable"></a>@PathVariable</h2><p>用法:</p><ul><li>路徑上寫的參數要和 @PathVariable 定義的一樣用途:取得放在 URL 路徑裡面的值</li><li>required = true/false (預設 true),如果是 false 就不用代入但可能會因此有 null 報 exception</li></ul><p><code>http://localhost:8080/test4/123/Sean</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping("/test4/{id}/{name}")</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">test4</span><span class="params">(<span class="meta">@PathVariable</span> Integer id,</span></span><br><span class="line"><span class="params"> <span class="meta">@PathVariable</span> String name</span></span><br><span class="line"><span class="params"> )</span>{</span><br><span class="line"> System.out.println(<span class="string">"path id: "</span> + id);</span><br><span class="line"> System.out.println(<span class="string">"path name: "</span> + name);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"hello test4"</span>;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>發送 request,URL 上針對 {id}, {name} 部分帶入參數,後端就可以取得,注意定義的參數類型如果不匹配也會出現 400 bad request,像是如果你把 id 部分用文字帶入請求。<br><img src="https://i.imgur.com/mrKFW5z.png"></p><p>console 上面顯示<br><img src="https://i.imgur.com/5AIeMxn.png"></p><h2 id="小總結"><a href="#小總結" class="headerlink" title="小總結:"></a>小總結:</h2><p>針對一些用法上大家可以注意一下</p><table><thead><tr><th></th><th>Http method</th><th>required</th><th>defaultValue</th><th>常用情境</th></tr></thead><tbody><tr><td>@RequestParam</td><td><code>GET</code></td><td>O (預設 true)</td><td>O</td><td>查詢參數或表單數據</td></tr><tr><td>@RequestBody</td><td><code>POST</code>, <code>PUT</code></td><td>O (預設 true)</td><td>X</td><td>處理 JSON 或 XML 請求</td></tr><tr><td>@PathVariable</td><td><code>GET</code>, <code>PUT</code>, <code>DELETE</code></td><td>O (預設 true)</td><td>X</td><td>URL 路徑中的變數</td></tr><tr><td>@RequestHeader</td><td><code>GET</code>、<code>POST</code></td><td>O (預設 true)</td><td>O</td><td>獲取 HTTP 請求 header</td></tr></tbody></table><hr><p>參考資料:</p><ol><li>Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)</li></ol>]]></content>
<categories>
<category> Spring Boot </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Spring Boot </tag>
<tag> http request </tag>
</tags>
</entry>
<entry>
<title>Rails 套件操作,Devise(google 第三方登入(下))</title>
<link href="/2022/11/02/Rails/Rails-%E5%A5%97%E4%BB%B6%E6%93%8D%E4%BD%9C%EF%BC%8CDevise%EF%BC%88google-%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%85%A5%EF%BC%88%E4%B8%8B%EF%BC%89/"/>
<url>/2022/11/02/Rails/Rails-%E5%A5%97%E4%BB%B6%E6%93%8D%E4%BD%9C%EF%BC%8CDevise%EF%BC%88google-%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%85%A5%EF%BC%88%E4%B8%8B%EF%BC%89/</url>
<content type="html"><![CDATA[<p>今天接著安裝完成 <code>'omniauth-google-oauth2'</code>、<code>'omniauth-rails_csrf_protection'</code>這兩個套件之後,我們接續後面的步驟吧。</p><h3 id="設定-routes"><a href="#設定-routes" class="headerlink" title="設定 routes"></a>設定 routes</h3><p>記得之前用 devise 產生的 controller 中有一個 omniauth_callbacks_controller.rb 這個檔案嗎?<br>現在可以拿來用了喔,但我們要告訴 devise 要去使用這個 controller 必須要設定好 routes</p><figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rails g <span class="symbol">devise:</span>controllers users</span><br></pre></td></tr></table></figure><p>如同前面章節有提到,這些產生的 controller 必須要設定好路徑才可以使用喔。如果沒有產生的話可以自己建立 app/controller/users/omniauth_callbacks_controller.rb 到這個路徑。</p><figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devise_for <span class="symbol">:users</span>, <span class="symbol">controllers:</span> { <span class="symbol">omniauth_callbacks:</span> <span class="string">'users/omniauth_callbacks'</span> }</span><br></pre></td></tr></table></figure><h3 id="設定-callback-controller"><a href="#設定-callback-controller" class="headerlink" title="設定 callback controller"></a>設定 callback controller</h3><p>這邊是當 google 授權後會帶著得到那邊相關的資料來到我們的網域,就會用 callbacks controller 接下來這些資料,也就是前面填寫給 google 的 已授權的重新導向 url 位置。接下來之後我們要把這些資料拿來比對是否資料庫內已經有對應存在的帳號。</p><figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Users::OmniauthCallbacksController</span> < <span class="title class_ inherited__">Devise::OmniauthCallbacksController</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">google_oauth2</span></span><br><span class="line"> <span class="comment"># 需要在model中實作定義下面 `from_omniauth` 的方法 (app/models/user.rb)</span></span><br><span class="line"> <span class="variable">@user</span> = User.from_omniauth(request.env[<span class="string">'omniauth.auth'</span>])</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> <span class="variable">@user</span>.persisted?</span><br><span class="line"> flash[<span class="symbol">:notice</span>] = I18n.t <span class="string">'devise.omniauth_callbacks.success'</span>, <span class="symbol">kind:</span> <span class="string">'Google'</span></span><br><span class="line"> sign_in_and_redirect <span class="variable">@user</span>, <span class="symbol">event:</span> <span class="symbol">:authentication</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> session[<span class="string">'devise.google_data'</span>] = request.env[<span class="string">'omniauth.auth'</span>].except(<span class="string">'extra'</span>)</span><br><span class="line"> redirect_to new_user_registration_url, <span class="symbol">alert:</span> <span class="variable">@user</span>.errors.full_messages.join(<span class="string">"\n"</span>)</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="設定-model"><a href="#設定-model" class="headerlink" title="設定 model"></a>設定 model</h3><p>先加入 devise 提供的 :omniauthable 的方法到 model 上面</p><figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">devise <span class="symbol">:database_authenticatable</span>, <span class="symbol">:registerable</span>,</span><br><span class="line"> <span class="symbol">:recoverable</span>, <span class="symbol">:rememberable</span>, <span class="symbol">:validatable</span>,</span><br><span class="line"> <span class="symbol">:omniauthable</span>, <span class="symbol">omniauth_providers:</span> [<span class="symbol">:google_oauth2</span>]</span><br></pre></td></tr></table></figure><p>前面在 callback controller 可以看到我們需要對 User 這個 model 定義類別方法 <code>from_omniauth</code> 在 user.rb 中,解析授權回來的資料,並且會從資料庫中比對跟授權回來的 email 資料相同的 user 來建立一個,注意註解下面的部分是處理當沒有對應的 email 或相關註冊填寫的資料時就建立給一個出來進行登入。</p><p>如果你定義 user 寫入資料庫的部分有要填入其他欄位或是只要特定的可以自己調整,預設只有 email 跟 password,像我當初專案是多加入 username,所以就改成下面這樣。</p><figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">self</span>.from_omniauth(access_token)</span><br><span class="line"> data = access_token.info</span><br><span class="line"> user = User.where(<span class="symbol">email:</span> data[<span class="string">'email'</span>]).first</span><br><span class="line"> <span class="comment"># 下面處理是當資料庫內部沒有對應的帳號密碼時,建立給我們專案資料庫去對應每次授權的 token。</span></span><br><span class="line"> user |<span class="params"></span>|= User.create(</span><br><span class="line"> (<span class="symbol">username:</span> data[<span class="string">'username'</span>] |<span class="params"></span>| data[<span class="string">'email'</span>].split(<span class="string">'@'</span>).first,</span><br><span class="line"> <span class="symbol">email:</span> data[<span class="string">'email'</span>],</span><br><span class="line"> <span class="symbol">password:</span> Devise.friendly_token[<span class="number">0</span>,<span class="number">20</span>]</span><br><span class="line"> )</span><br><span class="line"> user</span><br><span class="line"> <span class="keyword">end</span></span><br></pre></td></tr></table></figure><h3 id="最後可以回去看到-view-的部分"><a href="#最後可以回去看到-view-的部分" class="headerlink" title="最後可以回去看到 view 的部分"></a>最後可以回去看到 view 的部分</h3><p>如果你有用他產生預設的 view 應該會直接看到相關連結自動產生在頁面上,你也可以自己找出路徑客製自己藥的東西,可以透過下面找到相關路徑</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rails routes -c omniauth</span><br></pre></td></tr></table></figure><p><img src="https://i.imgur.com/OBARANG.png"></p><p>填入對應的 link_to 位置應該就可以看到。就可以測試看看是否可以正常使用。</p><p>---<br>參考資料:</p><ol><li><a href="https://www.youtube.com/watch?v=XuXC8BvJM4Y&ab_channel=SupeRailsbyYaroslavShmarov">YarpslavShmarov 教學影片</a></li><li><a href="https://github.com/zquestz/omniauth-google-oauth2">omniauth-google-oauth2</a></li><li><a href="https://medium.com/tingyiiii/rails%E5%AF%A6%E4%BD%9C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%85%A5-google-2a0851b74193">Rails 實作第三方登入-Google</a></li></ol>]]></content>
<categories>
<category> Rails套件 </category>
</categories>
<tags>
<tag> Ruby </tag>
<tag> Rails </tag>
<tag> Devise </tag>
<tag> 套件 </tag>
<tag> 會員系統 </tag>
<tag> google </tag>
<tag> github </tag>
<tag> Omniauth </tag>
<tag> Oauth </tag>
</tags>