-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrss2.xml
998 lines (612 loc) · 530 KB
/
rss2.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
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Emac</title>
<link>http://emacoo.cn/</link>
<atom:link href="/rss2.xml" rel="self" type="application/rss+xml"/>
<description></description>
<pubDate>Sun, 05 May 2024 14:49:09 GMT</pubDate>
<generator>http://hexo.io/</generator>
<item>
<title>【Mini AI Agent】如何用 100 行代码构建一个最小智能体?</title>
<link>http://emacoo.cn/AI/mini-gpt-agent/</link>
<guid>http://emacoo.cn/AI/mini-gpt-agent/</guid>
<pubDate>Sun, 05 May 2024 09:00:00 GMT</pubDate>
<description>
<blockquote>
<p>今年 3 月份,知名人工智能科学家吴恩达(Andrew Ng)在社交平台 X 上发帖提到,<strong>“我认为 AI 代理工作流程将在今年推动 AI 的大规模进步——甚至可能比下一代基础模型还要多……GPT-3.5(零样本)的正确率为 48.1
</description>
<content:encoded><![CDATA[<blockquote><p>今年 3 月份,知名人工智能科学家吴恩达(Andrew Ng)在社交平台 X 上发帖提到,<strong>“我认为 AI 代理工作流程将在今年推动 AI 的大规模进步——甚至可能比下一代基础模型还要多……GPT-3.5(零样本)的正确率为 48.1%,GPT-4(零样本)为 67.0%,而在智能体循环中,GPT-3.5 的正确率高达 95.1%”</strong>。此贴发出之后,引发了业界广泛关注。有人表示,这代表着 AI 发展中的范式转变。</p><p><img src="andrew-ng.png" alt></p><p>本文首先对智能体(AI Agent)的概念做一个简单介绍,然后详细拆解一个仅用 100 行代码构建的极简智能体应用。</p></blockquote><h2 id="1-什么是智能体?"><a href="#1-什么是智能体?" class="headerlink" title="1 什么是智能体?"></a>1 什么是智能体?</h2><p><strong>智能体(AI Agent)</strong>是一种超越简单文本生成的人工智能系统,它使用大语言模型(LLM)作为其核心计算引擎,使其能够进行对话、推理、执行任务,展现一定程度的自主性。</p><p>在智能体架构中,核心功能可以归纳为三个步骤的循环:<strong>感知-决策-行动</strong>。智能体首先通过感知机制收集环境信息,然后基于该信息和预设目标,通过决策机制制定行动计划,最终通过动作执行机制实施这些计划。</p><p><img src="ai-agent.png" alt></p><p><em>图:智能体架构示意</em></p><h2 id="2-示例:Mini-AI-Agent"><a href="#2-示例:Mini-AI-Agent" class="headerlink" title="2 示例:Mini AI Agent"></a>2 示例:Mini AI Agent</h2><p>了解了智能体的概念,接下来我们一起一步步拆解一个仅用 100 行代码构建的最小智能体应用,耗时约 1 个小时。</p><h3 id="2-1-效果演示"><a href="#2-1-效果演示" class="headerlink" title="2.1 效果演示"></a>2.1 效果演示</h3><p>先来看一下效果演示,</p><p><img src="ai-agent-demo.gif" alt></p><p><em>图:Mini AI Agent应用演示</em></p><p>看似平淡无奇的两次问答,实际上已经体现了智能体的核心循环:<strong>感知-决策-行动</strong>。</p><ul><li><strong>感知</strong>:接收问题</li><li><strong>决策</strong>:理解问题,确定目标,然后通过推理决定使用何种工具(即制定计划)</li><li><strong>行动</strong>:使用工具获取信息,然后生成答案</li></ul><h3 id="2-2-环境准备"><a href="#2-2-环境准备" class="headerlink" title="2.2 环境准备"></a>2.2 环境准备</h3><h4 id="申请账号:百度千帆"><a href="#申请账号:百度千帆" class="headerlink" title="申请账号:百度千帆"></a>申请账号:百度千帆</h4><ol><li>访问<a href="https://cloud.baidu.com/product/wenxinworkshop" target="_blank" rel="noopener">百度智能云千帆</a>,注册账号并登录<a href="https://console.bce.baidu.com/qianfan/overview" target="_blank" rel="noopener">千帆大模型控制台</a></li><li>打开模型服务-应用接入页面,创建应用,记下 API Key 和 Secret Key 备用</li><li>打开模型服务-在线服务页面,找到 ERNIE-3.5-8K(支持函数功能),开通付费(不用担心,非常便宜,100 次调用才 2 毛钱)</li></ol><p>PS: 本文只是以百度千帆为例,大家可以根据自身经验,替换成其他任何支持函数功能的大模型,比如智谱清言、Azure、OpenAI等。</p><h4 id="搭建本地开发环境:Jupyter-Notebook"><a href="#搭建本地开发环境:Jupyter-Notebook" class="headerlink" title="搭建本地开发环境:Jupyter Notebook"></a>搭建本地开发环境:Jupyter Notebook</h4><ol><li>访问 <a href="https://www.anaconda.com/download/success" target="_blank" rel="noopener">Anaconda 官网</a>,下载安装包并安装 Anaconda</li><li>命令行运行 <code>conda install jupyter notebook</code>,安装 Jupyter Notebook</li><li>打开 GitHub 示例项目 <a href="https://github.com/emac/langchain-samples" target="_blank" rel="noopener">emac/langchain-samples</a>,git clone 到本地</li><li>命令行打开 langchain-samples 目录,运行 <code>jupyter notebook</code>,打开 Jupyter Notebook</li></ol><h3 id="2-3-程序拆解"><a href="#2-3-程序拆解" class="headerlink" title="2.3 程序拆解"></a>2.3 程序拆解</h3><p>准备好环境之后,就可以进入程序员最喜欢的实操环节了!</p><h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">!pip install langchain langchain-community langchain-core gradio</span><br></pre></td></tr></table></figure><p>依赖说明:</p><ul><li><a href="https://github.com/langchain-ai/langchain" target="_blank" rel="noopener">langchain</a>: 最著名的开发大语言模型应用的开源框架,没有之一</li><li><a href="https://www.gradio.app/" target="_blank" rel="noopener">gradio</a>: 一个用于快速构建机器学习模型的交互式 Web 应用的 Python 库</li></ul><h4 id="初始化大模型"><a href="#初始化大模型" class="headerlink" title="初始化大模型"></a>初始化大模型</h4><figure class="highlight python"><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">from</span> langchain_community.llms <span class="keyword">import</span> QianfanLLMEndpoint</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 初始化千帆"</span>)</span><br><span class="line">os.environ[<span class="string">"QIANFAN_AK"</span>]=<span class="string">'千帆应用的 API Key'</span></span><br><span class="line">os.environ[<span class="string">"QIANFAN_SK"</span>]=<span class="string">'千帆应用的 Secret Key'</span></span><br><span class="line"></span><br><span class="line">llm = QianfanLLMEndpoint(streaming=<span class="literal">True</span>,</span><br><span class="line"> model=<span class="string">"ERNIE-3.5-8K"</span>,</span><br><span class="line"> temperature=<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line">response = llm.invoke(<span class="string">"上海春天一般哪个月开始?"</span>)</span><br><span class="line">print(response)</span><br></pre></td></tr></table></figure><p>执行之前先替换之前记录的千帆应用的 API Key 和 Secret Key。</p><p>程序解读:</p><ol><li>初始化环境变量,创建一个千帆 LLM 实例</li><li>发起(人生)第一次大模型 API 调用,如果不成功则返回检查环境和依赖</li></ol><h4 id="定义函数"><a href="#定义函数" class="headerlink" title="定义函数"></a>定义函数</h4><figure class="highlight python"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain <span class="keyword">import</span> PromptTemplate, LLMChain</span><br><span class="line"><span class="keyword">from</span> langchain.chains <span class="keyword">import</span> LLMRequestsChain</span><br><span class="line"><span class="keyword">from</span> langchain_core.tools <span class="keyword">import</span> tool</span><br><span class="line"><span class="keyword">from</span> langchain_core.utils.function_calling <span class="keyword">import</span> convert_to_openai_tool</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 定义函数"</span>)</span><br><span class="line"><span class="meta">@tool</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">search_ip</span><span class="params">(question:str, ip:str)</span> -> str:</span></span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 首先获取输入的IP地址的位置信息,然后回答输入的问题</span></span><br><span class="line"><span class="string"> @param question: 问题</span></span><br><span class="line"><span class="string"> @param ip: IP地址</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> prompt_template = <span class="string">"""以下是IP地址'{ip}'的位置信息:</span></span><br><span class="line"><span class="string"> >>> {requests_result} <<<</span></span><br><span class="line"><span class="string"> 根据以上位置信息,回答以下这个问题:</span></span><br><span class="line"><span class="string"> >>> {question} <<<"""</span></span><br><span class="line"> prompt = PromptTemplate(</span><br><span class="line"> input_variables=[<span class="string">"question"</span>, <span class="string">"ip"</span>, <span class="string">"requests_result"</span>],</span><br><span class="line"> template=prompt_template</span><br><span class="line"> )</span><br><span class="line"> chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))</span><br><span class="line"> inputs = {</span><br><span class="line"> <span class="string">"question"</span>: question,</span><br><span class="line"> <span class="string">"ip"</span>: ip,</span><br><span class="line"> <span class="string">"url"</span>: <span class="string">"https://api.songzixian.com/api/ip?dataSource=generic_ip&ip="</span> + ip</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> chain.invoke(inputs)</span><br><span class="line"></span><br><span class="line"><span class="meta">@tool</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">search_phone</span><span class="params">(question:str, phone:str)</span> -> str:</span></span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 首先获取输入的手机号码的归属地信息,然后回答输入的问题</span></span><br><span class="line"><span class="string"> @param question: 问题</span></span><br><span class="line"><span class="string"> @param phone: 手机号码</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> prompt_template = <span class="string">"""以下是手机号码'{phone}'的归属地信息:</span></span><br><span class="line"><span class="string"> >>> {requests_result} <<<</span></span><br><span class="line"><span class="string"> 根据以上归属地信息,回答以下这个问题:</span></span><br><span class="line"><span class="string"> >>> {question} <<<"""</span></span><br><span class="line"> prompt = PromptTemplate(</span><br><span class="line"> input_variables=[<span class="string">"question"</span>, <span class="string">"phone"</span>, <span class="string">"requests_result"</span>],</span><br><span class="line"> template=prompt_template</span><br><span class="line"> )</span><br><span class="line"> chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))</span><br><span class="line"> inputs = {</span><br><span class="line"> <span class="string">"question"</span>: question,</span><br><span class="line"> <span class="string">"phone"</span>: phone,</span><br><span class="line"> <span class="string">"url"</span>: <span class="string">"https://api.songzixian.com/api/phone-location?dataSource=phone_number_location&phoneNumber="</span> + phone</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> chain.invoke(inputs)</span><br><span class="line"></span><br><span class="line">functions=[convert_to_openai_tool(search_ip)[<span class="string">'function'</span>],convert_to_openai_tool(search_phone)[<span class="string">'function'</span>]]</span><br><span class="line">print(functions)</span><br></pre></td></tr></table></figure><p>程序解读:</p><ul><li><p>定义 <code>search_ip</code> 和 <code>search_phone</code> 两个工具函数,背后连接一个免费的第三方 API 接口平台,用来获取指定 IP 的位置信息和指定手机号的归属地信息,然后再结合原始问题,通过大模型生成最终回答。</p></li><li><p>tool/ convert_to_openai_tool: 用于生成函数定义的注解和工具方法</p></li><li><p><a href="https://python.langchain.com/docs/modules/model_io/prompts/quick_start/#prompttemplate" target="_blank" rel="noopener">PromptTemplate</a>: Prompt 模板,支持变量</p></li><li><p><a href="https://python.langchain.com/docs/use_cases/apis/#going-deeper" target="_blank" rel="noopener">LLMRequestsChain</a>: 一个基于 URL 请求的大模型链,先调用 URL 获取数据,然后将数据传给一个已绑定 Prompt 的大模型链获取答案</p></li></ul><h4 id="绑定函数"><a href="#绑定函数" class="headerlink" title="绑定函数"></a>绑定函数</h4><figure class="highlight python"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain.schema <span class="keyword">import</span> HumanMessage</span><br><span class="line"><span class="keyword">from</span> langchain_community.chat_models <span class="keyword">import</span> QianfanChatEndpoint</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"></span><br><span class="line"><span class="comment"># 此处不能设定streaming=True,否则无法激活函数回调</span></span><br><span class="line">chat = QianfanChatEndpoint(model=<span class="string">"ERNIE-3.5-8K"</span>,</span><br><span class="line"> temperature=<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_response</span><span class="params">(message)</span>:</span></span><br><span class="line"> print(<span class="string">"# 绑定函数"</span>)</span><br><span class="line"> result = chat.invoke([HumanMessage(content=message)],</span><br><span class="line"> functions=functions)</span><br><span class="line"> print(result)</span><br><span class="line"></span><br><span class="line"> function_call_info = result.additional_kwargs.get(<span class="string">"function_call"</span>, <span class="literal">None</span>)</span><br><span class="line"> print(function_call_info)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> function_call_info:</span><br><span class="line"> print(<span class="string">"# 直接返回"</span>)</span><br><span class="line"> <span class="keyword">return</span> result.content</span><br><span class="line"></span><br><span class="line"> print(<span class="string">"# 调用函数"</span>)</span><br><span class="line"> function_name = function_call_info[<span class="string">"name"</span>]</span><br><span class="line"> function_args = json.loads(function_call_info[<span class="string">"arguments"</span>])</span><br><span class="line"> function_result = eval(function_name)(function_args)</span><br><span class="line"> print(function_result)</span><br><span class="line"> <span class="keyword">return</span> function_result[<span class="string">"output"</span>]</span><br></pre></td></tr></table></figure><p>程序解读:</p><ul><li>创建一个千帆 Chat 实例,绑定之前定义的两个函数,传入用户输入的问题(对应智能体的<strong>感受</strong>环节),然后发起调用,根据调用结果执行不同任务(对应智能体的<strong>决策</strong>环节):<ul><li>如果调用结果没有提示函数调用(<code>result.additional_kwargs.get("function_call", None)</code>),则直接返回结果</li><li>否则,根据大模型提示的函数名和请求参数,通过反射调用相应的函数(对应智能体的<strong>执行</strong>环节),然后返回结果</li></ul></li></ul><h4 id="构建应用"><a href="#构建应用" class="headerlink" title="构建应用"></a>构建应用</h4><figure class="highlight python"><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">import</span> gradio <span class="keyword">as</span> gr</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">submit</span><span class="params">(message, chat_history)</span>:</span></span><br><span class="line"> bot_message = get_response(message)</span><br><span class="line"> <span class="comment"># 保存历史对话记录,用于显示</span></span><br><span class="line"> chat_history.append((message, bot_message))</span><br><span class="line"> <span class="keyword">return</span> <span class="string">""</span>, chat_history</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 创建交互"</span>)</span><br><span class="line"><span class="keyword">with</span> gr.Blocks() <span class="keyword">as</span> demo:</span><br><span class="line"> chatbot = gr.Chatbot(height=<span class="number">240</span>) <span class="comment"># 对话框</span></span><br><span class="line"> msg = gr.Textbox(label=<span class="string">"Prompt"</span>) <span class="comment"># 输入框</span></span><br><span class="line"> submitBtn = gr.Button(<span class="string">"Submit"</span>) <span class="comment"># 提交按钮</span></span><br><span class="line"> clearBtn = gr.ClearButton([msg, chatbot]) <span class="comment"># 清除按钮</span></span><br><span class="line"> <span class="comment"># 提交</span></span><br><span class="line"> msg.submit(submit, inputs=[msg, chatbot], outputs=[msg, chatbot]) </span><br><span class="line"> submitBtn.click(submit, inputs=[msg, chatbot], outputs=[msg, chatbot])</span><br><span class="line"> </span><br><span class="line">gr.close_all()</span><br><span class="line">demo.launch()</span><br></pre></td></tr></table></figure><p>程序解读:</p><ul><li>创建智能体应用,构建一个用户和智能体的对话框,将用户输入的消息传给后台创建的大模型,实时获取响应</li><li>此处可以看到,借助 Gradio 框架,短短几行代码,就可以构建出一个简洁的对话框应用,非常 Nice!</li></ul><p>至此,一个极简的智能体应用就构建成功了,前后仅用 100 行代码。完整代码参见 <a href="https://github.com/emac/langchain-samples/blob/main/mini-ai-agent.ipynb" target="_blank" rel="noopener">GitHub</a>。</p><h4 id="彩蛋:智能体如何思考?"><a href="#彩蛋:智能体如何思考?" class="headerlink" title="彩蛋:智能体如何思考?"></a>彩蛋:智能体如何思考?</h4><p>看完演示,拆解完程序,你可能对智能体里的<strong>“智能“</strong>两字的理解还是有点模模糊糊。其实要真正理解这一点,光看程序还不够,得看下大模型的响应结果(见下)。注意其中 <code>response_metadata</code> 有一个很有意思的 <code>thoughts</code> 字段(<code>用户想要知道一个特定IP地址的地理位置信息,我需要使用search_ip工具来获取这个信息</code>),其代表了大模型的思考过程,是不是和人类非常类似?</p><figure class="highlight"><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"># 问题:120.230.93.202属于哪个城市?</span><br><span class="line"># 大模型响应结果</span><br><span class="line">additional_kwargs={'finish_reason': 'function_call', 'request_id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'search_info': [], 'function_call': {'name': 'search_ip', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}} </span><br><span class="line"></span><br><span class="line">response_metadata={'token_usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}, 'model_name': 'ERNIE-3.5-8K', 'finish_reason': 'function_call', 'id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'created': 1714900176, 'result': '', 'is_truncated': False, 'need_clear_history': False, 'function_call': {'name': 'search_ip', 'thoughts': '用户想要知道一个特定IP地址的地理位置信息,我需要使用search_ip工具来获取这个信息。', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}, 'usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}} id='run-0706ab6f-a21f-4352-bbb1-1f1a0f13c426-0'</span><br></pre></td></tr></table></figure><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>本文基于百度千帆大模型,使用 Langchain 和 Gradio 框架,用短短 100 行代码就构建出一个极简的智能体应用。该应用能够根据用户问题,选择不同的工具获取信息,并生成最终回答,体现了智能体最核心的三步循环:感知-决策-行动。基于这个演示应用,相信聪明的你可以构建出更复杂、更智能的智能体应用,欢迎<a href="https://github.com/emac/emac.github.io/issues" target="_blank" rel="noopener">留言</a>交流。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="https://mp.weixin.qq.com/s/WOI-owwovML5g2olnO28GQ" target="_blank" rel="noopener">吴恩达:别光盯着GPT-5,用GPT-4做个智能体可能提前达到GPT-5的效果</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/AI/mini-gpt-agent/#disqus_thread</comments>
</item>
<item>
<title>GPT四问</title>
<link>http://emacoo.cn/AI/gpt-forecast/</link>
<guid>http://emacoo.cn/AI/gpt-forecast/</guid>
<pubDate>Mon, 01 Jan 2024 13:00:00 GMT</pubDate>
<description>
<blockquote>
<p>前两周,ZA技术社区举办了一场主题为“<a href="https://live.csdn.net/room/wl5875/fL36YEyS" target="_blank" rel="noopener">未来,程序员职业会消失吗?</a>”的Ge
</description>
<content:encoded><![CDATA[<blockquote><p>前两周,ZA技术社区举办了一场主题为“<a href="https://live.csdn.net/room/wl5875/fL36YEyS" target="_blank" rel="noopener">未来,程序员职业会消失吗?</a>”的Geek圆桌派,我作为业务研发的代表参加了此次直播。由于直播时间有限,未能充分表达我的观点,故写此篇博客进行详述。另一方面,也想借此篇博客,印证十年后我的这些观点是否还成立。</p></blockquote><h2 id="Q1:你对“程序员会不会被-AI-取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?"><a href="#Q1:你对“程序员会不会被-AI-取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?" class="headerlink" title="Q1:你对“程序员会不会被 AI 取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?"></a>Q1:你对“程序员会不会被 AI 取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?</h2><blockquote><p>本文中的 AI 特指这一波以 ChatGPT 为代表的泛人工智能。</p></blockquote><p>这个问题比较大,我想从三个角度来回答。</p><p><strong>首先,要明确目前 AI 的边界或者说局限</strong>。在我看来,主要有三点。</p><p><strong>1、计算能力差</strong>。下图是去年 6 月份 UC 伯克利主导的 MT-Bench 基准测试中各个知名 LLM(Large Language Model,大语言模型) 的得分。可以看到,<strong>所有 LLM 在数学这一类别的得分都是全类别中最低的</strong>。</p><p><img src="mt-bench.png" alt></p><p>又比如去年火爆上海的数学中考 22 题,不管用 ChatGPT 还是 GPT-4,都没法给出正确的答案。</p><p><img src="gpt4-22.png" alt></p><p>关于这一点,我认为本质上的原因是 GPT 是一个语言模型,而非计算模型。</p><p><strong>2、无法生产新知识</strong>。<strong>GPT 的本质是知识压缩</strong>,把海量的人类社会的知识压缩到 96 层神经网络中,从而“涌现”出某种类智能。表面上看,你问 GPT 任何一个问题,GPT 都能有模有样的给出一个符合逻辑的回答,但实际上,<strong>GPT 并不真正理解它回答的内容</strong>,所以才会有鲁迅和周树人是不是同一个人的热梗。</p><p><strong>要生产新知识,GPT 必须拥有进化的能力</strong>。而从进化论的角度来看,任何生物要进化必然经历三个过程,交配、变异、淘汰。交配是为了交换基因,变异是产生新的更适应环境的基因,然后淘汰掉那些不适应环境的基因。从这点来看,GPT 显然都不满足。</p><p><strong>3、没有产生意识</strong>。何为意识?意识如何产生?虽然人类目前仍然无法给出一个准确的回答,但作为一个必要条件,感觉和欲望是意识的基本特征(引自《未来简史》)。显然,<strong>GPT 既无感觉也无欲望,因此也就没有意识</strong>。</p><p>理清目前 AI 的三点局限之后,接着<strong>从分工的角度看一下程序员的发展趋势</strong>。简单来说,<strong>程序员可分为三类,开发、测试和运维</strong>。其中,开发可以进一步细分为业务开发、架构开发和算法开发,测试分为功能测试和自动化测试,运维分为传统运维和云平台运维。</p><p>先说开发,大部分的开发都是业务开发,<strong>业务开发本质上就是将业务需求“翻译”成机器代码</strong>,而“翻译”恰恰是 GPT 的强项,加上 code-davinci 项目的加持,<strong>如果说将来某一天程序员会被 AI 取代,业务开发首当其冲</strong>。架构开发和算法开发,需要较强的抽象能力和计算能力,这两点恰恰对应目前 AI 的前两点局限,所以被 AI 取代的可能性较低。再看测试和运维,目前行业的趋势是自动化测试逐步取代功能测试,云平台运维逐步取代传统运维,AI 的出现一定程度上加速了这一过程,大浪淘沙,<strong>最终留下的只能是测试架构和运维架构</strong>。这意味着,<strong>传统意义上的或者说狭义上的程序员会越来越少</strong>。举个例子,OpenAI 官网显示,为 ChatGPT 项目做出贡献的人员只有区区 87 人。</p><p>另一方面,<strong>广义上的程序员会越来越多</strong>。何为程序员?简单来说就是会使用编程语言编写程序的人。我们知道,像 Java、C++ 这些编程语言属于第三代编程语言,SQL 属于第四代编程语言,那么<strong>基于自然语言交互的 GPT 是不是可以算是第五代编程语言</strong>?<strong>使用 GPT 完成特定任务的人是不是可以算是一类新的程序员</strong>?</p><blockquote><p>If you can say it, you can do it.</p><p>- Silvio Savarese, Salesforce执行副总裁和首席科学家</p></blockquote><p>最后,<strong>程序员会不会被 AI 取代,还要看人类社会对待 AI 的态度,以及与之配套的 AI 安全法规如何制定</strong>。由于不受化学规律限制,AI 的发展速度远超人类,我们必须让这个过程变慢,让整个社会适应这个变化,并且制定出一套道德或者说安全法规,让我们能够安全的使用 AI,否则我们的文明就有被摧毁的风险。</p><p><img src="prometheus.png" alt></p><p><em>就像《普罗米修斯》开头一幕,碳基生命会不会是硅基生命的前传?</em></p><p>早在 2023 年 3 月,包括图灵奖得主 Yoshua Bengio、伯克利计算机科学教授 Stuart Russell、特斯拉 CEO 埃隆·马斯克、苹果联合创始人 Steve Wozniak 等在内的数千名对人工智能领域关注的学者、企业家、教授发起了一封公开信,强烈呼吁:<strong>暂停训练比 GPT-4 更强大的系统,期限为六个月</strong>,理由是这些模型对社会和人类存在潜在风险。</p><p>美国有个名叫<strong>对齐研究中心(Alignment Research Center)</strong>的非营利研究机构,致力将人工智能的行为对齐人类的价值观和预期利益。OpenAI 在发布 GPT-4 之前,就曾请求对齐研究中心评估该模型对权力追求行为的能力和潜在风险。</p><h2 id="Q2:所以这不得不提到一个现实,AI-是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应-AI-社会的到来?"><a href="#Q2:所以这不得不提到一个现实,AI-是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应-AI-社会的到来?" class="headerlink" title="Q2:所以这不得不提到一个现实,AI 是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应 AI 社会的到来?"></a>Q2:所以这不得不提到一个现实,AI 是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应 AI 社会的到来?</h2><p>先来看一则新闻,</p><blockquote><p>2023 年 5 月 2 日,代表 11,500 名编剧的美国编剧工会 WGA 因与影视制片人联盟持续存在的劳资纠纷而宣布罢工,AI 特别是 AIGC 已经成为了此次冲突的核心。自 2023 年初以来越来越受到关注的 ChatGPT,已经影响到好莱坞乃至整个影视行业。漫威最新播出的影视剧《秘密入侵》,就已经将 AI 运用于制作过程,生成了开场字幕,并饱受争议。编剧工会在谈判过程中要求不允许 AI 获得署名,并且不能要求编剧根据 AI 写好的内容进行修改,因为这样也会显著减少工作时长。与此同时,在未经允许的情况下,制作方不可以将工会成员的剧本进行 AI 训练。</p></blockquote><p>无论你承认或者不承认,AI 已经对当今社会的很多行业产生了不小的冲击。<strong>所谓大语言模型,本质上就是知识</strong>。<strong>一个好的程序员是一个好的模型,一个好的教师是一个好的模型</strong>。当 AI 发展到一定阶段,<strong>所有的知识工作者都有被取代的可能</strong>。美国普林斯顿大学教授爱德华·费尔滕(Edward Felten)甚至还提出了一个<strong>“职业AI暴露指数”(AIOE)</strong>,像客服、秘书、翻译、助教这些职业都属于高 AI 暴露率职业,程序员只能算中等 AI 暴露率职业。试想,<strong>哪个老板会抗拒能力强、又忠诚、价格还便宜的 AI 员工</strong>?</p><p><img src="boss.png" alt></p><p>虽然 AI 或早或晚、或多或少会取代或者部分取代一些职业,但<strong>同时也会创造一些新的职业,比如标注师、提示工程师、AI 研究员这类 AI 周边职业</strong>。另一方面,那些 AI 边界之外的职业我相信也会出现一个爆发式增长,特别是<strong>心理师</strong>这个职业。为什么这么说?自 18 世纪六十年代第一次工业革命以来,工作已经成为人的本能。而一旦进入 AI 时代,如《未来简史》所言,绝大部分人将沦为“无价值的群体”,大部分人将不再需要工作。在这个过程中,大量人会出现心理问题,<strong>人们被迫寻找工作之外的意义,尝试在社区、家庭、艺术、运动、精神领域和自我探索中找到目的,从追求财富自由转向追求精神自由</strong>。</p><blockquote><p>在成为自己这件事情上,没有人比得过你。</p><p>- 纳瓦尔</p></blockquote><h2 id="Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些-AI-的知识?培养哪些方面的技能?"><a href="#Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些-AI-的知识?培养哪些方面的技能?" class="headerlink" title="Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些 AI 的知识?培养哪些方面的技能?"></a>Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些 AI 的知识?培养哪些方面的技能?</h2><blockquote><p>取代你的不是AI,而是使用AI的人。</p></blockquote><p>可以用 <strong>ASK (Attitude态度,Skill技能,Knowledge知识)模型</strong>来回答这个问题。</p><p>在态度层面,不管是作为程序员,还是普通人,我们都应该<strong>接受 AI</strong>,让 AI 成为个人工作、生活的得力助手,千万不要像好莱坞编剧那样视 AI 为敌,拒之千里之外。</p><p>在技能层面,作为普通人,要学会使用 AI,着重<strong>提升自身的任务分解能力、信息整合能力,特别是学习能力</strong>。作为程序员,则要具备一定程度的 AI 开发能力,比如开发 GPT 插件、基于 LangChain + Embedding 的 AI 应用、基于 Llama 的自定义模型等。</p><p>在知识层面,<strong>通识教育会变得越来越重要,知识的广度比深度更重要</strong>,对于程序员而言,了解机器学习、神经网络、强化学习、Transformer等这些 AI 的基本概念和原理是最低的要求。</p><h2 id="Q4:我们今天做一个大胆的预测,猜猜-10-年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)"><a href="#Q4:我们今天做一个大胆的预测,猜猜-10-年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)" class="headerlink" title="Q4:我们今天做一个大胆的预测,猜猜 10 年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)"></a>Q4:我们今天做一个大胆的预测,猜猜 10 年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)</h2><p>虽然我不知道 AI 何时会产生真正的智能,但有一点我敢肯定,<strong>AI 一定会越来越小,越来越快</strong>。</p><p>关于计算机性能有个摩尔定律,当价格不变时,集成电路上可容纳的晶体管数目,约每隔 18 个月便会增加一倍,性能也将提升一倍。1946 年,第一代电子管计算机占地 150 平方米,重 30 吨,每秒 5000 次运算。到了2023年,最新款的苹果手机 15 Pro,6.1 英寸的屏幕,重 187 克,主频达到 3.78 GHz。两者相比,后者重量降低了 6 个数量级,速度却提升了 6 个数量级。</p><p>类似的事情大概率也会发生在 AI 身上。为了产生类比人类的“智能“,ChatGPT 每次训练要消耗 90 多万度电,相当于 1200 个中国人一年的生活用电量。而人类大脑的运行功率只有区区 20 瓦。单就能耗这一点,就有巨大的提升空间。</p><p>就 10 年而言,我相信程序员这一职业不会产生本质上的变化,但<strong>程序员的电脑上一定会出现各式各样的 Copilot</strong>,有协助开发的,有协助测试的,甚至有协助开会的。再往后看,当 AI 遇上元宇宙、脑机接口,<strong>程序员可能就彻底成为一个数字化职业</strong>。</p><p><img src="meta.png" alt></p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>面对飞速发展的 AI ,人类将何去何从?是像对待克隆人技术一样明令禁止,还是像对待核武器一样通过《不扩散核武器条约》进行限制,抑或是像《三体》中的降临派一样全面迎接 AI 时代的到来?且看十年之后。</p>]]></content:encoded>
<comments>http://emacoo.cn/AI/gpt-forecast/#disqus_thread</comments>
</item>
<item>
<title>故障模型哪家强?PDR 模型来帮忙</title>
<link>http://emacoo.cn/arch/pdr/</link>
<guid>http://emacoo.cn/arch/pdr/</guid>
<pubDate>Sun, 17 Apr 2022 13:00:00 GMT</pubDate>
<description>
<blockquote>
<p>搞安全的同学都知道,有一个非常著名的网络安全模型叫 <a href="https://baike.baidu.com/item/PDR/17563449" target="_blank" rel="noopener">PDR 模型</a>,提出者是
</description>
<content:encoded><![CDATA[<blockquote><p>搞安全的同学都知道,有一个非常著名的网络安全模型叫 <a href="https://baike.baidu.com/item/PDR/17563449" target="_blank" rel="noopener">PDR 模型</a>,提出者是美国国际互联网安全系统公司(ISS),其核心论断是网络安全是一个时间问题,对应的公式为 <code>Et = Dt + Rt - Pt</code>,其中:</p><ul><li><strong>Et</strong> (Exposure) 暴露时间,系统暴露在攻击下的时间;</li><li><strong>Pt</strong> (Prevent) 防御时间,系统扛住外部攻击的时间,或者说攻击者成功渗透的整个时间;</li><li><strong>Dt</strong> (Detect) 检测时间,安全检测系统发现攻击所需要的时间;</li><li><strong>Rt</strong> (Response) 响应时间,发现攻击到攻击路径被切断,攻击被中止的整个时间。</li></ul><p>PDR 模型直观、易懂,为安全防护工作提供了比较实用的指导框架。在系统可能出现的各类故障中,安全只是其中一种,既然 PDR 模型能指导解决安全问题,那么 PDR 模型是否也能指导解决其他故障呢?我认为是肯定的。</p></blockquote><h1 id="1-什么是-PDR-故障模型?"><a href="#1-什么是-PDR-故障模型?" class="headerlink" title="1 什么是 PDR 故障模型?"></a>1 什么是 PDR 故障模型?</h1><p>对照 PDR 模型,先来看一下故障的生命周期,</p><p><img src="PDR.png" alt></p><p>从上图可以非常直观的看出,为了缩短故障时间(Failure Time),我们要想办法尽可能的缩短检测时间(Detect Time)和响应时间(Response Time),同时延长防御时间(Prevent Time)。缩短检测时间对应提升监控、告警能力,缩短响应时间对应提升故障修复、CI/CD能力,延长防御时间对应提升系统的容错能力或者说健壮性。这里比较有意思的一点是关于防御时间,只要我们能把防御时间延长到足够长(超过检测时间和响应时间之和),那么故障就没有机会造成实际影响,也就等同于“消灭”了故障。</p><h1 id="2-防火优于灭火"><a href="#2-防火优于灭火" class="headerlink" title="2 防火优于灭火"></a>2 防火优于灭火</h1><blockquote><p>魏文侯曰:‘子昆弟三人其孰最善为医?’扁鹊曰:‘长兄最善,中兄次之,扁鹊最为下。’</p><p>—— 《鶡冠子·卷下·世贤第十六》</p></blockquote><p>扁鹊三兄弟中,扁鹊 Rt 能力强,二哥 Dt 能力强,大哥 Pt 能力强,但此 Pt 非彼 Pt。在上述 PDR 模型中,Pt 是指故障发生之后的防御能力,而大哥的 Pt 能力是指故障(疾病)发生之前的防御能力,也即防患未然的能力。</p><p>受限于各方面因素,绝大多数情况下,故障防御时间是小于检测时间和响应时间之和的,因此,一旦系统出现故障,就难免会造成一些实际影响。那么有没有办法避免这类影响呢?有的,向扁鹊大哥学习,防火优于灭火。如何做到防患未然呢?以史为鉴,可以知兴替,也即故障复盘。</p><p>故障复盘是一件极其重要的事情,它是如此重要以至于大多数人都低估了其重要性。小到个人,大到公司,从大大小小各类故障中总结经验教训,学到很多书本上学不到的知识,从而获得最大程度的提升,这也是成长性团队的重要特征。网上关于故障复盘的资料很多,这里我只想强调三点,</p><p>第一,故障复盘越早做效果越好(当然是在故障被妥善处理之后)。注意,处理故障过程中,应尽可能保留故障现场,同时做好过程数据备份,以便事后进行复盘。</p><p>第二,在整个故障复盘过程中,应秉持对事不对人的原则,一切从事实出发,用事实说话,这样才能找到真正的根本原因,并据此提出行之有效的改进措施。</p><p>第三,每项改进措施应指定唯一的责任人,并区分优先级,对于高优先级的改进措施,应明确闭环时间(比如 1 个月)。</p><p>最后,推荐一篇陈皓老师的专栏(文末参考资料第 2 篇),同时我在附录里贴了一份故障复盘报告模板,希望大家在平时工作中能够用到。</p><h1 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h1><p>在今天这篇文章里,我首先提出了一个专门用于故障处理的 PDR 模型,然后给出了一些故障复盘的原则,希望能够对你有所帮助。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><p><em>故障复盘报告模板</em></p><p><img src="template.jpeg" alt></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://time.geekbang.org/column/article/1059" target="_blank" rel="noopener">左耳听风-故障处理最佳实践:应对故障</a></li><li><a href="https://time.geekbang.org/column/article/1064" target="_blank" rel="noopener">左耳听风-故障处理最佳实践:故障改进</a></li><li><a href="https://www.huxiu.com/article/242040.html" target="_blank" rel="noopener">余晟以为-浅谈“黑匣子思维”</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/pdr/#disqus_thread</comments>
</item>
<item>
<title>不仅仅是一把瑞士军刀 —— Apifox的野望和不足</title>
<link>http://emacoo.cn/arch/apifox-vision/</link>
<guid>http://emacoo.cn/arch/apifox-vision/</guid>
<pubDate>Sat, 12 Mar 2022 16:00:00 GMT</pubDate>
<description>
<p>声明:本文内容不涉及任何 Apifox 的功能介绍,一来网上这方面的文章已经汗牛充栋,二来 Apifox 本身的用户体验做的非常好,对于开发者而言学习成本基本为零。</p>
<blockquote>
<p>阮一峰:不管你是前端开发还是后端开发,只要项目是服务架构,它可能会大
</description>
<content:encoded><![CDATA[<p>声明:本文内容不涉及任何 Apifox 的功能介绍,一来网上这方面的文章已经汗牛充栋,二来 Apifox 本身的用户体验做的非常好,对于开发者而言学习成本基本为零。</p><blockquote><p>阮一峰:不管你是前端开发还是后端开发,只要项目是服务架构,它可能会大大提升你的开发效率。</p><p>虫师:我们很难把它描述为一款接口管理工具或接口自动化测试工具,它增强了团队协作能力,这对一个研发团队而言很重要。</p><p>池建强:Apifox,这是一代更比一代强。</p></blockquote><h2 id="什么是Apifox?"><a href="#什么是Apifox?" class="headerlink" title="什么是Apifox?"></a>什么是Apifox?</h2><p>看了一众大咖们对 <a href="https://www.apifox.cn/" target="_blank" rel="noopener">Apifox</a> 赞不绝口,你可能会好奇 Apifox 究竟是何方神圣?根据<a href="https://www.apifox.cn/help/app/introduce/" target="_blank" rel="noopener">官方定义</a>,Apifox 是 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台,定位 Postman + Swagger + Mock + JMeter。如果你也曾使用过 Apifox,相信你会深表赞同。</p><p><img src="apifox-platform.png" alt></p><p>那么问题来了,在盛行小而美的 API 工具的当下,为什么会横空出世一个“瑞士军刀”般存在的 Apifox?答案就在 Apifox 的宗旨里面:节省研发团队的每一分钟。</p><p>在 Apifox 之前,为了达成对 API 语义的理解和实现上的一致性,前端、后端、测试使出十八般武艺,定义 API 用 Swagger,生成文档用 YAPI,前端自测用 Mock,接口测试用 Postman,性能测试用 JMeter,各类配置、数据、链接满天飞,重要的事情说三遍啊说三遍。有了 Apifox 之后,前端、后端、测试之间原本去中心化的 P2P 通讯方式变成以 Apifox 为中心的星型通讯方式,通讯对象从原本充满不确定性的人,变成稳定可靠的平台,各类配置、数据、链接也有了统一管理的地方,团队通讯成本和 API 管理成本大幅降低。</p><p><img src="apifox-purpose.png" alt></p><h2 id="Apifox的野望"><a href="#Apifox的野望" class="headerlink" title="Apifox的野望"></a>Apifox的野望</h2><p>如果你认为一体化协作平台就是 Apifox 的一切,那你可能低估了 Apifox 的野心。</p><p>先来看下 Apifox 的收费模式,</p><p><img src="apifox-price.png" alt></p><p>是的,你没有看错,免费版即享“无任何限制”,不限团队人数、不限功能、不限项目数、不限接口数,如此奢华的免费套餐,放眼全网也很难找到第二家(SaaS 平台)。</p><p>再来看下<a href="https://www.apifox.cn/help/app/changelog/" target="_blank" rel="noopener">更新日志</a>,留意以下更新:</p><ol><li>[2021-03-14] 1.2.0 新增【在线分享接口文档】功能。</li><li>[2021-10-29] 1.4.10 上线 API Hub功能。1)通过API Hub查找/发现他人公开的 API 项目。2)可将项目发布到API Hub(设置为公开项目即可),允许任何人通过API Hub访问、克隆该项目。</li><li>[2022-01-11] 1.4.17 公开项目支持通过 web 访问、运行。</li></ol><p>看懂了吗?<a href="https://www.apifox.cn/apihub/" target="_blank" rel="noopener">API Hub</a> 才是 Apifox 真正的野望,打造开放 API 共享平台,连接各类企业级 API,加速企业商业创新,成为企业之间的“交友”平台。有了 API Hub,企业之间谈合作,见面第一句话就是:PPT is cheap, show me the API!</p><h2 id="Apifox的不足"><a href="#Apifox的不足" class="headerlink" title="Apifox的不足"></a>Apifox的不足</h2><p>要配得上如此宏大的野心,在我看来,Apifox 无论是架构上还是产品功能上都还有很长的路要走。</p><p>从架构上来看,首先要做的是提升项目中模型的地位。创建完一个新项目,首先应该定义模型,然后才是接口。我们知道,模型是一个软件的骨架,是一个系统的核心。接口是系统外在能力的呈现,模型是系统内在逻辑的载体。一旦脱离了模型,接口就是无源之水,无本之木。</p><p>其次,作为接口的诞生地,Apifox 不妨制定或者倡导一些好的 API 设计规约,像 <a href="https://google.aip.dev/general" target="_blank" rel="noopener">Google AIP (API Improvements Proposal)</a>,<a href="https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design" target="_blank" rel="noopener">Microsoft RESTFul API Design</a>等。</p><p>从产品功能来看,不要局限于 Postman + Swagger + Mock + JMeter 这句 Slogan,以用户为中心,添加诸如一键生成单元测试代码、自动化测试源码编辑模式等实用功能,解决更多实际 API 开发过程中的痛点。</p><p>除此之外,官方资料中似乎没有看到大规模团队协作的案例,无论是SaaS版本还是私有化部署版本,平台所能支持的团队体量大小暂时未知。</p><h2 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h2><p>借微服务之东风,Apifox 自 2020 年 12 月 28 日推出 1.0 版本以来,以其独特的集成优势、优秀的用户体验,在国内IT界一时风光无两。不过在国外,似乎知者寥寥,stackoverflow 上甚至查无此人。随着今年 2 月份 2.0 英文版的推出,相信很快会吸引众多国外开发者的目光,祝愿 Apifox 走出国门,走向世界,早日成为国产软件之光!</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.apifox.cn/help/#_20-%E5%88%86%E9%92%9F%E5%AD%A6%E4%BC%9A-apifox-%F0%9F%91%8D" target="_blank" rel="noopener">20 分钟学会 Apifox</a></li><li><a href="https://www.apifox.cn/help/app/introduce/" target="_blank" rel="noopener">Apifox 介绍</a></li><li><a href="https://www.infoq.com/articles/API-Design-Joshua-Bloch/" target="_blank" rel="noopener">Joshua Bloch: Bumper-Sticker API Design</a></li><li><a href="https://mp.weixin.qq.com/s/qWrSyzJ54YEw8sLCxAEKlA" target="_blank" rel="noopener">深度 | API 设计最佳实践的思考</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/apifox-vision/#disqus_thread</comments>
</item>
<item>
<title>告别2021,清零2022</title>
<link>http://emacoo.cn/notes/2021-fin/</link>
<guid>http://emacoo.cn/notes/2021-fin/</guid>
<pubDate>Fri, 24 Dec 2021 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>所谓清零思维,就是说职场人每过两年,都要对自己做一次清零,以对自己在市场中的“估值”,有一个清醒的认识。</p>
<p>– 老K</p>
</blockquote>
<h1 id="System-in-read"><a href="#System-
</description>
<content:encoded><![CDATA[<blockquote><p>所谓清零思维,就是说职场人每过两年,都要对自己做一次清零,以对自己在市场中的“估值”,有一个清醒的认识。</p><p>– 老K</p></blockquote><h1 id="System-in-read"><a href="#System-in-read" class="headerlink" title="System.in.read()"></a>System.in.read()</h1><h2 id="Books"><a href="#Books" class="headerlink" title="Books"></a>Books</h2><ul><li><a href="http://e.dangdang.com/products/1901208652.html" target="_blank" rel="noopener">《银行数字化转型》- 付晓岩</a></li></ul><h2 id="Columns"><a href="#Columns" class="headerlink" title="Columns"></a>Columns</h2><ul><li><a href="https://time.geekbang.org/column/intro/313?tab=catalog" target="_blank" rel="noopener"><软件设计之美> - 郑晔</a></li><li><a href="https://time.geekbang.org/column/intro/113?tab=catalog" target="_blank" rel="noopener"><技术管理实战 36 讲> - 刘建国</a></li><li><a href="https://time.geekbang.org/column/intro/100017301?tab=catalog" target="_blank" rel="noopener"><数据结构与算法之美> - 王争</a></li><li><a href="https://time.geekbang.org/column/intro/100076501?tab=catalog" target="_blank" rel="noopener"><说透数字化转型> - 付晓岩</a></li><li><a href="https://time.geekbang.org/column/intro/100012001?tab=catalog" target="_blank" rel="noopener"><邱岳的产品实战> - 邱岳</a></li></ul><h1 id="System-out-print"><a href="#System-out-print" class="headerlink" title="System.out.print()"></a>System.out.print()</h1><h2 id="PPT"><a href="#PPT" class="headerlink" title="PPT"></a>PPT</h2><ul><li><a href="https://slides.com/emacooshen/confucius" target="_blank" rel="noopener">穿越时空的对话:《论语》中的管理智慧 (一)</a></li><li><a href="https://slides.com/emacooshen/xxl-job" target="_blank" rel="noopener">XXL-JOB 原理浅析</a></li><li><a href="https://slides.com/emacooshen/lifelong-learning" target="_blank" rel="noopener">穿越时空的对话:像孔子一样终身学习</a></li><li><a href="https://slides.com/emacooshen/x10" target="_blank" rel="noopener">10x 程序员工作法极简笔记</a></li><li><a href="https://docs.google.com/presentation/d/1Kgryi2z65PKQdZ7LanZHFPApUaWcFSLyx_X0v4dQmVU/edit?usp=sharing" target="_blank" rel="noopener">如何做好组织协同</a></li></ul><h2 id="Blog"><a href="#Blog" class="headerlink" title="Blog"></a>Blog</h2><p>(惭愧)</p><h2 id="Run"><a href="#Run" class="headerlink" title="Run"></a>Run</h2><p><img src="2021-Run.jpeg" alt></p>]]></content:encoded>
<comments>http://emacoo.cn/notes/2021-fin/#disqus_thread</comments>
</item>
<item>
<title>代码评审赋魅</title>
<link>http://emacoo.cn/arch/the-beauty-of-code-review/</link>
<guid>http://emacoo.cn/arch/the-beauty-of-code-review/</guid>
<pubDate>Sat, 19 Sep 2020 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p><img src="dead-delta.png" alt></p>
<p>先来看一个令无数技术Leader闻风丧胆的项目“死亡”三角,业务压力引发代码质量下降,代码质量下降引发开发效率下降,开发效率下降又加重了业务压力,最终导致业务压力山大,乃至
</description>
<content:encoded><![CDATA[<blockquote><p><img src="dead-delta.png" alt></p><p>先来看一个令无数技术Leader闻风丧胆的项目“死亡”三角,业务压力引发代码质量下降,代码质量下降引发开发效率下降,开发效率下降又加重了业务压力,最终导致业务压力山大,乃至项目烂尾。如何破解?方法有很多,像精简业务需求、增加开发人手、升级技术架构等,很多时候需要多管齐下,但凡打掉这个“死亡”三角中的任何一角,就能终止这个恶性循环,甚至逆转为良性循环。</p><p>代码评审(Code Revew,简称CR)的首要打击目标显然是“烂代码”。避免“烂代码”的最好时机是写代码的时候,其次是代码评审的时候。IBM 的 Orbit 项目有 50 万行代码,使用了 11 级的代码检查(其中包含代码评审),结果是项目提前交付,并且只有通常预期错误的 1%。一份对 AT&T 的一个 200 多人组织的研究报告显示,在引入代码评审后,生产率提高了 14%,缺陷减少了 90%。那到底什么是代码评审?如何进行代码评审?继续往下看。</p></blockquote><h2 id="1-CR-祛魅"><a href="#1-CR-祛魅" class="headerlink" title="1 CR 祛魅"></a>1 CR 祛魅</h2><blockquote><p>我个人认为代码有这几种级别:1)可编译,2)可运行,3)可测试,4)可读,5)可维护,6)可重用。通过自动化测试的代码只能达到第3)级,而通过CODE REVIEW的代码少会在第4)级甚至更高。</p><p>—— 陈皓</p></blockquote><p>下面 8 条有关 CR 的阐述,你觉得哪些是正确的?</p><ol><li>搞形式主义,存粹是浪费时间</li><li>CR 是保证程序正确性的手段</li><li>CR 是保证代码规范的手段</li><li>CR 是 Leader 的事,跟我没关系</li><li>我只看指给我的 CR,其他 CR 跟我没关系</li><li>没有时间 CR,直接 Merge</li><li>CR 必须一行不落从头看到尾</li><li>CR 必须一次完成</li></ol><p>———————————————————————————————— 请仔细思考 60 秒</p><p>3…2…1…时间到,你的答案是几条?很抱歉,在我看来,没有一条是正确的。1、4、5、6 是送分题,显然都是错误的。7 是眉毛胡子一把抓,CR 就像读书,不是所有的书都适合精度,也不是所有的代码都需要评审。8 是任务心态,为了 CR 而 CR,CR 的目的不是完成 CR,而在于提升代码质量,你写代码时也不会一次完成所有功能。比较有争议的是 2 和 3,诚然,正确性和代码规范都是 CR 要关注的方面,但这并不意味着 CR 要保证正确性和代码规范(CR 也没法保证),保证正确性的主要手段是测试(单元测试,集成测试,契约测试,功能测试,自动化测试等),而保证代码规范主要依靠代码规范检查工具(像常用的 checkstyle 和 PMD)。</p><p><img src="guo.png" alt></p><p>CR 到底是什么?依我所见,CR 本质上是一种讨论,一种严肃的、专业的、异步的以文字形式呈现的讨论,随意性和情绪化是 CR 的大忌。什么叫随意性?未经审视的评论。什么叫情绪化?因时而异,因人而异。高水平的 CR 首先要忘掉自己。</p><p><img src="nature-of-code-review.png" alt></p><h2 id="2-知:CR-的三重境界"><a href="#2-知:CR-的三重境界" class="headerlink" title="2 知:CR 的三重境界"></a>2 知:CR 的三重境界</h2><p>技术水平决定了 CR 的下限,认知高度决定了 CR 的上限。所以说 CR 水平高不高,最终还是看认知水平。认识 CR 有三重境界,分别是执行层、团队层和文化层。</p><h3 id="2-1-执行层:昨夜西风凋碧树,独上高楼,望尽天涯路"><a href="#2-1-执行层:昨夜西风凋碧树,独上高楼,望尽天涯路" class="headerlink" title="2.1 执行层:昨夜西风凋碧树,独上高楼,望尽天涯路"></a>2.1 执行层:昨夜西风凋碧树,独上高楼,望尽天涯路</h3><p>第一层为执行层,顾名思义就是通过如何做来认识 CR。以下列举 CR 时需重点关注的六个方面,并辅以相应的例子便于理解。</p><p>1)关注<strong>代码规范</strong>。命名是第一位的,一个令人费解的命名背后往往隐藏着一个设计纰漏。其他诸如空白字符、换行、注释等问题,也会影响代码的可读性和可理解性。</p><p><img src="example-naming.png" alt></p><p><img src="example-white-characters.png" alt></p><p>2)避免<strong>重复代码</strong>。编程法则第一条,Don’t repeat yourself. 重复代码是万恶之首,重复代码人人得而诛之!</p><p><img src="example-dry.png" alt></p><p>3)降低<strong>圈复杂度</strong>。什么是圈复杂度?简单来说就是代码中 if/case/for/while 出现的次数。圈复杂度越高,BUG 率越高。如果一个方法的圈复杂度达到 3 或者更高,那么 CR 时就要多看两眼。</p><p>4)关注<strong>性能问题</strong>。性能问题虽然不常见,可一旦暴雷往往就是大问题。CR 时看到循环,记得多留一个心眼。</p><p><img src="example-perf.png" alt></p><p>5)关注<strong>分布式事务</strong>。涉及远程服务调用,或者跨库更新的场景,都应考虑是否存在分布式事务问题,以及适用何种处理方式,是依赖框架保证强一致性,还是记录异常数据保证最终一致性,抑或是直接忽略?</p><p>6)关注<strong>架构设计</strong>。代码有代码规范,架构有架构规范。面对一个新功能的 MR(Merge Request),除了检查架构规范,还应推敲其架构设计,比如是否符合整洁架构三原则,无依赖环原则,稳定依赖原则,稳定抽象原则。</p><p><img src="example-arch-convention.png" alt></p><p><img src="example-arch-design.png" alt></p><p>除了线上 CR,还有一种特殊的线下 CR 方法,就是跳过 MR,直接拉取代码,进行整体 CR,将评审意见在代码中标记为 <code>TODO</code> 或者 <code>FIXME</code>,然后 @ 相关开发进行改进。这样做最大的好处,就是避免受单个 MR 的影响,掉入只见树木不见森林的陷阱。</p><p><img src="example-offline.png" alt></p><h3 id="2-2-团队层:衣带渐宽终不悔,为伊消得人憔悴"><a href="#2-2-团队层:衣带渐宽终不悔,为伊消得人憔悴" class="headerlink" title="2.2 团队层:衣带渐宽终不悔,为伊消得人憔悴"></a>2.2 团队层:衣带渐宽终不悔,为伊消得人憔悴</h3><p>接下来再看第二层,如何从团队视角认识 CR。前面说了,CR 本质上是一种讨论,培根说过「读书使人完整,讨论使人完备」,从个人到团队,CR 分别意味着什么?</p><ul><li><p>提升<strong>自我觉察</strong>能力:这是从个人视角来看,当你知道你写的代码会有另一双眼睛来审阅,那你写代码时就会保持一份警觉,放弃天知、地知、我知、你不知的幻想,认认真真写好每一行代码。</p></li><li><p>建立良好<strong>开发节奏</strong>:这是从团队视角来看,CR 是团队的同步器,每个人既是自己 MR 的作者,又是别人 MR 的评审者,从 MR 到 CR,再从 CR 到 MR,构成了每个工作日最动听的乐章。</p></li><li><p>高频次的<strong>团队活动</strong>:这也是从团队视角来看,CR 既然是讨论,那么就不仅仅是一个人的事,而是一种团队活动,一种高频次、高质量、低成本的极具性价比的团队活动。</p></li></ul><h3 id="2-3-文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处"><a href="#2-3-文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处" class="headerlink" title="2.3 文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处"></a>2.3 文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处</h3><p>最后是文化层,CR 既是传帮带文化的重要组成,又是工程师文化的日常体现。</p><ul><li><p><strong>传帮带文化</strong>的重要组成:资深工程师 CR 初级工程师的代码,可以给予高频次、高质量的指导;初级工程师 CR 资深工程师的代码,可以欣赏、学习高手如何把玩代码,取其精华去其糟粕。</p></li><li><p><strong>工程师文化</strong>的日常体现:协作、高效、进取、影响力,这些在各大互联网公司的工程师文化中频频出现的关键词,无一不与 CR 紧密相连。不夸张的说,工程师文化香不香,就看 CR 做的好不好。</p></li></ul><h2 id="3-行:CR-高效之法"><a href="#3-行:CR-高效之法" class="headerlink" title="3 行:CR 高效之法"></a>3 行:CR 高效之法</h2><p>认识完 CR,我们再来探讨一下如何高效的进行 CR。在我看来,高效 CR 首先有赖于以下几个客观条件和主观条件。</p><p>客观上来看,<strong>和谐的工程师文化</strong>和<strong>清晰的代码规范</strong>是高效 CR 的两块基石。所谓和谐的工程师文化,就是说团队对代码秉持开放的心态,不藏着掖着,以写好代码为荣,以写坏代码为耻,持续关注代码质量。而清晰的代码规范,一方面提高了代码的可读性,另一方面也统一了编码风格,极大的减少了不同代码风格对评审者注意力的干扰。</p><p>主观上来看,对评审者而言,第一要端正态度,<strong>保持谦卑的心态</strong>,人非圣贤孰能无过,择其善者而从之,其不善者而改之。第二要谨记<strong>评审的对象是代码,而不是人</strong>,你写下的每一条评审意见都应基于客观事实和数据,做到有理有据,不带个人情绪。</p><p>基于我多年 CR 的实操经验,结合<a href="https://github.com/google/eng-practices/blob/master/review/index.md" target="_blank" rel="noopener">Google Code Review Developer Guide</a>,我整理了一些高效 CR 的最佳实践,供你参考:</p><ul><li>依据个人偏好每天固定几个时间段专门用于 CR,我的习惯是出门前和下班前。CR 耗费的脑力丝毫不亚于编码,甚至更高,CR 过程中需要高度集中注意力。清醒的头脑和无干扰的环境,是提出高质量的评审意见的秘诀。</li><li>除了固定时间段,任务切换期间也是 CR 的好时机。</li><li>每次 CR 尽量控制在 15~30 分钟以内,超过 30 分钟应休息一会。</li><li>收到 MR 之后,先判断一下 MR 的性质,如果是 Bug Fix 类型的 MR,应尽快评审,如果是新功能 MR,则可以等待下一个 CR 窗口。</li><li>从收到 MR 到 CR 结束,最长不要超过 1 个工作日。</li><li>开始 CR 之前先要搞清楚 MR 要解决的问题背景。</li><li>CR 就像读书,先看目录(改动的文件列表),再精读重点章节(包含核心业务逻辑的代码),最后扫读剩余章节。</li><li>如果改动的文件数量较多,可以打开 IDE,切换到源分支,方便在 CR 过程中随时打开相关代码进行阅读。</li><li>评审核心代码时,如果发现严重问题,应立刻终止 CR,找 MR 提交者当面讨论。</li><li>如果 MR 提交者对评审意见提出异议,评审者应找提交者当面讨论,避免在评论区互踢皮球。</li><li>合并代码之前应确保所有评审意见都被妥善处理。</li><li>记得点赞。CR 不是只能提意见,看到优雅的代码不要吝啬你的表扬。</li></ul><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>2019 年 Stack Overflow 的一份<a href="https://insights.stackoverflow.com/survey/2019" target="_blank" rel="noopener">调查报告</a>显示,超过 7 成的程序员会把 CR 当做日常工作的一部分,近 1/3 的程序员每周在 CR 上花费 2~3 个小时,还有 1/3 的程序员每周花费 4~5 个小时。心里默默算一下,你是在拖后腿还是领路者?如果你还没做过 CR,那么赶紧行动起来;如果你已经在 CR,很好,请继续保持。一花一世界,一叶一菩提,码中自有乾坤。CR,走起!</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="https://slides.com/emacooshen/codereview2" target="_blank" rel="noopener">如何完成一次高质量的CODE REVIEW?(2020版)</a></li><li><a href="https://coolshell.cn/articles/1302.html" target="_blank" rel="noopener">陈皓:CODE REVIEW中的几个提示</a></li><li><a href="https://coolshell.cn/articles/11432.html" target="_blank" rel="noopener">陈皓:从CODE REVIEW 谈如何做技术</a></li><li><a href="https://www.techug.com/post/effective-code-reviews.html" target="_blank" rel="noopener">如何做有效的代码审查?我有这些建议</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/the-beauty-of-code-review/#disqus_thread</comments>
</item>
<item>
<title>【JDK 11】关于 Java 模块系统,看这一篇就够了</title>
<link>http://emacoo.cn/coding/java-module-system/</link>
<guid>http://emacoo.cn/coding/java-module-system/</guid>
<pubDate>Tue, 30 Jun 2020 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>继 2014 年 3 月 Java 8 发布之后,时隔 4 年,2018 年 9 月,Java 11 如期发布,其间间隔了 Java 9 和 Java 10 两个非LTS(Long Term Support)版本。作为最新的LTS版本,相比 Jav
</description>
<content:encoded><![CDATA[<blockquote><p>继 2014 年 3 月 Java 8 发布之后,时隔 4 年,2018 年 9 月,Java 11 如期发布,其间间隔了 Java 9 和 Java 10 两个非LTS(Long Term Support)版本。作为最新的LTS版本,相比 Java 8,Java 11 包含了模块系统、改用 G1 作为默认 GC 算法、反应式流 Flow、新版 HttpClient 等诸多特性。作为 JDK 11 升级系列的第一篇,本文将介绍此次升级最重要的特性——模块系统。</p></blockquote><h2 id="1-模块系统简介"><a href="#1-模块系统简介" class="headerlink" title="1 模块系统简介"></a>1 模块系统简介</h2><p>如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。模块系统,项目代号 <a href="http://openjdk.java.net/projects/jigsaw/" target="_blank" rel="noopener">Jigsaw</a>,最早于 2008 年 8 月提出(比 Martin Fowler <a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">提出</a>微服务还早 6 年),2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。</p><p>那么什么是模块系统?官方的<a href="https://www.oracle.com/corporate/features/understanding-java-9-modules.html" target="_blank" rel="noopener">定义</a>是<code>A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.</code>如<em>图-1</em>所示,模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 <code>module-info.class</code> 文件,也即 <code>module descriptor</code>。 <code>module descriptor</code> 包含以下信息:</p><ul><li>模块名称</li><li>依赖哪些模块</li><li>导出模块内的哪些包(允许直接 <code>import</code> 使用)</li><li>开放模块内的哪些包(允许通过 Java 反射访问)</li><li>提供哪些服务</li><li>依赖哪些服务</li></ul><p><img src="jigsaw2.png" alt></p><p><em>图-1: Java 9 Module</em></p><p>也就是说,任意一个 jar 文件,只要加上一个合法的 <code>module descriptor</code>,就可以升级为一个模块。这个看似微小的改变,到底可以带来哪些好处?在我看来,至少带来四方面的好处。</p><p>第一,原生的依赖管理。有了模块系统,Java 可以根据 <code>module descriptor</code> 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。同时,由于模块系统不允许不同模块导出相同的包(即 <code>split package</code>,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。</p><p>第二,精简 JRE。引入模块系统之后,JDK 自身被划分为 94 个模块(参见<em>图-2</em>)。通过 Java 9 新增的 <code>jlink</code> 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小 JRE 大小。得益于此,JRE 11 的大小仅为 JRE 8 的 53%,从 218.4 MB缩减为 116.3 MB,JRE 中广为诟病的巨型 jar 文件 <code>rt.jar</code> 也被移除。更小的 JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。</p><p><img src="jigsaw3.png" alt></p><p><em>图-2: The Modular JDK</em></p><p>第三,更好的兼容性。自打 Java 出生以来,就只有 4 种包可见性,这让 Java 对面向对象的三大特征之一封装的支持大打折扣,类库维护者对此叫苦不迭,只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用,擅自使用后果自负云云。Java 9 之后,利用 <code>module descriptor</code> 中的 <code>exports</code> 关键词,模块维护者就精准控制哪些类可以对外开放使用,哪些类只能内部使用,换句话说就是不再依赖文档,而是由编译器来保证。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。</p><p><img src="public.png" alt></p><p><em>图-3: Java Accessibility</em></p><p>第四,提升 Java 语言开发效率。Java 9 之后,Java 像开挂了一般,一改原先一延再延的风格,严格遵循每半年一个大版本的发布策略,从 2017 年 9 月到 2020 年 3 月,从 Java 9 到 Java 14,三年时间相继发布了 6 个版本,无一延期,参见<em>图-4</em>。这无疑跟模块系统的引入有莫大关系。前文提到,Java 9 之后,JDK 被拆分为 94 个模块,每个模块有清晰的边界(<code>module descriptor</code>)和独立的单元测试,对于每个 Java 语言的开发者而言,每个人只需要关注其所负责的模块,开发效率因此大幅提升。这其中的差别,就好比单体应用架构升级到微服务架构一般,版本迭代速度不快也难。</p><p><img src="lifecycle.png" alt></p><p><em>图-4: Java SE Lifecycle</em></p><h2 id="2-基础篇"><a href="#2-基础篇" class="headerlink" title="2 基础篇"></a>2 基础篇</h2><h3 id="2-1-module-descriptor"><a href="#2-1-module-descriptor" class="headerlink" title="2.1 module descriptor"></a>2.1 module descriptor</h3><p>上面提到,模块的核心在于 <code>module descriptor</code>,对应根目录下的 <code>module-info.class</code> 文件,而这个 class 文件是由源代码根目录下的 <code>module-info.java</code> 编译生成。Java 为 <code>module-info.java</code> 设计了专用的语法,包含 <code>module</code>、 <code>requires</code>、<code>exports</code> 等多个关键词(参见<em>图-5</em>)。</p><p><img src="commands.png" alt></p><p><em>图-5: module-info.java 语法</em></p><p>语法解读:</p><ul><li><code>[open] module <module></code>: 声明一个模块,模块名称应全局唯一,不可重复。加上 <code>open</code> 关键词表示模块内的所有包都允许通过 Java 反射访问,模块声明体内不再允许使用 <code>opens</code> 语句。</li><li><code>requires [transitive] <module></code>: 声明模块依赖,一次只能声明一个依赖,如果依赖多个模块,需要多次声明。加上 <code>transitive</code> 关键词表示传递依赖,比如模块 A 依赖模块 B,模块 B 传递依赖模块 C,那么模块 A 就会自动依赖模块 C,类似于 Maven。</li><li><code>exports <package> [to <module1>[, <module2>...]]</code>: 导出模块内的包(允许直接 <code>import</code> 使用),一次导出一个包,如果需要导出多个包,需要多次声明。如果需要定向导出,可以使用 <code>to</code> 关键词,后面加上模块列表(逗号分隔)。</li><li><code>opens <package> [to <module>[, <module2>...]]</code>: 开放模块内的包(允许通过 Java 反射访问),一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放,可以使用 <code>to</code> 关键词,后面加上模块列表(逗号分隔)。</li><li><code>provides <interface | abstract class> with <class1>[, <class2> ...]</code>: 声明模块提供的 Java SPI 服务,一次可以声明多个服务实现类(逗号分隔)。</li><li><code>uses <interface | abstract class></code>: 声明模块依赖的 Java SPI 服务,加上之后模块内的代码就可以通过 <code>ServiceLoader.load(Class)</code> 一次性加载所声明的 SPI 服务的所有实现类。</li></ul><h3 id="2-2-p-amp-m-参数"><a href="#2-2-p-amp-m-参数" class="headerlink" title="2.2 -p & -m 参数"></a>2.2 -p & -m 参数</h3><p>Java 9 引入了一系列新的参数用于编译和运行模块,其中最重要的两个参数是 <code>-p</code> 和 <code>-m</code>。<code>-p</code> 参数指定模块路径,多个模块之间用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同时适用于 <code>javac</code> 命令和 <code>java</code> 命令,用法和Java 8 中的 <code>-cp</code> 非常类似。<code>-m</code> 参数指定待运行的模块主函数,输入格式为<code>模块名/主函数所在的类名</code>,仅适用于 <code>java</code> 命令。两个参数的基本用法如下:</p><ul><li><p><code>javac -p <module_path> <source></code></p></li><li><p><code>java -p <module_path> -m <module>/<main_class></code></p></li></ul><h3 id="2-3-Demo-示例"><a href="#2-3-Demo-示例" class="headerlink" title="2.3 Demo 示例"></a>2.3 Demo 示例</h3><p>为了帮助你理解 <code>module descriptor</code> 语法和新的 Java 参数,我专门设计了一个<a href="https://github.com/emac/jmods-demo" target="_blank" rel="noopener">示例工程</a>,其内包含了 5 个模块:</p><ul><li>mod1 模块: 主模块,展示了使用服务实现类的两种方式。</li><li>mod2a 模块: 分别导出和开放了一个包,并声明了两个服务实现类。</li><li>mod2b 模块: 声明了一个未公开的服务实现类。</li><li>mod3 模块: 定义 SPI 服务(<code>IEventListener</code>),并声明了一个未公开的服务实现类。</li><li>mod4 模块: 导出公共模型类。</li></ul><p><img src="demo.png" alt></p><p><em>图-6: 包含 5 个模块的示例工程</em></p><p>先来看一下主函数,方式 1 展示了直接使用 mod2 导出和开放的两个 <code>IEventListener</code> 实现类,方式 2 展示了通过 Java SPI 机制使用所有的 <code>IEventListener</code> 实现类,无视其导出/开放与否。方式 2 相比 方式 1,多了两行输出,分别来自于 mod2b 和 mod3 通过 <code>provides</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EventCenter</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ReflectiveOperationException </span>{</span><br><span class="line"> <span class="comment">// 方式1:通过exports和opens</span></span><br><span class="line"> System.out.println(<span class="string">"Demo: Direct Mode"</span>);</span><br><span class="line"> <span class="keyword">var</span> listeners = <span class="keyword">new</span> ArrayList<IEventListener>();</span><br><span class="line"> <span class="comment">// 使用导出类</span></span><br><span class="line"> listeners.add(<span class="keyword">new</span> EchoListener());</span><br><span class="line"> <span class="comment">// 使用开放类</span></span><br><span class="line"> <span class="comment">// compile error: listeners.add(new ReflectEchoListener());</span></span><br><span class="line"> listeners.add((IEventListener<String>) Class.forName(<span class="string">"mod2a.opens.ReflectEchoListener"</span>).getDeclaredConstructor().newInstance());</span><br><span class="line"> <span class="keyword">var</span> event = Events.newEvent();</span><br><span class="line"> listeners.forEach(l -> l.onEvent(event));</span><br><span class="line"> System.out.println();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 方式2:通过SPI</span></span><br><span class="line"> System.out.println(<span class="string">"Demo: SPI Mode"</span>);</span><br><span class="line"> <span class="comment">// 加载所有的IEventListener实现类,无视其导出/开放与否</span></span><br><span class="line"> var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());</span><br><span class="line"> <span class="comment">// compile error: listeners.add(new InternalEchoListener());</span></span><br><span class="line"> <span class="comment">// compile error: listeners.add(new SpiEchoListener());</span></span><br><span class="line"> <span class="keyword">var</span> event2 = Events.newEvent();</span><br><span class="line"> listeners2.forEach(l -> l.onEvent(event2));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><em>代码-1: mod1.EventCenter.java</em></p><p>命令行下执行<code>./build_mods.sh</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></pre></td><td class="code"><pre><span class="line">Demo: Direct Mode</span><br><span class="line">[echo] Event received: <span class="number">68</span>eb4671-c057-<span class="number">4</span>bc2-<span class="number">9653</span>-c31f5e3f72d2</span><br><span class="line">[reflect echo] Event received: <span class="number">68</span>eb4671-c057-<span class="number">4</span>bc2-<span class="number">9653</span>-c31f5e3f72d2</span><br><span class="line"></span><br><span class="line">Demo: SPI Mode</span><br><span class="line">[spi echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[reflect echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[internal echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br></pre></td></tr></table></figure><p><em>代码-2: EventCenter 结果输出</em></p><h2 id="3-进阶篇"><a href="#3-进阶篇" class="headerlink" title="3 进阶篇"></a>3 进阶篇</h2><p>看到这里,相信创建和运行一个新的模块应用对你而言已经不是问题了,可问题是老的 Java 8 应用怎么办?别着急,我们先来了解两个高级概念,未命名模块(unnamed module)和自动模块(automatic module)。</p><p><img src="cp.png" alt></p><p><em>图-7: 未命名模块 vs 自动模块</em></p><p>一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。注意,自动模块也属于命名模块的范畴,其名称是模块系统基于 jar 文件名自动推导得出的,比如 com.foo.bar-1.0.0.jar 文件推导得出的自动模块名是 com.foo.bar。<em>图-7</em>列举了未命名模块和自动模块行为上的区别,除此之外,两者还有一个关键区别,分裂包规则适用于自动模块,但对未命名模块无效,也即多个未命名模块可以导出同一个包,但自动模块不允许。</p><p>未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 <code>module descriptor</code>),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。进一步的解读可以参阅<a href="http://openjdk.java.net/projects/jigsaw/spec/sotms/" target="_blank" rel="noopener">官方白皮书</a>的相关章节。</p><p>基于未命名模块和自动模块,相应的就产生了两种老版本应用的迁移策略,或者说模块化策略。</p><h3 id="3-1-Bottom-up-自底向上策略"><a href="#3-1-Bottom-up-自底向上策略" class="headerlink" title="3.1 Bottom-up 自底向上策略"></a>3.1 Bottom-up 自底向上策略</h3><p>第一种策略,叫做自底向上(bottom-up)策略,即根据 jar 包依赖关系(如果依赖关系比较复杂,可以使用 <code>jdeps</code> 工具进行分析),沿着依赖树自底向上对 jar 包进行模块化改造(在 jar 包的源代码根目录下添加合法的模块描述文件 <code>module-info.java</code>)。初始时,所有 jar 包都是非模块化的,全部置于类路径下(转为未命名模块),应用以传统方式启动。然后,开始自底向上对 jar 包进行模块化改造,改造完的 jar 包就移到模块路径下,这期间应用仍以传统方式启动。最后,等所有 jar 包都完成模块化改造,应用改为 <code>-m</code> 方式启动,这也标志着应用已经迁移为真正的 Java 9 应用。以上面的示例工程为例,</p><p><img src="bottom-up.png" alt></p><p><em>图-8: Bottom-up模块化策略</em></p><p>1) 假设初始时,所有 jar 包都是非模块化的,此时应用运行命令为:</p><p><code>java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter</code></p><p>2) 对 mod3 和 mod4 进行模块化改造。完成之后,此时 mod1, mod2a, mod2b 还是普通的 jar 文件,新的运行命令为:</p><p><code>java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter</code></p><p>对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径,这个很好理解,因为这两个 jar 包已经改造成了真正的模块。其次,多了一个额外的参数 <code>--add-modules mod3,mod4</code>,这是为什么呢?这就要谈到模块系统的模块发现机制了。</p><p>不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module),然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块(observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。那么根模块是如何确定的呢?对于运行时而言,如果应用是通过 <code>-m</code> 方式启动的,那么根模块就是 <code>-m</code> 指定的主模块;如果应用是通过传统方式启动的,那么根模块就是所有的 <code>java.*</code> 模块即 JRE(参见<em>图-2</em>)。回到前面的例子,如果不加 <code>--add-modules</code> 参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块,就会报 <code>java.lang.ClassNotFoundException</code> 异常。如你所想,<code>--add-modules</code> 参数的作用就是手动指定额外的根模块,这样应用就可以正常运行了。</p><p>3) 接着完成 mod2a、mod2b 的模块化改造,此时运行命令为:</p><p><code>java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter</code></p><p>由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到 <code>--add-modules</code> 参数里了。</p><p>4) 最后完成 mod1 的模块化改造,最终运行命令就简化为:</p><p><code>java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter</code></p><p>注意此时应用是以 <code>-m</code> 方式启动,并且指定了 mod1 为主模块(也是根模块),因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。</p><h3 id="3-2-Top-down-自上而下策略"><a href="#3-2-Top-down-自上而下策略" class="headerlink" title="3.2 Top-down 自上而下策略"></a>3.2 Top-down 自上而下策略</h3><p>自底向上策略很容易理解,实施路径也很清晰,但它有一个隐含的假设,即所有 jar 包都是可以模块化的,那如果其中有 jar 包无法进行模块化改造(比如 jar 包是一个第三方类库),怎么办?别慌,我们再来看第二种策略,叫做自上而下(top-down)策略。</p><p>它的基本思路是,根据 jar 包依赖关系,从主应用开始,沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类,一类是可以改造的,一类是无法改造的。对于第一类,我们仍然采用自底向上策略进行改造,直至主应用完成改造,对于第二类,需要从一开始就放入模块路径,即转为自动模块。这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包,这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块,并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外,其它命名模块是不允许访问未命名模块的类),这样就保证自动模块自身可以照常访问其他类。等到主应用完成模块化改造,应用的启动方式就可以改为 <code>-m</code> 方式。</p><p>还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造,那么最终改造完之后,虽然应用运行命令和之前一样还是<code>java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter</code>,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造,借由模块系统转为自动模块。</p><p><img src="top-down.png" alt></p><p><em>图-9: Top-down模块化策略</em></p><p>看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况,如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块,要么自己动手丰衣足食 Hack 一个版本。当然,你也可以试试找到这些自动模块的维护者们,让他们 PK 一下决定谁才是这个分裂包的主人。</p><h2 id="4-番外篇"><a href="#4-番外篇" class="headerlink" title="4 番外篇"></a>4 番外篇</h2><p>有关模块系统的介绍到这就基本结束了,简单回顾一下,首先我介绍了什么是模块、模块化的好处,接着给出了定义模块的语法,和编译、运行模块的命令,并辅以一个示例工程进行说明,最后详细阐述了老版本应用模块化改造的思路。现在我们再来看一些跟模块系统比较相似的框架和工具,以进一步加深你对模块系统的理解。</p><h3 id="4-1-vs-OSGi"><a href="#4-1-vs-OSGi" class="headerlink" title="4.1 vs OSGi"></a>4.1 vs OSGi</h3><p>说起模块化,尤其在 Java 界,那么肯定绕不过 OSGi 这个模块系统的鼻祖。OSGi 里的 bundle 跟模块系统里的模块非常相似,都是以 jar 文件的形式存在,每个 bundle 有自己的名称,也会定义依赖的 bundle、导出的包、发布的服务等。所不同的是,OSGi bundle 可以定义版本,还有生命周期的概念,包括 installed、resolved、uninstalled、starting、active、stopping 6 种状态,所有 bundle 都由 OSGi 容器进行管理,并且在同一个 OSGi 容器里面允许同时运行同一个 bundle 的多个版本,甚至每个 bundle 有各自独立的 classloader。以上种种特性使得 OSGi 框架变得非常重,在微服务盛行的当下,越来越被边缘化。</p><h3 id="4-2-vs-Maven"><a href="#4-2-vs-Maven" class="headerlink" title="4.2 vs Maven"></a>4.2 vs Maven</h3><p>Maven 的依赖管理和模块系统存在一些相似之处,Maven 里的 artifact 对应模块 ,都是以 jar 文件的形式存在,有名称,可以声明传递依赖。不同之处在于,Maven artifact 支持版本,但缺少包一级的信息,也没有服务的概念。如果 Java 一出生就带有模块系统,那么 Maven 的依赖管理大概率就会直接基于模块系统来设计了。</p><h3 id="4-3-vs-ArchUnit"><a href="#4-3-vs-ArchUnit" class="headerlink" title="4.3 vs ArchUnit"></a>4.3 vs ArchUnit</h3><p>ArchUnit 在包可见性方面的控制能力和模块系统相比,有过之而无不及,并且可以细化到类、方法、属性这一级。但 ArchUnit 缺少模块一级的控制,模块系统的出现正好补齐了 ArchUnit 这一方面的短板,两者相辅相成、相得益彰,以后落地架构规范也省了很多口水。</p><h2 id="5-彩蛋"><a href="#5-彩蛋" class="headerlink" title="5 彩蛋"></a>5 彩蛋</h2><p>如果你能看到这里,恭喜你已经赢了 90% 的读者。为了表扬你的耐心,免费赠送一个小彩蛋,给你一个 jar 文件,如何用最快的速度判别它是不是一个模块?它又是如何定义的?试试看 <code>jar -d -f <jar_file></code>。</p><p>有关 Java 模块系统的介绍就到这里了,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。下期再见。</p><h2 id="6-参考"><a href="#6-参考" class="headerlink" title="6 参考"></a>6 参考</h2><ul><li><a href="https://www.ibm.com/developerworks/cn/java/the-new-features-of-Java-9/index.html" target="_blank" rel="noopener">Java 9 新特性概述</a></li><li><a href="https://openjdk.java.net/jeps/261" target="_blank" rel="noopener">JEP 261: Module System</a></li><li><a href="http://tutorials.jenkov.com/java/modules.html" target="_blank" rel="noopener">Java Modules</a></li><li><a href="https://www.oracle.com/corporate/features/understanding-java-9-modules.html" target="_blank" rel="noopener">Understanding Java 9 Modules</a></li><li><a href="https://www.oracle.com/java/java9-screencasts.html" target="_blank" rel="noopener">Java 9 Expert Insights</a></li><li><a href="https://www.cnblogs.com/IcanFixIt/p/6947763.html" target="_blank" rel="noopener">Java 9 揭秘(2. 模块化系统)</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/coding/java-module-system/#disqus_thread</comments>
</item>
<item>
<title>【Spring Cloud】详解Feign常用配置</title>
<link>http://emacoo.cn/backend/spring-cloud-feign-timeout/</link>
<guid>http://emacoo.cn/backend/spring-cloud-feign-timeout/</guid>
<pubDate>Sat, 21 Mar 2020 16:00:00 GMT</pubDate>
<description>
<h2 id="1-Feign常用配置"><a href="#1-Feign常用配置" class="headerlink" title="1 Feign常用配置"></a>1 Feign常用配置</h2><p>搭载着Spring Cloud的顺风车,Feign正以席卷之势成为使
</description>
<content:encoded><![CDATA[<h2 id="1-Feign常用配置"><a href="#1-Feign常用配置" class="headerlink" title="1 Feign常用配置"></a>1 Feign常用配置</h2><p>搭载着Spring Cloud的顺风车,Feign正以席卷之势成为使用Spring架构的大大小小互联网公司发起HTTP调用的首选框架。基于接口的声明式定义、客户端负载均衡、断路器和后备方法(fallback)是Feign相对上一代HTTP调用框架(比如Spring Template,Apache HttpClient)的四大优势。</p><p>类似于Retrofit和OkHttp的关系,Feign实际上是对普通HTTP客户端的一层封装,其目的是降低集成成本、提升可靠性。Feign支持三种HTTP客户端,包括JDK自带的HttpURLConnection、Apache HttpClient和Square OkHttp,默认使用Apache HttpClient。</p><ul><li>HttpURLConnection:不支持线程池,一般不会选用。</li><li>HttpClient:相比OkHttp,HttpClient并没有明显的优势,可能是因为使用更广泛,所以被Feign选为默认实现。从5.0版本开始才支持HTTP/2。</li><li>OkHttp:开发Android应用的首选HTTP客户端,支持HTTP/2,通过设置<code>feign.okhttp.enabled=true</code>启用。</li></ul><p>Feign提供了两大类配置属性来配置上述三种HTTP客户端,<code>feign.client.*</code>和<code>feign.httpclient.*</code>,前者支持按实例进行配置(注解-1),后者全局共享一套配置,包含线程池配置,但只影响HttpClient和OkHttp,不影响HttpURLConnection,具体关系见下表。</p><blockquote><p>注解-1:所谓按实例进行配置,就是指每个FeignClient实例都可以通过<code>feign.client.<feignClientName>.*</code>来单独进行配置,注意首字母小写。而<code>feign.client.default.*</code>表示默认配置。</p></blockquote><table><thead><tr><th>HTTP客户端</th><th>连接超时时间</th><th>请求超时时间</th><th>线程存活时间</th><th>线程池最大连接数(全局)</th><th>线程池最大连接数(单个HOST)</th></tr></thead><tbody><tr><td>HttpURLConnection</td><td>feign.client.<code>[default|<feignClientName>].connect-timeout</code><br>默认值:10秒</td><td>feign.client.<code>[default|<feignClientName>].read-timeout</code><br>默认值:60秒</td><td>N/A</td><td>N/A</td><td>N/A</td></tr><tr><td>HttpClient</td><td>feign.httpclient.connection-timeout<br>默认值:2秒</td><td>默认值:-1(RequestConfig.Builder.socketTimeout)</td><td>feign.httpclient.time-to-live<br>默认值:900秒</td><td>feign.httpclient.max-connections<br>默认值:200</td><td>feign.httpclient.max-connections-per-route<br>默认值:50</td></tr><tr><td>OkHttp</td><td>feign.httpclient.connection-timeout<br>默认值:2秒</td><td>feign.client.<code>[default|<feignClientName>].read-timeout</code><br>默认值:10秒</td><td>feign.httpclient.time-to-live<br>默认值:900秒</td><td>feign.httpclient.max-connections<br>默认值:200</td><td>N/A</td></tr></tbody></table><p>从上表可以看到,Feign提供了两个连接超时配置,HttpURLConnection使用<code>feign.client.[default|<feignClientName>].connect-timeout</code>,而HttpClient和OkHttp则使用<code>feign.httpclient.connection-timeout</code>,这一点要尤其注意。</p><h2 id="2-启用Hystrix"><a href="#2-启用Hystrix" class="headerlink" title="2 启用Hystrix"></a>2 启用Hystrix</h2><p>通过设置<code>feign.hystrix.enabled=true</code>可以启用Feign的断路器支持(基于Hystrix)。跟Feign一样,Hystrix也支持按实例进行配置,详细配置属性参见<a href="https://github.com/Netflix/Hystrix/wiki/Configuration" target="_blank" rel="noopener">官方文档</a>。</p><p>由于Hystrix默认的请求超时时间为1秒,很容易触发超时异常,所以往往需要调大。调大超时时间有两种方式,</p><ul><li>第一种方式,通过<code>hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds</code>设置默认超时时间,影响所有请求。</li><li>第二种方式,如果你不想改变所有请求的超时时间,那么可以通过<code>hystrix.command.<HystrixCommandKey>.execution.isolation.thread.timeoutInMilliseconds</code>单独设置某个Hystrix Command的超时时间。那么问题来了,Feign下面,这个Hystrix Command Key到底是什么呢,是和Feign Client Name一样吗?答案是否定的。Feign下面,一个Hystrix Command对应的是Feign Client的一个方法,因此Hystrix Command Key的定义为<code><FeignClientName>#<methodName>(<arg1ClassName>,<arg2ClassName>...)</code>,注意首字母大写,详见<code>SetterFactory.Default#create()</code>方法。</li></ul><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>不管是Spring还是Spring Cloud,由于整个生态过于庞大,因此即便是官方文档,也只能勉强覆盖各个组件的大体框架,一旦深入细节就只能靠开发者自己研读源码来寻找答案。就像Linus Torvalds说的,<code>Talk is cheap. Show me the code.</code></p><p><img src="quote-talk-is-cheap-show-me-the-code-linus-torvalds.jpg" alt></p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="https://cloud.spring.io/spring-cloud-openfeign/reference/html/" target="_blank" rel="noopener">Spring Cloud OpenFeign</a></li><li><a href="https://www.cnblogs.com/wlandwl/p/feign.html" target="_blank" rel="noopener">Spring cloud Feign 深度学习与应用</a></li><li><a href="https://www.jianshu.com/p/c836a283631e" target="_blank" rel="noopener">Spring Cloud组件那么多超时设置,如何理解和运用?</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-cloud-feign-timeout/#disqus_thread</comments>
</item>
<item>
<title>MySQL Connect/J 8.0时区陷阱</title>
<link>http://emacoo.cn/devops/mysql-timezone/</link>
<guid>http://emacoo.cn/devops/mysql-timezone/</guid>
<pubDate>Tue, 17 Dec 2019 16:00:00 GMT</pubDate>
<description>
<p><img src="timezone-map.jpg" alt></p>
<p>最近公司正在升级Spring Boot版本(从1.5升级到2.1),其间踩到一个非常隐晦的MySQL时区陷阱,具体来说,就是数据库读出的历史数据的时间和实际时间差了14个小时,而新写入的数据又都
</description>
<content:encoded><![CDATA[<p><img src="timezone-map.jpg" alt></p><p>最近公司正在升级Spring Boot版本(从1.5升级到2.1),其间踩到一个非常隐晦的MySQL时区陷阱,具体来说,就是数据库读出的历史数据的时间和实际时间差了14个小时,而新写入的数据又都正常。如果你之前也是使用默认的MySQL时区配置,那么大概率会碰到这个问题,深究其背后的原因又涉及到很多技术细节,故整理出来分享给大家。</p><p>首先来看一下原因。升级到Boot 2.1之后,MySQL Connect/J版本也随之升级到8.0,会优先使用连接参数(<code>serverTimezone</code>)中指定的时区,如果没有指定,则再使用数据库配置的时区,参考下面的<a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-other-changes.html" target="_blank" rel="noopener">官宣</a>(对应的源代码是<code>com.mysql.cj.protocol.a.NativeProtocol#configureTimezone()</code>)。由于我们之前数据库连接参数没有指定时区,并且数据库配置的是默认的<code>CST</code>时区(美国中部时区,即-6:00),所以读取出来的时间出现偏差。</p><blockquote><p>Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true:</p><ul><li>The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.)</li><li>The server’s time zone is overridden by setting the Connector/J connection property <code>serverTimezone</code> (for example, <code>serverTimezone=Europe/Paris</code>).</li></ul></blockquote><p>找到原因之后,解决办法就比较直白了,</p><p>方法一:数据库的连接参数添加<code>serverTimezone=Asia/Shanghai</code>或者<code>serverTimezone=GMT%2B8</code>。Boot 1.5下不需要添加此参数,但添加了也无妨。</p><p>方法二:修改MySQL数据库的time_zone配置,改为<code>+8:00</code>(默认是<code>SYSTEM</code>)。采用此方法,则不需要修改数据库连接参数。</p><p>方法二显然更优,一次修改,终生受益。但要注意,对于升级到Boot 2.1之后新生成的那批数据,如果包含时间类型的字段并且该字段值是应用指定的而不是数据库生成的(例如<code>DEFAULT CURRENT_TIMESTAMP</code>),那么需要手动修复(加上偏差的小时数)。</p><p>两个解决办法都很简单,有同学马上会问,为什么Boot 1.5下没有这个问题?为什么Boot 2.0下读取历史数据存在14个小时的偏差,而新生成的数据又是好的?要回答这两个问题,看官宣就不够了,需要读一下MySQL Connect/J的源代码。</p><p>谜题一,为什么Boot 1.5下没有这个问题?答案隐藏在<code>com.mysql.jdbc.ResultSetImpl</code>和<code>com.mysql.jdbc.ConnectionImpl</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="comment">// 源代码:com.mysql.jdbc.ResultSetImpl</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> TimeZone <span class="title">getDefaultTimeZone</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// useLegacyDatetimeCode默认为true,因此使用connection的默认时区</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.useLegacyDatetimeCode ? <span class="keyword">this</span>.connection.getDefaultTimeZone() : <span class="keyword">this</span>.serverTimeZoneTz;</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源代码:com.mysql.jdbc.ConnectionImpl</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ConnectionImpl</span><span class="params">(String hostToConnectTo, <span class="keyword">int</span> portToConnectTo, Properties info, String databaseToConnectTo, String url)</span> <span class="keyword">throws</span> SQLException </span>{</span><br><span class="line"> <span class="comment">// connection的默认时区使用的是JVM的默认时区,一般为操作系统的时区</span></span><br><span class="line"> <span class="comment">// We store this per-connection, due to static synchronization issues in Java's built-in TimeZone class...</span></span><br><span class="line"> <span class="keyword">this</span>.defaultTimeZone = TimeUtil.getDefaultTimeZone(getCacheDefaultTimezone());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Boot 1.5下,MySQL Connect/J默认使用操作系统的时区(Asia/Shanghai,即+8:00),而忽略连接参数或者数据库指定的时区,因此不管是读数据还是写数据都是使用统一的时区,因此不存在时间偏差。</p><p>谜题二,为什么Boot 2.0下读取历史数据存在14个小时的偏差,而新生成的数据又是好的?升级到Boot 2.0之后,MySQL Connect/J改为使用数据库配置的CST时区,而历史数据是在Boot 1.5下的Asia/Shanghai时区生成的,因此读出来存在14(-6:00和+8:00之间)个小时的偏差。对于新生成的数据,由于同处在CST时区下,因此没有偏差。</p><p>解完这两个谜题,你可能还有些疑惑。那么接下来,结合数据流转的顺序,我们再来分析一下数据流转过程中时区的变化。</p><p><img src="mysql-timezone.png" alt></p><p>设定Application-1为数据生产方,Application-2为数据消费方,TZ-IN1为Application-1所处的时区,TZ-IN2为Application-1写入数据库的时区,TZ-OUT1为Application-2读出数据库的时区,TZ-OUT2为Application-2所处的时区。如前所述,TZ-IN2和TZ-OUT1由连接参数或者数据库配置决定。</p><p>整个数据流转过程,会涉及3次显式的时区转换和1次隐式的时区转换。</p><ul><li>转换①(显式):TZ-IN1转TZ-IN2,这个转换由MySQL Connect/J完成(参考<code>com.mysql.cj.ClientPreparedQueryBindings#setTimestamp()</code>,限于篇幅,此处不再展开分析)。</li><li>转换②(隐式):TZ-IN2转无时区,MySQL内部存储时间类型的字段时或者忽略时区(DateTime类型)或者使用UTC(Timestamp类型),参考MySQL官宣的时间类型部分。</li><li>转换③(显式):无时区转TZ-OUT1,将MySQL读出的无时区时间置为TZ-OUT1时区(参考<code>com.mysql.cj.result.SqlTimestampValueFactory#localCreateFromTimestamp()</code>)。</li><li>转换④(显式):TZ-OUT1转TZ-OUT2,这个转换由Application-2负责,一般在DAO层完成。</li></ul><p>仔细分析这4次时区转换,其中①、②、③都是由MySQL完成,正确性不用怀疑,但由于TZ-IN2和TZ-OUT1都是由应用指定,如果两者值不相同,那么最后结果就会出现偏差(我们踩到的就是这个坑)。至于④,那么就得靠应用来保证正确性了,一般也不会出错。说句题外话,不管是时区转换,还是其他类型的数据转换(比如字符集转换),我们可以发现,正确转换的关键在于数据接收方必须使用和数据发送方相同的格式。这看上去像是一句废话,却是解决此类问题的底层心法。</p><p>至此,这个MySQL Connect/J 8.0的时区陷阱就算被填平了,希望你从中有所收获。</p>]]></content:encoded>
<comments>http://emacoo.cn/devops/mysql-timezone/#disqus_thread</comments>
</item>
<item>
<title>从零搭建一个基于Istio的服务网格</title>
<link>http://emacoo.cn/devops/istio-tutorial/</link>
<guid>http://emacoo.cn/devops/istio-tutorial/</guid>
<pubDate>Wed, 02 May 2018 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p><a href="http://emacoo.cn/arch/service-mesh-overview/">上篇</a>文章从微服务1.0时代的三大痛点(技术门槛高,多语言支持不足和代码侵入性强)说起,由此引出服务网格的起源和演化历史。但古语有云
</description>
<content:encoded><![CDATA[<blockquote><p><a href="http://emacoo.cn/arch/service-mesh-overview/">上篇</a>文章从微服务1.0时代的三大痛点(技术门槛高,多语言支持不足和代码侵入性强)说起,由此引出服务网格的起源和演化历史。但古语有云<code>纸上得来终觉浅,绝知此事要躬行</code>,不亲自撸一遍命令,怎敢跟人提服务网格?本篇我将教大家如何在本地从零搭建一个基于<a href="https://istio.io/" target="_blank" rel="noopener">Istio</a>的服务网格,从而对服务网格有一个更直观的认识。</p></blockquote><h2 id="1-通关密码:上上下下左左右右ABAB"><a href="#1-通关密码:上上下下左左右右ABAB" class="headerlink" title="1 通关密码:上上下下左左右右ABAB"></a>1 通关密码:上上下下左左右右ABAB</h2><ul><li>原料:Mac一台,VPN账号一枚</li><li>做法:依序安装和运行<a href="https://kubernetes.io/" target="_blank" rel="noopener">Kubernetes</a>,<a href="https://github.com/kubernetes/minikube" target="_blank" rel="noopener">Minikube</a>,Istio</li></ul><p><img src="mario.png" alt></p><h2 id="2-穿墙大法:Shadowsocks"><a href="#2-穿墙大法:Shadowsocks" class="headerlink" title="2 穿墙大法:Shadowsocks"></a>2 穿墙大法:Shadowsocks</h2><p>无论是Kubernetes、Minikube还是Istio,官方提供的安装文档都非常详尽,只要英文过关,依葫芦画瓢基本上都能跑通。但如果你在国内,还得加一个必要条件,学会如何<a href="https://zh.wikipedia.org/zh-hans/%E7%AA%81%E7%A0%B4%E7%BD%91%E7%BB%9C%E5%AE%A1%E6%9F%A5" target="_blank" rel="noopener">突破网络审查</a>,俗称翻墙。</p><p>Mac下的翻墙软件我首推<a href="https://shadowsocks.org/en/index.html" target="_blank" rel="noopener">Shadowsocks</a>,同时支持Socks5代理和HTTP代理,最新版本可以从<a href="https://github.com/shadowsocks/ShadowsocksX-NG/releases" target="_blank" rel="noopener">GitHub</a>下载。</p><h2 id="3-小Boss-kubectl"><a href="#3-小Boss-kubectl" class="headerlink" title="3 小Boss: kubectl!"></a>3 小Boss: kubectl!</h2><h3 id="3-1-安装"><a href="#3-1-安装" class="headerlink" title="3.1 安装"></a>3.1 安装</h3><p>Kubernetes是Istio首推的运行平台,因此作为第一步,我们首先来安装kubectl,Kubernetes的命令行工具,用来控制Kubernetes集群。根据<a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/" target="_blank" rel="noopener">官方文档</a>,Mac下安装kubectl只需要一行命令,<code>brew install kubectl</code>,这简单、极致的用户体验让你感动的想哭。But wait…</p><h3 id="3-2-穿墙1-Brew"><a href="#3-2-穿墙1-Brew" class="headerlink" title="3.2 穿墙1: Brew"></a>3.2 穿墙1: Brew</h3><p>你敲完命令,踌躇满志的按下回车之后,可能会发现,屏幕迟迟没有输出,10秒,30秒,1分钟,3分钟,10分钟。。。恭喜你,你被墙了!</p><p><a href="https://brew.sh/" target="_blank" rel="noopener">Brew</a>默认的镜像源是GitHub,而GitHub时不时会被墙,即使不被墙访问速度有时也慢的令人发指,导致Brew命令也常常超时甚至失败。解决办法要么<a href="https://segmentfault.com/a/1190000008274997" target="_blank" rel="noopener">换源</a>,要么给GitHub配上Socks5代理。对码农而言,我更推荐后一种,方法如下:</p><p>1) 打开~/.gitconfig文件,如果不存在则新建</p><p>2) 在文件末尾添加如下配置并保存:</p><figure class="highlight plain"><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">[http "https://github.com"]</span><br><span class="line"> proxy = socks5://127.0.0.1:1086</span><br><span class="line">[https "https://github.com"]</span><br><span class="line"> proxy = socks5://127.0.0.1:1086</span><br></pre></td></tr></table></figure><p><em>注:<code>socks5://127.0.0.1:1086</code>是Shadowsocks默认开启的Socks5代理地址。</em></p><p>配上Socks5代理之后,一般就可以妥妥的运行Brew命令了。</p><h3 id="3-3-验证"><a href="#3-3-验证" class="headerlink" title="3.3 验证"></a>3.3 验证</h3><p>安装好kubectl之后,直接运行<code>kubectl version</code>查看版本号。完整的kubectl命令列表在<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands" target="_blank" rel="noopener">这里</a>可以找到。如果想进一步学习常见的kubectl命令,可以访问<a href="https://www.katacoda.com/courses/kubernetes/playground" target="_blank" rel="noopener">Kubernetes Playground</a>完成在线练习。</p><h2 id="4-中Boss-Minikube"><a href="#4-中Boss-Minikube" class="headerlink" title="4 中Boss: Minikube!"></a>4 中Boss: Minikube!</h2><h3 id="4-1-安装"><a href="#4-1-安装" class="headerlink" title="4.1 安装"></a>4.1 安装</h3><p>安装完kubectl,接下来就是在本地搭建Kubernetes集群,Minikube是最简单的一种搭建方式,它通过VM模拟了一个单节点的Kubernetes集群。<a href="https://kubernetes.io/docs/tutorials/stateless-application/hello-minikube/" target="_blank" rel="noopener">官方文档</a>给出了Mac下的安装命令。</p><figure class="highlight plain"><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">curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 && \</span><br><span class="line">chmod +x minikube && \</span><br><span class="line">sudo mv minikube /usr/local/bin/</span><br></pre></td></tr></table></figure><p>Minikube默认使用的VM Driver是<a href="https://www.virtualbox.org/" target="_blank" rel="noopener">VirutalBox</a>,因此启动Minikube之前,还要安装VirtualBox。</p><h3 id="4-2-启动"><a href="#4-2-启动" class="headerlink" title="4.2 启动"></a>4.2 启动</h3><p>安装好Minikube和VirutalBox之后,可运行如下命令第一次启动Minikube:</p><p><code>minikube start --docker-env HTTP_PROXY=http://<本机IP>:1087 --docker-env HTTPS_PROXY=http://<本机IP>:1087</code></p><p><em>注:官方文档给出的启动命令带有<code>--vm-driver=xhyve</code>,而事实上最新版本的Minikube已经废弃了xhyve driver,应去除。</em></p><h3 id="4-3-穿墙2-Docker"><a href="#4-3-穿墙2-Docker" class="headerlink" title="4.3 穿墙2: Docker"></a>4.3 穿墙2: Docker</h3><p>你可能已经注意到,上面的启动命令中带了两个<code>--docker-env</code>参数,都指向了Shadowsocks开启的HTTP代理,为啥呢?还是因为墙。Minikube默认使用Docker作为容器运行时,并在VM中内置了一个Docker守护进程,使用的镜像源是<a href="https://hub.docker.com/" target="_blank" rel="noopener">DockerHub</a>。如果你经常使用Docker,那你一定知道在国内使用Docker一般都要修改镜像源(比如阿里云的<a href="https://cr.console.aliyun.com/#/accelerator" target="_blank" rel="noopener">容器镜像服务</a>)或者使用代理,否则拉取速度也是慢的令人发指。由于Minikube使用的是内置的Docker守护进程,使用代理更为方便,但要注意,开启Shadowsocks HTTP代理时,需要修改代理的侦听地址为本机IP,而不是默认的<code>127.0.0.1</code>,否则在VM中的Docker守护进程是无法访问到这个代理的。</p><p><em>注:<code>--docker-env</code>参数只有在第一次启动Minikube时需要,之后启动直接运行<code>minikube start</code>即可。如果需要修改代理地址,可编辑<code>~/.minikube/machines/minikube/config.json</code>文件。</em></p><h3 id="4-4-验证"><a href="#4-4-验证" class="headerlink" title="4.4 验证"></a>4.4 验证</h3><p>安装完Minikube之后,就可以试着创建第一个Kubernetes服务了,具体步骤参考<a href="https://kubernetes.io/docs/getting-started-guides/minikube/#quickstart" target="_blank" rel="noopener">官方文档</a>。</p><h2 id="5-大Boss-Istio"><a href="#5-大Boss-Istio" class="headerlink" title="5 大Boss: Istio!"></a>5 大Boss: Istio!</h2><h3 id="5-1-安装"><a href="#5-1-安装" class="headerlink" title="5.1 安装"></a>5.1 安装</h3><p>拿到了kubectl和Minikube两大神器,搭建Istio可以说是水到渠成了。基本步骤如下,</p><p>1) 启动Minikube</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></pre></td><td class="code"><pre><span class="line">minikube start \</span><br><span class="line"> --extra-config=controller-manager.ClusterSigningCertFile=<span class="string">"/var/lib/localkube/certs/ca.crt"</span> \</span><br><span class="line"> --extra-config=controller-manager.ClusterSigningKeyFile=<span class="string">"/var/lib/localkube/certs/ca.key"</span> \</span><br><span class="line"> --extra-config=apiserver.Admission.PluginNames=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota \</span><br><span class="line"> --kubernetes-version=v1.9.0</span><br></pre></td></tr></table></figure><p>2) 下载并解压Istio安装包</p><p><code>curl -L https://git.io/getLatestIstio | sh -</code></p><p>3) 进入安装目录(假设为<code>istio-0.7</code>),将<code>bin/</code>目录添加到<code>PATH</code>环境变量</p><figure class="highlight bash"><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"><span class="built_in">cd</span> istio-0.7</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PWD</span>/bin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure><p>4) 部署Istio的核心组件(包括外部流量网关<a href="https://istio.io/docs/tasks/traffic-management/ingress.html" target="_blank" rel="noopener">Ingress</a>, 管理Envoy实例生命周期的<a href="https://istio.io/docs/concepts/traffic-management/pilot.html" target="_blank" rel="noopener">Pilot</a>以及执行访问控制和使用策略的<a href="https://istio.io/docs/concepts/policy-and-control/mixer.html" target="_blank" rel="noopener">Mixer</a>)到Kubernetes</p><p><code>kubectl apply -f install/kubernetes/istio.yaml</code></p><p><em>注:如果你需要启用Istio的<a href="https://istio.io/docs/concepts/security/mutual-tls.html" target="_blank" rel="noopener">Mutual TLS Authentication</a>(服务身份验证)功能,可以改为运行<code>kubectl apply -f install/kubernetes/istio-auth.yaml</code>。</em></p><p>至此,一个基于Istio的服务网格就算安装完成了。One more thing,还记得上篇文章提到的服务网格所独有的边车模式吗?为了将一个具体的服务接入Istio,需要为每一个服务实例创建一个配套的边车进程。根据<a href="https://istio.io/docs/setup/kubernetes/sidecar-injection.html" target="_blank" rel="noopener">官方文档</a>,Istio提供手动和自动两种方式来创建边车进程,前者发生于部署阶段,而后者发生于Pod创建阶段,推荐使用后者,具体步骤参考官方文档,限于篇幅,这里就不再赘述。</p><h3 id="5-2-验证"><a href="#5-2-验证" class="headerlink" title="5.2 验证"></a>5.2 验证</h3><p>安装完Istio之后,可运行<code>kubectl get pods -n istio-system</code>查看所有Istio相关的Pods,确保这些Pods都处于<code>Running</code>状态。然后,你就可以开始Istio的探索之旅了,建议从官方提供的<a href="https://istio.io/docs/guides/bookinfo.html" target="_blank" rel="noopener">Bookinginfo</a>示例应用起步,这里就不再展开。</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></pre></td><td class="code"><pre><span class="line">NAME READY STATUS RESTARTS AGE</span><br><span class="line">istio-ca-59f6dcb7d9-5mll5 1/1 Running 18 42d</span><br><span class="line">istio-ingress-779649ff5b-x2qmn 1/1 Running 26 42d</span><br><span class="line">istio-mixer-7f4fd7dff-6l5g5 3/3 Running 54 42d</span><br><span class="line">istio-pilot-5f5f76ddc8-6867m 2/2 Running 36 42d</span><br><span class="line">istio-sidecar-injector-7947777478-gzcfz 1/1 Running 9 41d</span><br></pre></td></tr></table></figure><h2 id="6-小结"><a href="#6-小结" class="headerlink" title="6 小结"></a>6 小结</h2><p>以上就是搭建一个基于Istio的服务网格的基本教程,希望能够帮助你对服务网格有一个更直观的认识。有关服务网格的进一步介绍,以后有机会我再跟你分享。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p><h2 id="7-参考"><a href="#7-参考" class="headerlink" title="7 参考"></a>7 参考</h2><ul><li><a href="https://istio.io/docs/concepts/what-is-istio/overview.html" target="_blank" rel="noopener">Istio - Overview</a></li><li><a href="http://istio.doczh.cn/" target="_blank" rel="noopener">Istio官方文档中文版</a></li><li><a href="https://zhuanlan.zhihu.com/p/29586032" target="_blank" rel="noopener">数人云|万字解读:Service Mesh服务网格新生代–Istio</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/devops/istio-tutorial/#disqus_thread</comments>
</item>
<item>
<title>服务网格:微服务进入2.0时代</title>
<link>http://emacoo.cn/arch/service-mesh-overview/</link>
<guid>http://emacoo.cn/arch/service-mesh-overview/</guid>
<pubDate>Fri, 30 Mar 2018 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>微服务自<a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">2014年3月</a>由Martin Fowler首次
</description>
<content:encoded><![CDATA[<blockquote><p>微服务自<a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">2014年3月</a>由Martin Fowler首次提出以来,在<a href="http://projects.spring.io/spring-cloud/" target="_blank" rel="noopener">Spring Cloud</a>、<a href="http://dubbo.apache.org/" target="_blank" rel="noopener">Dubbo</a>等各类微服务框架的帮助下,以燎原之势席卷了整个IT技术界,成为了最主流的分布式应用解决方案。但仍然还有很多问题没有得到根本性的解决,比如技术门槛高、多语言支持不足、代码侵入性强等。如何应对这些挑战成为了下一代微服务首要回答的问题。直到服务网格(Service Mesh)被提出,这一切都有了答案。</p></blockquote><h2 id="1-微服务之殇"><a href="#1-微服务之殇" class="headerlink" title="1 微服务之殇"></a>1 微服务之殇</h2><p>时光回到2017年初,那时所有主流的微服务框架,不管是类库性质的<a href="https://twitter.github.io/finagle/" target="_blank" rel="noopener">Finagle</a>、<a href="https://github.com/Netflix/Hystrix" target="_blank" rel="noopener">Hystrix</a>,还是框架性质的Spring Cloud、Dubbo,本质上都归于应用内解决方案,都存在以下三个问题:</p><ul><li><strong>技术门槛高</strong>:随着微服务实施水平的不断深化,除了基础的<a href="http://emacoo.cn/arch/microservice-registry-center/">服务发现</a>、<a href="http://emacoo.cn/arch/microservice-config/">配置中心</a>和<a href="http://emacoo.cn/arch/microservice-oauth2/">授权管理</a>之外,团队将不可避免的在服务治理层面面临各类新的挑战,包括但不限于分布式跟踪、熔断降级、灰度发布、故障切换等,这对团队提出了非常高的技术要求。</li></ul><p><img src="service-governance.jpg" alt></p><p><em>图片出处:<a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></em></p><ul><li><strong>多语言支持不足</strong>:对于稍具规模的团队,尤其在高速成长的互联网创业公司,多语言的技术栈是常态,跨语言的服务调用也是常态,但目前开源社区上并没有一套统一的、跨语言的微服务技术栈。</li><li><strong>代码侵入性强</strong>:主流的微服务框架(比如Spring Cloud、Dubbo)或多或少都对业务代码有一定的侵入性,框架替换成本高,导致业务团队配合意愿低,微服务落地困难。</li></ul><p>这些问题加起来导致的结果就是,在实施微服务的过程中,小团队Hold不住,大公司推不动。</p><h2 id="2-另辟蹊径"><a href="#2-另辟蹊径" class="headerlink" title="2 另辟蹊径"></a>2 另辟蹊径</h2><p>如何解决上述三个问题呢?最容易想到的是代理模式,在LB层(比如<a href="http://nginx.org/" target="_blank" rel="noopener">Nginx</a>、<a href="https://httpd.apache.org/" target="_blank" rel="noopener">Apache HTTP Server</a>)处理所有的服务调用,以及部分服务治理问题(比如分布式跟踪、熔断降级)。但这个方案有两个显著的缺点,第一,中心化架构,代理端自身的性能和可用性将是整个系统的瓶颈;第二,运维复杂度高,业务团队笑了,运维团队哭了。</p><blockquote><p>难道这就是桃园吗?</p></blockquote><p>服务网格(Service Mesh)应运而生!自2016年9月Linkerd第一次公开使用之后,伴随着<a href="https://linkerd.io/" target="_blank" rel="noopener">Linkerd</a>、<a href="https://www.envoyproxy.io/" target="_blank" rel="noopener">Envoy</a>、<a href="https://istio.io/" target="_blank" rel="noopener">Istio</a>、<a href="https://www.nginx.com/products/" target="_blank" rel="noopener">NGINX Application Platform</a>、<a href="https://conduit.io/" target="_blank" rel="noopener">Conduit</a>等新框架如雨后春笋般不断涌现,在微服务之后,服务网格和它的边车(Sidecar)模式引领了IT技术界2017一整年的走向。</p><h2 id="3-服务网格"><a href="#3-服务网格" class="headerlink" title="3 服务网格"></a>3 服务网格</h2><h3 id="3-1-元定义"><a href="#3-1-元定义" class="headerlink" title="3.1 元定义"></a>3.1 元定义</h3><p>首先,我们来看一下服务网格的提出者William Morgan是如何描述它的。</p><blockquote><p>A service mesh is a dedicated infrastructure layer for handling service-to-service communication. Consists of a control plane and data plane (service proxies act as “mesh”). - William Morgan, <a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></p></blockquote><p>上面这段话非常清晰的指明了服务网格的职责,即处理服务间通讯,这正是服务治理的核心所在。而<code>a dedicated infrastructure layer</code>这几个单词将服务网格和之前所有的微服务框架(framework)划清了界限,也即服务网格独立于具体的服务而存在,这从根本上解决了前文提到的老的微服务框架在多语言支持和代码侵入方面存在的问题。并且,由于服务网格的独立性,业务团队不再需要操心服务治理相关的复杂度,全权交给服务网格处理即可。</p><p>那你可能会问,这不跟之前提到的代理模式差不多吗?区别在于服务网格独创的边车模式。针对每一个服务实例,服务网格都会在同一主机上一对一并行部署一个边车进程,接管该服务实例所有对外的网络通讯(参见下图)。这样就去除了代理模式下中心化架构的瓶颈。同时,借助于良好的框架封装,运维成本也可以得到有效的控制。</p><p><img src="linkerd-service-mesh-diagram.png" alt></p><p><em>图片出处:<a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></em></p><h3 id="3-2-演化史"><a href="#3-2-演化史" class="headerlink" title="3.2 演化史"></a>3.2 演化史</h3><p>追本溯源,服务网格从无到有可分为三个演化阶段(参见下图)。第一个阶段,每个服务各显神通,自行处理对外通讯。第二个阶段,所有服务使用统一的类库进行通讯。第三个阶段,服务不再关心通讯细节,统统交给边车进程,就像在TCP/IP协议中,应用层只需把要传输的内容告诉TCP层,由TCP层负责将所有内容原封不动的送达目的端,整个过程中应用层并不需要关心实际传输过程中的任何细节。</p><p><img src="pattern-network.png" alt></p><p><img src="pattern-library.png" alt></p><p><img src="pattern-sidecar.png" alt></p><p><em>图片出处:<a href="http://philcalcado.com/2017/08/03/pattern_service_mesh.html" target="_blank" rel="noopener">Pattern: Service Mesh</a></em></p><h3 id="3-3-时间线"><a href="#3-3-时间线" class="headerlink" title="3.3 时间线"></a>3.3 时间线</h3><p>最后,再来回看一下服务网格年轻的历史。虽然服务网格的正式提出是在2016年9月,但其实早在2013年,Airbnb就提出了类似的想法——<a href="https://medium.com/airbnb-engineering/smartstack-service-discovery-in-the-cloud-4b8a080de619" target="_blank" rel="noopener">SmartStack</a>,只不过SmartStack局限于服务发现,并没有引起太多关注,类似的还有Netflix的Prana和唯品会的OSP Local Proxy。2016年服务网格提出之后,以Linkerd和Envoy为代表的框架开始崭露头角,并于2017年先后加入CNCF基金(Cloud Native Computing Foundation),最终促使了一代新贵Istio的诞生。2018年,Istio将发布1.0版本,这也许意味着微服务开始进入2.0时代。</p><p><img src="history.jpg" alt></p><p><em>图片出处:<a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></em></p><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上就是我对服务网格的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。下一篇我会教大家如何在本地从零搭建一个基于Istio的服务网格,敬请期待。</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></li><li><a href="http://philcalcado.com/2017/08/03/pattern_service_mesh.html" target="_blank" rel="noopener">Pattern: Service Mesh</a></li><li><a href="https://servicemesh.gitbooks.io/awesome-servicemesh/" target="_blank" rel="noopener">Awesome Service Mesh</a></li><li><a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></li><li><a href="http://www.infoq.com/cn/articles/2017-service-mesh?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=%E6%9E%B6%E6%9E%84%20&%20%E8%AE%BE%E8%AE%A1-articles" target="_blank" rel="noopener">解读2017之Service Mesh:群雄逐鹿烽烟起</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/service-mesh-overview/#disqus_thread</comments>
</item>
<item>
<title>零基础玩转Serverless</title>
<link>http://emacoo.cn/arch/serverless-tutorial/</link>
<guid>http://emacoo.cn/arch/serverless-tutorial/</guid>
<pubDate>Sat, 03 Feb 2018 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p><a href="http://emacoo.cn/arch/serverless-overview/">上篇</a>文章首先指出了Serverless=No Server这一常见误区,然后明确定义了<strong>函数</strong>这个Ser
</description>
<content:encoded><![CDATA[<blockquote><p><a href="http://emacoo.cn/arch/serverless-overview/">上篇</a>文章首先指出了Serverless=No Server这一常见误区,然后明确定义了<strong>函数</strong>这个Serverless中的核心概念,接着介绍了Serverless的4个关键特性:运行成本更低、自动扩缩容、事件驱动、无状态性,最后分析了Serverless和微服务、DevOps之间的关联关系。为了帮助大家更直观的理解Serverless,本文将介绍三种在<a href="https://aws.amazon.com/cn/lambda/" target="_blank" rel="noopener">AWS Lambda</a>上创建函数的方式。</p></blockquote><h2 id="1-Hello-AWS-Lambda"><a href="#1-Hello-AWS-Lambda" class="headerlink" title="1 Hello, AWS Lambda!"></a>1 Hello, AWS Lambda!</h2><p><img src="aws-lambda-123.png" alt></p><h3 id="1-1-注册AWS账户"><a href="#1-1-注册AWS账户" class="headerlink" title="1.1 注册AWS账户"></a>1.1 注册AWS账户</h3><p>首先,打开Amazon AWS<a href="https://amazonaws-china.com/cn/" target="_blank" rel="noopener">官网</a>,点击右上角<strong>注册</strong>按钮开始注册流程。</p><p>注册AWS除了邮箱、地址、手机号(用于接受语音验证码)等基本信息之外,还需要绑定一张信用卡(银联、MasterCard、VISA),绑卡过程中会发生一笔1美元的信用卡预授权扣费。</p><p>注册成功之后,即可获赠<a href="https://amazonaws-china.com/cn/free/" target="_blank" rel="noopener">AWS免费套餐</a>大礼包,包括12个月免费的基础IaaS & PaaS服务(比如EC2, S3, RDS等),以及永久免费的AWS Lambda<a href="https://amazonaws-china.com/cn/lambda/pricing/" target="_blank" rel="noopener">免费套餐</a>(包括每月100万个免费请求以及每月400000GB-秒的计算时间,对于个人使用而言完全是足够了)。</p><h3 id="1-2-创建函数"><a href="#1-2-创建函数" class="headerlink" title="1.2 创建函数"></a>1.2 创建函数</h3><p>接下来,就来创建第一个AWS Lambda函数吧。</p><p>1) 登录AWS,点击最上方的菜单栏<strong>服务->计算:Lambda</strong>,进入Lambda控制台。<br>2) 在页面上找到并点击<strong>创建函数</strong>按钮。<br>3) 作为第一个函数,选择<strong>从头开始创作</strong>,输入函数名称<code>hello-lambda</code>,运行语言选择<code>Node.js 6.10</code>,角色选择系统默认创建的<code>service-role/admin</code>,点击<strong>创建函数</strong>完成创建。</p><p><img src="hello-lambda.png" alt></p><h3 id="1-3-简单测试"><a href="#1-3-简单测试" class="headerlink" title="1.3 简单测试"></a>1.3 简单测试</h3><p>新函数创建好之后,就可以开始测试了。在函数详情页的右上角找到并点击<strong>测试</strong>按钮,第一次会提示你先创建一个测试事件,输入名称,使用默认模板完成创建。回到详情页,再次点击<strong>测试</strong>按钮,就会触发测试。测试完成之后,展开详细信息,就可以看到具体的响应结果,以及本次测试产生的计费时间。</p><p><img src="test-result.png" alt></p><h3 id="1-4-公网测试"><a href="#1-4-公网测试" class="headerlink" title="1.4 公网测试"></a>1.4 公网测试</h3><p>函数详情页的测试按钮是最简单的一种测试Lambda函数的方式,但这种方式仅限于AWS内网,如果想在公网环境下进行测试,该如何操作呢?最自然的方式是绑定API Gateway,将函数转化为可公开调用的API。</p><h4 id="1-4-1-绑定API-Gateway"><a href="#1-4-1-绑定API-Gateway" class="headerlink" title="1.4.1 绑定API Gateway"></a>1.4.1 绑定API Gateway</h4><p>1) 同样是函数详情页,在左侧找到<strong>添加触发器</strong>,点击<strong>API Gateway</strong>,保持默认设置完成添加。<br>2) 修改函数代码,返回符合API Gateway格式要求的响应结果,参考<a href="https://amazonaws-china.com/cn/premiumsupport/knowledge-center/malformed-502-api-gateway/" target="_blank" rel="noopener">这里</a>。<br>3) 保存上述改动。</p><figure class="highlight plain"><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">exports.handler = (event, context, callback) => {</span><br><span class="line"> var responseBody = {</span><br><span class="line"> "key3": "value3",</span><br><span class="line"> "key2": "value2",</span><br><span class="line"> "key1": "value1"</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> var response = {</span><br><span class="line"> "statusCode": 200,</span><br><span class="line"> "headers": {</span><br><span class="line"> "my_header": "my_value"</span><br><span class="line"> },</span><br><span class="line"> "body": JSON.stringify(responseBody),</span><br><span class="line"> "isBase64Encoded": false</span><br><span class="line"> };</span><br><span class="line"> callback(null, response);</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p><em>示例函数代码</em></p><p>点击最上方的菜单栏<strong>服务->网络和内容分发:API Gateway</strong>,进入API Gateway控制台,在左侧导航栏应该能够看到<strong>API->LambdaMicroservice</strong>,说明函数已经成功绑定。依次点击<strong>API->LambdaMicroservice->阶段->prod->/->hello-lambda->GET</strong>,记下调用URL。</p><h4 id="1-4-2-创建用户"><a href="#1-4-2-创建用户" class="headerlink" title="1.4.2 创建用户"></a>1.4.2 创建用户</h4><p>API Gateway默认使用的鉴权方式是AWS_IAM,即调用方必须拥有特定的IAM Permssions才能调用API,参考<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html" target="_blank" rel="noopener">这里</a>。具体来说,需要一个拥有<code>execute-api:Invoke</code>权限的用户。</p><p>1) 点击最上方的菜单栏<strong>服务->安全、身份与合规:IAM</strong>,进入IAM控制台。<br>2) 点击左侧导航栏<strong>用户</strong>,进入用户面板。<br>3) 点击<strong>添加用户</strong>按钮,输入用户名,访问类型选择<strong>编程访问</strong>,点击<strong>下一步:权限</strong>。<br>4) 选择<strong>直接附加现有策略</strong>,搜索并选中<code>AmazonAPIGatewayInvokeFullAccess</code>,完成创建。<br>5) 返回用户列表页,点击刚刚创建的用户进入用户详情页,点击<strong>安全证书->创建访问密钥</strong>,记下<strong>访问密钥 ID</strong>和<strong>私有访问密钥</strong>。</p><h4 id="1-4-3-使用Postman测试API"><a href="#1-4-3-使用Postman测试API" class="headerlink" title="1.4.3 使用Postman测试API"></a>1.4.3 使用Postman测试API</h4><p>做完前两步的准备工作,就可以使用Postman进行测试了。</p><p>1) 下载并启动<a href="https://www.getpostman.com/" target="_blank" rel="noopener">Postman</a>。<br>2) 创建一个新的请求,<strong>Authorization</strong>选择<code>AWS Signature</code>,输入之前记下的URL、AccessKey(访问密钥 ID)和SecretKey(私有访问密钥),AWS Region填入URL中紧邻<strong>amazonaws.com</strong>的一个子域名,Service Name填入<code>execute-api</code>。<br>3) 点击<strong>Send</strong>,稍等一会,应该就能看到正常的响应结果。</p><p><img src="postman-request.png" alt></p><p>进一步信息可参考<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-use-postman-to-call-api.html" target="_blank" rel="noopener">这里</a>。</p><h2 id="2-加餐一:Spring-Cloud-Function"><a href="#2-加餐一:Spring-Cloud-Function" class="headerlink" title="2 加餐一:Spring Cloud Function"></a>2 加餐一:Spring Cloud Function</h2><p>除了Node.js,AWS Lambda还支持Java 8、C#、Go、Python等多种运行语言。接下来,就以一个<a href="http://cloud.spring.io/spring-cloud-function/spring-cloud-function.html" target="_blank" rel="noopener">Spring Cloud Function</a>(简称SCF)应用为例,展示如何创建一个Java 8的函数。</p><blockquote><p>SCF是Spring社区提供的一个以函数为核心的开发框架。除了本地运行,SCF应用还可以部署到AWS、Azure、OpenWhisk等多种Serverless平台。最新的发布版本是1.0.0.M3。</p></blockquote><p><strong>打包应用:</strong></p><p>1) <code>git clone</code>SCF<a href="https://github.com/spring-cloud/spring-cloud-function" target="_blank" rel="noopener">官方仓库</a>。<br>2) 进入<strong>spring-cloud-function-samples/function-sample-aws</strong>目录,运行<code>mvn clean package</code>。<br>3) 运行成功后在<strong>target</strong>目录下可以找到名为<strong>function-sample-aws-1.0.0.BUILD-SNAPSHOT-aws.jar</strong>的应用包。</p><p><strong>创建函数:</strong></p><p>1) 和之前一样,进入Lambda控制台,点击<strong>创建函数</strong>按钮,运行语言选择<code>Java 8</code>,完成创建。<br>2) 进入函数详情页,点击<strong>函数代码->上传</strong>按钮,选择之前打好的应用包,处理程序改为<code>org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler</code>。<br>3) 保存修改。</p><p><strong>测试函数:</strong></p><p>1) 进入函数详情页,点击右上角的<strong>测试</strong>按钮,填入<code>{"value": "hello, lambda!"}</code>创建新的测试事件。<br>2) 再次点击<strong>测试</strong>按钮,触发第一次测试。不出意外,第一次测试会提示失败,错误消息类似于<code>errorMessage": "2018-02-04T13:09:59.745Z b1c9b0a1-09ac-11e8-9fdf-858e20f0ff70 Task timed out after 3.00 seconds"</code>。出错的直接原因是函数设置的超时时间太短(默认3秒),根本原因是函数的无状态性,每次函数调用都要经历一次冷启动,这对于Node应用没有太大问题,但对于Java 8应用,即便是一个最简单的Hello World应用,完成一次冷启动至少需要5到10秒。<br>3) 修改<strong>基本设置->内存</strong>为<code>512MB</code>,<strong>基本设置->超时</strong>为<code>5分钟</code>,保存然后重新测试。这一次测试应该可以成功,返回结果为<code>{"value": "HELLO, LAMBDA!"}</code>。</p><h2 id="3-加餐二:serverless-toolkit"><a href="#3-加餐二:serverless-toolkit" class="headerlink" title="3 加餐二:serverless toolkit"></a>3 加餐二:serverless toolkit</h2><p>除了直接在AWS后台创建函数,还有一种更为简便的方式,使用<a href="https://serverless.com/" target="_blank" rel="noopener">serverless.com</a>平台提供的serverless toolkit。</p><p><img src="serverless-toolkit.png" alt></p><p>操作非常简单,这里就不展开了,不过有两点需要注意:</p><ul><li>在将应用部署到AWS之前,先要创建一个拥有<code>AdministratorAccess</code>权限的用户,参考<a href="https://serverless.com/framework/docs/providers/aws/guide/credentials/" target="_blank" rel="noopener">这里</a>。</li><li>默认创建的应用鉴权为空,即可以在公网直接访问。</li></ul><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上简单介绍了三种在AWS Lambda上创建函数的方式,希望对你理解Serverless有所帮助。有关Serverless其他特性的研究,以后有机会我再跟你分享。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="http://serverless.ink/" target="_blank" rel="noopener">Serverless 应用开发指南</a></li><li><a href="https://spring.io/blog/2017/07/05/introducing-spring-cloud-function" target="_blank" rel="noopener">Introducing Spring Cloud Function</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/serverless-tutorial/#disqus_thread</comments>
</item>
<item>
<title>我的2018书单</title>
<link>http://emacoo.cn/notes/2018-booklist/</link>
<guid>http://emacoo.cn/notes/2018-booklist/</guid>
<pubDate>Sun, 31 Dec 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>2018,愚者暗于成事,智者见于未萌。</p>
</blockquote>
<h2 id="在读"><a href="#在读" class="headerlink" title="在读"></a>在读</h2><p><img src="237375
</description>
<content:encoded><![CDATA[<blockquote><p>2018,愚者暗于成事,智者见于未萌。</p></blockquote><h2 id="在读"><a href="#在读" class="headerlink" title="在读"></a>在读</h2><p><img src="23737589-1_w_2.jpg" alt></p><h2 id="已读"><a href="#已读" class="headerlink" title="已读"></a>已读</h2>]]></content:encoded>
<comments>http://emacoo.cn/notes/2018-booklist/#disqus_thread</comments>
</item>
<item>
<title>所谓Serverless,你理解对了吗?</title>
<link>http://emacoo.cn/arch/serverless-overview/</link>
<guid>http://emacoo.cn/arch/serverless-overview/</guid>
<pubDate>Sun, 31 Dec 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>随着DevOps和微服务的理念日渐被IT业界所接受,另一个新名词Serverless也<a href="https://trends.google.com/trends/explore?date=today%205-y&amp;q=serverle
</description>
<content:encoded><![CDATA[<blockquote><p>随着DevOps和微服务的理念日渐被IT业界所接受,另一个新名词Serverless也<a href="https://trends.google.com/trends/explore?date=today%205-y&q=serverless" target="_blank" rel="noopener">开始</a>进入人们的视野。尤其在今年4月份国内两大云服务厂商阿里云、腾讯云先后推出各自的Serverless产品之后,Serverless一时洛阳纸贵。那到底什么是Serverless,它跟DevOps和微服务又有什么样的联系呢?本文将尝试揭开Serverless的神秘面纱,让你一睹为快。</p></blockquote><h2 id="1-Serverless-No-Server"><a href="#1-Serverless-No-Server" class="headerlink" title="1 Serverless != No Server"></a>1 Serverless != No Server</h2><p>首先,必须澄清的是Serverless并不能按字面上理解为无服务器,而是说对应用开发者而言,不再需要<strong>操心</strong>大部分跟服务器相关的事务,比如服务器选购、应用运行环境配置、负载均衡、日志搜集、系统监控等,这些事情统统交给Serverless平台即可,应用开发者唯一需要做的就是编写应用代码,实现业务逻辑。为了避免歧义,本文将保留使用Serverless,而不是其通常的中文翻译无服务器。</p><p>Serverless最早由Amazon提出,第一个Serverless平台是2014年年底推出的<a href="https://aws.amazon.com/cn/lambda/" target="_blank" rel="noopener">AWS Lambda</a>,应用开发者只需要上传代码或者应用包,即可发布一个应用。之后全球各大云服务厂商都纷纷推出各自的Serverless平台,比如<a href="https://cloud.google.com/functions/" target="_blank" rel="noopener">Google Cloud Functions</a>,<a href="https://azure.microsoft.com/en-us/services/functions/" target="_blank" rel="noopener">Azure Functions</a>,<a href="https://www.ibm.com/cloud/functions" target="_blank" rel="noopener">IBM Cloud Functions</a>,以及前面提到的<a href="https://www.aliyun.com/product/fc" target="_blank" rel="noopener">阿里云函数计算</a>和<a href="https://www.aliyun.com/product/fc" target="_blank" rel="noopener">腾讯云无服务器云函数</a>等。在云服务厂商之外,开源社区也涌现出很多优秀的Serverless框架,比如<a href="https://openwhisk.apache.org/" target="_blank" rel="noopener">Apache OpenWhisk</a>,<a href="http://cloud.spring.io/spring-cloud-function/" target="_blank" rel="noopener">Spring Cloud Function</a>,<a href="https://github.com/lambadaframework/lambadaframework" target="_blank" rel="noopener">Lambada Framework</a>,<a href="https://webtask.io/" target="_blank" rel="noopener">webtask</a>等。</p><p>根据<a href="https://martinfowler.com/articles/serverless.html" target="_blank" rel="noopener">Serverless Architectures</a>一文,Serverless应用可以细分为BaaS和FaaS两类,</p><ul><li>BaaS: Backend as a Service,这里的Backend可以指代任何第三方提供的应用和服务,比如提供云数据库服务的<a href="https://firebase.google.com/" target="_blank" rel="noopener">Firebase</a>和<a href="http://parseplatform.org/" target="_blank" rel="noopener">Parse</a>,提供统一用户身份验证服务的<a href="https://auth0.com/" target="_blank" rel="noopener">Auth0</a>和<a href="https://aws.amazon.com/cn/cognito/" target="_blank" rel="noopener">Amazon Cognito</a>等。</li><li>FaaS: Functions as a Service,应用以函数的形式存在,并由第三方云平台托管运行,比如之前提到的AWS Lambda,Google Cloud Functions等。</li></ul><p>本文主要讨论的是FaaS,这也是目前各类Serverless平台和框架主要支持的类型。</p><h2 id="2-函数即应用"><a href="#2-函数即应用" class="headerlink" title="2 函数即应用"></a>2 函数即应用</h2><blockquote><p>当我们讨论函数时,我们到底在讨论什么?</p></blockquote><p>函数,往大了说可以是一个应用的main函数,往小了说也可以是一个简单的加法函数,那到底该如何理解FaaS中的函数呢?先来看张图。</p><p><img src="faas.png" alt></p><p>左侧的Monolith即我们常说的单体应用,中间是微服务,右侧就是FaaS中的函数(为了避免歧义,如不特殊指明,下文提到的函数都是指代FaaS中的函数)。如同一个单体应用可以按业务模块拆分成多个微服务,一个微服务也可以按使用场景拆分成多个函数。比如一个广告微服务,至少可以拆分出实时竞价、展示计数、报表查询等多个函数。也就是说,FaaS中的函数和微服务中的API是同一粒度的。但不同于API,在Serverless架构下,每个函数都是独立部署,按需执行。那这样的拆分有意义吗?接着往下看。</p><h2 id="3-搞懂Serverless的4把钥匙"><a href="#3-搞懂Serverless的4把钥匙" class="headerlink" title="3 搞懂Serverless的4把钥匙"></a>3 搞懂Serverless的4把钥匙</h2><p>和其他架构相比,Serverless有以下4个特点。</p><h3 id="3-1-运行成本更低"><a href="#3-1-运行成本更低" class="headerlink" title="3.1 运行成本更低"></a>3.1 运行成本更低</h3><p>无论是过去的IDC,还是如今的云主机,本质上都是一种包月计费模式,也就是说,不管有没有用户访问你的应用,也不管你有没有部署应用,你都要付相同的钱。但对于Serverless应用,你只需要根据实际使用的资源量(比如AWS Lambda是按<code>内存大小*计算时间</code>计算资源量)进行付费,也即用多少,付多少,相当于移动网络的按流量计费模式。那为什么说使用这种模式就能降低运行成本呢?</p><p><img src="inconsistent-traffic-pattern.png" alt></p><p>红线以下的长方形面积代表了传统包月计费模式下你所需要支付的成本,而蓝色区域的面积则代表了按流量计费模式下的成本,显然后者要远低于前者。根据福布斯2015年发布的一份<a href="https://www.forbes.com/forbes/welcome/?toURL=https://www.forbes.com/sites/benkepes/2015/06/03/30-of-servers-are-sitting-comatose-according-to-research/&refURL=&referrer=#2f4944612c2" target="_blank" rel="noopener">研究报告</a>,从全年来看,一个典型的数据中心里的服务器平均资源使用率只有可怜的5%到15%,也就是说如果全部使用Serverless,理论上至少可以节省80%的运行成本。</p><p>按流量计费的另一个隐藏的好处是任何的性能提升都可以直接的反应到运行成本上,这让技术人员的价值也有了更充分的体现。</p><h3 id="3-2-自动扩缩容"><a href="#3-2-自动扩缩容" class="headerlink" title="3.2 自动扩缩容"></a>3.2 自动扩缩容</h3><p>Serverless第二个常被提及的特点是自动扩缩容。前面说了函数即应用,一个函数只做一件事,可以独立的进行扩缩容,而不用担心影响其他函数,并且由于粒度更小,扩缩容速度也更快。而对于单体应用和微服务,借助于各种容器编排技术,虽然也能实现自动扩缩容,但由于粒度关系,相比函数,始终会存在一定的资源浪费。比如一个微服务提供两个API,其中一个API需要进行扩容,而另一个并不需要,那么这时候扩容,对于不需要的API就是一种浪费。</p><h3 id="3-3-事件驱动"><a href="#3-3-事件驱动" class="headerlink" title="3.3 事件驱动"></a>3.3 事件驱动</h3><p>函数本质上实现的是一种<a href="https://en.wikipedia.org/wiki/IPO_model" target="_blank" rel="noopener">IPO</a>(Input-Process-Output)模型,它是短暂的,是即用即走的。这点是函数区别于单体应用和微服务的另一个特征。不管是单体应用,还是微服务,都是系统中的常驻进程,套用一句流行语,就是你来或不来,我都在这里,不舍不弃。而函数不一样,既不发布任何服务,没有请求时也不消耗任何资源,只有当请求来了,才会消耗资源进行响应,服务完立刻释放资源。正是由于这一点,函数天然的适用于任何事件驱动的业务场景,比如广告竞价,身份验证,定时任务,以及一些新兴的IoT应用。</p><p><img src="event-driven-iot.png" alt></p><p><em>OpenWhisk给出的一个IoT电冰箱的<a href="https://www.slideshare.net/DanielKrook/openwhisk-a-platform-for-cloud-native-serverless-event-driven-apps?ref=https://developer.ibm.com/opentech/2016/09/06/what-makes-serverless-attractive/" target="_blank" rel="noopener">案例</a></em></p><h3 id="3-4-无状态性"><a href="#3-4-无状态性" class="headerlink" title="3.4 无状态性"></a>3.4 无状态性</h3><p>函数的IPO本质决定了函数的另一个特征,无状态性。无状态一方面有助于提高函数的可重用性和可迁移性,但另一方面也带来了一些性能上的损失。第一,函数不是常驻进程,这就意味着每来一个请求,函数都要经历一次冷启动,这对编译型语言编写的应用不啻为一场噩梦(以Spring Boot为例,即便是一个最简单的Hello World应用,至少也需要5秒钟才能启动完毕)。第二,每服务完一个请求,函数所在的进程就会被杀掉,也就是说使用内存进行缓存对函数而言不再有意义。第三,由于每次启动都可能被调度到新的服务器上,任何基于本地磁盘的缓存技术也就不再适用。从第二点和第三点可知,函数只能使用外存(比如Redis,数据库)进行缓存,而操作外存都需要通过网络,性能跟内存、本地硬盘相比差了一到两个数量级。</p><h2 id="4-DevOps-gt-NoOps"><a href="#4-DevOps-gt-NoOps" class="headerlink" title="4 DevOps => NoOps"></a>4 DevOps => NoOps</h2><blockquote><p>如果说Agile+IaaS促成了DevOps,那么Agile+PaaS就孕育了Serverless。</p></blockquote><p>理解了什么是Serverless,再来看看它和DevOps的关系。DevOps虽然做了很多Dev的事,但底牌还是Ops(好比猫熊虽然长得像猫,但实际上还是熊)。但Serverless不同,从本质上说,它是把Ops外包给第三方平台,让Dev专注于业务逻辑的实现而不用操心Ops相关的工作,最终的结果就是绝大多数企业不再需要Ops这个岗位。它和DevOps最大的共同点就是帮助企业缩短产品上市的时间。</p><h2 id="5-小结"><a href="#5-小结" class="headerlink" title="5 小结"></a>5 小结</h2><p>以上就是我对Serverless的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。下一篇我会手把手教大家如何在AWS Lambda部署一个基于Spring Cloud Function的Serverless应用,敬请期待。</p><h2 id="6-参考"><a href="#6-参考" class="headerlink" title="6 参考"></a>6 参考</h2><ul><li><a href="https://martinfowler.com/articles/serverless.html" target="_blank" rel="noopener">Serverless Architectures</a></li><li><a href="https://developer.ibm.com/opentech/2016/09/06/what-makes-serverless-attractive/" target="_blank" rel="noopener">What makes serverless architectures so attractive?</a></li><li><a href="http://www.infoq.com/cn/articles/practical-serverless-computing?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=%E6%9E%B6%E6%9E%84%20&%20%E8%AE%BE%E8%AE%A1-articles" target="_blank" rel="noopener">InfoQ虚拟研讨会:无服务器计算的实践方法</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA%3D%3D&chksm=88931c6cbfe4957a702e66221e1bf997c4ba5a66de279294b08cccadd3ff5d6cabf103657484&idx=1&mid=2649694991&mpshare=1&scene=23&sn=818dea0cb058a08ac6b66ee865204630&srcid=0907dIsFi2ho3ez9orBMGatf" target="_blank" rel="noopener">Serverless云函数架构精解</a></li><li><a href="http://www.infoq.com/cn/news/2017/06/tengxun-cloud-serverless?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=DevOps" target="_blank" rel="noopener">姗姗来迟的Serverless如何助力微服务和DevOps</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/serverless-overview/#disqus_thread</comments>
</item>
<item>
<title>【Spring 5】响应式Web框架实战(下)</title>
<link>http://emacoo.cn/backend/spring5-reactive-tutorial2/</link>
<guid>http://emacoo.cn/backend/spring5-reactive-tutorial2/</guid>
<pubDate>Mon, 17 Jul 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spa
</description>
<content:encoded><![CDATA[<blockquote><p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在内的大批新技术应运而生。其中以<a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a>和<a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>为代表的响应式(Reactive)编程技术针对的就是经典的大数据4V定义(Volume,Variety,Velocity,Value)中的Velocity,即高并发问题,而在即将发布的Spring 5中,也引入了响应式编程的支持。在接下来的几周,我会围绕响应式编程分三期与你分享我的一些学习心得。本篇是第三篇(下),通过一个简单的Spring 5示例应用,探一探即将于下月底发布的Spring 5的究竟。</p><p>前情概要:</p><ul><li><a href="http://emacoo.cn/backend/spring5-overview/">【Spring 5】响应式Web框架前瞻</a></li><li><a href="http://emacoo.cn/backend/reactive-overview/">响应式编程总览</a></li><li><a href="http://emacoo.cn/backend/spring5-reactive-tutorial/">【Spring 5】响应式Web框架实战(上)</a></li></ul></blockquote><h2 id="1-回顾"><a href="#1-回顾" class="headerlink" title="1 回顾"></a>1 回顾</h2><p><a href="http://emacoo.cn/backend/spring5-reactive-tutorial/">上篇</a>介绍了如何使用Spring MVC注解实现一个响应式Web应用(以下简称RP应用),本篇接着介绍另一种实现方式——Router Functions。</p><h2 id="2-实战"><a href="#2-实战" class="headerlink" title="2 实战"></a>2 实战</h2><h3 id="2-1-Router-Functions"><a href="#2-1-Router-Functions" class="headerlink" title="2.1 Router Functions"></a>2.1 Router Functions</h3><p><img src="pipeline.png" alt></p><p>Router Functions是Spring 5新引入的一套Reactive风格(基于Flux和Mono)的函数式接口,主要包括<code>RouterFunction</code>,<code>HandlerFunction</code>和<code>HandlerFilterFunction</code>,分别对应Spring MVC中的<code>@RequestMapping</code>,<code>@Controller</code>和<code>HandlerInterceptor</code>(或者Servlet规范中的<code>Filter</code>)。</p><p>和Router Functions搭配使用的是两个新的请求/响应模型,<code>ServerRequest</code>和<code>ServerResponse</code>,这两个模型同样提供了Reactive风格的接口。</p><h3 id="2-2-示例代码"><a href="#2-2-示例代码" class="headerlink" title="2.2 示例代码"></a>2.2 示例代码</h3><p>下面接着看我GitHub上的<a href="https://github.com/emac/spring5-features-demo" target="_blank" rel="noopener">示例工程</a>里的例子。</p><h4 id="2-2-1-自定义RouterFunction和HandlerFilterFunction"><a href="#2-2-1-自定义RouterFunction和HandlerFilterFunction" class="headerlink" title="2.2.1 自定义RouterFunction和HandlerFilterFunction"></a>2.2.1 自定义RouterFunction和HandlerFilterFunction</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RestaurantServer</span> <span class="keyword">implements</span> <span class="title">CommandLineRunner</span> </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> RestaurantHandler restaurantHandler;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 注册自定义RouterFunction</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> RouterFunction<ServerResponse> <span class="title">restaurantRouter</span><span class="params">()</span> </span>{</span><br><span class="line"> RouterFunction<ServerResponse> router = route(GET(<span class="string">"/reactive/restaurants"</span>).and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::findAll)</span><br><span class="line"> .andRoute(GET(<span class="string">"/reactive/delay/restaurants"</span>).and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::findAllDelay)</span><br><span class="line"> .andRoute(GET(<span class="string">"/reactive/restaurants/{id}"</span>).and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::get)</span><br><span class="line"> .andRoute(POST(<span class="string">"/reactive/restaurants"</span>).and(accept(APPLICATION_JSON_UTF8)).and(contentType(APPLICATION_JSON_UTF8)), restaurantHandler::create)</span><br><span class="line"> .andRoute(DELETE(<span class="string">"/reactive/restaurants/{id}"</span>).and(accept(APPLICATION_JSON_UTF8)), restaurantHandler::delete)</span><br><span class="line"> <span class="comment">// 注册自定义HandlerFilterFunction</span></span><br><span class="line"> .filter((request, next) -> {</span><br><span class="line"> <span class="keyword">if</span> (HttpMethod.PUT.equals(request.method())) {</span><br><span class="line"> <span class="keyword">return</span> ServerResponse.status(HttpStatus.BAD_REQUEST).build();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> next.handle(request);</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> router;</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">(String... args)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> RouterFunction<ServerResponse> router = restaurantRouter();</span><br><span class="line"> <span class="comment">// 转化为通用的Reactive HttpHandler</span></span><br><span class="line"> HttpHandler httpHandler = toHttpHandler(router);</span><br><span class="line"> <span class="comment">// 适配成Netty Server所需的Handler</span></span><br><span class="line"> ReactorHttpHandlerAdapter httpAdapter = <span class="keyword">new</span> ReactorHttpHandlerAdapter(httpHandler);</span><br><span class="line"> <span class="comment">// 创建Netty Server</span></span><br><span class="line"> HttpServer server = HttpServer.create(<span class="string">"localhost"</span>, <span class="number">9090</span>);</span><br><span class="line"> <span class="comment">// 注册Handler并启动Netty Server</span></span><br><span class="line"> server.newHandler(httpAdapter).block();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,使用Router Functions实现RP应用时,你需要自己创建和管理容器,也就是说Spring 5并没有针对Router Functions提供IoC支持,这是Router Functions和Spring MVC相比最大的不同。除此之外,你需要通过<code>RouterFunction</code>的API(而不是注解)来配置路由表和过滤器。对于简单的应用,这样做问题不大,但对于上规模的应用,就会导致两个问题:1)Router的定义越来越庞大;2)由于URI和Handler分开定义,路由表的维护成本越来越高。那为什么Spring 5会选择这种方式定义Router呢?接着往下看。</p><h4 id="2-2-2-自定义HandlerFunction"><a href="#2-2-2-自定义HandlerFunction" class="headerlink" title="2.2.2 自定义HandlerFunction"></a>2.2.2 自定义HandlerFunction</h4><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">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RestaurantHandler</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 扩展ReactiveCrudRepository接口,提供基本的CRUD操作</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> RestaurantRepository restaurantRepository;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * spring-boot-starter-data-mongodb-reactive提供的通用模板</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ReactiveMongoTemplate reactiveMongoTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">RestaurantHandler</span><span class="params">(RestaurantRepository restaurantRepository, ReactiveMongoTemplate reactiveMongoTemplate)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.restaurantRepository = restaurantRepository;</span><br><span class="line"> <span class="keyword">this</span>.reactiveMongoTemplate = reactiveMongoTemplate;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<ServerResponse> <span class="title">findAll</span><span class="params">(ServerRequest request)</span> </span>{</span><br><span class="line"> Flux<Restaurant> result = restaurantRepository.findAll();</span><br><span class="line"> <span class="keyword">return</span> ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<ServerResponse> <span class="title">findAllDelay</span><span class="params">(ServerRequest request)</span> </span>{</span><br><span class="line"> Flux<Restaurant> result = restaurantRepository.findAll().delayElements(Duration.ofSeconds(<span class="number">1</span>));</span><br><span class="line"> <span class="keyword">return</span> ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<ServerResponse> <span class="title">get</span><span class="params">(ServerRequest request)</span> </span>{</span><br><span class="line"> String id = request.pathVariable(<span class="string">"id"</span>);</span><br><span class="line"> Mono<Restaurant> result = restaurantRepository.findById(id);</span><br><span class="line"> <span class="keyword">return</span> ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<ServerResponse> <span class="title">create</span><span class="params">(ServerRequest request)</span> </span>{</span><br><span class="line"> Flux<Restaurant> restaurants = request.bodyToFlux(Restaurant<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> Flux<Restaurant> result = restaurants</span><br><span class="line"> .buffer(<span class="number">10000</span>)</span><br><span class="line"> .flatMap(rs -> reactiveMongoTemplate.insert(rs, Restaurant<span class="class">.<span class="keyword">class</span>))</span>;</span><br><span class="line"> <span class="keyword">return</span> ok().contentType(APPLICATION_JSON_UTF8).body(result, Restaurant<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<ServerResponse> <span class="title">delete</span><span class="params">(ServerRequest request)</span> </span>{</span><br><span class="line"> String id = request.pathVariable(<span class="string">"id"</span>);</span><br><span class="line"> Mono<Void> result = restaurantRepository.deleteById(id);</span><br><span class="line"> <span class="keyword">return</span> ok().contentType(APPLICATION_JSON_UTF8).build(result);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对比<a href="http://emacoo.cn/backend/spring5-reactive-tutorial/">上篇</a>的<code>RestaurantController</code>,主要有两点区别:</p><ul><li>所有方法的参数和返回值类型固定为ServerRequest和Mono<serverresponse>以符合<code>HandlerFunction</code>的定义,所有请求相关的对象(queryParam, pathVariable,header, session等)都通过ServerRequest获取。</serverresponse></li><li>由于去除了路由信息,<code>RestaurantHandler</code>变得非常函数化,可以说就是一组相关的<code>HandlerFunction</code>的集合,同时各个方法的可复用性也大为提升。这就回答了上一小节提出的疑问,即以牺牲可维护性为代价,换取更好的函数特性。</li></ul><h3 id="2-3-单元测试"><a href="#2-3-单元测试" class="headerlink" title="2.3 单元测试"></a>2.3 单元测试</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">SpringBootTest</span></span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">RestaurantHandlerTests</span> <span class="keyword">extends</span> <span class="title">BaseUnitTests</span> </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> RouterFunction<ServerResponse> restaurantRouter;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> WebTestClient <span class="title">prepareClient</span><span class="params">()</span> </span>{</span><br><span class="line"> WebTestClient webClient = WebTestClient.bindToRouterFunction(restaurantRouter)</span><br><span class="line"> .configureClient().baseUrl(<span class="string">"http://localhost:9090"</span>).responseTimeout(Duration.ofMinutes(<span class="number">1</span>)).build();</span><br><span class="line"> <span class="keyword">return</span> webClient;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>和针对Controller的单元测试相比,编写Handler的单元测试的主要区别在于初始化<code>WebTestClient</code>方式的不同,测试方法的主体可以完全复用。</p><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>到此,有关响应式编程的介绍就暂且告一段落。回顾这四篇文章,我先是从响应式宣言说起,然后介绍了响应式编程的基本概念和关键特性,并且详解了Spring 5中和响应式编程相关的新特性,最后以一个示例应用结尾。希望读完这些文章,对你理解响应式编程能有所帮助。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="http://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#web-reactive" target="_blank" rel="noopener">Spring Framework Reference - WebFlux framework</a></li><li><a href="https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/reactive/server" target="_blank" rel="noopener">spring-framework Reactive Tests</a></li><li><a href="https://github.com/poutsma/web-function-sample" target="_blank" rel="noopener">poutsma/web-function-sample</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring5-reactive-tutorial2/#disqus_thread</comments>
</item>
<item>
<title>【Spring 5】响应式Web框架实战(上)</title>
<link>http://emacoo.cn/backend/spring5-reactive-tutorial/</link>
<guid>http://emacoo.cn/backend/spring5-reactive-tutorial/</guid>
<pubDate>Wed, 28 Jun 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spa
</description>
<content:encoded><![CDATA[<blockquote><p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在内的大批新技术应运而生。其中以<a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a>和<a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>为代表的响应式(Reactive)编程技术针对的就是经典的大数据4V定义(Volume,Variety,Velocity,Value)中的Velocity,即高并发问题,而在即将发布的Spring 5中,也引入了响应式编程的支持。在接下来的几周,我会围绕响应式编程分三期与你分享我的一些学习心得。本篇是第三篇,通过一个简单的Spring 5示例应用,探一探即将于下月底发布的Spring 5的究竟。</p><p>前情概要:</p><ul><li><a href="http://emacoo.cn/backend/spring5-overview/">【Spring 5】响应式Web框架前瞻</a></li><li><a href="http://emacoo.cn/backend/reactive-overview/">响应式编程总览</a></li></ul></blockquote><h2 id="1-回顾"><a href="#1-回顾" class="headerlink" title="1 回顾"></a>1 回顾</h2><p>通过前两篇的介绍,相信你对响应式编程和Spring 5已经有了一个初步的了解。下面我将以一个简单的Spring 5应用为例,介绍如何使用Spring 5快速搭建一个响应式Web应用(以下简称RP应用)。</p><h2 id="2-实战"><a href="#2-实战" class="headerlink" title="2 实战"></a>2 实战</h2><h3 id="2-1-环境准备"><a href="#2-1-环境准备" class="headerlink" title="2.1 环境准备"></a>2.1 环境准备</h3><p>首先,从GitHub下载我的这个示例应用,地址是<a href="https://github.com/emac/spring5-features-demo" target="_blank" rel="noopener">https://github.com/emac/spring5-features-demo</a>。</p><p>然后,从MongoDB<a href="https://www.mongodb.com/download-center#community" target="_blank" rel="noopener">官网</a>下载最新版本的MongoDB,然后在命令行下运行<code>mongod &</code>启动服务。</p><p>现在,可以先试着跑一下项目中自带的测试用例。</p><p><code>./gradlew clean build</code></p><h3 id="2-2-依赖介绍"><a href="#2-2-依赖介绍" class="headerlink" title="2.2 依赖介绍"></a>2.2 依赖介绍</h3><p>接下来,看一下这个示例应用里的和响应式编程相关的依赖。</p><figure class="highlight plain"><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">compile('org.springframework.boot:spring-boot-starter-webflux')</span><br><span class="line">compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')</span><br><span class="line">testCompile('io.projectreactor.addons:reactor-test')</span><br></pre></td></tr></table></figure><ul><li>spring-boot-starter-webflux: 启用Spring 5的RP(Reactive Programming)支持,这是使用Spring 5开发RP应用的必要条件,就好比spring-boot-starter-web之于传统的Spring MVC应用。</li><li>spring-boot-starter-data-mongodb-reactive: Spring 5中新引入的针对MongoDB的Reactive Data扩展库,允许通过统一的RP风格的API操作MongoDB。</li><li>io.projectreactor.addons:reactor-test: <a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>(Spring 5默认使用的RP框架)提供的官方测试工具库。</li></ul><h3 id="2-3-示例代码"><a href="#2-3-示例代码" class="headerlink" title="2.3 示例代码"></a>2.3 示例代码</h3><p>不知道你是否还记得,在本系列第一篇<a href="http://emacoo.cn/backend/spring5-overview/">【Spring 5】响应式Web框架前瞻</a>里提到,Spring 5提供了Spring MVC注解和Router Functions两种方式来编写RP应用。本篇我就先用大家最熟悉的MVC注解来展示如何编写一个最简单的RP 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><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></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="class"><span class="keyword">class</span> <span class="title">RestaurantController</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 扩展ReactiveCrudRepository接口,提供基本的CRUD操作</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> RestaurantRepository restaurantRepository;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * spring-boot-starter-data-mongodb-reactive提供的通用模板</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ReactiveMongoTemplate reactiveMongoTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">RestaurantController</span><span class="params">(RestaurantRepository restaurantRepository, ReactiveMongoTemplate reactiveMongoTemplate)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.restaurantRepository = restaurantRepository;</span><br><span class="line"> <span class="keyword">this</span>.reactiveMongoTemplate = reactiveMongoTemplate;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping</span>(<span class="string">"/reactive/restaurants"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Flux<Restaurant> <span class="title">findAll</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurantRepository.findAll();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping</span>(<span class="string">"/reactive/restaurants/{id}"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<Restaurant> <span class="title">get</span><span class="params">(@PathVariable String id)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurantRepository.findById(id);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping</span>(<span class="string">"/reactive/restaurants"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Flux<Restaurant> <span class="title">create</span><span class="params">(@RequestBody Flux<Restaurant> restaurants)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurants</span><br><span class="line"> .buffer(<span class="number">10000</span>)</span><br><span class="line"> .flatMap(rs -> reactiveMongoTemplate.insert(rs, Restaurant<span class="class">.<span class="keyword">class</span>))</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@DeleteMapping</span>(<span class="string">"/reactive/restaurants/{id}"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<Void> <span class="title">delete</span><span class="params">(@PathVariable String id)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurantRepository.deleteById(id);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,实现一个RP Controller和一个普通的Controller是非常类似的,最核心的区别是,优先使用RP中最基础的两种数据类型,<code>Flux</code>(对应多值)和<code>Mono</code>(单值),尤其是方法的参数和返回值。即便是空返回值,也应封装为<code>Mono<Void></code>。这样做的目的是,使得应用能够以一种统一的符合RP规范的方式处理数据,最理想的情况是从最底层的数据库(或者其他系统外部调用),到最上层的Controller层,所有数据都不落地,经由各种<code>Flux</code>和<code>Mono</code>铺设的“管道”,直供调用端。就像农夫山泉那句著名的广告词,我们不生产水,我们只是大自然的搬运工。</p><h3 id="2-4-单元测试"><a href="#2-4-单元测试" class="headerlink" title="2.4 单元测试"></a>2.4 单元测试</h3><p>和非RP应用的单元测试相比,RP应用的单元测试主要是使用了一个Spring 5新引入的测试工具类,<code>WebTestClient</code>,专门用于测试RP应用。</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">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">SpringBootTest</span></span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">RestaurantControllerTests</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testNormal</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> <span class="comment">// start from scratch</span></span><br><span class="line"> restaurantRepository.deleteAll().block();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// prepare</span></span><br><span class="line"> WebTestClient webClient = WebTestClient.bindToController(<span class="keyword">new</span> RestaurantController(restaurantRepository, reactiveMongoTemplate)).build();</span><br><span class="line"> Restaurant[] restaurants = IntStream.range(<span class="number">0</span>, <span class="number">100</span>)</span><br><span class="line"> .mapToObj(String::valueOf)</span><br><span class="line"> .map(s -> <span class="keyword">new</span> Restaurant(s, s, s))</span><br><span class="line"> .toArray(Restaurant[]::<span class="keyword">new</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// create</span></span><br><span class="line"> webClient.post().uri(<span class="string">"/reactive/restaurants"</span>)</span><br><span class="line"> .accept(MediaType.APPLICATION_JSON_UTF8)</span><br><span class="line"> .syncBody(restaurants)</span><br><span class="line"> .exchange()</span><br><span class="line"> .expectStatus().isOk()</span><br><span class="line"> .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)</span><br><span class="line"> .expectBodyList(Restaurant<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class"> .<span class="title">hasSize</span>(100)</span></span><br><span class="line"><span class="class"> .<span class="title">consumeWith</span>(<span class="title">rs</span> -> <span class="title">Flux</span>.<span class="title">fromIterable</span>(<span class="title">rs</span>)</span></span><br><span class="line"><span class="class"> .<span class="title">log</span>()</span></span><br><span class="line"><span class="class"> .<span class="title">subscribe</span>(<span class="title">r1</span> -> </span>{</span><br><span class="line"> <span class="comment">// get</span></span><br><span class="line"> webClient.get()</span><br><span class="line"> .uri(<span class="string">"/reactive/restaurants/{id}"</span>, r1.getId())</span><br><span class="line"> .accept(MediaType.APPLICATION_JSON_UTF8)</span><br><span class="line"> .exchange()</span><br><span class="line"> .expectStatus().isOk()</span><br><span class="line"> .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)</span><br><span class="line"> .expectBody(Restaurant<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class"> .<span class="title">consumeWith</span>(<span class="title">r2</span> -> <span class="title">Assert</span>.<span class="title">assertEquals</span>(<span class="title">r1</span>, <span class="title">r2</span>))</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>创建<code>WebTestClient</code>实例时,首先要绑定一下待测试的RP Controller。可以看到,和业务类一样,编写RP应用的单元测试,同样也是数据不落地的流式风格。</p><p>在示例应用中可以找到更多的单元测试。</p><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>以上就是Spring 5里第一种,相信也将会是最常用的编写RP应用的实现方式。介于篇幅原因,这篇就先到这里。下篇我将详细介绍第二种方式,Router Functions。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="http://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#web-reactive" target="_blank" rel="noopener">Spring Framework Reference - WebFlux framework</a></li><li><a href="https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/reactive/server" target="_blank" rel="noopener">spring-framework Reactive Tests</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring5-reactive-tutorial/#disqus_thread</comments>
</item>
<item>
<title>响应式编程总览</title>
<link>http://emacoo.cn/backend/reactive-overview/</link>
<guid>http://emacoo.cn/backend/reactive-overview/</guid>
<pubDate>Tue, 20 Jun 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spa
</description>
<content:encoded><![CDATA[<blockquote><p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在内的大批新技术应运而生。其中以<a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a>和<a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>为代表的响应式(Reactive)编程技术针对的就是经典的大数据4V定义(Volume,Variety,Velocity,Value)中的Velocity,即高并发问题,而在即将发布的Spring 5中,也引入了响应式编程的支持。在接下来的几周,我会围绕响应式编程分三期与你分享我的一些学习心得。本篇是第二篇,以Reactor框架为例介绍响应式编程的几个关键特性。</p><p>前情概要:</p><ul><li><a href="http://emacoo.cn/backend/spring5-overview/">【Spring 5】响应式Web框架前瞻</a></li></ul></blockquote><h2 id="1-响应式编程总览"><a href="#1-响应式编程总览" class="headerlink" title="1 响应式编程总览"></a>1 响应式编程总览</h2><blockquote><p>In computing, reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. - <a href="https://en.wikipedia.org/wiki/Reactive_programming" target="_blank" rel="noopener">Reactive programming - Wikipedia</a></p></blockquote><p>在上述响应式编程(以下简称RP)的定义中,除了异步编程,还包含两个重要的关键词:</p><ul><li>Data streams: 即数据流,分为静态数据流(比如数组,文件)和动态数据流(比如事件流,日志流)两种。基于数据流模型,RP得以提供一套统一的Stream风格的数据处理接口。和Java 8中的Stream API相比,RP API除了支持静态数据流,还支持动态数据流,并且允许复用和同时接入多个订阅者。</li><li>The propagation of change: 变化传播,简单来说就是以一个数据流为输入,经过一连串操作转化为另一个数据流,然后分发给各个订阅者的过程。这就有点像函数式编程中的组合函数,将多个函数串联起来,把一组输入数据转化为格式迥异的输出数据。</li></ul><p>一个容易混淆的概念是响应式设计,虽然它的名字中也包含了“响应式”三个字,但其实和RP完全是两码事。响应式设计是指网页能够自动调整布局和样式以适配不同尺寸的屏幕,属于网站设计的范畴,而RP是一种关注系统可响应性,面向数据流的编程思想或者说编程框架。</p><h3 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h3><p>从本质上说,RP是一种异步编程框架,和其他框架相比,RP至少包含了以下三个特性:</p><ul><li>描述而非执行:在你最终调用<code>subscribe()</code>方法之前,从发布端到订阅端,没有任何事会发生。就好比无论多长的水管,只要水龙头不打开,水管里的水就不会流动。为了提高描述能力,RP提供了比Stream丰富的多的多的API,比如<code>buffer()</code>, <code>merge()</code>, <code>onErrorMap()</code>等。</li><li>提高吞吐量: 类似于HTTP/2中的连接复用,RP通过线程复用来提高吞吐量。在传统的Servlet容器中,每来一个请求就会发起一个线程进行处理。受限于机器硬件资源,单台服务器所能支撑的线程数是存在一个上限的,假设为T,那么应用同时能处理的请求数(吞吐量)必然也不会超过T。但对于一个使用<a href="http://emacoo.cn/backend/spring5-overview/">Spring 5</a>开发的RP应用,如果运行在像Netty这样的异步容器中,无论有多少个请求,用于处理请求的线程数是相对固定的,因此最大吞吐量就有可能超过T。</li><li>背压(Backpressure)支持:简单来说,背压就是一种反馈机制。在一般的Push模型中,发布者既不知道也不关心订阅者的处理速度,当数据的发布速度超过处理速度时,需要订阅者自己决定是缓存还是丢弃。如果使用RP,决定权就交回给发布者,订阅者只需要根据自己的处理能力问发布者请求相应数量的数据。你可能会问这不就是Pull模型吗?其实是不同的。在Pull模型中,订阅者每次处理完数据,都要重新发起一次请求拉取新的数据,而使用背压,订阅者只需要发起一次请求,就能连续不断的重复请求数据。</li></ul><h3 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h3><p>了解了RP的这些特性,你可能已经猜想到RP有哪些适用场景了。一般来说,RP适用于高并发、带延迟操作的场景,比如以下这些情况(的组合):</p><ul><li>一次请求涉及多次外部服务调用</li><li>非可靠的网络传输</li><li>高并发下的消息处理</li><li>弹性计算网络</li></ul><h3 id="代价"><a href="#代价" class="headerlink" title="代价"></a>代价</h3><blockquote><p>Every coin has two sides.</p></blockquote><p>和任何框架一样,有优势必然就有劣势。RP的两个比较大的问题是:</p><ul><li>虽然复用线程有助于提高吞吐量,但一旦在某个回调函数中线程被卡住,那么这个线程上所有的请求都会被阻塞,最严重的情况,整个应用会被拖垮。</li><li>难以调试。由于RP强大的描述能力,在一个典型的RP应用中,大部分代码都是以链式表达式的形式出现,比如<code>flux.map(String::toUpperCase).doOnNext(s -> LOG.info("UC String {}", s)).next().subscribe()</code>,一旦出错,你将很难定位到具体是哪个环节出了问题。所幸的是,RP框架一般都会提供一些工具方法来辅助进行调试。</li></ul><h2 id="2-Reactor实战"><a href="#2-Reactor实战" class="headerlink" title="2 Reactor实战"></a>2 Reactor实战</h2><p>为了帮助你理解上面说的一些概念,下面我就通过几个测试用例,演示RP的两个关键特性:提高吞吐量和背压。完整的代码可参见我GitHub上的<a href="https://github.com/emac/demo-reactor" target="_blank" rel="noopener">示例工程</a>。</p><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testImperative</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> _runInParallel(CONCURRENT_SIZE, () -> {</span><br><span class="line"> ImperativeRestaurantRepository.INSTANCE.insert(load);</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">_runInParallel</span><span class="params">(<span class="keyword">int</span> nThreads, Runnable task)</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> ExecutorService executorService = Executors.newFixedThreadPool(nThreads);</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < nThreads; i++) {</span><br><span class="line"> executorService.submit(task);</span><br><span class="line"> }</span><br><span class="line"> executorService.shutdown();</span><br><span class="line"> executorService.awaitTermination(<span class="number">1</span>, TimeUnit.MINUTES);</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testReactive</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> CountDownLatch latch = <span class="keyword">new</span> CountDownLatch(CONCURRENT_SIZE);</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < CONCURRENT_SIZE; i++) {</span><br><span class="line"> ReactiveRestaurantRepository.INSTANCE.insert(load).subscribe(s -> {</span><br><span class="line"> }, e -> latch.countDown(), latch::countDown);</span><br><span class="line"> }</span><br><span class="line"> latch.await();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用例解读:</p><ul><li>第一个测试用例使用的是多线程+MongoDB Driver,同时起100个线程,每个线程往MongoDB中插入10000条数据,总共100万条数据,平均用时15秒左右。</li><li>第二个测试用例使用的是Reactor+MongoDB Reactive Streams Driver,同样是插入100万条数据,平均用时不到10秒,吞吐量提高了50%!</li></ul><h3 id="背压"><a href="#背压" class="headerlink" title="背压"></a>背压</h3><p>在演示测试用例之前,先看两张图,帮助你更形象的理解什么是背压。</p><p><img src="backpressure-pull.jpg" alt></p><p><img src="backpressure-push.jpg" alt></p><p><em>图片出处:<a href="https://www.slideshare.net/StephaneManciot/psug-52-dataflow-and-simplified-reactive-programming-with-akkastreams" target="_blank" rel="noopener">Dataflow and simplified reactive programming</a></em></p><p>两张图乍一看没啥区别,但其实是完全两种不同的背压策略。第一张图,发布速度(100/s)远大于订阅速度(1/s),但由于背压的关系,发布者严格按照订阅者的请求数量发送数据。第二张图,发布速度(1/s)小于订阅速度(100/s),当订阅者请求100个数据时,发布者会积满所需个数的数据再开始发送。可以看到,通过背压机制,发布者可以根据各个订阅者的能力动态调整发布速度。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@BeforeEach</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeEach</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// initialize publisher</span></span><br><span class="line"> AtomicInteger count = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line"> timerPublisher = Flux.create(s -></span><br><span class="line"> <span class="keyword">new</span> Timer().schedule(<span class="keyword">new</span> TimerTask() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"> s.next(count.getAndIncrement());</span><br><span class="line"> <span class="keyword">if</span> (count.get() == <span class="number">10</span>) {</span><br><span class="line"> s.complete();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }, <span class="number">100</span>, <span class="number">100</span>)</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testNormal</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> CountDownLatch latch = <span class="keyword">new</span> CountDownLatch(<span class="number">1</span>);</span><br><span class="line"> timerPublisher</span><br><span class="line"> .subscribe(r -> System.out.println(<span class="string">"Continuous consuming "</span> + r),</span><br><span class="line"> e -> latch.countDown(),</span><br><span class="line"> latch::countDown);</span><br><span class="line"> latch.await();</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testBackpressure</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"> CountDownLatch latch = <span class="keyword">new</span> CountDownLatch(<span class="number">1</span>);</span><br><span class="line"> AtomicReference<Subscription> timerSubscription = <span class="keyword">new</span> AtomicReference<>();</span><br><span class="line"> Subscriber<Integer> subscriber = <span class="keyword">new</span> BaseSubscriber<Integer>() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">hookOnSubscribe</span><span class="params">(Subscription subscription)</span> </span>{</span><br><span class="line"> timerSubscription.set(subscription);</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">hookOnNext</span><span class="params">(Integer value)</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"consuming "</span> + value);</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">hookOnComplete</span><span class="params">()</span> </span>{</span><br><span class="line"> latch.countDown();</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">hookOnError</span><span class="params">(Throwable throwable)</span> </span>{</span><br><span class="line"> latch.countDown();</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"> timerPublisher.onBackpressureDrop().subscribe(subscriber);</span><br><span class="line"> <span class="keyword">new</span> Timer().schedule(<span class="keyword">new</span> TimerTask() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"> timerSubscription.get().request(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"> }, <span class="number">100</span>, <span class="number">200</span>);</span><br><span class="line"> latch.await();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用例解读:</p><ul><li>第一个测试用例演示了在理想情况下,即订阅者的处理速度能够跟上发布者的发布速度(以100ms为间隔产生10个数字),控制台从0打印到9,一共10个数字,和发布端一致。</li><li>第二个测试用例故意调慢了订阅者的处理速度(每200ms处理一个数字),同时发布者采用了Drop的背压策略,结果控制台只打印了一半的数字(0,2,4,6,8),另外一半的数字由于背压的原因被发布者Drop掉了,并没有发给订阅者。</li></ul><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>通过上面的介绍,不难看出RP实际上是一种内置了发布者订阅者模型的异步编程框架,包含了线程复用,背压等高级特性,特别适用于高并发、有延迟的场景。</p><p>以上就是我对响应式编程的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。下一篇我将综合前两篇的内容,详解一个完整的Spring 5示例应用,敬请期待。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="https://spring.io/blog/2016/04/19/understanding-reactive-types" target="_blank" rel="noopener">Understanding Reactive types</a></li><li><a href="https://www.slideshare.net/SpringCentral/designing-implementing-and-using-reactive-apis" target="_blank" rel="noopener">Designing, Implementing, and Using Reactive APIs</a></li><li><a href="https://www.slideshare.net/SpringCentral/imperative-to-reactive-web-applications" target="_blank" rel="noopener">Imperative to Reactive Web Applications</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/reactive-overview/#disqus_thread</comments>
</item>
<item>
<title>【Spring 5】响应式Web框架前瞻</title>
<link>http://emacoo.cn/backend/spring5-overview/</link>
<guid>http://emacoo.cn/backend/spring5-overview/</guid>
<pubDate>Mon, 29 May 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spa
</description>
<content:encoded><![CDATA[<blockquote><p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在内的大批新技术应运而生。其中以<a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a>和<a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>为代表的响应式(Reactive)编程技术针对的就是经典的大数据4V定义(Volume,Variety,Velocity,Value)中的Velocity,即高并发问题,而在即将发布的Spring 5中,也引入了响应式编程的支持。在接下来的几周,我会围绕响应式编程分三期与你分享我的一些学习心得。作为第一篇,首先从Spring 5谈起。</p></blockquote><h2 id="1-响应式宣言"><a href="#1-响应式宣言" class="headerlink" title="1 响应式宣言"></a>1 响应式宣言</h2><p>和<a href="http://agilemanifesto.org/" target="_blank" rel="noopener">敏捷宣言</a>一样,说起响应式编程,必先提到响应式宣言。</p><blockquote><p>We want systems that are Responsive, Resilient, Elastic and Message Driven. We call these Reactive Systems. - <a href="http://www.reactivemanifesto.org" target="_blank" rel="noopener">The Reactive Manifesto</a></p></blockquote><p><img src="reactive-manifesto.png" alt></p><p><em>图片出处:<a href="http://www.reactivemanifesto.org/" target="_blank" rel="noopener">The Reactive Manifesto</a></em></p><p>不知道是不是为了向敏捷宣言致敬,响应式宣言中也包含了4组关键词:</p><ul><li>Responsive: 可响应的。要求系统尽可能做到在任何时候都能及时响应。</li><li>Resilient: 可恢复的。要求系统即使出错了,也能保持可响应性。</li><li>Elastic: 可伸缩的。要求系统在各种负载下都能保持可响应性。</li><li>Message Driven: 消息驱动的。要求系统通过异步消息连接各个组件。</li></ul><p>可以看到,对于任何一个响应式系统,首先要保证的就是可响应性,否则就称不上是响应式系统。从这个意义上来说,动不动就蓝屏的Windows系统显然不是一个响应式系统。</p><p>PS: 如果你赞同响应式宣言,不妨到<a href="http://www.reactivemanifesto.org" target="_blank" rel="noopener">官网</a>上留下的你电子签名,我的编号是18989,试试看能不能找到我。</p><h2 id="2-Spring-5前瞻"><a href="#2-Spring-5前瞻" class="headerlink" title="2 Spring 5前瞻"></a>2 Spring 5前瞻</h2><p>作为Java世界首个响应式Web框架,Spring 5最大的亮点莫过于提供了完整的端到端响应式编程的支持。</p><p><img src="webflux-overview.png" alt></p><p><em>图片出处:<a href="http://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/" target="_blank" rel="noopener">Spring Framework Reference Documentation</a></em></p><p>左侧是传统的基于Servlet的Spring Web MVC框架,右侧是5.0版本新引入的基于Reactive Streams的Spring WebFlux框架,从上到下依次是Router Functions,WebFlux,Reactive Streams三个新组件。</p><ul><li>Router Functions: 对标@Controller,@RequestMapping等标准的Spring MVC注解,提供一套函数式风格的API,用于创建Router,Handler和Filter。</li><li>WebFlux: 核心组件,协调上下游各个组件提供响应式编程支持。</li><li><a href="http://www.reactive-streams.org/" target="_blank" rel="noopener">Reactive Streams</a>: 一种支持背压(Backpressure)的异步数据流处理标准,主流实现有RxJava和Reactor,Spring WebFlux默认集成的是Reactor。</li></ul><p>在Web容器的选择上,Spring WebFlux既支持像Tomcat,Jetty这样的的传统容器(前提是支持Servlet 3.1 Non-Blocking IO API),又支持像Netty,Undertow那样的异步容器。不管是何种容器,Spring WebFlux都会将其输入输出流适配成<code>Flux<DataBuffer></code>格式,以便进行统一处理。</p><p>值得一提的是,除了新的Router Functions接口,Spring WebFlux同时支持使用老的Spring MVC注解声明Reactive Controller。和传统的MVC Controller不同,Reactive Controller操作的是非阻塞的ServerHttpRequest和ServerHttpResponse,而不再是Spring MVC里的HttpServletRequest和HttpServletResponse。</p><p>下面是我GitHub上的示例工程里的一个<a href="https://github.com/emac/spring5-features-demo/blob/master/src/main/java/cn/emac/demo/spring5/reactive/controllers/RestaurantController.java" target="_blank" rel="noopener">例子</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="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RestaurantController</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> RestaurantRepository restaurantRepository;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ReactiveMongoTemplate reactiveMongoTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">RestaurantController</span><span class="params">(RestaurantRepository restaurantRepository, ReactiveMongoTemplate reactiveMongoTemplate)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.restaurantRepository = restaurantRepository;</span><br><span class="line"> <span class="keyword">this</span>.reactiveMongoTemplate = reactiveMongoTemplate;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping</span>(<span class="string">"/reactive/restaurants"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Flux<Restaurant> <span class="title">findAll</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurantRepository.findAll();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@GetMapping</span>(<span class="string">"/reactive/restaurants/{id}"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Mono<Restaurant> <span class="title">get</span><span class="params">(@PathVariable String id)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> restaurantRepository.findById(id);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@PostMapping</span>(<span class="string">"/reactive/restaurants"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Flux<Restaurant> <span class="title">create</span><span class="params">(@RequestBody Restaurant[] restaurants)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Flux.just(restaurants)</span><br><span class="line"> .log()</span><br><span class="line"> .flatMap(r -> Mono.just(r).subscribeOn(Schedulers.parallel()), <span class="number">10</span>)</span><br><span class="line"> .flatMap(reactiveMongoTemplate::insert);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>除了响应式编程支持,Spring 5还包括了很多Java程序员期待已久的特性,包括JDK 9,<a href="http://emacoo.cn/arch/junit5/">Junit 5</a>,Servlet 4以及HTTP/2支持。目前Spring 5的最新版本是<a href="https://spring.io/blog/2017/05/08/spring-framework-5-0-goes-rc1" target="_blank" rel="noopener">RC1</a>,而Spring Boot也刚刚发布了<a href="https://spring.io/blog/2017/05/16/spring-boot-2-0-0-m1-available-now" target="_blank" rel="noopener">2.0.0 M1</a>版本。根据Spring<a href="https://spring.io/blog/2015/12/03/spring-framework-5-0-roadmap-update" target="_blank" rel="noopener">官方博客</a>,Spring 5将在JDK 9 <a href="http://www.java9countdown.xyz/" target="_blank" rel="noopener">GA</a>之后随即发布,也就是今年的7月底前后。</p><p>以上就是我对Spring 5中有关响应式编程支持的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。下一篇我将聊一下我对响应式编程的一些理解,敬请期待。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework" target="_blank" rel="noopener">New in Spring 5: Functional Web Framework</a></li><li><a href="https://www.slideshare.net/InfoQ/spring-framework-5-preview-roadmap" target="_blank" rel="noopener">Spring Framework 5 - Preview & Roadmap</a></li><li><a href="https://www.slideshare.net/AliakseiZhynhiarousk/spring-framework-5-history-and-reactive-features" target="_blank" rel="noopener">Spring Framework 5: History and Reactive features</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring5-overview/#disqus_thread</comments>
</item>
<item>
<title>面向开发的测试技术(三):Web自动化测试</title>
<link>http://emacoo.cn/arch/test-web-automation/</link>
<guid>http://emacoo.cn/arch/test-web-automation/</guid>
<pubDate>Sun, 14 May 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-D
</description>
<content:encoded><![CDATA[<blockquote><p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-Driven Development)</a>开发理念以来,开发和测试的边界变的越来越模糊,从原本上下游的依赖关系,逐步演变成你中有我、我中有你的互赖关系,甚至很多公司设立了新的QE(Quality Engineer)职位。和传统的QA(Quality Assurance)不同,QE的主要职责是通过工程化的手段保证项目质量,这些手段包括但不仅限于编写单元测试、集成测试,搭建自动化测试流程,设计性能测试等。可以说,QE身上兼具了QA的质量意识和开发的工程能力。我会从开发的角度分三期聊聊QE这个亦测试亦开发的角色所需的基本技能。</p><p>前情概要:</p><ul><li><a href="http://emacoo.cn/arch/test-mock">面向开发的测试技术(一):Mock</a></li><li><a href="http://emacoo.cn/arch/test-performance">面向开发的测试技术(二):性能测试</a></li></ul></blockquote><h2 id="1-QE的成人礼:从功能测试到自动化测试"><a href="#1-QE的成人礼:从功能测试到自动化测试" class="headerlink" title="1 QE的成人礼:从功能测试到自动化测试"></a>1 QE的成人礼:从功能测试到自动化测试</h2><p>作为QE三部曲的最后一篇,这篇我们聊一下Web自动化测试。相比于前两篇Mock技术和性能测试,自动化测试可以说是最接近传统功能测试(也即手工测试)的一种测试技术,也可以说是区分QE和QA的分水岭。而Web自动化测试作为最常见的一类自动化测试,相关的资料和工具也是最丰富的。</p><h2 id="2-自动化测试的利弊"><a href="#2-自动化测试的利弊" class="headerlink" title="2 自动化测试的利弊"></a>2 自动化测试的利弊</h2><p>在介绍具体的Web自动化测试技术之前,首先看一下自动化测试和功能测试的区别。在我看来,两者最大的区别在于测试人员身份的不同。在功能测试中,测试人员既要设计测试用例,又要运行手工测试,既是导演又是演员,既是教练又是球员。而在自动化测试中,演员和球员的角色都被机器所取代,测试人员只负责设计测试用例和编写自动化测试脚本。除此之外,相对于功能测试,自动化测试的不同还包括:</p><h3 id="2-1-自动化测试的优势"><a href="#2-1-自动化测试的优势" class="headerlink" title="2.1 自动化测试的优势"></a>2.1 自动化测试的优势</h3><ul><li>更快的测试速度,带来更高的测试效率。一般而言,运行一遍功能测试都要以小时为单位,有的甚至以天为单位。而自动化测试则一般都在分钟级别,如果运行在分布式环境下,甚至可以降到秒级。由此可见,通过自动化测试,测试人员可以省去大量的手工测试时间,从而有更多时间去熟悉业务和完善测试用例,在提高自身测试效率的同时,也有助于提升整体的软件质量。</li><li>提高测试覆盖率。要理解这一点,首先要从<a href="http://www.jianshu.com/p/ab31fef12f2f" target="_blank" rel="noopener">正交测试法</a>说起。假设一个测试场景涉及3个测试因素,每个测试因素有3种可能的取值(水平),那么根据正交测试法,总共需要设计8个(<code>因素数*(最大水平数-1)+1</code>)测试用例。测试场景越复杂,所需的测试用例越多。当测试场景的复杂度超过一定程度后,纯手工的功能测试显然就无力覆盖所有的测试用例了,并且随着复杂度的升高,测试覆盖率会越来越低。然而,借助自动化测试脚本,无论测试场景多复杂,都能保证一定的测试覆盖率。</li><li>更好的稳定性和可扩展性。功能测试靠人,自动化测试靠机器,因此,无论是运行测试的稳定性,还是测试能力的可扩展性(比如从测试1个应用变为测试10个应用),自动化测试都远超功能测试。</li></ul><p>根据上面的描述,你就不难推导出自动化测试适用的测试场景了:</p><ul><li>回归测试。每一次应用发布,都伴随着一次回归测试。对于重复性的工作,机器显然更适合。</li><li>兼容性测试。不管是Web测试,还是App测试,兼容性测试都是必不可少的一环。以Web测试为例,同样的测试用例,需要在不同的浏览器上分别运行一遍,这对测试人员而言不可谓不是一种折磨。</li><li>大规模测试。如果一次测试涉及的测试用例过多(比如100+),功能测试难免会有遗漏或者重复,而自动化测试可以轻松确保一个不少,一个也不多。</li></ul><p>一图以蔽之,自动化测试的优势可概括为下图:</p><p><img src="web-automation.png" alt></p><h3 id="2-2-自动化测试的局限"><a href="#2-2-自动化测试的局限" class="headerlink" title="2.2 自动化测试的局限"></a>2.2 自动化测试的局限</h3><p>说了这么多自动化测试的好处,但自动化测试也不是万能的,再来看一下它的局限所在:</p><ul><li>不低的技术门槛。不论是使用哪种自动化测试框架,对于测试人员而言,都存在一定的技术门槛,一般至少需要学习并掌握一门编程语言。</li><li>可观的开发成本和维护成本。跟任何程序一样,无论是编写自动化测试脚本,还是在需求变化时修改脚本,都需要花费大量的时间。</li><li>需求要稳定。自动化测试的前提是测试用例要稳定,而测试用例稳定的前提是需求要稳定。对于临时的或者说一次性的需求,自动化测试往往是得不偿失的。</li><li>应用周期长。应用的生命周期越长,自动化测试节省的时间越多,带来的价值也越大。</li></ul><p>应该说,功能测试是自动化测试的基础,自动化测试是功能测试的补充,两者相互依赖,又相互促进。测试人员两手都要抓,两手都要硬。</p><h2 id="3-如何进行Web自动化测试?"><a href="#3-如何进行Web自动化测试?" class="headerlink" title="3 如何进行Web自动化测试?"></a>3 如何进行Web自动化测试?</h2><p>接下来我以<a href="http://www.seleniumhq.org/" target="_blank" rel="noopener">Selenium</a>为例,介绍一下如何进行Web自动化测试。</p><h3 id="3-1-Selenium简介"><a href="#3-1-Selenium简介" class="headerlink" title="3.1 Selenium简介"></a>3.1 Selenium简介</h3><p>Selenium是目前最流行的Web自动化测试框架之一,支持主流的浏览器和操作系统,同时支持多种编程语言接入。无论是测试,还是开发,都可以轻松上手。最新的版本是3.4.0。</p><p>同类的Web自动化测试框架还有:</p><ul><li>开源:<a href="https://watir.com/" target="_blank" rel="noopener">Watir</a>, <a href="http://www.sikuli.org/" target="_blank" rel="noopener">Sikuli</a>, <a href="http://www.fitnesse.org/" target="_blank" rel="noopener">FitNess</a></li><li>商业:<a href="https://saas.hpe.com/en-us/software/uft" target="_blank" rel="noopener">HP UFT(QTP)</a>, <a href="https://www.ibm.com/developerworks/downloads/r/rft/" target="_blank" rel="noopener">IBM RFT</a></li></ul><p><img src="selenium-vs-others.png" alt></p><p><em>图片出处:<a href="https://www.edureka.co/testing-with-selenium-webdriver" target="_blank" rel="noopener">https://www.edureka.co/testing-with-selenium-webdriver</a></em></p><h4 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h4><ul><li>Selenium IDE: 一款Firefox插件,以图形化方式支持录制脚本、自动生成脚本等功能。用于本地开发和调试TC(Test Case)。</li><li>Selenium WebDriver: 通过各浏览器厂商提供的原生Driver,指挥浏览器进行各类页面操作。</li></ul><p><img src="selenium-webdriver.png" alt></p><p><em>图片出处:<a href="https://www.slideshare.net/sethmcl/join-the-darkside-nightwatchjs" target="_blank" rel="noopener">Join the darkside: Selenium testing with Nightwatch.js</a></em></p><ul><li>Selenium RC(已废弃): 通过植入统一的JS脚本,指挥浏览器进行各类页面操作。兼容性比较差,2.0以后已废弃。</li><li>Selenium Grid: 适用于分布式环境下运行大量的TC,Hub根据TC的环境要求分发给各个符合条件的Node执行。</li></ul><p><img src="selenium-grid.png" alt></p><p><em>图片出处:<a href="https://www.slideshare.net/sethmcl/join-the-darkside-nightwatchjs" target="_blank" rel="noopener">Join the darkside: Selenium testing with Nightwatch.js</a></em></p><h4 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h4><ul><li>多浏览器支持:除了三大浏览器Firefox, Chrome, IE之外,还支持Android, iOS内置的浏览器。</li><li>多平台支持:三大操作系统Linux, Mac, Windows上面都可以运行。</li><li>多语言支持:可以用Python, Java, Node, Ruby等编写TC。</li><li>录制脚本(仅限IDE):记录Firefox上的各类页面操作,自动生成HTML格式的TC。</li><li>自动生成脚本(仅限IDE):将录制的HTML格式的TC转化成任意其他语言的TC。</li><li>Headless:支持在命令行下,执行各类TC脚本。</li><li>分布式支持:通过Selenium Grid将TC分发到各个节点执行。</li></ul><h3 id="3-2-入门:Selenium-IDE"><a href="#3-2-入门:Selenium-IDE" class="headerlink" title="3.2 入门:Selenium IDE"></a>3.2 入门:Selenium IDE</h3><p>首先安装Firefox,然后下载<a href="https://addons.mozilla.org/en-US/firefox/addon/selenium-ide/" target="_blank" rel="noopener">Selenium IDE插件</a>。通过Firefox的Tools->Selenium IDE菜单项可以启动Selenium IDE,操作界面如下:</p><p><img src="selenium-ide.png" alt></p><p>使用Selenium IDE生成自动化测试脚本的一般步骤是:</p><ol><li>选择Action->Record菜单项或者点击右上角的小红点录制原始测试脚本</li><li>以调试模式运行脚本-查看日志-修改脚本直至脚本可以稳定的运行</li><li>保存测试脚本(仅限IDE运行)或者通过File->Export Test Suites As…菜单项导出其他语言的测试脚本(可在命令行下运行)</li></ol><p>进一步信息可以参考<a href="http://www.seleniumhq.org/docs/02_selenium_ide.jsp" target="_blank" rel="noopener">官方文档</a>。</p><h3 id="3-3-进阶:Selenium-WebDriver"><a href="#3-3-进阶:Selenium-WebDriver" class="headerlink" title="3.3 进阶:Selenium WebDriver"></a>3.3 进阶:Selenium WebDriver</h3><p>以Python3 + Firefox为例,</p><p>1) 命令行下运行<code>pip install selenium==3.3.0</code>安装selenium</p><ul><li>由于最新版的Selenium Python package<a href="https://github.com/SeleniumHQ/selenium/issues/3808" target="_blank" rel="noopener">不支持Grid</a>,只能降级安装3.3.0版本</li></ul><p>2) 下载<a href="https://github.com/mozilla/geckodriver/releases" target="_blank" rel="noopener">Mozilla GeckoDriver</a>,解压然后添加到系统Path</p><p>准备就绪后,打开命令行,试着运行之前从Selenium IDE导出的Python测试脚本,也可以直接手写脚本。</p><p>示例脚本:</p><figure class="highlight python"><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">from</span> selenium <span class="keyword">import</span> webdriver</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.common.keys <span class="keyword">import</span> Keys</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.support.ui <span class="keyword">import</span> WebDriverWait</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.support <span class="keyword">import</span> expected_conditions <span class="keyword">as</span> EC</span><br><span class="line"></span><br><span class="line">browser = webdriver.Firefox()</span><br><span class="line">browser.get(<span class="string">'http://www.baidu.com/'</span>)</span><br><span class="line"><span class="keyword">assert</span> <span class="string">'百度一下,你就知道'</span> == browser.title</span><br><span class="line">kw = browser.find_element_by_id(<span class="string">"kw"</span>);</span><br><span class="line">kw.send_keys(<span class="string">'selenium'</span>)</span><br><span class="line">kw.send_keys(Keys.RETURN)</span><br><span class="line"></span><br><span class="line">WebDriverWait(browser, <span class="number">10</span>).until(EC.title_contains(<span class="string">'selenium_百度搜索'</span>))</span><br><span class="line"><span class="keyword">assert</span> browser.find_element_by_css_selector(<span class="string">"div.nums"</span>).is_displayed()</span><br><span class="line">print(<span class="string">"Test pass!"</span>)</span><br><span class="line">browser.quit()</span><br></pre></td></tr></table></figure><p>进一步信息可以参考<a href="http://www.seleniumhq.org/docs/03_webdriver.jsp" target="_blank" rel="noopener">官方文档</a>和<a href="https://seleniumhq.github.io/selenium/docs/api/py/api.html" target="_blank" rel="noopener">Selenium Python API</a>。</p><h3 id="3-4-高阶:Selenium-Grid"><a href="#3-4-高阶:Selenium-Grid" class="headerlink" title="3.4 高阶:Selenium Grid"></a>3.4 高阶:Selenium Grid</h3><p>前面提到,使用Selenium Grid可以轻松搭建一个分布式的自动化测试环境,特别适合运行大规模的测试用例和兼容性测试(各个节点运行不同的WebDriver)。</p><p>利用官方提供的<a href="https://hub.docker.com/u/selenium/" target="_blank" rel="noopener">Docker镜像</a>,可以在本地启动多个容器来搭建一个Selenium Grid环境,以2个运行<a href="http://phantomjs.org/" target="_blank" rel="noopener">phantomjs</a> WebDriver的节点的Grid为例:</p><ol><li>启动Hub: docker run -d -p 4444:4444 –name hub selenium/hub</li><li>启动Node-1: docker run -d –link hub:hub –name pnode1 selenium/node-phantomjs</li><li>启动Node-2: docker run -d –link hub:hub –name pnode2 selenium/node-phantomjs</li></ol><p>等所有容器成功启动之后,打开浏览器访问<code>http://<ip-of-local-docker-machine:4444></code>,就可以看到Selenium Grid的控制台了。</p><p><img src="selenium-grid-demo.png" alt></p><p>然后修改测试脚本指向本地Selenium Grid的服务地址,就可以通过Selenium Grid运行测试了。</p><figure class="highlight python"><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">browser = webdriver.Remote(</span><br><span class="line">command_executor=<span class="string">"http://192.168.99.100:4444/wd/hub"</span>, </span><br><span class="line">desired_capabilities={<span class="string">'browserName'</span>: <span class="string">'phantomjs'</span>})</span><br><span class="line">browser.implicitly_wait(<span class="number">30</span>)</span><br></pre></td></tr></table></figure><p>进一步信息信息参考<a href="http://www.seleniumhq.org/docs/07_selenium_grid.jsp" target="_blank" rel="noopener">官方文档</a>和<a href="https://github.com/SeleniumHQ/selenium/wiki/Grid2" target="_blank" rel="noopener">Wiki</a>。</p><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上就是我对自动化测试的一些见解,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。</p><p>至此,有关QE所需掌握的3个基本测试技术的介绍就告一段落。无论是Mock,还是性能测试,自动化测试,本质上都只有一个目的,解放测试人员的生产力,让测试人员回归软件质量(Quality)本身。如果你想了解更多测试相关的技术,也欢迎到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言,以后有机会我会再聊一些这方面的话题。感谢大家的关注。</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="http://www.jianshu.com/p/ab31fef12f2f" target="_blank" rel="noopener">软件测试正交测试法</a></li><li><a href="https://www.slideshare.net/cuelogic/automation-testing-by-selenium-web-driver" target="_blank" rel="noopener">Automation Testing by Selenium Web Driver</a></li><li><a href="https://www.slideshare.net/tourdedave/selenium-tips-tricks" target="_blank" rel="noopener">Selenium Tips & Tricks</a></li><li><a href="http://elementalselenium.com/tips" target="_blank" rel="noopener">Elemental Selenium Tips</a></li><li><a href="https://juejin.im/post/58ef6ab30ce463006b98f205" target="_blank" rel="noopener">腾讯云上 Selenium 用法示例</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA%3D%3D&ascene=0&idx=1&mid=2649693775&mpshare=1&nettype=WIFI&pass_ticket=vadG8dJ4VDRY%2Fw2OgMlLPch1Nj6uN9CVB4qRTfudvz6y97RV%2BH%2FB0hjqhbGg3MiH&sn=5b91936d398309b29ab5c0f6025d0b6c" target="_blank" rel="noopener">Docker环境下运行Python + Selenium + Chrome</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/test-web-automation/#disqus_thread</comments>
</item>
<item>
<title>面向开发的测试技术(二):性能测试</title>
<link>http://emacoo.cn/arch/test-performance/</link>
<guid>http://emacoo.cn/arch/test-performance/</guid>
<pubDate>Tue, 09 May 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-D
</description>
<content:encoded><![CDATA[<blockquote><p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-Driven Development)</a>开发理念以来,开发和测试的边界变的越来越模糊,从原本上下游的依赖关系,逐步演变成你中有我、我中有你的互赖关系,甚至很多公司设立了新的QE(Quality Engineer)职位。和传统的QA(Quality Assurance)不同,QE的主要职责是通过工程化的手段保证项目质量,这些手段包括但不仅限于编写单元测试、集成测试,搭建自动化测试流程,设计性能测试等。可以说,QE身上兼具了QA的质量意识和开发的工程能力。我会从开发的角度分三期聊聊QE这个亦测试亦开发的角色所需的基本技能。</p><p>前情概要:</p><ul><li><a href="http://emacoo.cn/arch/test-mock">面向开发的测试技术(一):Mock</a></li></ul></blockquote><h2 id="1-什么是性能测试?"><a href="#1-什么是性能测试?" class="headerlink" title="1 什么是性能测试?"></a>1 什么是性能测试?</h2><p>先来看一下维基百科里对性能测试的定义,</p><blockquote><p>In software engineering, performance testing is in general, a testing practice performed to determine how a system performs in terms of responsiveness and stability under a particular workload. - Wikipedia</p></blockquote><p>注意上述定义中有三个关键词:</p><ul><li>responsiveness,即响应时间,请求发出去之后,服务端需要多久才能返回结果,显然响应时间越短,性能越好。</li><li>stability,即稳定性,同样的请求,不同时刻发出去,响应时间差别越小,稳定性越好,性能也越好。</li><li>workload,即负载,同一时刻服务端收到的请求数量,其中单位时间内<strong>成功处理</strong>的请求数量即吞吐量,吞吐量越大,性能越好。</li></ul><p>响应时间和吞吐量是衡量应用性能好坏最重要的两个指标。对于绝大多数应用,刚开始的时候,响应时间最短;随着负载的增大,吞吐量快速上升,响应时间也逐渐变长;当负载超过某一个值之后,响应时间会突然呈指数级放大,同时吞吐量也应声下跌,应用性能急剧下降,整个过程如下:</p><p><img src="throughput-latency.png" alt></p><p><em>图片出处:<a href="http://coolshell.cn/articles/17381.html" target="_blank" rel="noopener">性能测试应该怎么做?</a></em></p><h2 id="2-性能测试的目的"><a href="#2-性能测试的目的" class="headerlink" title="2 性能测试的目的"></a>2 性能测试的目的</h2><p>了解了应用性能变化的普遍规律,性能测试的目的也就有了答案:针对某一应用,找出响应时间和吞吐量的量化关系,找到应用性能变化的临界点。你可能会问,知道了这些有什么用呢?在我看来,至少有3个层面的好处:</p><p>第一,有的放矢,提高资源利用率。性能测试的过程就是量化性能的过程,有了各种性能数据,你才能对应用性能进行定量分析,找到并解决潜在的性能问题,从而提高资源利用率。</p><p>第二,科学的进行容量规划。找到了应用性能变化的临界点,也就很容易找到单节点的性能极限,这是进行容量规划的重要决策依据。比如某一应用在单节点下的极限吞吐量是2000 QPS,那么面对10000 QPS的流量,至少需要部署5个节点。</p><p>第三,改善QoS(Quality of Service)。很多时候,资源是有限的,面对超出服务能力的流量,为了保证QoS,必须做出取舍(比如限流降级,开关预案等),应用性能数据是设计QoS方案的重要依据。</p><h2 id="3-性能测试的三个常见误区"><a href="#3-性能测试的三个常见误区" class="headerlink" title="3 性能测试的三个常见误区"></a>3 性能测试的三个常见误区</h2><h3 id="误区1:只看平均值,不懂TP95-TP99"><a href="#误区1:只看平均值,不懂TP95-TP99" class="headerlink" title="误区1:只看平均值,不懂TP95/TP99"></a>误区1:只看平均值,不懂TP95/TP99</h3><p>用平均值来衡量响应时间是性能测试中最常见的误区。从第1小节的插图可以看出,随着吞吐量的增大,响应时间会逐渐变长,当达到最大吞吐量之后,响应时间会开始加速上升,尤其是排在后面的请求。在这个时刻,如果只看平均值,你往往察觉不到问题,因为大部分请求的响应时间还是很短的,慢请求只占一个很小的比例,所以平均值变化不大。但实际上,可能已经有超过1%,甚至5%的请求的响应时间已经超出设计的范围了。</p><p>更科学、更合理的指标是看TP95或者TP99响应时间。TP是Top Percentile的缩写,是一个统计学术语,用来描述一组数值的分布特征。以TP95为例,假设有100个数字,从小到大排序之后,第95个数字的值就是这组数字的TP95值,表示至少有95%的数字是小于或者等于这个值。</p><p>以一次具体的性能测试为例,</p><p><img src="top-percentiles.png" alt></p><p><img src="latency-statistics.png" alt></p><p>总共有1000次请求,平均响应时间是58.9ms,TP95是123.85ms(平均响应时间的2.1倍),TP99是997.99ms(平均响应时间的16.9倍)。假设应用设计的最大响应时间是100ms,单看平均时间是完全符合要求的,但实际上已经有超过50个请求失败了。如果看TP95或者TP99,问题就很清楚了。</p><h3 id="误区2:只关注响应时间和吞吐量,忽视请求成功率"><a href="#误区2:只关注响应时间和吞吐量,忽视请求成功率" class="headerlink" title="误区2:只关注响应时间和吞吐量,忽视请求成功率"></a>误区2:只关注响应时间和吞吐量,忽视请求成功率</h3><p>虽说衡量应用性能好坏最主要是看响应时间和吞吐量,但这里有个大前提,所有请求(如果做不到所有,至少也要绝大多数请求,比如99.9%)都被成功处理了,而不是返回一堆错误码。如果不能保证这一点,那么再低的响应时间,再高的吞吐量都是没有意义的。</p><h3 id="误区3:忘了测试端也存在性能瓶颈"><a href="#误区3:忘了测试端也存在性能瓶颈" class="headerlink" title="误区3:忘了测试端也存在性能瓶颈"></a>误区3:忘了测试端也存在性能瓶颈</h3><p>性能测试的第三个误区是只关注服务端,而忽略了测试端本身可能也存在限制。比如测试用例设置了10000并发数,但实际运行用例的机器最大只支持5000并发数,如果只看服务端的数据,你可能会误以为服务端最大就只支持5000并发数。如果遇到这种情况,或者换用更高性能的测试机器,或者增加测试机器的数量。</p><h2 id="4-如何进行性能测试?"><a href="#4-如何进行性能测试?" class="headerlink" title="4 如何进行性能测试?"></a>4 如何进行性能测试?</h2><p>介绍完性能测试相关的一些概念之后,再来看一下有哪些工具可以进行性能测试。</p><h3 id="4-1-JMeter"><a href="#4-1-JMeter" class="headerlink" title="4.1 JMeter"></a>4.1 JMeter</h3><p><a href="http://jmeter.apache.org/" target="_blank" rel="noopener">JMeter</a>可能是最常用的性能测试工具。它既支持图形界面,也支持命令行,属于黑盒测试的范畴,对非开发人员比较友好,上手也非常容易。图形界面一般用于编写、调试测试用例,而实际的性能测试建议还是在命令行下运行。</p><p><img src="jmeter-group.png" alt></p><p><em>并发设置</em></p><p><img src="jmeter-request.png" alt></p><p><em>请求参数</em></p><p><img src="jmeter-aggregate-graph.png" alt></p><p><em>结果报表</em></p><p>命令行下的常用命令:</p><ul><li>设置JVM参数:JVM_ARGS=”-Xms2g -Xmx2g”</li><li>运行测试:jmeter -n -t <jmx_file></li><li>运行测试同时生成报表:jmeter -n -t <jmx_file> -l <log_file> -e -o <report_dir></li></ul><p>除了JMeter,其他常用的性能测试工具还有<a href="http://httpd.apache.org/docs/2.2/programs/ab.html" target="_blank" rel="noopener">ab</a>, <a href="http://www.acme.com/software/http_load/" target="_blank" rel="noopener">http_load</a>, <a href="https://github.com/wg/wrk" target="_blank" rel="noopener">wrk</a>以及商用的<a href="http://www8.hp.com/us/en/software-solutions/loadrunner-load-testing/index.html" target="_blank" rel="noopener">LoaderRunner</a>。</p><h3 id="4-2-JMH"><a href="#4-2-JMH" class="headerlink" title="4.2 JMH"></a>4.2 JMH</h3><p>如果测试用例比较复杂,或者负责性能测试的人员具有一定的开发能力,也可以考虑使用一些框架编写单独的性能测试程序。对于Java开发人员而言,<a href="http://openjdk.java.net/projects/code-tools/jmh/" target="_blank" rel="noopener">JMH</a>是一个推荐的选择。类似于JUnit,JMH提供了一系列注解用于编写测试用例,以及一个运行测试的引擎。事实上,即将发布的<a href="https://dzone.com/articles/microbenchmarking-comes-to-java-9" target="_blank" rel="noopener">JDK 9</a>默认就会包含JMH。</p><p>下面是我GitHub上的<a href="https://github.com/emac/spring-boot-features-demo" target="_blank" rel="noopener">示例工程</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><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">@BenchmarkMode</span>(Mode.Throughput)</span><br><span class="line"><span class="meta">@Fork</span>(<span class="number">1</span>)</span><br><span class="line"><span class="meta">@Threads</span>(Threads.MAX)</span><br><span class="line"><span class="meta">@State</span>(Scope.Benchmark)</span><br><span class="line"><span class="meta">@Warmup</span>(iterations = <span class="number">1</span>, time = <span class="number">3</span>)</span><br><span class="line"><span class="meta">@Measurement</span>(iterations = <span class="number">3</span>, time = <span class="number">3</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">VacationClientBenchmark</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> VacationClient vacationClient;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Setup</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setUp</span><span class="params">()</span> </span>{</span><br><span class="line"> VacationClientConfig clientConfig = <span class="keyword">new</span> VacationClientConfig(<span class="string">"http://localhost:3000"</span>);</span><br><span class="line"> vacationClient = <span class="keyword">new</span> VacationClient(clientConfig);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Benchmark</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">benchmarkIsWeekend</span><span class="params">()</span> </span>{</span><br><span class="line"> VacationRequest request = <span class="keyword">new</span> VacationRequest();</span><br><span class="line"> request.setType(PERSONAL);</span><br><span class="line"> OffsetDateTime lastSunday = OffsetDateTime.now().with(TemporalAdjusters.previous(SUNDAY));</span><br><span class="line"> request.setStart(lastSunday);</span><br><span class="line"> request.setEnd(lastSunday.plusDays(<span class="number">1</span>));</span><br><span class="line"></span><br><span class="line"> Asserts.isTrue(vacationClient.isWeekend(request).isSuccess());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 仅限于IDE中运行</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RunnerException </span>{</span><br><span class="line"> Options opt = <span class="keyword">new</span> OptionsBuilder()</span><br><span class="line"> .include(VacationClientBenchmark<span class="class">.<span class="keyword">class</span>.<span class="title">getSimpleName</span>())</span></span><br><span class="line"><span class="class"> .<span class="title">build</span>()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> Runner(opt).run();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中:</p><ul><li>@BenchmarkMode: 性能测试模式,支持Throughput,AverageTime,SingleShotTime等多种模式。</li><li>@Fork: 设置运行性能测试的Fork进程数,默认是0,表示共用JMH主进程。</li><li>@Threads: 并发数,Threads.MAX表示同系统的CPU核数。</li><li>@Warmup和@Measurement: 分别设置预热和实际性能测试的运行轮数,每轮持续的时间等</li><li>@Setup和@Benchmark: 等同于JUnit里的@BeforeClass和@Test</li></ul><p>在命令行下,使用JMH框架编写的性能测试程序只能以Jar包的形式运行(Main函数固定为org.openjdk.jmh.Main),因此一般会针对每个JMH程序单独维护一个项目。如果是Maven项目,可以使用官方提供的jmh-java-benchmark-archetype,如果是Gradle项目,可以使用<a href="https://github.com/melix/jmh-gradle-plugin" target="_blank" rel="noopener">jmh-gradle-plugin</a>插件。</p><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上就是我对性能测试的一些见解,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。下一篇我将聊一下Web的自动化测试,敬请期待。</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="https://segmentfault.com/a/1190000008219543" target="_blank" rel="noopener">关于性能测试的几个要点</a></li><li><a href="http://coolshell.cn/articles/17381.html" target="_blank" rel="noopener">性能测试应该怎么做?</a></li><li><a href="http://jm.taobao.org/2016/12/23/20161223/" target="_blank" rel="noopener">阿里双十一大促,技术准备只做了这两件事情?</a></li><li><a href="http://jm.taobao.org/2016/12/29/20161229/" target="_blank" rel="noopener">阿里资深技术专家丁宇谈双11高可用架构演进之路</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/test-performance/#disqus_thread</comments>
</item>
<item>
<title>面向开发的测试技术(一):Mock</title>
<link>http://emacoo.cn/arch/test-mock/</link>
<guid>http://emacoo.cn/arch/test-mock/</guid>
<pubDate>Sun, 30 Apr 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-D
</description>
<content:encoded><![CDATA[<blockquote><p>引子:自上世纪末Kent Beck提出<a href="https://en.wikipedia.org/wiki/Test-driven_development" target="_blank" rel="noopener">TDD(Test-Driven Development)</a>开发理念以来,开发和测试的边界变的越来越模糊,从原本上下游的依赖关系,逐步演变成你中有我、我中有你的互赖关系,甚至很多公司设立了新的QE(Quality Engineer)职位。和传统的QA(Quality Assurance)不同,QE的主要职责是通过工程化的手段保证项目质量,这些手段包括但不仅限于编写单元测试、集成测试,搭建自动化测试流程,设计性能测试等。可以说,QE身上兼具了QA的质量意识和开发的工程能力。从这篇开始,我会从开发的角度分三期聊聊QE这个亦测试亦开发的角色所需的基本技能。</p></blockquote><h2 id="1-什么是Mock?"><a href="#1-什么是Mock?" class="headerlink" title="1 什么是Mock?"></a>1 什么是Mock?</h2><p>在软件测试领域,Mock的意思是模拟,简单来说,就是通过某种技术手段模拟测试对象的行为,返回预先设计的结果。这里的关键词是<strong>预先设计</strong>,也就是说对于任意被测试的对象,可以根据具体测试场景的需要,返回特定的结果。打个比方,就像BBC纪录片里面的假企鹅,可以根据拍摄需要作出不同的反应。</p><h2 id="2-Mock有什么用?"><a href="#2-Mock有什么用?" class="headerlink" title="2 Mock有什么用?"></a>2 Mock有什么用?</h2><p>理解了什么是Mock,再来看Mock有哪些用途。首先,Mock可以用来解除测试对象对外部服务的依赖(比如数据库,第三方接口等),使得测试用例可以<strong>独立运行</strong>。不管是传统的单体应用,还是现在流行的微服务,这点都特别重要,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性。可迁移性是指,如果要在一个新的测试环境中运行相同的测试用例,那么除了要保证测试对象自身能够正常运行,还要保证所有依赖的外部服务也能够被正常调用。稳定性是指,如果外部服务不可用,那么测试用例也可能会失败。通过Mock去除外部依赖之后,不管是测试用例的可迁移性还是稳定性,都能够上一个台阶。</p><p><img src="standalone.png" alt></p><p>Mock的第二个好处是替换外部服务调用,<strong>提升测试用例的运行速度</strong>。任何外部服务调用至少是跨进程级别的消耗,甚至是跨系统、跨网络的消耗,而Mock可以把消耗降低到进程内。比如原来一次秒级的网络请求,通过Mock可以降至毫秒级,整整3个数量级的差别。</p><p>Mock的第三个好处是<strong>提升测试效率</strong>。这里说的测试效率有两层含义。第一层含义是单位时间运行的测试用例数,这是运行速度提升带来的直接好处。而第二层含义是一个QE单位时间创建的测试用例数。如何理解这第二层含义呢?以单体应用为例,随着业务复杂度的上升,为了运行一个测试用例可能需要准备很多测试数据,与此同时还要尽量保证多个测试用例之间的测试数据互不干扰。为了做到这一点,QE往往需要花费大量的时间来维护一套可运行的测试数据。有了Mock之后,由于去除了测试用例之间共享的数据库依赖,QE就可以针对每一个或者每一组测试用例设计一套独立的测试数据,从而很容易的做到不同测试用例之间的数据隔离性。而对于微服务,由于一个微服务可能级联依赖很多其他的微服务,运行一个测试用例甚至需要跨系统准备一套测试数据,如果没有Mock,基本上可以说是不可能的。因此,不管是单体应用还是微服务,有了Mock之后,QE就可以省去大量的准备测试数据的时间,专注于测试用例本身,自然也就提升了单人的测试效率。</p><h2 id="3-如何Mock?"><a href="#3-如何Mock?" class="headerlink" title="3 如何Mock?"></a>3 如何Mock?</h2><p>说了这么多Mock的好处,那么究竟如何在测试中使用Mock呢?针对不同的测试场景,可以选择不同的Mock框架。</p><h3 id="3-1-Mockito"><a href="#3-1-Mockito" class="headerlink" title="3.1 Mockito"></a>3.1 Mockito</h3><p>如果测试对象是一个方法,尤其是涉及数据库操作的方法,那么<a href="http://site.mockito.org/" target="_blank" rel="noopener">Mockito</a>可能是最好的选择。作为使用最广泛的Mock框架,Mockito出于<a href="http://easymock.org/" target="_blank" rel="noopener">EasyMock</a>而胜于EasyMock,乃至被默认集成进Spring Testing。其实现原理是,通过CGLib在运行时为每一个被Mock的类或者对象动态生成一个<strong>代理对象</strong>,返回<strong>预先设计</strong>的结果。集成Mockito的基本步骤是:</p><ol><li>标记被Mock的类或者对象,生成代理对象</li><li>通过Mockito API定制代理对象的行为</li><li>调用代理对象的方法,获得预先设计的结果</li></ol><p>下面是我GitHub上的<a href="https://github.com/emac/spring-boot-features-demo" target="_blank" rel="noopener">示例工程</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="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">SpringBootTest</span></span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">SignonServiceTests</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 测试对象,一个服务类</span></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> SignonService signonService;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 被Mock的类,被服务类所依赖的一个DAO类</span></span><br><span class="line"> <span class="meta">@MockBean</span></span><br><span class="line"> <span class="keyword">private</span> SignonDao dao;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testFindAll</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// SignonService#findAll()内部会调用SignonDao#findAll()</span></span><br><span class="line"> <span class="comment">// 如果不做定制,所有被Mock的类默认返回空</span></span><br><span class="line"> List<Signon> signons = signonService.findAll();</span><br><span class="line"> assertTrue(CollectionUtils.isEmpty(signons));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 定制返回结果</span></span><br><span class="line"> Signon signon = <span class="keyword">new</span> Signon();</span><br><span class="line"> signon.setUsername(<span class="string">"foo"</span>);</span><br><span class="line"> when(dao.findAll()).thenReturn(Lists.newArrayList(signon));</span><br><span class="line"></span><br><span class="line"> signons = signonService.findAll();</span><br><span class="line"> <span class="comment">// 验证返回结果和预先设计的结果一致</span></span><br><span class="line"> assertEquals(<span class="number">1</span>, signons.size());</span><br><span class="line"> assertEquals(<span class="string">"foo"</span>, signons.get(<span class="number">0</span>).getUsername());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从上面的测试用例可以看到,通过Mock服务类所依赖的DAO类,我们可以跳过所有的数据库操作,任意定制返回结果,从而专注于测试服务类内部的业务逻辑。这是传统的非Mock测试所难以实现的。</p><p><em>注意:Mockito不支持Mock私有方法或者静态方法,如果要Mock这类方法,可以使用<a href="https://github.com/powermock/powermock" target="_blank" rel="noopener">PowerMock</a>。</em></p><h3 id="3-2-WireMock"><a href="#3-2-WireMock" class="headerlink" title="3.2 WireMock"></a>3.2 WireMock</h3><p>如果说Mocketo是瑞士军刀,可以Mock Everything,那么<a href="http://wiremock.org/" target="_blank" rel="noopener">WireMock</a>就是为微服务而生的倚天剑。和处在对象层的Mockito不同,WireMock针对的是<strong>API</strong>。假设有两个微服务,Service-A和Service-B,Service-A里的一个API(姑且称为API-1),依赖于Service-B,那么使用传统的测试方法,测试API-1时必然需要同时启动Service-B。如果使用WireMock,那么就可以<strong>在Service-A端</strong>Mock所有依赖的Service-B的API,从而去掉Service-B这个外部依赖。</p><p>同样看一个我GitHub上的<a href="https://github.com/emac/spring-boot-features-demo" target="_blank" rel="noopener">示例工程</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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">WebMvcTest</span>(<span class="title">VacationController</span>.<span class="title">class</span>)</span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">VacationControllerTests</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Mock被依赖的另一个微服务</span></span><br><span class="line"> <span class="meta">@Rule</span></span><br><span class="line"> <span class="keyword">public</span> WireMockRule wireMockRule = <span class="keyword">new</span> WireMockRule(<span class="number">3001</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> MockMvc mockMvc;</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> ObjectMapper objectMapper;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Before</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">before</span><span class="params">()</span> <span class="keyword">throws</span> JsonProcessingException </span>{</span><br><span class="line"> <span class="comment">// 定制返回结果</span></span><br><span class="line"> JsonResult<Boolean> expected = JsonResult.ok(<span class="keyword">true</span>);</span><br><span class="line"> stubFor(get(urlPathEqualTo(<span class="string">"/api/vacation/isWeekend"</span>))</span><br><span class="line"> .willReturn(aResponse()</span><br><span class="line"> .withStatus(OK.value())</span><br><span class="line"> .withHeader(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE)</span><br><span class="line"> .withBody(objectMapper.writeValueAsString(expected))));</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testIsWeekendProxy</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="comment">// 构造请求参数</span></span><br><span class="line"> VacationRequest request = <span class="keyword">new</span> VacationRequest();</span><br><span class="line"> request.setType(PERSONAL);</span><br><span class="line"> OffsetDateTime lastSunday = OffsetDateTime.now().with(TemporalAdjusters.previous(SUNDAY));</span><br><span class="line"> request.setStart(lastSunday);</span><br><span class="line"> request.setEnd(lastSunday.plusDays(<span class="number">1</span>));</span><br><span class="line"></span><br><span class="line"> MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(<span class="string">"/vacation/isWeekend"</span>);</span><br><span class="line"> request.toMap().forEach((k, v) -> builder.param(k, v));</span><br><span class="line"> JsonResult<Boolean> expected = JsonResult.ok(<span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line"> mockMvc.perform(builder)</span><br><span class="line"> <span class="comment">// 验证返回结果和预先设计的结果一致</span></span><br><span class="line"> .andExpect(status().isOk())</span><br><span class="line"> .andExpect(content().contentType(APPLICATION_JSON_UTF8))</span><br><span class="line"> .andExpect(content().string(objectMapper.writeValueAsString(expected)));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>和Mockito类似,在测试用例中集成WireMock的基本步骤是:</p><ol><li>声明代理服务,以替代被Mock的微服务</li><li>通过WireMock API定制代理服务的返回结果</li><li>调用代理服务,获得预先设计的结果</li></ol><p>值得一提的是,除了API方式的集成,WireMock还支持以Jar包的形式独立运行,从配置文件中加载预先设计的响应结果,以替代被Mock的微服务。更多信息可以参阅<a href="http://wiremock.org/docs/" target="_blank" rel="noopener">官方文档</a>。</p><p>其他类似的Mock API的框架还有OkHttp的<a href="https://github.com/square/okhttp/tree/master/mockwebserver" target="_blank" rel="noopener">mockwebserver</a>,<a href="https://github.com/dreamhead/moco" target="_blank" rel="noopener">moco</a>和<a href="http://www.mock-server.com/" target="_blank" rel="noopener">mockserver</a>。mockwebserver也属于嵌入式Mock框架的范畴,但功能过于简单。moco,mockserver虽然功能完善,但需要独立部署,和WireMock相比不具有优势。</p><h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上就是我对Mock技术的一些见解,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。最后还要说一句,Mock技术虽然强大,但主要还是适用于单元测试,在集成测试,性能测试,自动化测试等其他测试领域使用并不多。</p><h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul><li><a href="https://www.linkedin.com/pulse/moving-from-quality-assurance-engineering-brief-history-nitin-mehra" target="_blank" rel="noopener">Moving from Quality Assurance to Quality Engineering. A brief history in time and what lies ahead.</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/test-mock/#disqus_thread</comments>
</item>
<item>
<title>【Spring】详解Spring MVC中不同格式的POST请求参数的数据类型转换过程</title>
<link>http://emacoo.cn/backend/spring-converter/</link>
<guid>http://emacoo.cn/backend/spring-converter/</guid>
<pubDate>Sat, 22 Apr 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>你也许写过很多Controller,那你可曾和我一样好奇最初字符串格式的HTTP请求参数如何转化成类型各异的Controller方法参数?</p>
</blockquote>
<p>引子:假设现在有一个Long型的请求参数,需要转化为OffsetD
</description>
<content:encoded><![CDATA[<blockquote><p>你也许写过很多Controller,那你可曾和我一样好奇最初字符串格式的HTTP请求参数如何转化成类型各异的Controller方法参数?</p></blockquote><p>引子:假设现在有一个Long型的请求参数,需要转化为OffsetDateTime类型的方法参数,请问如何实现?</p><h2 id="1-常见的POST请求格式"><a href="#1-常见的POST请求格式" class="headerlink" title="1 常见的POST请求格式"></a>1 常见的POST请求格式</h2><p>首先,让我们看一下3种常见的POST请求格式:</p><ul><li><code>application/x-www-form-urlencoded</code>: 默认的表单提交格式,不支持文件</li><li><code>multipart/form-data</code>: 用于上传文件,同时也支持普通类型的参数</li><li><code>application/json</code>: 提交JSON格式的raw数据,适用于AJAX请求和REST风格的接口</li></ul><p>对于不同类型的请求格式,Spring有着不同的转换过程(从请求参数到方法参数),请看下图。</p><h2 id="2-Spring-MVC中的数据类型转换过程"><a href="#2-Spring-MVC中的数据类型转换过程" class="headerlink" title="2 Spring MVC中的数据类型转换过程"></a>2 Spring MVC中的数据类型转换过程</h2><p><img src="spring-converter.png" alt></p><p>从上图可以看到,Spring在解析请求参数时,会根据请求格式进入到不同的转换流程:</p><ul><li>如果是<strong>非raw请求</strong>(即包含参数数组),则交由ModelAttributeMethodProcessor处理,ModelAttributeMethodProcessor再调用Spring Converter SPI对请求参数逐个进行转换。</li><li>如果是<strong>raw请求</strong>,则交由RequestResponseBodyMethodProcessor处理,对于JSON格式的请求体,会再调用MappingJackson2HttpMessageConverter,最终通过ObjectMapper完成转换。</li></ul><p><i>*关于Spring Converter SPI的进一步解读,可参考<a href="http://jinnianshilongnian.iteye.com/blog/1723270" target="_blank" rel="noopener">这篇文章</a></i></p><p>回到开头的那个问题,答案就很简单了。如果是非raw请求,则需要实现一个自定义的Long->OffsetDatetime的Converter;如果是raw请求,则确保ObjectMapper中包含一个Long->OffsetDatetime的反序列化器,注册Jackon自带的JavaTimeModule即可。</p><h3 id="2-1-如何注册自定义Converter?"><a href="#2-1-如何注册自定义Converter?" class="headerlink" title="2.1 如何注册自定义Converter?"></a>2.1 如何注册自定义Converter?</h3><p>以Spring Boot为例,</p><p>1. 实现<code>org.springframework.core.convert.converter.Converter</code>接口生成一个自定义Converter。<br><figure class="highlight plain"><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">public class OffsetDateTimeConverter implements Converter<String, OffsetDateTime> {</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public OffsetDateTime convert(String source) {</span><br><span class="line"> if (!NumberUtils.isNumber(source)) {</span><br><span class="line"> return null;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Long milli = NumberUtils.createLong(source);</span><br><span class="line"> return OffsetDateTime.ofInstant(Instant.ofEpochMilli(milli), systemDefault());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>2. 选择一个标注@Configuration注解的配置类,继承<code>org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter</code>,然后覆盖addFormatters方法,注册自定义Converter。<br><figure class="highlight plain"><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">@Configuration</span><br><span class="line">public class WebConfig extends WebMvcConfigurerAdapter {</span><br><span class="line">@Override</span><br><span class="line"> public void addFormatters(FormatterRegistry registry) {</span><br><span class="line"> registry.addConverter(new OffsetDateTimeConverter());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="2-2-如何注册自定义Jackson-Deserializer和Serializer?"><a href="#2-2-如何注册自定义Jackson-Deserializer和Serializer?" class="headerlink" title="2.2 如何注册自定义Jackson Deserializer和Serializer?"></a>2.2 如何注册自定义Jackson Deserializer和Serializer?</h3><p>以Spring Boot为例,</p><p>1. 继承<code>com.fasterxml.jackson.databind.JsonDeserializer</code>和<code>com.fasterxml.jackson.databind.JsonSerializer</code>生成自定义Jackson Deserializer和Serializer。</p><p>2. 继承<code>com.fasterxml.jackson.databind.module.SimpleModule</code>生成一个自定义Jackson Module,在其中添加自定义的Jackson Deserializer和Serializer。</p><p>3. 选择一个标注@Configuration注解的配置类,通过@Bean注解将自定义的Jackson Module注册为Bean,Spring Boot会自动发现和注册这个Module到默认的ObjectMapper中。</p><p>示例代码参见下一小节。</p><h2 id="3-更多示例"><a href="#3-更多示例" class="headerlink" title="3 更多示例"></a>3 更多示例</h2><h3 id="3-1-演示Controller"><a href="#3-1-演示Controller" class="headerlink" title="3.1 演示Controller"></a>3.1 演示Controller</h3><blockquote><p>演示3种常见的GET, POST请求参数的数据类型转换。</p></blockquote><figure class="highlight plain"><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">@RestController</span><br><span class="line">@Validated</span><br><span class="line">public class VacationController implements IController {</span><br><span class="line"></span><br><span class="line"> private static final List<DayOfWeek> WEEKENDS = Lists.newArrayList(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * 转换GET请求参数</span><br><span class="line"> */</span><br><span class="line"> @RequestMapping(value = "/isWeekend", method = RequestMethod.GET)</span><br><span class="line"> public JsonResult<Boolean> isWeekend(@Valid VacationRequest request) {</span><br><span class="line"> return JsonResult.ok(WEEKENDS.contains(request.getStart().getDayOfWeek()));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * 转换POST请求体</span><br><span class="line"> */</span><br><span class="line"> @RequestMapping(value = "/approve", method = RequestMethod.POST)</span><br><span class="line"> public JsonResult<VacationApproval> vacate(@RequestBody @Valid VacationRequest request) {</span><br><span class="line"> return JsonResult.ok(VacationApproval.approve(request));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * 转换POST请求参数</span><br><span class="line"> */</span><br><span class="line"> @RequestMapping(value = "/deny", method = RequestMethod.POST)</span><br><span class="line"> public JsonResult<VacationApproval> deny(@Valid VacationRequest request) {</span><br><span class="line"> return JsonResult.ok(VacationApproval.deny(request));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="3-2-自定义Enum-Converter(用于非raw格式的请求)"><a href="#3-2-自定义Enum-Converter(用于非raw格式的请求)" class="headerlink" title="3.2 自定义Enum Converter(用于非raw格式的请求)"></a>3.2 自定义Enum Converter(用于非raw格式的请求)</h3><blockquote><p>基于特定属性的枚举数据类型转换器,如果无法找到,再尝试用枚举名进行转换。</p></blockquote><figure class="highlight plain"><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">public static class CustomEnumConverter<T extends Enum<T>> implements Converter<String, T> {</span><br><span class="line"></span><br><span class="line"> private Class<T> enumCls;</span><br><span class="line"> private String prop;</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * @param enumCls 枚举类型</span><br><span class="line"> * @param prop 属性名</span><br><span class="line"> */</span><br><span class="line"> public CustomEnumConverter(Class<T> enumCls, String prop) {</span><br><span class="line"> this.enumCls = enumCls;</span><br><span class="line"> this.prop = prop;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public T convert(String source) {</span><br><span class="line"> if (StringUtils.isEmpty(source)) {</span><br><span class="line"> return null;</span><br><span class="line"> }</span><br><span class="line"> return Enums.getEnum(enumCls, prop, source).orElseGet(() -></span><br><span class="line"> Stream.of(enumCls.getEnumConstants())</span><br><span class="line"> .filter(e -> e.name().equals(source))</span><br><span class="line"> .findFirst().orElse(null)</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="3-3-自定义Module(用于raw格式的请求)"><a href="#3-3-自定义Module(用于raw格式的请求)" class="headerlink" title="3.3 自定义Module(用于raw格式的请求)"></a>3.3 自定义Module(用于raw格式的请求)</h3><blockquote><p>用于注册自定义Enum Serializer和Enum Deserializer。</p></blockquote><figure class="highlight plain"><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">public class CustomEnumModule extends SimpleModule {</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * @param prop 属性名</span><br><span class="line"> */</span><br><span class="line"> public CustomEnumModule(@NotNull String prop){</span><br><span class="line"> Asserts.notBlank(prop);</span><br><span class="line"></span><br><span class="line"> addDeserializer(Enum.class, new CustomEnumDeserializer(prop));</span><br><span class="line"> addSerializer(Enum.class, new CustomEnumSerializer(prop));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="3-3-1-自定义Enum-Serializer"><a href="#3-3-1-自定义Enum-Serializer" class="headerlink" title="3.3.1 自定义Enum Serializer"></a>3.3.1 自定义Enum Serializer</h4><blockquote><p>自定义枚举序列化器,查找特定属性并进行序列化,如果无法找到,则序列化为枚举名。</p></blockquote><figure class="highlight plain"><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">@Slf4j</span><br><span class="line">public class CustomEnumSerializer extends JsonSerializer<Enum> {</span><br><span class="line"></span><br><span class="line"> private String prop;</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * @param prop 属性名</span><br><span class="line"> */</span><br><span class="line"> public CustomEnumSerializer(@NotNull String prop) {</span><br><span class="line"> Asserts.notBlank(prop);</span><br><span class="line"></span><br><span class="line"> this.prop = prop;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public void serialize(Enum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {</span><br><span class="line"> if (value == null) {</span><br><span class="line"> gen.writeNull();</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> try {</span><br><span class="line"> PropertyDescriptor pd = getPropertyDescriptor(value, prop);</span><br><span class="line"> if (pd == null || pd.getReadMethod() == null) {</span><br><span class="line"> gen.writeString(value.name());</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> Method m = pd.getReadMethod();</span><br><span class="line"> m.setAccessible(true);</span><br><span class="line"> gen.writeObject(m.invoke(value));</span><br><span class="line"> } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {</span><br><span class="line"> throw new CommonException(e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="3-3-2-自定义Enum-Deserializer"><a href="#3-3-2-自定义Enum-Deserializer" class="headerlink" title="3.3.2 自定义Enum Deserializer"></a>3.3.2 自定义Enum Deserializer</h4><blockquote><p>自定义枚举反序列化器,根据特定属性进行反序列化,如果无法找到,再尝试用枚举名进行反序列化。</p></blockquote><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">public class CustomEnumDeserializer extends JsonDeserializer<Enum> implements ContextualDeserializer {</span><br><span class="line"></span><br><span class="line"> @Setter</span><br><span class="line"> private Class<Enum> enumCls;</span><br><span class="line"></span><br><span class="line"> private String prop;</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * @param prop 属性名</span><br><span class="line"> */</span><br><span class="line"> public CustomEnumDeserializer(@NotNull String prop) {</span><br><span class="line"> Asserts.notBlank(prop);</span><br><span class="line"></span><br><span class="line"> this.prop = prop;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public Enum deserialize(JsonParser parser, DeserializationContext ctx) throws IOException {</span><br><span class="line"> String text = parser.getText();</span><br><span class="line"> return Enums.getEnum(enumCls, prop, text).orElseGet(() -></span><br><span class="line"> Stream.of(enumCls.getEnumConstants())</span><br><span class="line"> .filter(e -> e.name().equals(text))</span><br><span class="line"> .findFirst().orElse(null)</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public JsonDeserializer createContextual(DeserializationContext ctx, BeanProperty property) throws JsonMappingException {</span><br><span class="line"> Class rawCls = ctx.getContextualType().getRawClass();</span><br><span class="line"> Asserts.isTrue(rawCls.isEnum());</span><br><span class="line"></span><br><span class="line"> Class<Enum> enumCls = (Class<Enum>) rawCls;</span><br><span class="line"> CustomEnumDeserializer clone = new CustomEnumDeserializer(prop);</span><br><span class="line"> clone.setEnumCls(enumCls);</span><br><span class="line"> return clone;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>完整代码可以参见我在GitHub上的<a href="https://github.com/emac/spring-boot-features-demo" target="_blank" rel="noopener">示例工程</a>。</p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul><li><a href="https://imququ.com/post/four-ways-to-post-data-in-http.html" target="_blank" rel="noopener">四种常见的 POST 提交数据方式</a></li><li><a href="http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/" target="_blank" rel="noopener">Spring Boot Reference Guide</a></li><li><a href="http://jinnianshilongnian.iteye.com/blog/1723270" target="_blank" rel="noopener">SpringMVC数据类型转换</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-converter/#disqus_thread</comments>
</item>
<item>
<title>JUnit 5的前世今生</title>
<link>http://emacoo.cn/arch/junit5/</link>
<guid>http://emacoo.cn/arch/junit5/</guid>
<pubDate>Sat, 08 Apr 2017 16:00:00 GMT</pubDate>
<description>
<h2 id="起源"><a href="#起源" class="headerlink" title="起源"></a>起源</h2><blockquote>
<p>前事不忘,后事之师。–《战国策·赵策一》</p>
</blockquote>
<p>对Java程序员而言,JUni
</description>
<content:encoded><![CDATA[<h2 id="起源"><a href="#起源" class="headerlink" title="起源"></a>起源</h2><blockquote><p>前事不忘,后事之师。–《战国策·赵策一》</p></blockquote><p>对Java程序员而言,JUnit无疑是使用最广泛的单元测试框架。自2006年初JUnit 4发布之后,11年间陆陆续续更新了13个小版本,最新的4.12版本是在2014年底发布的。在现今新技术、新框架层出不穷的IT圈,JUnit的版本更新速度不可谓不缓慢,这一点上,和去年同期发布的<a href="http://emacoo.cn/devops/jenkins-2-0-from-ci-to-cd/">Jenkins 2.0</a>如出一辙,后者也花了11年才升级了一个大版本。让我们回到2006年初,看看当时的Java程序员都玩些啥?</p><ul><li><a href="https://en.wikipedia.org/wiki/Java_(programming_language" target="_blank" rel="noopener">Java 5</a>发布1年多,很多公司还在使用JDK 1.4,离Java 6正式发布还有大半年,更别提Java 8了。</li><li><a href="https://en.wikipedia.org/wiki/Spring_Framework" target="_blank" rel="noopener">Spring</a>初出茅庐,离第一桶金Jolt和JAX大奖还有几个月,离2.0发布还有大半年,更别提Spring Boot和Spring Cloud了。</li><li><a href="https://en.wikipedia.org/wiki/Apache_Maven" target="_blank" rel="noopener">Maven</a>比Spring老道一点,不过也只是2.0 Beta阶段,更别提Maven 3了。</li></ul><p>了解了JUnit 4的出生背景,就不难理解如今JUnit 4的问题所在了。</p><ul><li>Extension:相对于其应用的体量,JUnit的可扩展性不可谓不粗糙。Runner和Rules是扩展JUnit的主要手段,前者虽然强大,但是粒度太粗,扩展者需要从零实现全测试周期的支持,并且一个单元测试类只能绑定一个Runner,无法同时使用多个Runner。Rules虽然解决了粒度的问题,但扩展能力非常有限,只能作用于测试周期的特定阶段。</li><li>Modularity:在Maven之前,一个Java应用依赖的Jar包往往都是人工进行管理的,既繁琐又容易出错,因此,那个时候all-in-one风格的fat Jar更受程序员欢迎。但是当Maven,Ivy以及之后Gradle接管了应用的依赖管理之后,模块化成为了主流,以前的fat Jar由于包的大小、更容易导致依赖冲突等原因逐渐的不再受到青睐,甚至被打入冷宫。</li><li>Java 8:距离Java 8正式发布已经整整过去3年,Java程序员也渐渐习惯了使用Lambda表达式,对于一些新生代的Java程序员而言,没有Lambda表达式甚至都不会写Java代码了。</li></ul><p>以上这些局限就构成了JUnit 5的起源。除此之外,JUnit 5还有更大的野心,JUnit as a Platform。</p><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><blockquote><p>你的是我的,我的还是我的。</p></blockquote><p>什么是JUnit as a Platform?既然敢叫Platform,那目光自然就不能局限于JUnit自己了。通过引入JUnit Platform,JUnit 5不但可以运行新老版本的JUnit测试,甚至还可以运行别人家的单元测试,比如TestNG。套用一个现在流行的词,就叫降维打击。</p><p><img src="junit-5-architecture.png" alt><br><em>图片出处:<a href="https://www.infoq.com/articles/JUnit-5-Early-Test-Drive" target="_blank" rel="noopener">JUnit 5 - An Early Test Drive - Part 1</a></em></p><p>从上图可以看到,完整的JUnit 5平台从上至下分为四层:</p><ol><li>面向developer的API,比如各种测试注解</li><li>特定于某一单元测试框架的测试引擎,比如JUnit 4,JUnit 5,TestNG</li><li>通用的测试引擎,是对第2层各种单元测试框架的抽象</li><li>面向IDE的启动器,用于调度和执行各个单元测试</li></ol><p>前两层体现的是JUnit as a Tool,还属于Junit原本的范畴,而后两层体现的就是JUnit as a Platform,JUnit 5也借此实现了十年磨一剑的凤凰涅槃。有了JUnit as a Platform这把屠龙刀,相比于其他单元测试框架,可以说JUnit 5占据了技术制高点,可以预期,未来JUnit的版图将进一步扩大,将触角延伸至更多的测试领域。</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><p>分析完JUnit 5架构上的调整,接下来再看一下除了Java 8的支持,JUnit 5具体包含了哪些值得一提的新特性。</p><ul><li>兼容性:为了最大程度的保证对JUnit 4以及更早版本的兼容性,JUnit 5启用了新的命名空间org.junit.jupiter.api.*,通过引入junit-vintage-engine模块,支持老的命名空间org.junit.*,并且新老版本可以共存于同一项目,独立运行互不影响。</li><li>新的注解:<ul><li>@DisplayName, @Tag注解:引入更灵活的基于字符串的@Tag注解,取代老的基于类的@Category注解,配以@DisplayName注解,让管理大量单元测试变得更容易。</li><li>@Nested注解:支持BDT(Behavior Driven Testing)风格的单元测试。</li></ul></li><li><a href="http://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests" target="_blank" rel="noopener">Parameterized Tests</a>:批量生成参数化的单元测试,比如针对一个枚举类,为其每一个枚举值生成一个单元测试。</li><li><a href="http://junit.org/junit5/docs/current/user-guide/#writing-tests-dynamic-tests" target="_blank" rel="noopener">Dynamic Tests</a>:程序化生成单元测试,比如动态从数据库中读取数据,生成单元测试。</li><li><a href="http://junit.org/junit5/docs/current/user-guide/#extensions" target="_blank" rel="noopener">Extention</a>:JUnit 5提供了多种扩展方式,涉及测试周期的各个阶段,比如激活条件,参数解析,回调函数,异常处理等。通过实现特定接口,配合@ExtendWith注解,你很容易就可以重新定义一个单元测试的执行流程。</li></ul><p>为了更好的理解这些特性,不妨去GitHub看一下JUnit官方的<a href="https://github.com/junit-team/junit5-samples" target="_blank" rel="noopener">示例工程</a>。</p><h2 id="Roadmap"><a href="#Roadmap" class="headerlink" title="Roadmap"></a>Roadmap</h2><p>根据最新的JUnit<a href="https://github.com/junit-team/junit5/wiki/Roadmap" target="_blank" rel="noopener">官方文档</a>,JUnit 5将于今年第三季度的某一时刻发布,在此之前,还将发布若干个RC版本。目前最新的稳定版本是5.0.0-M4。</p><p>你,准备好了吗?</p><h2 id="附录:JUnit-5和JUnit-4的快速对比"><a href="#附录:JUnit-5和JUnit-4的快速对比" class="headerlink" title="附录:JUnit 5和JUnit 4的快速对比"></a>附录:JUnit 5和JUnit 4的快速对比</h2><p><img src="junit-5-vs-junit-4-9-638.jpg" alt><br><img src="junit-5-vs-junit-4-10-638.jpg" alt><br><em>图片出处:<a href="https://www.slideshare.net/rkmael/junit-5-vs-junit-4" target="_blank" rel="noopener">JUnit 5 vs JUnit 4</a></em></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://junit.org/junit5/docs/current/user-guide/" target="_blank" rel="noopener">JUnit 5 User Guide</a></li><li><a href="https://www.infoq.com/articles/JUnit-5-Early-Test-Drive" target="_blank" rel="noopener">JUnit 5 - An Early Test Drive - Part 1</a></li><li><a href="https://www.slideshare.net/SpringCentral/testing-with-spring-43-junit-5-and-beyond" target="_blank" rel="noopener">Testing with Spring 4.3, JUnit 5, and Beyond</a></li><li><a href="https://objectpartners.com/2016/07/26/junit-5-with-spring-boot-plus-kotlin/" target="_blank" rel="noopener">JUnit 5 with Spring Boot (plus Kotlin)</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/junit5/#disqus_thread</comments>
</item>
<item>
<title>【书友会】重读经典:《整洁代码》</title>
<link>http://emacoo.cn/notes/book-clean-code/</link>
<guid>http://emacoo.cn/notes/book-clean-code/</guid>
<pubDate>Sat, 04 Mar 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>继“<a href="http://emacoo.cn/coding/source-retrofit/">赏码会</a>”之后,最近和团队开始尝试一种新的技术活动形式——“书友会”。简单来说,就是一起选出一些经典的技术书籍,线下阅读,当面讨论,共同
</description>
<content:encoded><![CDATA[<blockquote><p>继“<a href="http://emacoo.cn/coding/source-retrofit/">赏码会</a>”之后,最近和团队开始尝试一种新的技术活动形式——“书友会”。简单来说,就是一起选出一些经典的技术书籍,线下阅读,当面讨论,共同进步。文末有具体的活动形式,欢迎到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>交流。</p></blockquote><p>作为“书友会”的第一期书籍,我们选择的是<a href="http://product.dangdang.com/20750190.html" target="_blank" rel="noopener">《代码整洁之道》</a>(后面简称《整洁》)。在我看来,《整洁》和另外两本经典之作<a href="http://product.dangdang.com/22543344.html" target="_blank" rel="noopener">《代码大全》</a>,<a href="http://product.dangdang.com/23734636.html" target="_blank" rel="noopener">《重构:改善既有代码的设计》</a>,是每一个程序员入行必读的三本基础技术书籍。这三本书能够帮助你建立正确的编程理念,养成良好的编程习惯。我建议可以按照《代码》,《重构》,《整洁》的顺序进行阅读。相对另外两本书,《整洁》涉及的面更广一些,内容也更有深度,比较适合有1~2年实际编程经验的程序员。不过即便是更资深的程序员,相信也能从中有所收获。我第一次读这本书还是在2010年,时隔7年重读这本经典之作,仍然感觉历久弥新。在介绍这本书之前,我们先来了解一个跟简洁有关的普适性原则。</p><h2 id="奥卡姆剃刀原则"><a href="#奥卡姆剃刀原则" class="headerlink" title="奥卡姆剃刀原则"></a>奥卡姆剃刀原则</h2><p><img src="occam.jpeg" alt></p><blockquote><p>奥卡姆剃刀定律(Occam’s Razor)又称“奥康的剃刀”,它是由14世纪逻辑学家、圣方济各会修士奥卡姆的威廉(William of Occam,约1285年至1349年)提出。这个原理称为“如无必要,勿增实体”,即“简单有效原理”。正如他在《箴言书注》2卷15题说“切勿浪费较多东西去做,用较少的东西,同样可以做好的事情。” – <a href="http://baike.baidu.com/item/%E5%A5%A5%E5%8D%A1%E5%A7%86%E5%89%83%E5%88%80%E5%8E%9F%E7%90%86" target="_blank" rel="noopener">百度百科</a></p></blockquote><p>对应到编程,奥卡姆剃刀原则至少对我们有两个启示:</p><ul><li>如无必要,勿增实体。一个常见的反例就是代码前大段的注释,《整洁》告诉我们,最好的注释是没有注释,在你写下大段注释之前,应该反思一下是不是有更简单的设计。</li><li>对于同一个需求,如果有两种实现方案,选择那个简单的。网上经常可以看到一些装逼的代码,比如<a href="http://coolshell.cn/articles/17524.html" target="_blank" rel="noopener">这篇</a>,复杂的方案不仅增加了理解成本,更要命的是让问题变得更隐蔽。</li></ul><h2 id="整洁之道"><a href="#整洁之道" class="headerlink" title="整洁之道"></a>整洁之道</h2><p><img src="wtfm.jpg" alt></p><p>《整洁》这本书虽然章节颇多(算上附录A有18章),但每章内容并不多,平均半小时左右就可以读完一章,并且章节之间相互独立,打乱顺序阅读也无妨。为了保证讨论质量,我们总共分了4次(每周1次)进行逐章讨论,每次3~6章,时间控制在2小时以内。总的来说,辅以配图和代码,整本书的阅读体验很流畅,处处闪耀着大师们的编程智慧,摘录一二如下:</p><blockquote><p>童子军军规:让营地比你来时更干净。</p><p>整洁的代码从不隐藏设计者的意图。</p><p>读和写花费时间的比例超过10:1。写新代码时,我们一直在读旧代码。</p><p>整洁的代码总是看起来像是某位特别在意它的人写的。</p><p>勒布朗法则:稍后等于永不。</p><p>如果代码不能保持整洁,你就会失去它们。</p></blockquote><p>最后一句是我加的,哈!想要了解更多整洁之道?快去阅读《代码整洁之道》吧。</p><h2 id="“书友会”活动形式参考"><a href="#“书友会”活动形式参考" class="headerlink" title="“书友会”活动形式参考"></a>“书友会”活动形式参考</h2><p>目的:</p><ul><li>阅读经典,精进实践能力,提升思考维度</li><li>在碎片化阅读的当下,培养静心读书的定力</li></ul><p>目标:</p><ul><li>写一篇读书笔记</li><li>结合书本动手做一些实验</li></ul><p>形式:</p><ul><li>每周一次,每次不少于2小时,全体参加</li><li>每期书友会开始之前,选择一人作为主席,每人(包括主席)作为讲师认领若干章节</li><li>主席负责确定讲师,制定当期读书计划(分多少次,每次涉及章节)</li><li>每次活动开始前,所有人按读书计划提前读完读完计划内的章节</li><li>每次活动开始后,按读书计划先后由各位讲师主持小组讨论,交流读书心得</li></ul><h2 id="链接"><a href="#链接" class="headerlink" title="链接"></a>链接</h2><ul><li><a href="http://design.jobbole.com/118190/" target="_blank" rel="noopener">设计法则: 奥卡姆的剃刀原理</a></li><li><a href="http://www.wtoutiao.com/p/37eLKm8.html" target="_blank" rel="noopener">吴伯凡:为什么我们今天还要读文学经典</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/notes/book-clean-code/#disqus_thread</comments>
</item>
<item>
<title>【赏码会】Redis的最佳拍档:Jedis</title>
<link>http://emacoo.cn/coding/source-jedis/</link>
<guid>http://emacoo.cn/coding/source-jedis/</guid>
<pubDate>Sat, 04 Feb 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>出门左拐:</p>
<ul>
<li><a href="http://emacoo.cn/coding/source-retrofit/">【赏码会】HTTP Client中的瑞士军刀:Retrofit</a></li>
</ul>
</block
</description>
<content:encoded><![CDATA[<blockquote><p>出门左拐:</p><ul><li><a href="http://emacoo.cn/coding/source-retrofit/">【赏码会】HTTP Client中的瑞士军刀:Retrofit</a></li></ul></blockquote><h2 id="Jedis简介"><a href="#Jedis简介" class="headerlink" title="Jedis简介"></a>Jedis简介</h2><p>作为Redis官方推荐的三个<a href="https://redis.io/clients#java" target="_blank" rel="noopener">Java Client</a>之一,Jedis推出时间最早,使用最为广泛(Spring默认使用的Redis Client就是Jedis),同时Star数也遥遥领先于另外两个。和其他Redis Client一样,Jedis通过<a href="https://redis.io/topics/protocol" target="_blank" rel="noopener">RESP协议</a>向Redis发送命令请求和解析响应数据。</p><h2 id="源码赏析"><a href="#源码赏析" class="headerlink" title="源码赏析"></a>源码赏析</h2><p><img src="jedis-class-diagram.png" alt></p><p>最新版本的Jedis代码行数超过18K,和Redis本身(20K)处于同一规模。面对如此庞大的项目,分模块阅读是必然之选。由于类的数量太多,本文只在类层面进行简单解读,不会涉及具体的源代码。值得一提的是,虽然Jedis的代码称不上规范,比如全局缺注释、某些类的长度过长,但由于绝大多数方法都很简短,加上清晰的命名和完善的单元测试,代码可读性并没有太大影响。</p><h3 id="Core-核心模块,实现RESP协议"><a href="#Core-核心模块,实现RESP协议" class="headerlink" title="Core: 核心模块,实现RESP协议"></a>Core: 核心模块,实现RESP协议</h3><ul><li>Jedis/BinaryJedis: 入口类,封装Redis的各种命令。</li><li>Client/BinaryClient/Connection: 与Redis进行具体的交互工作。</li><li>Protocol, RedisInputStream, RedisOutputStream: 实现RESP协议。</li></ul><h3 id="Sharding-提供Partitioning支持"><a href="#Sharding-提供Partitioning支持" class="headerlink" title="Sharding: 提供Partitioning支持"></a>Sharding: 提供<a href="https://redis.io/topics/partitioning" target="_blank" rel="noopener">Partitioning</a>支持</h3><ul><li>ShardedJedis/BinaryShardedJedis: 首先对传入的Key进行Hash计算(默认使用高性能、低碰撞率的<a href="https://sites.google.com/site/murmurhash/" target="_blank" rel="noopener">MurmurHash</a>算法),然后根据计算结果找到相应的Jedis实例,最后执行命令。</li></ul><h3 id="Pool-提供连接池和Sentinel支持"><a href="#Pool-提供连接池和Sentinel支持" class="headerlink" title="Pool: 提供连接池和Sentinel支持"></a>Pool: 提供连接池和<a href="https://redis.io/topics/sentinel" target="_blank" rel="noopener">Sentinel</a>支持</h3><ul><li>JedisPool: 基于<a href="https://commons.apache.org/proper/commons-pool/" target="_blank" rel="noopener">Apache Commons Pool</a>实现的连接池,通过JedisFactory获取Jedis实例。</li><li>JedisSentinelPool: 通过侦听”switch-master”事件,每当master切换时,调用JedisFactory重新初始化master连接信息。</li><li>ShardedJedisPool: 与JedisPool类似,通过ShardedJedisFactory获取ShardedJedis实例。</li></ul><h3 id="Pipeline-提供Pipelining和事务支持"><a href="#Pipeline-提供Pipelining和事务支持" class="headerlink" title="Pipeline: 提供Pipelining和事务支持"></a>Pipeline: 提供<a href="https://redis.io/topics/pipelining" target="_blank" rel="noopener">Pipelining</a>和<a href="https://redis.io/topics/transactions" target="_blank" rel="noopener">事务</a>支持</h3><ul><li>Pipeline: 通过Jedis#pipelined()获取实例。以类型安全的方式获取执行结果,通过BuilderFactory将Object类型的Response转化为期望的结果类型。<ul><li>非事务模式:构建Response Queue,然后通过Client#getMany()批量获取结果。</li><li>事务模式:通过MultiResponseBuilder缓存Response,然后批量获取结果。</li></ul></li><li>Transaction: 通过Jedis#multi()获取实例。天然的事务属性,通过Client#getMany()批量获取结果,但无法获取单条命令的结果,且类型非安全。</li><li>ShardedJedisPipeline: ShardedJedis#pipelined()获取实例。不同于Pipeline和Transaction,由于请求可能落到多个Client上,只能通过Client#getOne()挨个获取结果,类型非安全。</li></ul><h3 id="Cluster-提供Cluster支持"><a href="#Cluster-提供Cluster支持" class="headerlink" title="Cluster: 提供Cluster支持"></a>Cluster: 提供<a href="https://redis.io/topics/cluster-tutorial" target="_blank" rel="noopener">Cluster</a>支持</h3><ul><li>JedisCluster/BinaryJedisCluster: 通过JedisClusterConnectionHandler获取Jedis实例,然后执行命令。</li><li>JedisClusterConnectionHandler & JedisClusterInfoCache: 通过Collections#shuffle()随机返回一个Jedis实例。使用ReentrantReadWriteLock保证更新Cluster的Jedis实例列表时的线程安全性。</li><li>JedisClusterCommand: 通过retry机制获取有效的Jedis实例,然后再执行命令。</li></ul><h2 id="解惑"><a href="#解惑" class="headerlink" title="解惑"></a>解惑</h2><h3 id="Q1:为什么有那么多的Binary-类(BinaryJedis-BinaryClient-BinaryShardedJedis-BinaryJedisCluster),它们看上去跟非Binary的子类差不多啊?"><a href="#Q1:为什么有那么多的Binary-类(BinaryJedis-BinaryClient-BinaryShardedJedis-BinaryJedisCluster),它们看上去跟非Binary的子类差不多啊?" class="headerlink" title="Q1:为什么有那么多的Binary*类(BinaryJedis, BinaryClient, BinaryShardedJedis, BinaryJedisCluster),它们看上去跟非Binary的子类差不多啊?"></a>Q1:为什么有那么多的Binary*类(BinaryJedis, BinaryClient, BinaryShardedJedis, BinaryJedisCluster),它们看上去跟非Binary的子类差不多啊?</h3><p>A: Binary的父类与非Binary的子类表面的区别是不管是key,还是value,只要涉及字符串语义的参数,前者都用byte[]类型传参,而后者使用String类型。而深层次的原因,我认为跟RESP协议有关,RESP协议是面向字节的协议,对于性能要求极高的场景,使用Binary类有助于提高性能(因为减少了一次String到byte[]的转换)。</p><h3 id="Q2:Pipeline-Transaction以及普通的Jedis有何关联?"><a href="#Q2:Pipeline-Transaction以及普通的Jedis有何关联?" class="headerlink" title="Q2:Pipeline, Transaction以及普通的Jedis有何关联?"></a>Q2:Pipeline, Transaction以及普通的Jedis有何关联?</h3><p>A: 简单来说,Pipeline和Transaction是批处理运行模式,一次获取多条命令的执行结果,而Jedis只能一条一条获取。而Pipeline和Transaction的区别主要有两点:1)Pipeline同时支持事务模式和非事务模式,而Transaction支持事务模式。2)Pipeline类型安全,Transaction类型非安全。</p><h2 id="漫谈"><a href="#漫谈" class="headerlink" title="漫谈"></a>漫谈</h2><p>上面提到Jedis的代码规模很大,进一步分析排名靠前的几个大类,可以发现两个明显的特点:</p><p><img src="jedis-loc.png" alt></p><ol><li>方法很多,最多的一个类有250+方法,直接结果就是导致类的长度也很长(3000+)</li><li>大多数方法实现不超过5行,并且遵从同一结构</li></ol><p>单从缩减代码行数的角度来看,至少可以考虑两种方式:</p><ol><li>使用代码生成工具自动生成享有同一结构的方法</li><li>使用Java 8引入的Functional Interface简化代码</li></ol><h2 id="传送门"><a href="#传送门" class="headerlink" title="传送门"></a>传送门</h2><ul><li><a href="https://github.com/xetorthio/jedis" target="_blank" rel="noopener">Jedis GitHub</a></li><li><a href="https://redis.io/commands" target="_blank" rel="noopener">Redis Commands</a></li><li><a href="https://redis.io/documentation" target="_blank" rel="noopener">Redis Documentation</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/coding/source-jedis/#disqus_thread</comments>
</item>
<item>
<title>我的写作工具箱</title>
<link>http://emacoo.cn/notes/writing-toolbox/</link>
<guid>http://emacoo.cn/notes/writing-toolbox/</guid>
<pubDate>Sat, 07 Jan 2017 16:00:00 GMT</pubDate>
<description>
<p><img src="mac-coffee.jpg" alt></p>
<blockquote>
<p>出门左拐:</p>
<ul>
<li><a href="http://emacoo.cn/notes/why-i-write/">我们为什么要写作?</a></li>
<l
</description>
<content:encoded><![CDATA[<p><img src="mac-coffee.jpg" alt></p><blockquote><p>出门左拐:</p><ul><li><a href="http://emacoo.cn/notes/why-i-write/">我们为什么要写作?</a></li><li><a href="http://emacoo.cn/notes/how-i-write/">我是如何写作的?</a></li></ul></blockquote><p>作为写作三部曲的最后一篇,展示一下我的日常写作工具箱。</p><h2 id="Markdown"><a href="#Markdown" class="headerlink" title="Markdown"></a>Markdown</h2><p>不夸张的说,没有Markdown,就没有现在这一波以掘金,简书,SegmentFault为代表的写作热。在Markdown之前,摆在程序员面前的写作格式主要有Word和Wiki。先说Word。对于文科生思维的办公人群,Word是不二之选,但对于理科生思维的程序员而言,Word里面的各种模板、段落格式、页眉页脚,就像孙悟空头上的紧箍咒,一想就头疼,尤其是像我这样有代码洁癖的程序员,字没码几个,排版排了一遍又一遍。Word的第二个问题在于可传播性,由于Word是一种二进制格式,需要用特定的软件才能打开,而在移动互联网时代,很少有人有耐心在阅读之前先下载一个文档,更别说很多人手机上可能都没有能够打开Word的App。</p><p>再看Wiki。Wiki虽然没有可传播性的问题,但极度依赖于网络,在离线环境下,你是没法编辑一个Wiki的。Wiki的另一个问题在于缺少统一的规范,我用过很多Wiki网站,虽然大体上支持的格式相同,但在纯文本编辑模式下,很多格式(比如加粗,链接)的表示方式不尽相同,这就让文章的可迁移性大打折扣。</p><p>Markdown可以说解决了上述所有的问题,通过统一的规范,纯文本排版,让写作的人能够专注于内容本身,而不用操心格式,最大化写作效率。Markdown另一个对程序员友好的特性是纯文本格式,既方便离线编辑,也容易对文章进行版本化管理。</p><p>我本地用的Markdown编辑软件是<a href="http://www.sublimetext.com/" target="_blank" rel="noopener">Sublime</a>, 配合<a href="https://github.com/jonschlinkert/sublime-markdown-extended" target="_blank" rel="noopener">Markdown Extended</a>和<a href="https://github.com/adampresley/sublime-view-in-browser" target="_blank" rel="noopener">View In Browser</a>插件。</p><h2 id="画图工具"><a href="#画图工具" class="headerlink" title="画图工具"></a>画图工具</h2><p>为了帮助读者更好的理解文章内容,一般每篇文章我都会配一些图片。配图目的不同,来源也会不同。对于技术类文章,如果是介绍第三方框架,我会优先引用官方网站的图片,然后是一些我读到的比较好的文章。如果是介绍自己开发的系统或者方案,我会自己画图,之前用<a href="https://www.yworks.com/products/yed" target="_blank" rel="noopener">yEd</a>或者<a href="https://www.processon.com/" target="_blank" rel="noopener">processon</a>比较多,最近发现<a href="https://chrome.google.com/webstore/detail/gliffy-diagrams/bhmicilclplefnflapjmnngmkkkkpfad?hl=zh-CN" target="_blank" rel="noopener">Gliffy Diagrams For Chrome</a>这个神器后基本上就用这个了,有时也会用一下Keynote或者Powerpoint。对于非技术类文章,Google是最好的搜图利器。</p><h2 id="建站工具"><a href="#建站工具" class="headerlink" title="建站工具"></a>建站工具</h2><p>有了文章和配图,接下来就要为它们找一个容身之处。我的这个个人站点最早是搭建在Amazon的AWS上面,用的CMS系统是<a href="https://getgrav.org/" target="_blank" rel="noopener">Grav</a>,后来服务器到期,就切换到GitHub上面了,用的是目前最流行的<a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>,具体搭建步骤可以参考<a href="http://jiji262.github.io/2016/04/15/2016-04-15-hexo-github-pages-blog/" target="_blank" rel="noopener">这篇文章</a>。相对于Grav,Hexo更轻量,所有操作都可以在命令行下完成,支持一键发布到GitHub,非常方便。另一个我比较喜欢的Hexo的特性是草稿功能,对于一些你感兴趣但准备的还不够的主题,可以边做准备边写草稿,全部写完了再发布。</p><h2 id="写作环境"><a href="#写作环境" class="headerlink" title="写作环境"></a>写作环境</h2><p>对于程序员而言,写作是一种抽象程度更高的编程,需要放松的环境和专注的思考。对我而言,Mac,豆瓣FM,降噪耳机是不可或缺的陪伴。刻意的营造一些仪式感,可以帮助你更快的进入状态,比如一盏灯,一杯咖啡,一块超大鼠标垫。</p><p><img src="mi-mouse-pad.jpg" alt></p>]]></content:encoded>
<comments>http://emacoo.cn/notes/writing-toolbox/#disqus_thread</comments>
</item>
<item>
<title>我是如何写作的?</title>
<link>http://emacoo.cn/notes/how-i-write/</link>
<guid>http://emacoo.cn/notes/how-i-write/</guid>
<pubDate>Sun, 01 Jan 2017 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>出门左拐:</p>
<ul>
<li><a href="http://emacoo.cn/notes/why-i-write/">我们为什么要写作?</a></li>
</ul>
</blockquote>
<h2 id="我的写作心得"><a h
</description>
<content:encoded><![CDATA[<blockquote><p>出门左拐:</p><ul><li><a href="http://emacoo.cn/notes/why-i-write/">我们为什么要写作?</a></li></ul></blockquote><h2 id="我的写作心得"><a href="#我的写作心得" class="headerlink" title="我的写作心得"></a>我的写作心得</h2><p><a href="http://emacoo.cn/notes/why-i-write/">上篇</a>文章谈了我对写作的一些认识,这篇文章继续聊一下我现阶段的一些写作心得。最近在《得到》上听到一篇吴军老师谈写作的<a href="http://www.jianshu.com/p/e3710e135208" target="_blank" rel="noopener">文章</a>,深以为然,对照这篇文章,同时结合我的一些经验,分4个步骤介绍一下我的写作流程。</p><h3 id="1-选题"><a href="#1-选题" class="headerlink" title="1 选题"></a>1 选题</h3><p>选题不分好坏,只看适不适合。那对于一个程序员,什么是适合的选题?在我看来,至少要满足两个条件:第一,感兴趣的,第二,有一些实践经验。兴趣是最好的老师,也会激发你最大的热情。有了兴趣的指引,你才能投入百分之百的热情,坚持不懈的探寻答案,并且做到精益求精。如果少了兴趣,写出来的文章往往平淡无味,并且更大的可能是半途而废。除了兴趣,有一些相关的实践经验也是不可或缺的。古人云,纸上得来终觉浅,绝知此事要躬行。王阳明也说,知行合一。读再多书,看再多文章,如果不动手实践一番,那还是别人的知识,写出来的文章也至多只能算是人云亦云。</p><h3 id="2-准备"><a href="#2-准备" class="headerlink" title="2 准备"></a>2 准备</h3><p>确定主题之后,下一步就是做一些准备工作,俗称做功课。就写博客而言,我一般至少提前一周开始准备。准备的内容包括:</p><ol><li><p>拟定文章标题和大纲。这是第一步也是最关键的一步。一个好的标题应该明确的告知读者文章的目的,这样既能够有效的吸引目标读者,也能够提前筛选掉不适合的读者。大纲划定了文章的广度,同时也描绘了文章的逻辑结构。比如那个著名的把大象放进冰箱的实验,大纲就是3句话,第一,打开冰箱门,第二,把大象放进冰箱,第三,关上冰箱门。<br><img src="elephant.png" alt></p></li><li><p>复读先前收藏的文章和记录的笔记,再加一些扩展阅读。还记得上篇文章我说的写作的第一个好处吗?复读也是一个温故知新的过程。同一篇文章,第一次读和第二次读往往会有不同的收获,也可能激发一些新的思考。除此之外,围绕上一步拟定的标题和大纲,针对一些不确定的点,再找一些相关材料进行学习和求证,需要的话,可以再补做一些实验。</p></li><li><p>总结现有的实践经验。从理解到实践是一次知行升级,从实践到写作又是一次知行升级。围绕写作这个目的,往往需要对先前的实践成果进行总结和加工,配以一些必要的图表,帮助读者更容易的理解你想表达的意思。</p></li></ol><h3 id="3-写作"><a href="#3-写作" class="headerlink" title="3 写作"></a>3 写作</h3><p>做了一定的准备之后,就可以开始写作了。但有一点切记,准备永远是不够的。新手写作,往往在准备的阶段停留太久,以至于准备到最后就不了了之。有了第一步的大纲和第二步的素材,真正的写作相对就会容易一些。写作的过程就是通过合理的组织文字和素材,达成你写当下这篇文章的目的。写作的技巧有很多,对我而言,最实用的两条是:第一,<a href="http://www.paulgraham.com/talk.html" target="_blank" rel="noopener">Write like you talk</a>,第二,写完一句再写下一句。这里就不展开解释了,留给你自己理解。</p><h3 id="4-传播"><a href="#4-传播" class="headerlink" title="4 传播"></a>4 传播</h3><p>如果你跟我一样,想通过写作提升自己的影响力,那么最后一步传播是必不可少的,毕竟在知识大爆炸的时代,酒香也怕巷子深。我目前用到的传播手段比较简单,除了个人站点之外,主要还是个人投稿(比如掘金,SegmentFault,CSDN等)和偶尔的第三方约稿。未来,等多一些积累,我可能会开设自己的公众号。如果你对我的文章感兴趣,也欢迎在我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言约稿。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://www.paulgraham.com/writing44.html" target="_blank" rel="noopener">Writing, Briefly</a></li><li><a href="http://www.paulgraham.com/talk.html" target="_blank" rel="noopener">Write Like You Talk</a></li><li><a href="http://www.jianshu.com/p/e3710e135208" target="_blank" rel="noopener">成就只是你的副产品</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/notes/how-i-write/#disqus_thread</comments>
</item>
<item>
<title>我的2017书单</title>
<link>http://emacoo.cn/notes/2017-booklist/</link>
<guid>http://emacoo.cn/notes/2017-booklist/</guid>
<pubDate>Sat, 31 Dec 2016 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>2017,夕惕若厉,无咎</p>
</blockquote>
<h2 id="已读"><a href="#已读" class="headerlink" title="已读"></a>已读</h2><p><img src="24101711-1_w_
</description>
<content:encoded><![CDATA[<blockquote><p>2017,夕惕若厉,无咎</p></blockquote><h2 id="已读"><a href="#已读" class="headerlink" title="已读"></a>已读</h2><p><img src="24101711-1_w_8.jpg" alt><br><img src="23598879-1_w_1.jpg" alt><br><img src="23744408-1_w_1.jpg" alt><br><img src="23750530-1_w_2.jpg" alt><br><img src="23750533-1_w_2.jpg" alt><br><img src="1900596121-1_w_2.jpg" alt><br><img src="23453503-1_w_2.jpg" alt><br><img src="23685951-1_w_2.jpg" alt><br><img src="23574522-1_w_7.jpg" alt><br><img src="23517521-1_w_1.jpg" alt></p>]]></content:encoded>
<comments>http://emacoo.cn/notes/2017-booklist/#disqus_thread</comments>
</item>
<item>
<title>我的2016书单</title>
<link>http://emacoo.cn/notes/2016-booklist/</link>
<guid>http://emacoo.cn/notes/2016-booklist/</guid>
<pubDate>Fri, 30 Dec 2016 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>2016,不虚度</p>
</blockquote>
<h2 id="微信读书"><a href="#微信读书" class="headerlink" title="微信读书"></a>微信读书</h2><p><img src="23754178-
</description>
<content:encoded><![CDATA[<blockquote><p>2016,不虚度</p></blockquote><h2 id="微信读书"><a href="#微信读书" class="headerlink" title="微信读书"></a>微信读书</h2><p><img src="23754178-1_w_1.jpg" alt><br><img src="23363081-1_w_2.jpg" alt><br><img src="23855757-1_w_2.jpg" alt><br><img src="23678496-1_w_8.jpg" alt><br><img src="23232323-1_w_1.jpg" alt><br><img src="23634430-1_w_1.jpg" alt><br><img src="23595604-1_w_2.jpg" alt><br><img src="23347736-1_w_1.jpg" alt><br><img src="23654487-1_w_1.jpg" alt><br><img src="23795865-1_w_1.jpg" alt><br><img src="20629180-1_w_1.jpg" alt><br><img src="23539963-2_w_4.jpg" alt><br><img src="22937420-1_w_1.jpg" alt><br><img src="8851357-1_w_1.jpg" alt></p><h2 id="纸质"><a href="#纸质" class="headerlink" title="纸质"></a>纸质</h2><p><img src="9181074-1_w_1.jpg" alt><br><img src="23374205-1_w_2.jpg" alt><br><img src="23631999-1_w_2.jpg" alt></p>]]></content:encoded>
<comments>http://emacoo.cn/notes/2016-booklist/#disqus_thread</comments>
</item>
<item>
<title>我们为什么要写作?</title>
<link>http://emacoo.cn/notes/why-i-write/</link>
<guid>http://emacoo.cn/notes/why-i-write/</guid>
<pubDate>Sat, 24 Dec 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="为什么要写作?"><a href="#为什么要写作?" class="headerlink" title="为什么要写作?"></a>为什么要写作?</h2><p>要回答这个问题,先来看看大咖们怎么说:</p>
<blockquote>
<p>Paul Graha
</description>
<content:encoded><![CDATA[<h2 id="为什么要写作?"><a href="#为什么要写作?" class="headerlink" title="为什么要写作?"></a>为什么要写作?</h2><p>要回答这个问题,先来看看大咖们怎么说:</p><blockquote><p>Paul Graham (YC创始人): I think it’s far more important to write well than most people realize. Writing doesn’t just communicate ideas; it generates them.</p><p>Steve Yegge (Google): It’s become pretty clear to me that blogging is a source of both innovation and clarity. I have many of my best ideas and insights while blogging. Struggling to express things that you’re thinking or feeling helps you understand them better.</p><p>Shubhro Saha (Facebook): Software engineers should write because it promotes many of the same skills required in programming. A core skill in both disciplines is an ability to think clearly. The best software engineers are great writers because their prose is as logical and elegant as their code.</p><p>王建硕(百姓网创始人):我把写东西完全当作自己的一个旅程,不是为了说服任何人,也不是为了传递什么信息。最主要是为了自己。我发现只有能优美、简洁、准确的把一个想法表达出来,我才敢说自己思考过了。否则落笔时,才发现有些概念依然模糊,或者答案还没有浮出水面。不写出来,自己就会浑然不知。</p><p>鬼脚七(前淘宝搜索负责人):我知道一点,在我写文章的时候我很快乐,那是一种宁静的快乐。我每天睡觉经常只有6个来小时,白天我很忙碌,一直忙碌到晚上,但夜深人静时,就是我的欢乐时光。我每天很期待这一刻的到来,就像现在。</p><p>Fenny(前丁香园CTO):在写公众号之前,我已经写过近十年的博客,六年的推特,三四年的微博,可以说几乎每天都在用键盘写东西,但无奈天分有限,也未受过严格训练,写的并不好。偶有寸进,心下会暗自窃喜。写作让我思想更为自由,感谢写作带给我的愉悦。</p><p>Roy Li(黑客,连续创业者):就连我自己阅读的时候也像池院长所说的:自己看了都有收获。这便是写作的魅力,多尝试不同的角度思考和写作,对自身成长的帮助是不可估量的。</p></blockquote><p>就像一千个人眼里有一千个哈姆雷特,一千个人心中有一千个写作的理由。但有一点我相信是共通的,那就是有关自我成长。表面上看写作的对象是别人,但真实情况是,你是你自己最忠实的读者。你写的每一篇文章,第一个读者永远是你,阅读次数最多的也是你,最大的收益也归于你。在我看来,写作是完成自我迭代不可或缺的一环。身处移动互联网时代,我们的时间被极度的碎片化,大量的信息通过各种渠道涌入我们的大脑,感兴趣的、不感兴趣的,都在抢夺我们有限的注意力。写作的过程,就是将大脑中层出不穷、稍纵即逝的思维碎片整理成稳定、有序的文字的过程。这就好比Java里面的垃圾回收机制,一边将不再被引用的对象所占用的内存释放出来,一边将碎片化的内存重新整理成连续、有序的内存。</p><p><img src="gc.png" alt></p><h2 id="写作的益处"><a href="#写作的益处" class="headerlink" title="写作的益处"></a>写作的益处</h2><p>类似于<a href="http://emacoo.cn/coding/source-retrofit/">上篇</a>提到的阅读源代码,写作是更为典型的第二象限的事情(参见<a href="http://product.dangdang.com/23592549.html" target="_blank" rel="noopener">《高效能人士的七个习惯》</a>)。对我而言,写作至少意味着三个层面的益处。</p><p>第一,温故知新。写作是一个对已有知识重新思考的过程,在这个过程中很容易发现之前认知中的盲点,进而产生新的认知。并且在不断思考的过程中,有可能悟到一些宝贵的智慧。</p><p>第二,构建知识账本。知识账本是你所有可观测的知识的总和。每写出一篇文章,你的知识账本上就多了一条记录。并且只有写出来,你才有机会通过不断迭代来扩充你的认知。随着知识账本的不断累积,你的认知圈越来越大,相应的,接触到的未知领域也越来越多,你会变的越来越谦卑,离智慧也会越来越近。</p><p>第三,打造个人品牌,提升影响力。文章是最有效的传播手段,能够突破地理空间的限制,触达你原本难以企及的人群,并产生持续的影响力。一些好的文章或者著作,甚至可以跨越时间的长河,影响几十年后的读者。另一方面,写作是你和世界交流的窗口,你通过写作认识世界,世界也通过你写的文章认识你。</p><h2 id="为什么不写?"><a href="#为什么不写?" class="headerlink" title="为什么不写?"></a>为什么不写?</h2><p>既然写作有这么多好处,为什么还是有那么多人迟迟不愿动笔呢?没有时间或许是最常见的理由。Steve Yegge<a href="https://sites.google.com/site/steveyegge2/you-should-write-blogs" target="_blank" rel="noopener">一针见血</a>的指出了这个理由背后的根源:</p><blockquote><p>We’re all too busy to do things we don’t want to do.</p></blockquote><h2 id="我写作的经历"><a href="#我写作的经历" class="headerlink" title="我写作的经历"></a>我写作的经历</h2><p>有一点要强调一下,上文所说的写作是指主动写作,不包括那些应试的或者任务型的被动写作。在这个意义上,我写作的经历大致可以分为三个阶段:上大学之前的日记,大学阶段的博客以及现在的个人站点。虽然我工作近10年,真正意义上的写作却只能从去年年底建立这个个人站点为开始。相比现在很多毕业没几年就有了个人站点甚至还开设了公众号的90后,我算是起了个大早,赶了个晚集。好在写作这条路很长,如同瀚无边际的宇宙,什么时候开始并不重要,重要的是开始本身。</p><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><ul><li><a href="http://www.paulgraham.com/writing44.html" target="_blank" rel="noopener">Writing, Briefly</a></li><li><a href="http://www.shubhro.com/2014/12/27/software-engineers-should-write/" target="_blank" rel="noopener">Software engineers should write</a></li><li><a href="https://sites.google.com/site/steveyegge2/you-should-write-blogs" target="_blank" rel="noopener">You Should Write Blogs</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MjM5NzI0Mjg0MA==&mid=2652370987&idx=1&sn=c8083fd148a94fa2f4b74986b3045f03&scene=0#wechat_redirect" target="_blank" rel="noopener">有人写文章就是为了促进思考</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MjM5OTM3NjIyMA==&mid=10000469&idx=1&sn=958bb0eaecb241a2811f8e534172161c&scene=0#wechat_redirect" target="_blank" rel="noopener">做自己·爱生活–写在微信订阅量超过十万</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MjM5ODIyMTE0MA==&mid=2650968841&idx=1&sn=67895f27a4c65cb9c84ae48b27cbd5bc&scene=0#wechat_redirect" target="_blank" rel="noopener">小道消息,开通四年了</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MjM5MTc2MDEzMw==&mid=200149125&idx=1&sn=b3b85ffe7f180ed635a6a4c7bb56f064&scene=1#rd" target="_blank" rel="noopener">写作的角度</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/notes/why-i-write/#disqus_thread</comments>
</item>
<item>
<title>【赏码会】HTTP Client中的瑞士军刀:Retrofit</title>
<link>http://emacoo.cn/coding/source-retrofit/</link>
<guid>http://emacoo.cn/coding/source-retrofit/</guid>
<pubDate>Sat, 10 Dec 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>最近开始在GitHub上找一些优秀的开源项目,跟团队一起阅读源代码,每周一次,每次一个半小时左右,美其名曰“赏码会”(还记得
</description>
<content:encoded><![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>最近开始在GitHub上找一些优秀的开源项目,跟团队一起阅读源代码,每周一次,每次一个半小时左右,美其名曰“赏码会”(还记得《唐伯虎点秋香》那句“赏花赏月赏秋香”吗?)。为什么要阅读源代码?好处举不胜举,比如学习如何合理的命名,如何写出简洁、清晰的注释,如何编写有效的单元测试,知道良好的编码风格是什么样的。有一些积累之后,可以试试看找一找隐藏在代码里的设计模式,加一些新的单元测试,想一想如果自己实现会如何设计,亦或者尝试提交一个PR,修复一个Issue。阅读源代码可以说是有百利而无一害,属于典型的第二象限的事情(参见<a href="http://product.dangdang.com/23592549.html" target="_blank" rel="noopener">《高效能人士的七个习惯》</a>)。</p><h2 id="Retrofit简介"><a href="#Retrofit简介" class="headerlink" title="Retrofit简介"></a>Retrofit简介</h2><p>第一次“赏码会”我选的是<a href="https://github.com/square/retrofit" target="_blank" rel="noopener">Retrofit</a>项目,为什么选它呢?第一,小巧(核心代码不到5000行),第二,高Star(17+K),第三,平时一直在用。先简单介绍一下Retrofit这个框架。Retrofit是<a href="https://squareup.com/global/en/pos" target="_blank" rel="noopener">Square</a>公司开源的一个Java实现的轻量级HTTP Client框架,本质上是对Square公司另一个开源框架OkHTTP的一层type-safe的封装。所谓的type-safe,我的理解就是将OkHTTP原生的Request/Response对象通过类型安全的方式转化为其他任意类型的对象,比如String,用户自定义类型等。</p><p>面向接口的声明式API定义风格是Retrofit最受欢迎的特性,例如下面的GitHubService接口的listRepos方法定义了GitHub的<a href="https://developer.github.com/v3/repos/#list-user-repositories" target="_blank" rel="noopener">List user repositories</a> 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">GitHubService</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"users/{user}/repos"</span>)</span><br><span class="line"> Call<List<Repo>> listRepos(<span class="meta">@Path</span>(<span class="string">"user"</span>) String user);</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></pre></td><td class="code"><pre><span class="line">Retrofit retrofit = <span class="keyword">new</span> Retrofit.Builder()</span><br><span class="line"> .baseUrl(<span class="string">"https://api.github.com/"</span>)</span><br><span class="line"> .build();</span><br><span class="line">GitHubService service = retrofit.create(GitHubService<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">Call<List<Repo>> repos = service.listRepos(<span class="string">"octocat"</span>);</span><br></pre></td></tr></table></figure><p>有经验的Java程序员立刻就能看出,相对于其他的HTTP Client框架,比如<a href="https://hc.apache.org/" target="_blank" rel="noopener">Apache HttpClient</a>或者<a href="https://github.com/AsyncHttpClient/async-http-client" target="_blank" rel="noopener">Async Http Client</a>,使用Retrofit将使编程效率产生质的提升。</p><h2 id="核心类"><a href="#核心类" class="headerlink" title="核心类"></a>核心类</h2><p>从GitHub拉取Retrofit的源代码,导入retrofit子工程,核心代码都在retrofit包下。</p><p><img src="retrofit-project.png" alt></p><p>核心类列举如下:</p><ul><li>Retrofit: Retrofit框架的门面类,大多数情况下,你的代码中只需要用到它。</li><li>ServiceMethod: 对应接口类中的一个方法(比如上文中的listRepos),负责解析方法签名中用到的各种注解,生成最终的Request对象。</li><li>Call: 类似于Java 8里面的CompletableFuture,提供异步支持。</li><li>CallAdapter: Retrofit默认只接受Call<?>作为方法返回类型,如果需要使用其他类型,就要添加额外的CallAdapter。</li><li>Converter: Retrofit默认只接受Response和Void作为Call<?>的类型参数,如果需要使用其他类型,就要添加额外的Converter。</li></ul><h2 id="解惑"><a href="#解惑" class="headerlink" title="解惑"></a>解惑</h2><p>和任何形式的阅读(读书,读人,读心)一样,要读懂源代码,一定要带着问题去读。为了帮助理解上述几个核心类的关系,简单列举几个我阅读代码时思考的问题,</p><h3 id="Q1:为什么只有接口?实现类在哪?"><a href="#Q1:为什么只有接口?实现类在哪?" class="headerlink" title="Q1:为什么只有接口?实现类在哪?"></a>Q1:为什么只有接口?实现类在哪?</h3><p>A: 答案很简单,因为使用了JDK的动态代理,非常讨巧的设计。</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="keyword">public</span> <T> <span class="function">T <span class="title">create</span><span class="params">(<span class="keyword">final</span> Class<T> service)</span> </span>{</span><br><span class="line"> Utils.validateServiceInterface(service);</span><br><span class="line"> <span class="keyword">if</span> (validateEagerly) {</span><br><span class="line"> eagerlyValidateMethods(service);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> (T) Proxy.newProxyInstance(service.getClassLoader(), <span class="keyword">new</span> Class<?>[] { service },</span><br><span class="line"> <span class="keyword">new</span> InvocationHandler() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Platform platform = Platform.get();</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(Object proxy, Method method, Object... args)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> Throwable </span>{</span><br><span class="line"> <span class="comment">// If the method is a method from Object then defer to normal invocation.</span></span><br><span class="line"> <span class="keyword">if</span> (method.getDeclaringClass() == Object<span class="class">.<span class="keyword">class</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> method.invoke(<span class="keyword">this</span>, args);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (platform.isDefaultMethod(method)) {</span><br><span class="line"> <span class="keyword">return</span> platform.invokeDefaultMethod(method, service, proxy, args);</span><br><span class="line"> }</span><br><span class="line"> ServiceMethod<Object, Object> serviceMethod =</span><br><span class="line"> (ServiceMethod<Object, Object>) loadServiceMethod(method);</span><br><span class="line"> OkHttpCall<Object> okHttpCall = <span class="keyword">new</span> OkHttpCall<>(serviceMethod, args);</span><br><span class="line"> <span class="keyword">return</span> serviceMethod.callAdapter.adapt(okHttpCall);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="Q2:我不想用Call,怎样才能使用其他返回类型?"><a href="#Q2:我不想用Call,怎样才能使用其他返回类型?" class="headerlink" title="Q2:我不想用Call,怎样才能使用其他返回类型?"></a>Q2:我不想用Call,怎样才能使用其他返回类型?</h3><p>A: 刚才已经提到了,通过Retrofit.Builder#addCallAdapterFactory()添加相应的CallAdapter,例如想返回CompleteableFuture,可以使用Retrofit提供的<a href="https://github.com/square/retrofit/blob/master/retrofit-adapters/java8/src/main/java/retrofit2/adapter/java8/Java8CallAdapterFactory.java" target="_blank" rel="noopener">Java8CallAdapterFactory</a>。</p><h3 id="Q3:如何打印请求和响应日志?"><a href="#Q3:如何打印请求和响应日志?" class="headerlink" title="Q3:如何打印请求和响应日志?"></a>Q3:如何打印请求和响应日志?</h3><p>A: 跟动态设置Headers类似,可以自定义用于打印日志的<a href="https://github.com/square/okhttp/wiki/Interceptors" target="_blank" rel="noopener">OkHttp interceptor</a>,然后添加到自己创建的OkHttpClient实例,再绑定Retrofit.Builder#client()。</p><h2 id="漫谈"><a href="#漫谈" class="headerlink" title="漫谈"></a>漫谈</h2><p>总的来说,Retrofit框架设计精巧,上手简单,开发效率高,但也存在一些不足。第一,跟OkHTTP框架绑定太死,不像<a href="https://github.com/OpenFeign/feign" target="_blank" rel="noopener">Feign</a>那么灵活,支持多种Client。第二,和JDK的动态代理强绑定,对其他AOP方式不友好,比如这个<a href="https://github.com/square/retrofit/issues/2113" target="_blank" rel="noopener">Issue</a>提到的Hystrix集成问题。</p><p>今天先写到这里,未来我会不定期放一些“赏码会”的心得,欢迎到GitHub<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言</a>交流。</p><h2 id="传送门"><a href="#传送门" class="headerlink" title="传送门"></a>传送门</h2><ul><li><a href="http://square.github.io/retrofit/" target="_blank" rel="noopener">Retrofit 官网</a></li><li><a href="https://github.com/square/retrofit" target="_blank" rel="noopener">Retrofit GitHub</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/coding/source-retrofit/#disqus_thread</comments>
</item>
<item>
<title>微服务化改造系列之四:授权中心</title>
<link>http://emacoo.cn/arch/microservice-oauth2/</link>
<guid>http://emacoo.cn/arch/microservice-oauth2/</guid>
<pubDate>Sat, 03 Dec 2016 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>前情概要:</p>
<ul>
<li><a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></li>
<li><a href="http://emacoo.c
</description>
<content:encoded><![CDATA[<blockquote><p>前情概要:</p><ul><li><a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></li><li><a href="http://emacoo.cn/arch/microservice-registry-center/">微服务化改造系列之二:服务注册中心</a></li><li><a href="http://emacoo.cn/arch/microservice-config/">微服务化改造系列之三:配置中心</a></li></ul></blockquote><h2 id="授权中心概述"><a href="#授权中心概述" class="headerlink" title="授权中心概述"></a>授权中心概述</h2><p>这篇文章是微服务化改造系列的第四篇,主题是授权中心。有了服务注册中心和配置中心,下一步应该就可以发起服务调用了吧?Wait, 还有一个关键问题要解决。不同于单体应用内部的方法调用,服务调用存在一个服务授权的概念。打个比方,原本一家三兄弟住一屋,每次上山打猎喊一声就行,后来三兄弟分了家,再打猎就要挨家挨户敲门了。这一敲一应就是所谓的服务授权。</p><p>严格来说,服务授权包含鉴权(Authentication)和授权(Authorization)两部分。鉴权解决的是调用方身份识别的问题,即敲门的是谁。授权解决的是调用是否被允许的问题,即让不让进门。两者一先一后,缺一不可。为避免歧义,如不特殊指明,下文所述授权都是宽泛意义上的授权,即包含了鉴权。</p><p>常见的服务授权有三种,简单授权,协议授权和中央授权。</p><ul><li>简单授权:服务提供方并不进行真正的授权,而是依赖于外部环境进行自动授权,比如IP地址白名单,内网域名等。这就好比三兄弟互相留了一个后门。</li><li>协议授权:服务提供方和服务调用方事先约定一个密钥,服务调用方每次发起服务调用请求时,用约定的密钥对请求内容进行加密生成鉴权头(包含调用方唯一识别ID),服务提供方收到请求后,根据鉴权头找到相应的密钥对请求进行鉴权,鉴权通过后再决定是否授权此次调用。这就好比三兄弟之间约定敲一声是大哥,敲两声是二哥,敲三声是三弟。</li><li>中央授权:引入独立的授权中心,服务调用方每次发起服务调用请求时,先从授权中心获取一个授权码,然后附在原始请求上一起发给服务提供方,提供方收到请求后,先通过授权中心将授权码还原成调用方身份信息和相应的权限列表,然后决定是否授权此次调用。这就好比三兄弟每家家门口安装了一个110联网的指纹识别器,通过远程指纹识别敲门人的身份。</li></ul><p>一般来说,简单授权在业务规则简单、安全性要求不高的场景下用的比较多。而协议授权,比较适用于点对点或者C/S架构的服务调用场景,比如<a href="http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html" target="_blank" rel="noopener">Amazon S3 API</a>。对于网状结构的微服务而言,中央授权是三种方式中最适合也是最灵活的选择:</p><ol><li>简化了服务提供方的实现,让提供方专注于权限设计而非实现。</li><li>更重要的是提供了一套独立于服务提供方和服务调用方的授权机制,无需重新发布服务,只要在授权中心修改服务授权规则,就可以影响后续的服务调用。</li></ol><h3 id="OAuth"><a href="#OAuth" class="headerlink" title="OAuth"></a>OAuth</h3><p>说起具体的授权协议,很多人第一反应就是OAuth。事实上也的确如此,很多互联网公司的开放平台都是基于OAuth协议实现的,比如<a href="https://developers.google.com/identity/protocols/OAuth2" target="_blank" rel="noopener">Google APIs</a>, <a href="http://mp.weixin.qq.com/wiki/4/9ac2e7b1f1d22e9e57260f6553822520.html" target="_blank" rel="noopener">微信网页授权接口</a>。一次标准的OAuth授权过程如下:</p><p><img src="oauth2.png" alt></p><p>对应到微服务场景,服务提供方相当于上图中的Resource Server,服务调用方相当于Client,而授权中心相当于Authorization Server和Resource Owner的合体。</p><p>想了解更多关于OAuth的信息,可移步<a href="http://oauthlib.readthedocs.io/en/latest/oauth2/oauth2.html" target="_blank" rel="noopener">OAuth2</a>或者<a href="http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html" target="_blank" rel="noopener">OAuth2中文版</a>。</p><h3 id="Beared-Token"><a href="#Beared-Token" class="headerlink" title="Beared Token"></a>Beared Token</h3><p>在标准的OAuth授权过程中,Resource Server收到Client发来的请求后,需要到Authorization Server验证Access Token,并获取Client的进一步信息。通过OAuth 2.0版本引入中的Beared Token,我们可以省去这一次调用,将Client信息存入Access Token,并在Resource Server端完成Access Token的鉴权。主流的Beared Token有<a href="http://samltool.io/" target="_blank" rel="noopener">SAML</a>和<a href="https://jwt.io/introduction/" target="_blank" rel="noopener">JWT</a>两种格式,SAML基于XML,而JWT基于JSON。由于大多数微服务都使用JSON作为序列化格式,JWT使用的更为广泛。</p><h2 id="框架选型"><a href="#框架选型" class="headerlink" title="框架选型"></a>框架选型</h2><p>在选型OAuth框架时,我主要调研了<a href="https://github.com/apereo/cas" target="_blank" rel="noopener">CAS</a>,<a href="http://oltu.apache.org/" target="_blank" rel="noopener">Apache Oltu</a>,<a href="http://projects.spring.io/spring-security-oauth/" target="_blank" rel="noopener">Spring Security OAuth</a>和<a href="https://github.com/OAuth-Apis/apis" target="_blank" rel="noopener">OAuth-Apis</a>,对比如下:</p><p><img src="oauth2-frameworks.png" alt></p><p>不考虑实际业务场景,CAS和Spring Security OAuth相对另外两种框架,无论是集成成本还是可扩展性,都有明显优势。前文提到,由于我们选用了Spring Boot作为统一的微服务实现框架,Spring Security OAuth是更自然的选择,并且维护成本相对低一些(服务端)。</p><h2 id="最终方案"><a href="#最终方案" class="headerlink" title="最终方案"></a>最终方案</h2><p>最后我们基于Spring Security OAuth框架实现了自己的服务授权中心,鉴权部分做的比较简单,目前只支持私网认证。大致的服务授权流程如下:</p><p><img src="oauth2-interaction.png" alt></p><p><img src="oauth2-sequence.png" alt></p><p>值得一提的是,除了服务调用,我们的服务授权中心还增加了SSO的支持,通过微信企业号实现各个服务后台的单点登录/登出,以后有机会再详细介绍。</p><h2 id="冰山一角"><a href="#冰山一角" class="headerlink" title="冰山一角"></a>冰山一角</h2><p>至此,这个微服务化改造系列就算告一段落,等以后有了更多的积累,我会继续写下去。微服务是一个很大的话题,自Martin Fowler于<a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">2014年3月</a>提出以来,愈演愈热,并跟另一个话题容器化一起开创了一个全新的DevOps时代,引领了国内外大大小小各个互联网公司的技术走向,也影响了我们这一代程序员尤其是后端和运维的思维方式。从这个角度说,我写这个微服务化改造系列文章也是偶然中的必然,希望能给读过这些文章的你带来一些新的启发和思考。如果你对微服务也感兴趣或者有一些心得想跟我交流,欢迎在<a href="https://github.com/emac/emac.github.io/issues/1" target="_blank" rel="noopener">赞赏榜</a>上留下你的微信号。</p><blockquote><p>少年读书如隙中窥月,中年读书如庭中望月,老年读书如台上玩月,皆以阅历之浅深为所得之浅深耳。– 张潮 《幽梦影》</p></blockquote><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://oauthlib.readthedocs.io/en/latest/oauth2/oauth2.html" target="_blank" rel="noopener">OAuth2</a> - <a href="http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html" target="_blank" rel="noopener">中文版</a></li><li><a href="https://jwt.io/introduction/" target="_blank" rel="noopener">JWT</a></li><li><a href="https://github.com/apereo/cas" target="_blank" rel="noopener">CAS</a></li><li><a href="http://oltu.apache.org/" target="_blank" rel="noopener">Apache Oltu</a></li><li><a href="https://github.com/OAuth-Apis/apis" target="_blank" rel="noopener">OAuth-Apis</a></li><li><a href="http://projects.spring.io/spring-security-oauth/" target="_blank" rel="noopener">Spring Security OAuth</a></li><li><a href="http://projects.spring.io/spring-security-oauth/docs/oauth2.html" target="_blank" rel="noopener">OAuth 2 Developers Guide</a> </li><li><a href="https://blog.yorkxin.org/2013/09/30/oauth2-implementation-differences-among-famous-sites" target="_blank" rel="noopener">各大網站 OAuth 2.0 實作差異</a></li><li>参考示例:<a href="https://github.com/spring-projects/spring-security-oauth/tree/master/samples" target="_blank" rel="noopener">spring-security-oauth</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/microservice-oauth2/#disqus_thread</comments>
</item>
<item>
<title>微服务化改造系列之三:配置中心</title>
<link>http://emacoo.cn/arch/microservice-config/</link>
<guid>http://emacoo.cn/arch/microservice-config/</guid>
<pubDate>Sat, 26 Nov 2016 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>前情概要:</p>
<ul>
<li><a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></li>
<li><a href="http://emacoo.c
</description>
<content:encoded><![CDATA[<blockquote><p>前情概要:</p><ul><li><a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></li><li><a href="http://emacoo.cn/arch/microservice-registry-center/">微服务化改造系列之二:服务注册中心</a></li></ul></blockquote><h2 id="配置中心概述"><a href="#配置中心概述" class="headerlink" title="配置中心概述"></a>配置中心概述</h2><p>这篇文章是微服务化改造系列的第三篇,主题是配置中心。上一篇我们谈到服务注册中心,即通过提供某种注册和发现的机制,解决服务互通的问题。那么问题来了,一个服务如何知道服务注册中心的地址呢?这就涉及到服务配置了。我们知道,大至一个PaaS平台,小至一个缓存框架,一般都依赖于特定的配置以正常提供服务,微服务也不例外。</p><h3 id="配置分类"><a href="#配置分类" class="headerlink" title="配置分类"></a>配置分类</h3><ul><li>按配置的来源划分,主要有源代码(俗称hard-code),文件,数据库和远程调用。</li><li>按配置的适用环境划分,可分为开发环境,测试环境,预发布环境,生产环境等。</li><li>按配置的集成阶段划分,可分为编译时,打包时和运行时。编译时,最常见的有两种,一是源代码级的配置,二是把配置文件和源代码一起提交到代码仓库中。打包时,即在应用打包阶段通过某种方式将配置(一般是文件形式)打入最终的应用包中。运行时,是指应用启动前并不知道具体的配置,而是在启动时,先从本地或者远程获取配置,然后再正常启动。</li><li>按配置的加载方式划分,可分为单次加载型配置和动态加载型配置。</li></ul><h3 id="演变"><a href="#演变" class="headerlink" title="演变"></a>演变</h3><p>随着业务复杂度的上升和技术架构的演变,对应用的配置方式也提出了越来越高的要求。一个典型的演变过程往往是这样的,起初所有配置跟源代码一起放在代码仓库中;之后出于安全性的考虑,将配置文件从代码仓库中分离出来,或者放在CI服务器上通过打包脚本打入应用包中,或者直接放到运行应用的服务器的特定目录下,剩下的非文件形式的关键配置则存入数据库中。上述这种方式,在单体应用阶段非常常见,也往往可以运行的很好,但到了微服务阶段,面对爆发式增长的应用数量和服务器数量,就显得无能为力了。这时,就轮到配置中心大显身手了。那什么是配置中心?简单来说,就是一种统一管理各种应用配置的基础服务组件。</p><h2 id="框架选型"><a href="#框架选型" class="headerlink" title="框架选型"></a>框架选型</h2><p>选型一个合格的配置中心,至少需要满足如下4个核心需求:</p><ul><li>非开发环境下应用配置的保密性,避免将关键配置写入源代码</li><li>不同部署环境下应用配置的隔离性,比如非生产环境的配置不能用于生产环境</li><li>同一部署环境下的服务器应用配置的一致性,即所有服务器使用同一份配置</li><li>分布式环境下应用配置的可管理性,即提供远程管理配置的能力</li></ul><p>现在开源社区主流的配置中心框架有Spring Cloud Config和disconf,两者都满足了上述4个核心需求,但又有所区别。</p><h3 id="Spring-Cloud-Config"><a href="#Spring-Cloud-Config" class="headerlink" title="Spring Cloud Config"></a>Spring Cloud Config</h3><p><img src="spring-cloud-config.png" alt></p><p><a href="http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_spring_cloud_config" target="_blank" rel="noopener">Spring Cloud Config</a>可以说是一个为Spring量身定做的轻量级配置中心,巧妙的将应用运行环境映射为profile,应用版本映射为label。在服务端,基于特定的外部系统(Git、文件系统或者Vault)存储和管理应用配置;在客户端,利用强大的Spring配置系统,在运行时加载应用配置。</p><h3 id="disconf"><a href="#disconf" class="headerlink" title="disconf"></a>disconf</h3><p><img src="disconf.jpg" alt></p><p><a href="http://disconf.readthedocs.io/zh_CN/latest/index.html" target="_blank" rel="noopener">disconf</a>是前百度资深研发工程师廖绮绮的开源作品。在服务端,提供了完善的操作界面管理各种运行环境,应用和配置文件;在客户端,深度集成Spring,通过Spring AOP实现应用配置的自动加载和刷新。</p><h2 id="最终方案"><a href="#最终方案" class="headerlink" title="最终方案"></a>最终方案</h2><p>不管是Spring Cloud Config还是disconf,默认提供的客户端都深度绑定了Spring框架,这对非Spring应用而言无疑增加了集成成本,即便它们都提供了获取应用配置的API。最终我们还是选用了微服务化改造之前自研的Matrix作为配置中心,一方面,可以保持新老系统使用同一套配置服务,降低维护成本,另一方面,在满足4个核心需求的前提下,Matrix还提供了一些独有的能力。</p><ul><li>分离配置文件和配置项。对于配置文件,通过各类配套打包插件(sbt, maven, gradle),在打包时将配置文件打入应用包中,同时最小化对CI的侵入性;对于配置项,提供SDK,帮助应用从服务端获取配置项,同时支持简单的缓存机制。</li><li>增加应用版本维度,即对于同一应用,可以在服务端针对不同版本或版本区间维护不同的应用配置。</li><li>应用配置的版本化支持,类似于Git,可以将任一应用配置回退到任一历史版本。</li></ul><p>进一步信息可参考我之前写的Matrix<a href="https://www.zybuluo.com/emac/note/241756" target="_blank" rel="noopener">设计文档</a>。</p><p><img src="matrix.png" alt></p><p><em>Matrix架构图</em></p><p>下一篇我将给大家介绍微服务架构的另一个基础组件——授权中心,敬请期待!</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_spring_cloud_config" target="_blank" rel="noopener">Spring Cloud Config</a></li><li><a href="http://disconf.readthedocs.io/zh_CN/latest/index.html" target="_blank" rel="noopener">disconf</a></li><li><a href="https://www.zybuluo.com/emac/note/241756" target="_blank" rel="noopener">Matrix</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/microservice-config/#disqus_thread</comments>
</item>
<item>
<title>微服务化改造系列之二:服务注册中心</title>
<link>http://emacoo.cn/arch/microservice-registry-center/</link>
<guid>http://emacoo.cn/arch/microservice-registry-center/</guid>
<pubDate>Sat, 19 Nov 2016 16:00:00 GMT</pubDate>
<description>
<blockquote>
<p>前情概要:<a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></p>
</blockquote>
<h2 id="服务注册中心概述"><a href="#服
</description>
<content:encoded><![CDATA[<blockquote><p>前情概要:<a href="http://emacoo.cn/arch/microservice-overview">微服务化改造系列之一:总览</a></p></blockquote><h2 id="服务注册中心概述"><a href="#服务注册中心概述" class="headerlink" title="服务注册中心概述"></a>服务注册中心概述</h2><p>这篇文章是微服务化改造系列的第二篇,主题是服务注册中心。作为微服务架构最基础也是最重要的组件之一,服务注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的。更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。</p><p>设计或者选型一个服务注册中心,首先要考虑的就是服务注册与发现机制。纵观当下各种主流的服务注册中心解决方案,大致可归为三类:</p><ul><li>应用内:直接集成到应用中,依赖于应用自身完成服务的注册与发现,最典型的是Netflix提供的<a href="https://github.com/Netflix/eureka" target="_blank" rel="noopener">Eureka</a></li><li>应用外:把应用当成黑盒,通过应用外的某种机制将服务注册到注册中心,最小化对应用的侵入性,比如Airbnb的<a href="http://nerds.airbnb.com/smartstack-service-discovery-cloud/" target="_blank" rel="noopener">SmartStack</a>,HashiCorp的<a href="https://www.consul.io/" target="_blank" rel="noopener">Consul</a></li><li>DNS:将服务注册为DNS的SRV记录,严格来说,是一种特殊的应用外注册方式,<a href="https://github.com/skynetservices/skydns" target="_blank" rel="noopener">SkyDNS</a>是其中的代表</li></ul><p><em>注1:对于第一类注册方式,除了Eureka这种一站式解决方案,还可以基于ZooKeeper或者Etcd自行实现一套服务注册机制,这在大公司比较常见,但对于小公司而言显然性价比太低。</em></p><p><em>注2:由于DNS固有的缓存缺陷,本文不对第三类注册方式作深入探讨。</em></p><p>除了基本的服务注册与发现机制,从开发和运维角度,至少还要考虑如下五个方面:</p><ul><li>测活:服务注册之后,如何对服务进行测活以保证服务的可用性?</li><li>负载均衡:当存在多个服务提供者时,如何均衡各个提供者的负载?</li><li>集成:在服务提供端或者调用端,如何集成注册中心?</li><li>运行时依赖:引入注册中心之后,对应用的运行时环境有何影响?</li><li>可用性:如何保证注册中心本身的可用性,特别是消除单点故障?</li></ul><p>以下就围绕上述几个方面,简单分析一下Eureka,SmartStack,Consul的利弊。</p><h3 id="Eureka"><a href="#Eureka" class="headerlink" title="Eureka"></a>Eureka</h3><p><img src="eureka.png" alt></p><p>从设计角度来看,Eureka可以说是无懈可击,注册中心、提供者、调用者边界清晰,通过去中心化的集群支持保证了注册中心的整体可用性,但缺点是Eureka属于应用内的注册方式,对应用的侵入性太强,且只支持Java应用。</p><h3 id="SmartStack"><a href="#SmartStack" class="headerlink" title="SmartStack"></a>SmartStack</h3><p><img src="smartstack.png" alt></p><p>SmartStack可以说是三种方案中最复杂的,涉及了ZooKeeper、HAProxy、Nerve和Synapse四种异构组件,对运维提出了很高的要求。它最大的好处是对应用零侵入,且适用于任意类型的应用。</p><h3 id="Consul"><a href="#Consul" class="headerlink" title="Consul"></a>Consul</h3><p><img src="consul-standard.png" alt></p><p>Consul本质上属于应用外的注册方式,但可以通过SDK简化注册流程。而服务发现恰好相反,默认依赖于SDK,但可以通过Consul Template(下文会提到)去除SDK依赖。</p><h2 id="最终方案"><a href="#最终方案" class="headerlink" title="最终方案"></a>最终方案</h2><p>最终我们选择了Consul作为服务注册中心的实现方案,主要原因有两点:</p><ol><li>最小化对已有应用的侵入性,这也是贯穿我们整个微服务化改造的原则之一</li><li>降低运维的复杂度,Consul Agent既可以运行在服务器模式,又可以运行在客户端模式</li></ol><h3 id="Consul-Template"><a href="#Consul-Template" class="headerlink" title="Consul Template"></a>Consul Template</h3><p>上文提到使用Consul,默认服务调用者需要依赖Consul SDK来发现服务,这就无法保证对应用的零侵入性。所幸通过<a href="https://github.com/hashicorp/consul-template" target="_blank" rel="noopener">Consul Template</a>,可以定时从Consul集群获取最新的服务提供者列表并刷新LB配置(比如nginx的upstream),这样对于服务调用者而言,只需要配置一个统一的服务调用地址即可。改造后的调用关系如下:</p><p><img src="consul-template.png" alt></p><h3 id="Spring-Cloud-Consul"><a href="#Spring-Cloud-Consul" class="headerlink" title="Spring Cloud Consul"></a>Spring Cloud Consul</h3><p>由于我们选用了Spring Boot作为统一的微服务实现框架,很自然的,可以利用Spring Cloud提供的Consul组件进一步简化服务注册流程,省去额外的服务提供端的Consul配置。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.consul.io/docs/index.html" target="_blank" rel="noopener">CONSUL DOCUMENTATION</a></li><li><a href="https://github.com/hashicorp/consul-template" target="_blank" rel="noopener">consul-template</a></li><li><a href="http://cloud.spring.io/spring-cloud-consul/" target="_blank" rel="noopener">Spring Cloud Consul</a></li><li><a href="http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html" target="_blank" rel="noopener">Spring Cloud Netflix</a></li><li><a href="http://nobodyiam.com/2016/06/25/dive-into-eureka/" target="_blank" rel="noopener">Dive into Eureka</a></li><li><a href="http://nerds.airbnb.com/smartstack-service-discovery-cloud/" target="_blank" rel="noopener">SmartStack: Service Discovery in the Cloud</a></li><li><a href="http://jasonwilder.com/blog/2014/02/04/service-discovery-in-the-cloud/" target="_blank" rel="noopener">Open-Source Service Discovery</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/microservice-registry-center/#disqus_thread</comments>
</item>
<item>
<title>微服务化改造系列之一:总览</title>
<link>http://emacoo.cn/arch/microservice-overview/</link>
<guid>http://emacoo.cn/arch/microservice-overview/</guid>
<pubDate>Sat, 12 Nov 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="1-写在前面"><a href="#1-写在前面" class="headerlink" title="1 写在前面"></a>1 写在前面</h2><h3 id="背景"><a href="#背景" class="headerlink" title="背景"><
</description>
<content:encoded><![CDATA[<h2 id="1-写在前面"><a href="#1-写在前面" class="headerlink" title="1 写在前面"></a>1 写在前面</h2><h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>技术圈流行一句话,凡脱离业务谈架构的,都是耍流氓。作为微服务改造系列的第一篇博客,首先介绍一下实施这次技术改造的背景。</p><p>第一,我所在公司(简称XR)的后台服务采用的主技术栈是Scala,虽然开发效率很高,但也带来一系列的副作用。1.由于Scala语言强大的表达能力和丰富的函数式特性,很容易写出俗称“意大利面条”式的代码,一个类文件动辄上千行,代码的可读性非常差,导致可维护性也很差。2.编译Scala源码时首先需要将Scala源码转换成Java源码然后再通过JVM进行编译,加上隐式类型的存在进一步拖慢了编译期间的类型推导,Scala的编译速度比Java足足慢了一个数量级,这个差异在代码量少的时候还不明显,但随着代码量的上升,就成了团队的一个nightmare,试想本地全量编译一次需要10+分钟。3.Scala小众语言的标签决定了Scala程序员的稀缺性,晦涩难懂的官方文档拔高了学习曲线,后果就是高昂的招聘成本和漫长的培养时间。以上这些副作用不但抵消了先期开发效率上的优势,而且使得对新需求的响应能力越来越慢,技术负债也越垒越高。</p><p>第二,历经2年多的产品迭代,整个后台服务项目越来越庞大,已经成为一个典型意义上的单体应用(也就是Martin Fowler常说的monolithic application):1.各个业务模块犬牙交错,重复代码随处可见,补丁代码越打越多。2.任何一个改动都需要一次全量发布,哪怕是修改一句文案。</p><p>第三,与微服务化改造同时进行的是容器化改造,如果不对上述单体应用进行拆分,很多容器化带来的好处就会被削弱,甚至毫无意义,比如提高资源利用率(CPU型应用和内存型应用搭配部署),异构应用的环境隔离能力等。</p><h3 id="局限"><a href="#局限" class="headerlink" title="局限"></a>局限</h3><p>谷歌前研发总监Tiger曾经说过,一个系统的演化一般会经历三个阶段,首先是under-engineer,然后是over-engineer,最后才是right-engineer。考虑到参与此次微服务改造的人员有限(一人主导,多人配合),同时也是团队第一次尝试做这类系统性的改造,最后我们决定采取一条比较实用的改良式路线:</p><ol><li>最小化对已有应用的侵入性</li><li>偏好主流的微服务框架</li><li>只做必要的微服务治理</li></ol><p>第一条定下了此次改造的基调,降低了方案无法落地的风险,确保了项目的整体可行性。第二条让我们站在巨人的肩膀上,不重复造轮子,聚焦在问题本身,而不是工具。第三条缩减项目范围,避免过度工程,以战养兵,不打无用之仗。</p><h2 id="2-微服务简介"><a href="#2-微服务简介" class="headerlink" title="2 微服务简介"></a>2 微服务简介</h2><h3 id="3个关键词"><a href="#3个关键词" class="headerlink" title="3个关键词"></a>3个关键词</h3><p>有关微服务的定义,最权威的版本莫属微服务之父Martin Fowler在<a href="http://martinfowler.com/microservices/" target="_blank" rel="noopener">microservices</a>一文中所述:</p><blockquote><p>In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. – James Lewis and Martin Fowler</p></blockquote><p>注意其中有3个关键词,small,independently deployable和automated deployment。small对应的就是微服务的微,很多初次接触微服务的同学对微的理解往往会停留在实现层面,以为代码少就是微,但实际上,这里的微更多的是体现在逻辑层面。微服务的一个重要设计原则是share as little as possible,什么意思呢?就是说每个微服务应该设计成边界清晰不重叠,数据独享不共享,也就是我们常说的高内聚、低耦合。保证了small,才能做到independently deployable。而实现automated deployment的关键是DevOps文化,可参见Fowler另一篇谈<a href="http://martinfowler.com/bliki/DevOpsCulture.html" target="_blank" rel="noopener">DevOps</a>的文章。</p><p>需要提醒的是,随着业务复杂度的上升,一个微服务可能需要拆分为更多更细粒度的微服务,比方说,一开始只是一个简单的订单服务,后面逐步拆分出清算,支付,结算,对账等其他服务。</p><h3 id="康威定律"><a href="#康威定律" class="headerlink" title="康威定律"></a>康威定律</h3><p>与单体应用拆分为微服务的过程类似,随着公司规模的不断扩大,一个组织势必会分化出多个更小的组织。根据康威定律,组织结构决定系统结构,因此,从这个层面来说,微服务也是一种必然。</p><blockquote><p>康威定律(Conway’s Law):“Any organization that design a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure. - Melvin Conway, 1968</p></blockquote><p><img src="conway.png" alt></p><h3 id="取舍"><a href="#取舍" class="headerlink" title="取舍"></a>取舍</h3><p>从本质上来看,相对单体应用,微服务是以牺牲强一致性、提高部署复杂性为代价,换取更彻底的分布式特性,比如异构性和强隔离性。对应CAP理论,就是用Consistency换取Partition。异构性比较容易理解,通过定义统一的API规范(一般采用REST风格),每个微服务团队可以根据各自的能力矩阵选用最适合的技术栈,而不是所有人必须使用相同的技术栈。强隔离性指的是,对于一个典型的单体应用,隔离性最高只能体现到模块级别,由于共享同一个代码仓库,模块的边界往往比较模糊,需要人为定义很多规范来保证良好的隔离性,但无论如何强调,稍一疏忽,就会产生“越界”行为,时间愈长,维护隔离性的成本愈高。而到了微服务阶段,自带应用级别的隔离性,“越界”的成本大大提升,无需任何规范,架构本身就保证了隔离性。</p><p>另一方面,由于采用了分布式架构,微服务无法再简单的通过数据库事务来保证强一致性,而是通过消息中间件或者某种事务补偿机制来保证最终一致性,比如微信朋友圈的点赞,淘宝订单的物流状态。其次,在微服务阶段,随着应用数量的激增,一次发布往往涉及多个应用,加上异构性带来的部署方式的多样性,对团队的运维水平尤其是自动化水平提出了更高的要求,运维和开发的边界进一步模糊。</p><p><img src="http://martinfowler.com/bliki/images/microservicePrerequisites/sketch.png" alt></p><h3 id="领域知识"><a href="#领域知识" class="headerlink" title="领域知识"></a>领域知识</h3><p>除了组织架构和技术取舍,领域知识是另一个非常重要的决策因素。对于不熟悉的业务领域,很难第一次就把各个微服务的边界和接口定义正确,一旦开始开发,重构成本就会非常可观。反过来说,当对领域知识有了一定的积累,再重构一个单体应用就会容易的多。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>综上所述,虽然微服务看上去很美,但在决定采用微服务架构之前,不仅要仔细考量团队的技术水平(包括知识结构,理论深度,经验积累和技术氛围),还应综合考虑项目的时间范围,领域知识的熟悉程度,以及所在组织的规模架构。除非这些前提条件都满足,否则单体应用是更适合的选择,就像Fowler<a href="http://martinfowler.com/bliki/MonolithFirst.html" target="_blank" rel="noopener">建议</a>的那样。</p><p><img src="http://martinfowler.com/bliki/images/microservice-verdict/path.png" alt></p><h2 id="3-微服务化总览"><a href="#3-微服务化总览" class="headerlink" title="3 微服务化总览"></a>3 微服务化总览</h2><p><img src="microservice.png" alt></p><p>上图是XR微服务化第一阶段的整体架构图。可以看到,一些支撑微服务的必要组件都已包含其中:</p><ul><li>服务注册中心:所有服务注册到Consul集群,集成Nginx实现负载均衡,使用Hystrix实现简单的服务降级和熔断机制</li><li>CI/CD:利用<a href="http://emacoo.cn/devops/jenkins-pipeline-tips">Jenkins Pipeline</a>实现<a href="http://emacoo.cn/devops/ci-cd-hot-deployment">不停机发布</a></li><li>日志平台:扩展ELK加上Redis缓存</li><li>配置中心:使用自研的<a href="https://zybuluo.com/emac/note/241756" target="_blank" rel="noopener">Matrix</a>系统,最小化对已有应用的侵入性,保证异构系统的兼容性</li><li>授权中心:基于Spring Security OAuth,同时支持SSO</li><li>消息中心:选用RabbitMQ作为消息中间件</li><li>监控平台:利用Consul API获取服务状态,通过Zookeeper触发告警</li></ul><p>在微服务化系列的后续文章中,我会针对服务注册、配置中心和授权中心分别展开介绍实施过程中的一些细节和经验。敬请期待。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://martinfowler.com/microservices/" target="_blank" rel="noopener">Microservices Resource Guide</a></li><li><a href="http://slides.com/emacooshen/soa/#/" target="_blank" rel="noopener">企业基础架构浅析</a></li><li><a href="http://callistaenterprise.se/blogg/teknik/2015/03/25/an-operations-model-for-microservices/" target="_blank" rel="noopener">An operations model for Microservices</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzA5Nzc4OTA1Mw==&mid=407641457&idx=1&sn=183d27056f3bd8ef17e77a3c15dfb3dd" target="_blank" rel="noopener">实施微服务,我们需要哪些基础框架?</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzA5Nzc4OTA1Mw%3D%3D&from=timeline&idx=1&isappinstalled=0&mid=411129391&scene=2&sn=ebf06fb5cc4a5f57f86341ba4114cab8&srcid=0409K1M3NlgPnoCzUXN8wiFP" target="_blank" rel="noopener">架构的本质是管理复杂性,微服务本身也是架构演化的结果</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzA4MzQ1NjQ5Nw%3D%3D&idx=1&mid=402005063&sn=6b714f647c29afb15598a1ca3dbd78c2" target="_blank" rel="noopener">应用架构一团糟?如何将单体应用改造为微服务</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzIwMjE5MDU4OA==&mid=2653119912&idx=1&sn=d3b08b362de3d895fe0a088dcdc2380c&scene=23&srcid=0806PBMw9lhxnOpEJhLYuvCC#rd" target="_blank" rel="noopener">一个值得参考的服务化体系改造案例</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&idx=1&mid=2650993889&scene=0&sn=3d8edd0fa55be53d85235212be3a9505" target="_blank" rel="noopener">华为实施微服务架构的五大军规</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/microservice-overview/#disqus_thread</comments>
</item>
<item>
<title>【Java进阶】利用APT优雅的实现统一日志格式</title>
<link>http://emacoo.cn/coding/java-apt-logging/</link>
<guid>http://emacoo.cn/coding/java-apt-logging/</guid>
<pubDate>Sat, 09 Jul 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="统一日志格式的几种方式"><a href="#统一日志格式的几种方式" class="headerlink" title="统一日志格式的几种方式"></a>统一日志格式的几种方式</h2><p>无论是搭建日志平台还是进行大数据分析,统一日志格式都是一个重要的前提
</description>
<content:encoded><![CDATA[<h2 id="统一日志格式的几种方式"><a href="#统一日志格式的几种方式" class="headerlink" title="统一日志格式的几种方式"></a>统一日志格式的几种方式</h2><p>无论是搭建日志平台还是进行大数据分析,统一日志格式都是一个重要的前提条件。假设要统一成下面的日志格式,</p><blockquote><p>日志格式:[{系统}|{模块}]{描述}[param1=value1$param2=value2],例如:[API|Weixin]Weixin send message failed. [senderId=1234$receiverId=5678]</p></blockquote><p>常见的方法有:</p><ul><li>方法1:每次记录日志时,根据上下文在原始的消息内容前后分别加上合适的[{系统}|{模块}]前缀和参数后缀。</li><li>方法2:自定义日志类,将{系统}和{模块}作为构造函数的参数传入,并且在所提供的日志接口中自动格式化传入的参数数组。</li><li>方法3:自定义注解类声明所属的{系统}和{模块},然后通过AOP的方式,统一在日志中插入[{系统}|{模块}]前缀。</li><li>方法4:在方法2的基础上,自定义注解类声明所属的{系统}和{模块},然后通过APT自动生成自定义类型的log成员变量。</li></ul><p>方法1依赖于人工来保证统一的日志格式,方法3虽然简化了方法调用,但对性能有一定的影响。方法2是最常见的手段,但每个类都要显示声明log成员变量,略显冗余。方法4兼具方法2和方法3的优点,同时又避免了两者的不足,是一种优雅的实现方式,也是<a href="https://github.com/rzwitserloot/lombok" target="_blank" rel="noopener">lombok</a>所采用的方式。</p><p>下面就针对方法4,结合示例代码介绍一下相关技术。</p><h2 id="APT-编译期自动生成log成员变量"><a href="#APT-编译期自动生成log成员变量" class="headerlink" title="APT: 编译期自动生成log成员变量"></a>APT: 编译期自动生成log成员变量</h2><p><a href="http://docs.oracle.com/javase/6/docs/technotes/guides/apt/" target="_blank" rel="noopener">APT</a>的全称是Annotation Processing Tool,诞生于Java 6版本,主要用于在编译期根据不同的注解类生成或者修改代码。APT运行于独立的JVM进程中(编译之前),并且在一次编译过程中可能会被多次调用。</p><p>首先,声明一个包含{系统}和{模块}定义的日志注解类。注意@Retention应设置为RetentionPolicy.SOURCE,表示编译后擦除该注解信息。</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="comment">/**</span></span><br><span class="line"><span class="comment"> * 用于自动生成log成员变量.仅适用于class或enum,不适用于接口.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.SOURCE)</span><br><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> Slf4j {</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 系统名称.如果为空则取"-Dvlogging.system"系统属性,如果系统属性也为空,则取"Unknown".</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function">String <span class="title">system</span><span class="params">()</span> <span class="keyword">default</span> ""</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 模块名称.如果为空则取"-Dvlogging.module"系统属性,如果系统属性也为空,则取"Unknown".</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function">String <span class="title">module</span><span class="params">()</span> <span class="keyword">default</span> ""</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后,声明一个注解处理类,继承Java默认提供的AbstractProcessor类,其中:</p><ul><li>messager: 用于记录处理日志</li><li>trees: 用于解析Java AST树</li><li>maker: 用于生成Java AST节点</li><li>names: 用于生成Java AST节点名称</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Slf4jProcessor</span> <span class="keyword">extends</span> <span class="title">AbstractProcessor</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">(ProcessingEnvironment processingEnv)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.init(processingEnv);</span><br><span class="line"> messager = processingEnv.getMessager();</span><br><span class="line"> trees = Trees.instance(processingEnv);</span><br><span class="line"> Context context = ((JavacProcessingEnvironment) processingEnv).getContext();</span><br><span class="line"> maker = TreeMaker.instance(context);</span><br><span class="line"> names = Names.instance(context);</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>在process方法中调用Java Compiler API根据注解信息动态生成log日志成员变量:<br><br><code>private static final Logger log = LoggerFactory.getLogger(LoggerFactory.Type.SLF4J, annotatedClass.class, system, module);</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><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">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">process</span><span class="params">(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)</span> </span>{</span><br><span class="line"> <span class="comment">// 1 检查类型</span></span><br><span class="line"> roundEnv.getElementsAnnotatedWith(Slf4j<span class="class">.<span class="keyword">class</span>).<span class="title">stream</span>().<span class="title">forEach</span>(<span class="title">elm</span> -> </span>{</span><br><span class="line"> <span class="keyword">if</span> (elm.getKind() != ElementKind.CLASS && elm.getKind() != ElementKind.ENUM) {</span><br><span class="line"> messager.printMessage(Diagnostic.Kind.ERROR, <span class="string">"Only classes or enums can be annotated with "</span> + Slf4j<span class="class">.<span class="keyword">class</span>.<span class="title">getSimpleName</span>())</span>;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2 检查log成员变量是否已存在</span></span><br><span class="line"> TypeElement typeElm = (TypeElement) elm;</span><br><span class="line"> <span class="keyword">if</span> (typeElm.getEnclosedElements().stream()</span><br><span class="line"> .filter(e -> e.getKind() == ElementKind.FIELD && Logger.FIELD_NAME.equals(e.getSimpleName())).count() > <span class="number">0</span>) {</span><br><span class="line"> messager.printMessage(Diagnostic.Kind.WARNING, MessageFormat.format(<span class="string">"A member field named {0} already exists in the annotated class"</span>, Logger.FIELD_NAME));</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3 注入log成员变量</span></span><br><span class="line"> CompilationUnitTree cuTree = trees.getPath(typeElm).getCompilationUnit();</span><br><span class="line"> <span class="keyword">if</span> (cuTree <span class="keyword">instanceof</span> JCTree.JCCompilationUnit) {</span><br><span class="line"> JCTree.JCCompilationUnit cu = (JCTree.JCCompilationUnit) cuTree;</span><br><span class="line"> <span class="comment">// only process on files which have been compiled from source</span></span><br><span class="line"> <span class="keyword">if</span> (cu.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) {</span><br><span class="line"> _findType(cu, typeElm.getQualifiedName().toString()).ifPresent(type -> {</span><br><span class="line"> Slf4j slf4j = typeElm.getAnnotation(Slf4j<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> String system = slf4j.system();</span><br><span class="line"> String <span class="keyword">module</span> = slf4j.<span class="keyword">module</span>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 生成private static final Logger log = LoggerFactory.getLogger(LoggerFactory.Type.SLF4J, <annotatedClass>, <system>, <module>);</span></span><br><span class="line"> JCTree.JCExpression loggerType = _toExpression(Logger<span class="class">.<span class="keyword">class</span>.<span class="title">getCanonicalName</span>())</span>;</span><br><span class="line"> JCTree.JCExpression getLoggerMethod = _toExpression(LoggerFactory.class.getCanonicalName() + ".getLogger");</span><br><span class="line"> JCTree.JCExpression typeArg = _toExpression(LoggerFactory.Type.class.getCanonicalName() + "." + LoggerFactory.Type.SLF4J.name());</span><br><span class="line"> JCTree.JCExpression nameArg = _toExpression(typeElm.getQualifiedName() + <span class="string">".class"</span>);</span><br><span class="line"> JCTree.JCExpression systemArg = maker.Literal(system);</span><br><span class="line"> JCTree.JCExpression moduleArg = maker.Literal(<span class="keyword">module</span>);</span><br><span class="line"> JCTree.JCMethodInvocation getLoggerCall = maker.Apply(List.nil(), getLoggerMethod, List.of(typeArg, nameArg, systemArg, moduleArg));</span><br><span class="line"> JCTree.JCVariableDecl logField = maker.VarDef(</span><br><span class="line"> maker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL),</span><br><span class="line"> names.fromString(Logger.FIELD_NAME), loggerType, getLoggerCall);</span><br><span class="line"></span><br><span class="line"> _insertField(type, logField);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf</span>4j(system = <span class="string">"Vlogging"</span>, <span class="keyword">module</span> = <span class="string">"Integration"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">VloggingAnnotated</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> HashMap<String, String> params = <span class="keyword">new</span> HashMap<>();</span><br><span class="line"> params.put(<span class="string">"foo"</span>, <span class="string">"xyz"</span>);</span><br><span class="line"> log.info(VloggingAnnotated<span class="class">.<span class="keyword">class</span>.<span class="title">getCanonicalName</span>(), <span class="title">params</span>)</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由此可见,使用方法4,业务类只要加上自定义注解,然后正常调用日志API,就可以以统一的日志格式记录日志。</p><h3 id="输出示例"><a href="#输出示例" class="headerlink" title="输出示例"></a>输出示例</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2016-07-10 17:26:45 +0800 [INFO] from VloggingAnnotated in main - [Vlogging|Integration]com.xingren.v.logging.integration.VloggingAnnotated[foo=xyz]</span><br></pre></td></tr></table></figure><h2 id="IntelliJ-Plugin-自动生成PSI-Element,消除编译错误"><a href="#IntelliJ-Plugin-自动生成PSI-Element,消除编译错误" class="headerlink" title="IntelliJ Plugin: 自动生成PSI Element,消除编译错误"></a>IntelliJ Plugin: 自动生成PSI Element,消除编译错误</h2><p>至此,在命令行方式下,方法4已经可以正确运行。但在IDE环境中(比如IntelliJ,Eclipse),由于一般它们都会使用自定义的编译模型,需要额外实现一个插件来根据注解信息动态修改IDE的语法树,以避免编译错误。对于IntelliJ而言,使用的是<a href="http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi_elements.html" target="_blank" rel="noopener">PSI模型</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><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="comment">// 继承com.intellij.psi.augment.PsiAugmentProvider类</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@NotNull</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <Psi extends PsiElement> <span class="function">List<Psi> <span class="title">getAugments</span><span class="params">(@NotNull PsiElement psiElement, @NotNull Class<Psi> type)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> List<Psi> emptyResult = Collections.emptyList();</span><br><span class="line"> <span class="comment">// skip processing during index rebuild</span></span><br><span class="line"> <span class="keyword">final</span> Project project = psiElement.getProject();</span><br><span class="line"> <span class="keyword">if</span> (DumbService.isDumb(project)) {</span><br><span class="line"> <span class="keyword">return</span> emptyResult;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Expecting that we are only augmenting an PsiClass</span></span><br><span class="line"> <span class="comment">// Don't filter !isPhysical elements or code auto completion will not work</span></span><br><span class="line"> <span class="keyword">if</span> (!(psiElement <span class="keyword">instanceof</span> PsiExtensibleClass) || !psiElement.isValid()) {</span><br><span class="line"> <span class="keyword">return</span> emptyResult;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// filter non-field type</span></span><br><span class="line"> <span class="keyword">if</span> (!PsiField<span class="class">.<span class="keyword">class</span>.<span class="title">isAssignableFrom</span>(<span class="title">type</span>)) </span>{</span><br><span class="line"> <span class="keyword">return</span> emptyResult;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">final</span> PsiClass psiClass = (PsiClass) psiElement;</span><br><span class="line"> <span class="comment">// see AbstractClassProcessor#process()</span></span><br><span class="line"> PsiAnnotation psiAnnotation = PsiAnnotationUtil.findAnnotation(psiClass, Slf4j<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">null</span> == psiAnnotation) {</span><br><span class="line"> <span class="keyword">return</span> emptyResult;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// check cache first</span></span><br><span class="line"> <span class="keyword">if</span> (loggerCache.containsKey(psiClass.getQualifiedName())) {</span><br><span class="line"> <span class="keyword">return</span> Arrays.asList((Psi) loggerCache.get(psiClass.getQualifiedName()));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> PsiManager manager = psiClass.getContainingFile().getManager();</span><br><span class="line"> <span class="keyword">final</span> PsiElementFactory psiElementFactory = JavaPsiFacade.getElementFactory(project);</span><br><span class="line"> PsiType psiLoggerType = psiElementFactory.createTypeFromText(LOGGER_TYPE, psiClass);</span><br><span class="line"> LightFieldBuilder loggerField = <span class="keyword">new</span> LightFieldBuilder(manager, LOGGER_NAME, psiLoggerType);</span><br><span class="line"> LightModifierList modifierList = (LightModifierList) loggerField.getModifierList();</span><br><span class="line"> modifierList.addModifier(PsiModifier.PRIVATE);</span><br><span class="line"> modifierList.addModifier(PsiModifier.STATIC);</span><br><span class="line"> modifierList.addModifier(PsiModifier.FINAL);</span><br><span class="line"> loggerField.setContainingClass(psiClass);</span><br><span class="line"> loggerField.setNavigationElement(psiAnnotation);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> String loggerInitializerParameter = String.format(LOGGER_CATEGORY, psiClass.getName());</span><br><span class="line"> <span class="keyword">final</span> PsiExpression initializer = psiElementFactory.createExpressionFromText(String.format(LOGGER_INITIALIZER, loggerInitializerParameter), psiClass);</span><br><span class="line"> loggerField.setInitializer(initializer);</span><br><span class="line"> <span class="comment">// add to cache</span></span><br><span class="line"> loggerCache.put(psiClass.getQualifiedName(), loggerField);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Arrays.asList((Psi) loggerField);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://github.com/rzwitserloot/lombok" target="_blank" rel="noopener">GitHub: lombok</a></li><li><a href="https://github.com/mplushnikov/lombok-intellij-plugin" target="_blank" rel="noopener">GitHub: lombok-intellij-plugin</a></li><li><a href="http://hannesdorfmann.com/annotation-processing/annotationprocessing101" target="_blank" rel="noopener">ANNOTATION PROCESSING 101</a></li><li><a href="https://www.javacodegeeks.com/2015/09/java-compiler-api.html" target="_blank" rel="noopener">Java Compiler API</a></li><li><a href="http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started.html" target="_blank" rel="noopener">Creating Your First Plugin</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/coding/java-apt-logging/#disqus_thread</comments>
</item>
<item>
<title>【CI/CD】几种常见的不停机发布方式</title>
<link>http://emacoo.cn/devops/ci-cd-hot-deployment/</link>
<guid>http://emacoo.cn/devops/ci-cd-hot-deployment/</guid>
<pubDate>Wed, 08 Jun 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="何为不停机发布?"><a href="#何为不停机发布?" class="headerlink" title="何为不停机发布?"></a>何为不停机发布?</h2><p>本文所说的不停机发布,是指在<strong>不停止对外服务</strong>的前提下完成应用
</description>
<content:encoded><![CDATA[<h2 id="何为不停机发布?"><a href="#何为不停机发布?" class="headerlink" title="何为不停机发布?"></a>何为不停机发布?</h2><p>本文所说的不停机发布,是指在<strong>不停止对外服务</strong>的前提下完成应用的更新。与<a href="http://emacoo.cn/coding/play-hotdeploy">热部署</a>的区别在于,热部署关注于<strong>应用</strong>层面并且以<strong>不重启应用</strong>为前提,而不停机发布则关注于<strong>服务</strong>层面。随着摩尔定律逐渐逼近极限和多核时代的到来,分布式应用已经成为事实上的主流。下文首先给出一种通用的适用于分布式应用环境的不停机发布方式,然后再介绍Master/Worker这种常见的适用于单机应用的不停机发布方式。</p><h2 id="Cluster模式"><a href="#Cluster模式" class="headerlink" title="Cluster模式"></a>Cluster模式</h2><p>对于运行于集群环境的分布式应用,一般在应用之上都有一层负载均衡(LB)。如果在发布过程中,在更新任一节点(也可以是一组节点)前先关闭该节点对应的负载,更新完再打开负载,即可实现整体服务的不停机发布。在此基础上,为了保证服务的稳定性,可以加上备机的支持,即更新某一节点时,先挂上备机,更新完再卸下,依次轮换更新完所有节点后最后再升级备机,如下图所示:</p><p><img src="http://static.zybuluo.com/emac/zijlzk2pruasmrbmbaz8eev1/QQ20160405-3.png" alt></p><p><em>* 完整设计可以参考我写的另一篇<a href="https://www.zybuluo.com/emac/note/330205" target="_blank" rel="noopener">文章</a></em></p><p>上述发布过程其实就是一个简单的CD(Continuous Deployment)系统。作为一个参考实现,可以使用<a href="http://emacoo.cn/devops/jenkins-2-0-from-ci-to-cd">Jenkins 2.0 Pipeline</a>特性定义整个发布流程,使用<a href="https://github.com/cubicdaiya/ngx_dynamic_upstream" target="_blank" rel="noopener">Nginx Dynamic Upstream</a>插件操纵Nginx,然后配合脚本完成应用的启停和检测。</p><p><img src="QQ20160609-0.png" alt></p><h2 id="Master-Worker模式"><a href="#Master-Worker模式" class="headerlink" title="Master/Worker模式"></a>Master/Worker模式</h2><p>对于单机应用,由于不存在LB,一般由应用容器实现不停机发布特性,最常见是Master/Worker模式。容器中常驻一个master进程和多个work进程,master进程只负责加载程序和分发请求,由fork出来的worker进程完成具体工作。当容器收到更新应用的信号时,master进程重新加载更新后的程序,然后fork新的worker进程处理新的请求,而老的worker进程在处理完当前请求后就自动销毁。Ruby的<a href="https://github.com/blog/517-unicorn" target="_blank" rel="noopener">Unicorn</a>,PHP的<a href="http://php-fpm.org/about/" target="_blank" rel="noopener">FPM</a>都是采用了这套机制。</p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><p>不同于Master/Worker模式,erlang采用了另一种独特的方式实现了不停机发布。</p><blockquote><p>erlang VM为每个模块最多保存2份代码,当前版本’current’和旧版本’old’,当模块第一次被加载时,代码就是’current’版本。如果有新的代码被加载,’current’版本代码就变成了’old’版本,新的代码就成了’current’版本。erlang用两个版本共存的方法来保证任何时候总有一个版本可用,对外服务就不会停止。<br><br><br>—— 引自<a href="http://blog.csdn.net/mycwq/article/details/43372687" target="_blank" rel="noopener">分析erlang热更新实现机制</a></p></blockquote><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>不管是LB,还是Master/Worker,其基本思想都是在发布过程中,通过某种机制使得服务请求始终能够被系统的某个节点或者某个进程处理,从而保证了服务的可用性。</p>]]></content:encoded>
<comments>http://emacoo.cn/devops/ci-cd-hot-deployment/#disqus_thread</comments>
</item>
<item>
<title>【Jenkins】Pipeline使用进阶</title>
<link>http://emacoo.cn/devops/jenkins-pipeline-tips/</link>
<guid>http://emacoo.cn/devops/jenkins-pipeline-tips/</guid>
<pubDate>Sat, 21 May 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="无所不能,无所不及的Pipeline"><a href="#无所不能,无所不及的Pipeline" class="headerlink" title="无所不能,无所不及的Pipeline"></a>无所不能,无所不及的Pipeline</h2><p>得益于Jen
</description>
<content:encoded><![CDATA[<h2 id="无所不能,无所不及的Pipeline"><a href="#无所不能,无所不及的Pipeline" class="headerlink" title="无所不能,无所不及的Pipeline"></a>无所不能,无所不及的Pipeline</h2><p>得益于Jenkins内嵌的Groovy支持,加上<a href="https://jenkins.io/doc/pipeline/steps/" target="_blank" rel="noopener">丰富的Step库</a>,通过编写自定义Pipeline脚本你几乎可以实现任何复杂的构建、发布流程。下面简单谈谈使用Pipeline的四个段位。</p><h2 id="I-启蒙老师:Snipper-Generator"><a href="#I-启蒙老师:Snipper-Generator" class="headerlink" title="I. 启蒙老师:Snipper Generator"></a>I. 启蒙老师:Snipper Generator</h2><p>Jenkins晦涩的行文风格并没有随着2.0的发布有所改善,Step库的<a href="https://jenkins.io/doc/pipeline/steps/" target="_blank" rel="noopener">官方参考手册</a>成功的延续了Jenkins一贯的惜字如金风格,大多数Step都只有一句话的描述和一些参数类型,罕有使用样例,比如<a href="https://jenkins.io/doc/pipeline/steps/workflow-scm-step/#git-git" target="_blank" rel="noopener">Git Step</a>。要理解这些Step,基本靠脑补。好在Jenkins提供了一款良心产品,<a href="https://jenkins.io/doc/pipeline/#using-snippet-generator" target="_blank" rel="noopener">Snipper Generator</a>,帮助使用者在Pipeline配置界面3步生成正确的调用语句。</p><p><img src="QQ20160522-0.png" alt></p><h2 id="II-调试利器:Replay-Pipeline"><a href="#II-调试利器:Replay-Pipeline" class="headerlink" title="II. 调试利器:Replay Pipeline"></a>II. 调试利器:Replay Pipeline</h2><p>维护过CI的同学一定知道,在成功创建一个正确、稳定运行的CI任务之前,往往需要历经多次调试和优化,创建Pipeline更是如此。为了避免重复打开配置界面调整Pipeline脚本,Jenkins贴心的提供了<a href="https://jenkins.io/blog/2016/04/14/replay-with-pipeline/" target="_blank" rel="noopener">Replay</a>功能。打开任意一次执行历史,在左侧点击Replay按钮,即可复原该次执行所运行的Pipeline脚本,无论脚本来源是任务本身还是远程仓库。</p><p><img src="QQ20160522-1.png" alt></p><h2 id="III-隐藏秘籍:Workflow-Global-Library"><a href="#III-隐藏秘籍:Workflow-Global-Library" class="headerlink" title="III. 隐藏秘籍:Workflow Global Library"></a>III. 隐藏秘籍:Workflow Global Library</h2><p>很多人不知道,Jenkins默认会启动一个SSHD服务,用于在<a href="https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+CLI" target="_blank" rel="noopener">CLI</a>方式下执行一些Jenkins命令。Jenkins 2.0在此基础上,绑定了一个本地Git库(Workflow Global Library,简称WGL),用于上传一些全局共享的Groovy脚本,供同一Jenkins实例下所有Pipeline脚本调用。具体使用步骤如下:</p><ol><li><p>进入系统配置界面,找到SSH Server配置项,指定一个固定的SSH端口。<br><img src="QQ20160522-2.png" alt></p></li><li><p>进入当前用户的配置页面,绑定SSH Public Key。<br><img src="QQ20160522-3.png" alt></p></li><li><p>打开命令行,运行<code>git clone ssh://<user>@<host>:<port>/workflowLibs.git</code>拉取WGL。</p></li><li>在Git库的根目录下创建vars目录,编写Groovy脚本并存放于此,提交代码并Push至远程库。</li></ol><figure class="highlight groovy"><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">// hello.groovy,一个简单的示例Groovy脚本,定义了一个名为hello的全局方法</span></span><br><span class="line"><span class="keyword">def</span> call(name) {</span><br><span class="line"> echo <span class="string">"Hello, ${name}!"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="IV-如来神掌:Jenkins-Plugin"><a href="#IV-如来神掌:Jenkins-Plugin" class="headerlink" title="IV. 如来神掌:Jenkins Plugin"></a>IV. 如来神掌:Jenkins Plugin</h2><p>如果你需要同时维护多个Jenkins实例,那么WGL就不再适用了,因为每一个Jenkins实例你都需要上传一份脚本。这时就要祭出Jenkins Plugin大法,也即将共享的Groovy脚本封装到一个自定义Jenkins Plugin中,然后安装到需要的Jenkins实例中,以后也可以进行统一升级,有效降低了维护成本。要实现这一点,除了<a href="https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins" target="_blank" rel="noopener">传统的定义Jenkins Plugin的方法</a>,Jenkins<a href="https://jenkins.io/blog/2016/04/21/dsl-plugins/" target="_blank" rel="noopener">官方博客</a>还提供了另一种更为简便的封装方式,具体可以参考我的这个GitHub项目,<a href="https://github.com/emac/demo-pipeline-step" target="_blank" rel="noopener">demo-pipeline-step</a>。</p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><p>利用强大、灵活的Pipeline,我们可以像组装乐高玩具一般操纵Jenkins,根据实际情况构建所需的CI/CD流程。近期我设计的<a href="https://zybuluo.com/emac/note/330205" target="_blank" rel="noopener">Frigate</a>发布系统正式利用Jenkins Pipeline无缝衔接各个发布环节。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://jenkins.io/blog/2016/04/14/replay-with-pipeline/" target="_blank" rel="noopener">Replay a Pipeline with script edits</a></li><li><a href="https://jenkins.io/blog/2016/04/21/dsl-plugins/" target="_blank" rel="noopener">Making your own DSL with plugins, written in Pipeline script</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/devops/jenkins-pipeline-tips/#disqus_thread</comments>
</item>
<item>
<title>【Jenkins】2.0新时代:从CI到CD</title>
<link>http://emacoo.cn/devops/jenkins-2-0-from-ci-to-cd/</link>
<guid>http://emacoo.cn/devops/jenkins-2-0-from-ci-to-cd/</guid>
<pubDate>Sat, 30 Apr 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="2-0-破茧重生"><a href="#2-0-破茧重生" class="headerlink" title="2.0 破茧重生"></a>2.0 破茧重生</h2><p>自从去年9月底Jenkins的创始人<a href="https://github.com/
</description>
<content:encoded><![CDATA[<h2 id="2-0-破茧重生"><a href="#2-0-破茧重生" class="headerlink" title="2.0 破茧重生"></a>2.0 破茧重生</h2><p>自从去年9月底Jenkins的创始人<a href="https://github.com/kohsuke" target="_blank" rel="noopener">Kohsuke Kawaguchi</a>提出Jenkins 2.0(后称2.0)的<a href="https://docs.google.com/presentation/d/12ikbbQoMvus_l_q23BxXhYXnW9S5zsVNwIKZ9N8udg4/edit#slide=id.p" target="_blank" rel="noopener">愿景</a>和<a href="https://groups.google.com/forum/#!topic/jenkinsci-dev/vbXK7JJekFw/overview" target="_blank" rel="noopener">草案</a>之后,整个Jenkins社区为之欢欣鼓舞,不管是官方博客还是Google论坛,大家都在热烈讨论和期盼2.0的到来。4月20日,历经Alpha(2/29),Beta(3/24),RC(4/7)3个版本的迭代,2.0终于正式发布。这也是Jenkins面世11年以来(算上前身Hudson)的首次大版本升级。那么,这次升级具体包含了哪些内容呢?</p><h3 id="外部"><a href="#外部" class="headerlink" title="外部"></a>外部</h3><p>从外部来看,2.0最大的三个卖点分别是Pipeline as Code,全新的开箱体验和1.x兼容性。</p><p><strong>Pipeline as Code</strong>是2.0的精髓所在,是帮助Jenkins实现CI(Continuous Integration)到CD(Continuous Delivery)华丽转身的关键推手。所谓Pipeline,简单来说,就是一套运行于Jenkins上的工作流框架,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂发布流程(例如下图)。Pipeline的实现方式是一套Groovy DSL(类似Gradle),任何发布流程都可以表述为一段Groovy脚本,并且Jenkins支持从代码库直接读取脚本,从而实现了Pipeline as Code的理念。</p><p><img src="https://jenkins.io/images/pipeline/realworld-pipeline-flow.png" alt></p><p><strong>全新的开箱体验</strong>力图扭转我们印象中Jenkins十年不变的呆滞界面风格,不光Jenkins应用本身,官网排版、博客样式乃至域名都被重新设计。这些变化除了极大的改善了用户体验,更重要的是给人们传达一个清晰的信号,Jenkins不再仅仅是一个CI工具,而是蕴含着无限可能。</p><p><img src="https://jenkins.io/images/2.0-create-item.png" alt></p><p><img src="https://jenkins.io/images/2.0-config-dialog.png" alt></p><p><strong>1.x兼容性</strong>给所有老版本用户吃了一颗大大的定心丸,注意,是完全兼容哦。</p><h3 id="内部"><a href="#内部" class="headerlink" title="内部"></a>内部</h3><p>从内部来看,2.0主要包含了一些组件升级和JS模块化改造。</p><ul><li>升级Servlet版本到3.1,获取Web Sockets支持</li><li>升级内嵌的Groovy版本到2.4.6<ul><li>未来版本的Jenkins将会<a href="https://issues.jenkins-ci.org/browse/JENKINS-29068" target="_blank" rel="noopener">把Groovy彻底从内核中剥离</a>,此次Groovy升级只是第一步</li></ul></li><li>提供一个简化的JS类库给Plugin开发者使用</li></ul><h3 id="更好的容器化支持"><a href="#更好的容器化支持" class="headerlink" title="更好的容器化支持"></a>更好的容器化支持</h3><p>随着容器化技术(以Docker为代表)的不断升温,Jenkins紧随潮流,不仅同步上传2.0的Docker镜像,同时也在Pipeline中提供了默认的<a href="https://jenkins.io/doc/pipeline/steps/docker-workflow/" target="_blank" rel="noopener">Docker支持</a>。</p><p>除了上述内容,2.0还有一个比较有意思的改动,全局重命名Slave为Agent,看来在美国做IT政治正确性也是很重要啊。</p><h2 id="Pipeline-as-Code"><a href="#Pipeline-as-Code" class="headerlink" title="Pipeline as Code"></a>Pipeline as Code</h2><p>了解了2.0的概貌之后,回过来我们再看一下Pipeline as Code(后称Pipeline)产生的背景和具体构成。</p><h3 id="产生背景"><a href="#产生背景" class="headerlink" title="产生背景"></a>产生背景</h3><p>作为2.0的核心插件,Pipeline并不是一个新事物,它的前身是<a href="https://wiki.jenkins-ci.org/display/JENKINS/Workflow+Plugin" target="_blank" rel="noopener">Workflow Plugin</a>,而Workflow的诞生是受更早的<a href="https://wiki.jenkins-ci.org/display/JENKINS/Build+Flow+Plugin" target="_blank" rel="noopener">Build Flow Plugin</a>启发,由<a href="https://github.com/ndeloof" target="_blank" rel="noopener">Nicolas De Loof</a>于2012年4月发布第一个版本。而纵观Jenkins的几个竞争对手(<a href="https://docs.travis-ci.com/user/customizing-the-build/" target="_blank" rel="noopener">Travis CI</a>、<a href="https://www.phptesting.org/wiki/Adding-PHPCI-Support-to-Your-Projects" target="_blank" rel="noopener">phpci</a>、<a href="https://circleci.com/docs/configuration/" target="_blank" rel="noopener">circleci</a>),Pipeline早已不是什么新鲜概念。可以说这次Jenkins 2.0的发布是顺势而为,同时也是大势所趋。</p><p>如果要在更大范围探讨Pipelined的产生背景,我认为有三个层面的原因。</p><ul><li>第一层面,与不断增长的发布复杂度有关,其中一个典型场景就是灰度发布。原本只有大公司才有的灰度发布,随着敏捷开发实践的广泛采用、产品迭代周期的不断缩短、数据增长理念的深入人心,越来越多的中小公司也开始这一方面的探索,这对发布的需求也从点状的CI升级到线状的CD。这是Pipeline产生的第一个原因。</li><li>第二层面,与应用架构的模块化演变有关,以<a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">微服务</a>为代表,一次应用升级往往涉及到多个模块的协同发布,单个CI显然无法满足此类需求。这是Pipeline产生的第二个原因。</li><li>第三层面,与日益失控的CI数量有关。一方面,类似于Maven、pip、RubyGems这样的包管理工具使得有CI需求的应用呈爆发性增长,另一方面,受益于便捷的Git分支特性,即便对于同一个应用,往往也需要配置多个CI。随着CI数量的不断增长,集中式的任务配置就会成为一个瓶颈,这就需要把任务配置的职责下放到应用团队。这是Pipeline(as Code)产生的第三个原因。</li></ul><h3 id="具体构成"><a href="#具体构成" class="headerlink" title="具体构成"></a>具体构成</h3><p>说完背景,再看一下Pipeline的具体构成和特性。</p><p>基本概念:</p><ul><li>Stage: 一个Pipeline可以划分为若干个Stage,每个Stage代表一组操作。注意,Stage是一个逻辑分组的概念,可以跨多个Node。</li><li>Node: 一个Node就是一个Jenkins节点,或者是Master,或者是Agent,是执行Step的具体运行期环境。</li><li>Step: Step是最基本的操作单元,小到创建一个目录,大到构建一个Docker镜像,由各类Jenkins Plugin提供。</li></ul><p>具体构成:</p><ul><li>Jenkinsfile: Pipeline的定义文件,由Stage,Node,Step组成,一般存放于代码库根目录下。</li><li>Stage View: Pipeline的视觉展现,类似于下图。</li></ul><p><img src="QQ20160502-0.png" alt></p><p>2.0默认支持三种类型的Pipeline,普通Pipeline,Multibranch Pipeline和Organization Folders,后两种其实是批量创建一组普通Pipeline的快捷方式,分别对应于多分支的应用和多应用的大型组织。注意,要获取Organization Folders的支持需要额外安装Plugin。</p><p>值得一提的是,2.0有两个很重要的特性:</p><ul><li>Pausable: 类似于Bash的read命令,2.0允许暂停发布流程,等待人工确认后再继续,这个特性对于保证应用HA尤为重要。</li></ul><p><img src="QQ20160502-1.png" alt></p><ul><li>Durable: 发布过程中,如果Jenkins挂掉,正在运行中的Pipeline并不会受影响,也就是说Pipeline的进程独立于Jenkins进程本身。</li></ul><h3 id="示例Pipeline"><a href="#示例Pipeline" class="headerlink" title="示例Pipeline"></a>示例Pipeline</h3><p>上文所涉及的示例Pipeline可以在我的<a href="(https://github.com/emac/pagination/blob/master/Jenkinsfile">GitHub</a>)找到,如果有问题想跟我探讨,可以加我QQ: 7789059。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://jenkins.io/blog/2016/04/15/the-need-for-pipeline/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+ContinuousBlog+%28Jenkins%29" target="_blank" rel="noopener">The Need for Jenkins Pipeline</a></li><li><a href="https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+2.0" target="_blank" rel="noopener">Jenkins 2.0</a></li><li><a href="https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md" target="_blank" rel="noopener">Why Pipeline?</a></li><li><a href="https://jenkins.io/blog/2015/12/03/pipeline-as-code-with-multibranch-workflows-in-jenkins/" target="_blank" rel="noopener">Pipeline-as-code with Multibranch Workflows in Jenkins</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/devops/jenkins-2-0-from-ci-to-cd/#disqus_thread</comments>
</item>
<item>
<title>【Spring】基于Spring Data JPA的分页组件</title>
<link>http://emacoo.cn/backend/spring-data-jpa-pagination/</link>
<guid>http://emacoo.cn/backend/spring-data-jpa-pagination/</guid>
<pubDate>Sat, 09 Apr 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="分页的基本模型"><a href="#分页的基本模型" class="headerlink" title="分页的基本模型"></a>分页的基本模型</h2><p>做Web应用开发,或早或晚都会涉及到分页。按模型划分,分页包含页码(page),大小(size)和总
</description>
<content:encoded><![CDATA[<h2 id="分页的基本模型"><a href="#分页的基本模型" class="headerlink" title="分页的基本模型"></a>分页的基本模型</h2><p>做Web应用开发,或早或晚都会涉及到分页。按模型划分,分页包含页码(page),大小(size)和总条数(count),页码和大小面向的是前端页面,而总条数来自后端服务。按表现形式,可分为显式分页和隐式分页(比如上拉加载),显示分页一般用于PC端,而隐式分页一般用于Mobile端。按实现方式,或者由前端JS生成,或者是后端模板。</p><h2 id="分页的基本原理"><a href="#分页的基本原理" class="headerlink" title="分页的基本原理"></a>分页的基本原理</h2><p>一般而言,分页最终都是映射到数据库查询,在此场景下,不论何种分页框架,其基本原理都是基于SQL的<code>LIMIT [offset,] row_count</code>语法(数据库不同语法略有差别)。row_count对应的是size,而offset则是通过page*size计算得到(假设page从0计数),比如(page=1, size=10)对应(offset=10, row_count=10)。</p><p>介绍了分页的基本模型和原理后,接下来我结合一个基于Spring Data JPA的分页组件,阐述分页的一些实现要点。</p><h2 id="分页的实现要点"><a href="#分页的实现要点" class="headerlink" title="分页的实现要点"></a>分页的实现要点</h2><p>首先看一个典型的分页效果:</p><p><img src="pagination.png" alt="pagination"></p><p>整个分页组件由三部分组成,首页|上一页,下一页|末页,以及中间的页码组。显然,前两部分是固定的,而页码组是随着当前页码的变化而变化。其实现要点有三个,</p><ul><li>计算页码组:首先比较页码组默认长度和总页数,取较小值为页码组最终长度(L)。然后根据当前页码(P)和总页数(T)的关系,再细分为三种情况<ul><li>P+L/2<=L: 页码组从1开始计数,比如(P=2, T=6, L=5) -> 1,2,3,4,5</li><li>P+L>=T: 页码组从T倒数L开始计数,比如(P=5, T=6, L=5) -> 2,3,4,5,6</li><li>其他: 页码组从P-L/2+1开始计数,比如(P=4, T=6, L=5) -> 2,3,4,5,6</li></ul></li><li>按钮状态:首页,上一页,下一页,末页对应按钮的启用状态应随当前页码的值变化而变化,并且当前页码对应的按钮应该始终处于禁用状态。</li><li>页码显示:页面显示是从1开始,而数据库查询是从0开始。这一点不想清楚,边界情况就处理不好。</li></ul><p>具体实现细节,可参考我GitHub的一个示例项目,<a href="https://github.com/emac/pagination" target="_blank" rel="noopener">Pagination</a>。</p><h2 id="Spring-Data-JPA简介"><a href="#Spring-Data-JPA简介" class="headerlink" title="Spring Data JPA简介"></a>Spring Data JPA简介</h2><p><a href="http://projects.spring.io/spring-data-jpa/" target="_blank" rel="noopener">Spring Data JPA</a>隶属于<a href="http://projects.spring.io/spring-data/" target="_blank" rel="noopener">Spring Data</a>项目,通过一系列Spring风格的接口和注解,极大的简化了创建和开发JPA Repository的过程,同时提供了自定义查询,分页排序等高级特性的支持。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://docs.spring.io/spring-data/commons/docs/current/reference/html/" target="_blank" rel="noopener">Spring Data Commons - Reference Documentation</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-data-jpa-pagination/#disqus_thread</comments>
</item>
<item>
<title>【Spring】单应用多数据库的事务管理</title>
<link>http://emacoo.cn/backend/spring-transaction/</link>
<guid>http://emacoo.cn/backend/spring-transaction/</guid>
<pubDate>Sat, 26 Mar 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="单应用多数据库的事务管理"><a href="#单应用多数据库的事务管理" class="headerlink" title="单应用多数据库的事务管理"></a>单应用多数据库的事务管理</h2><p><a href="http://emacoo.cn/back
</description>
<content:encoded><![CDATA[<h2 id="单应用多数据库的事务管理"><a href="#单应用多数据库的事务管理" class="headerlink" title="单应用多数据库的事务管理"></a>单应用多数据库的事务管理</h2><p><a href="http://emacoo.cn/backend/spring-boot-multi-db">上篇</a>讲到单应用多数据库的配置,这次我们聊聊单应用多数据库的事务管理。首先我们来了解一下事务。</p><h2 id="什么是数据库事务?"><a href="#什么是数据库事务?" class="headerlink" title="什么是数据库事务?"></a>什么是数据库事务?</h2><blockquote><p>数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。<br><a href="http://baike.baidu.com/view/1298364.htm" target="_blank" rel="noopener">http://baike.baidu.com/view/1298364.htm</a></p></blockquote><p>举个栗子,银行的一次转账操作就可以理解成一个事务,A打钱给B,银行首先从A的账户里扣钱,然后把钱转到B的账户。如果只执行前一步,A肯定不乐意,如果是后一步,换银行不乐意。所以两步要么都执行,要么都不执行。</p><h2 id="单库事务和跨库事务有什么区别?"><a href="#单库事务和跨库事务有什么区别?" class="headerlink" title="单库事务和跨库事务有什么区别?"></a>单库事务和跨库事务有什么区别?</h2><p>一般而言,所谓的数据库事务都是针对单个数据库的事务,即单库事务。而跨库事务,顾名思义,是指涉及多个数据库的事务,理论上也必须满足ACID属性。两者最核心的区别在于,单库事务一般是由数据库保证的,俗称物理事务,而跨库事务一般是由应用保证的,俗称逻辑事务。与单库事务相比,跨库事务执行成本高,稳定性差,管理也更复杂,但在某些场景下,尤其是分布式应用环境下,又是不得不使用的技术。</p><p>再举个栗子,单库事务好比你从北京飞上海,到东航官网买张票就搞定了,而跨库事务好比北京飞纽约,到上海转机,就得买东航转上航的联票,出票就转由携程保证了。</p><h2 id="多数据库下的三种事务使用场景"><a href="#多数据库下的三种事务使用场景" class="headerlink" title="多数据库下的三种事务使用场景"></a>多数据库下的三种事务使用场景</h2><p>了解了单库事务和跨库事务之后,我们再来看看多数据库下的三种事务使用场景。假设有DB1,DB2两个数据库,分别对应ServiceA和ServiceB两个带上事务注解的服务类,根据调用关系,可细分为三种场景。</p><h3 id="场景一:仅调用ServiceA,ServiceA不调用ServiceB"><a href="#场景一:仅调用ServiceA,ServiceA不调用ServiceB" class="headerlink" title="场景一:仅调用ServiceA,ServiceA不调用ServiceB"></a>场景一:仅调用ServiceA,ServiceA不调用ServiceB</h3><p>这种情况等同于单库事务,无需特殊处理。</p><h3 id="场景二:仅调用ServiceA,ServiceA再调用ServiceB"><a href="#场景二:仅调用ServiceA,ServiceA再调用ServiceB" class="headerlink" title="场景二:仅调用ServiceA,ServiceA再调用ServiceB"></a>场景二:仅调用ServiceA,ServiceA再调用ServiceB</h3><p><img src="QQ20160327-0.png" alt></p><h3 id="场景三:先调用ServiceA,再调用ServiceB"><a href="#场景三:先调用ServiceA,再调用ServiceB" class="headerlink" title="场景三:先调用ServiceA,再调用ServiceB"></a>场景三:先调用ServiceA,再调用ServiceB</h3><p><img src="QQ20160327-1.png" alt></p><p>场景二和场景三是两种典型的跨库事务,Spring默认的事务管理并无法保证事务的属性。对于场景二,在调用ServiceB之后,如果ServiceA出错,ServiceB并不会回滚。而对于场景三,在调用ServiceB之前,ServiceA的事务已经完成,因此当ServiceB出错回滚时,ServiceA并不会同步回滚。</p><p>如何解决?前面说过,跨库事务一般是由应用保证,因此办法有很多。标准的方法是使用JTA框架进行两段式提交,比如开源的<a href="https://www.atomikos.com/Main/WebHome" target="_blank" rel="noopener">Atomikos</a>,<a href="https://github.com/bitronix/btm" target="_blank" rel="noopener">Bitronix</a>。粗暴一点,可以显式创建两个事务,将所有的服务调用包在其中。考虑到本文单应用的环境,还有第三种方式,根据所涉及的事务列表,动态构造调用链,把所有的服务调用封装到最内层,由外层的事务注解链保证跨库事务。</p><p>定义事务代理类,每一个类代理一个数据库事务:</p><pre><code>@Componentpublic class Db1TxBroker { @Transactional(DbConstants.TX_DB1) public <V> V inAccount(Callable<V> callable) { try { return callable.call(); } catch (Exception e) { throw new ServiceException(e); } }}</code></pre><p>负责生成调用链的服务基类:</p><pre><code>public abstract class BaseComboService { @Autowired private Db1TxBroker db1TxBroker; @Autowired private Db2TxBroker db2TxBroker; /** * 根据传入的事务链构造调用链,在最内层调用包含业务逻辑的callable. * * @param callable * @param txes 所涉及的完整事务列表(顺序无关) */ protected <V> V combine(Callable<V> callable, TX... txes) { if (callable == null) { return null; } Callable<V> combined = Stream.of(txes).filter(Objects::nonNull).distinct().reduce(callable, (r, tx) -> { switch (tx) { case DB1: return () -> db1TxBroker.inDb1(r); case DB2: return () -> db2TxBroker.inDb2(r); default: // should not happen return null; } }, (r1, r2) -> r2); try { return combined.call(); } catch (Exception e) { throw new ServiceException(e); } }}</code></pre><p>使用示例:</p><pre><code>@Servicepublic class DemoComboService extends BaseComboService { @Autowired private ServiceA serviceA; @Autowired private ServiceB serviceB; public void demo() { combine(() -> { serviceA.flyToShanghai(); serviceB.flyToNewYork(); return null; }, TX.DB1, TX.DB2); }}</code></pre><p>相比JTA,上述第三种方法最大的优点是更轻量,配置更简单,但只能工作在单个应用的环境。对于分布式应用,后者就无能为力了。这种方法本质上还是借助Spring的事务注解来保证跨库事务,如果将来Spring的事务注解支持JDK8的@Repeatable特性,那就可以直接在方法上加上多个事务注解来达到同样目的。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://spring.io/blog/2011/08/15/configuring-spring-and-jta-without-full-java-ee/" target="_blank" rel="noopener">Configuring Spring and JTA without full Java EE</a></li><li><a href="http://jinnianshilongnian.iteye.com/blog/1441271" target="_blank" rel="noopener">Spring的事务之编程式事务</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-transaction/#disqus_thread</comments>
</item>
<item>
<title>【Spring】Redis的两个典型应用场景</title>
<link>http://emacoo.cn/backend/spring-redis/</link>
<guid>http://emacoo.cn/backend/spring-redis/</guid>
<pubDate>Fri, 11 Mar 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="Redis简介"><a href="#Redis简介" class="headerlink" title="Redis简介"></a>Redis简介</h2><p><a href="http://redis.io/" target="_blank" rel="no
</description>
<content:encoded><![CDATA[<h2 id="Redis简介"><a href="#Redis简介" class="headerlink" title="Redis简介"></a>Redis简介</h2><p><a href="http://redis.io/" target="_blank" rel="noopener">Redis</a>是目前业界使用最广泛的内存数据存储。相比memcached,Redis支持更丰富的数据结构,例如hashes, lists, sets等,同时支持数据持久化。除此之外,Redis还提供一些类数据库的特性,比如事务,HA,主从库。可以说Redis兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。本文介绍Redis在Spring Boot中两个典型的应用场景。</p><h2 id="场景1:数据缓存"><a href="#场景1:数据缓存" class="headerlink" title="场景1:数据缓存"></a>场景1:数据缓存</h2><p>第一个应用场景是数据缓存,最典型的当属缓存数据库查询结果。对于高频读低频写的数据,使用缓存可以第一,加速读取过程,第二,降低数据库压力。通过引入spring-boot-starter-redis依赖和注册RedisCacheManager,Redis可以无缝的集成进Spring的缓存系统,自动绑定@Cacheable, @CacheEvict等缓存注解。</p><p>引入依赖:</p><pre><code><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId></dependency></code></pre><p>Redis配置(application.properties):</p><pre><code># REDIS (RedisProperties)spring.redis.host=localhostspring.redis.password=spring.redis.database=0</code></pre><p>注册RedisCacheManager:</p><pre><code>@Configuration@EnableCachingpublic class CacheConfig { @Autowired private JedisConnectionFactory jedisConnectionFactory; @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate()); return redisCacheManager; } @Bean public RedisTemplate<Object, Object> redisTemplate() { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>(); redisTemplate.setConnectionFactory(jedisConnectionFactory); // 开启事务支持 redisTemplate.setEnableTransactionSupport(true); // 使用String格式序列化缓存键 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); return redisTemplate; }}</code></pre><p>@Cachable, @CacheEvict使用,Redis中的存储结构可参见场景2中的配图:</p><pre><code>@Cacheable(value="signonCache", key="'petstore:signon:'+#username", unless="#result==null")public Signon findByName(String username) { return dao.fetchOneByUsername(username);}@CacheEvict(value="signonCache", key="'petstore:signon:'+#user.username")public void update(Signon user) { dao.update(user);}</code></pre><ul><li>@Cacheable: 插入缓存<ul><li>value: 缓存名称</li><li>key: 缓存键,一般包含被缓存对象的主键,支持Spring EL表达式</li><li>unless: 只有当查询结果不为空时,才放入缓存</li></ul></li><li>@CacheEvict: 失效缓存</li></ul><blockquote><p>Tip: Spring Redis默认使用JDK进行序列化和反序列化,因此被缓存对象需要实现java.io.Serializable接口,否则缓存出错。</p><p>Tip: 当被缓存对象发生改变时,可以选择更新缓存或者失效缓存,但一般而言,后者优于前者,因为执行速度更快。</p><p>Watchout! 在同一个Class内部调用带有缓存注解的方法,缓存并不会生效。</p></blockquote><h2 id="场景2:共享Session"><a href="#场景2:共享Session" class="headerlink" title="场景2:共享Session"></a>场景2:共享Session</h2><p>共享Session是第二个典型应用场景,这是利用了Redis的堆外内存特性。要保证分布式应用的可伸缩性,带状态的Session对象是绕不过去的一道坎。一种方式是将Session持久化到数据库中,缺点是读写成本太高。另一种方式是去Session化,比如Play直接将Session存到客户端的Cookie中,缺点是存储信息的大小受限。将Session缓存到Redis中,既保证了可伸缩性,同时又避免了前面两者的限制。</p><p>引入依赖:</p><pre><code><dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId></dependency></code></pre><p>Session配置:</p><pre><code>@Configuration@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400)public class SessionConfig {}</code></pre><ul><li>maxInactiveIntervalInSeconds: 设置Session失效时间,使用Redis Session之后,原Boot的server.session.timeout属性不再生效</li></ul><p>Redis中的session对象:</p><p><img src="QQ20160313-0.png" alt></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>上面结合示例代码介绍了数据缓存,共享Session两个Redis的典型应用场景,除此之外,还有分布式锁,全局计数器等高级应用场景,以后在其他文章中再详细介绍。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://docs.spring.io/spring-data/redis/docs/current/reference/html/" target="_blank" rel="noopener">Spring Data Redis</a></li><li><a href="http://jinnianshilongnian.iteye.com/blog/2001040" target="_blank" rel="noopener">Spring Cache抽象详解</a></li><li><a href="http://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-redis" target="_blank" rel="noopener">HttpSession with Redis</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-redis/#disqus_thread</comments>
</item>
<item>
<title>【Spring】关于Boot应用中集成Spring Security你必须了解的那些事</title>
<link>http://emacoo.cn/backend/spring-boot-security/</link>
<guid>http://emacoo.cn/backend/spring-boot-security/</guid>
<pubDate>Sat, 05 Mar 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="Spring-Security"><a href="#Spring-Security" class="headerlink" title="Spring Security"></a>Spring Security</h2><p>Spring Security是Sp
</description>
<content:encoded><![CDATA[<h2 id="Spring-Security"><a href="#Spring-Security" class="headerlink" title="Spring Security"></a>Spring Security</h2><p>Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架。除了常规的Authentication和Authorization之外,Spring Security还提供了诸如ACLs,LDAP,JAAS,CAS等高级特性以满足复杂场景下的安全需求。虽然功能强大,Spring Security的配置并不算复杂(得益于官方详尽的文档),尤其在3.2版本加入Java Configuration的支持之后,可以彻底告别令不少初学者望而却步的XML Configuration。在使用层面,Spring Security提供了多种方式进行业务集成,包括注解,Servlet API,JSP Tag,系统API等。下面就结合一些示例代码介绍Boot应用中集成Spring Security的几个关键点。</p><h3 id="1-核心概念"><a href="#1-核心概念" class="headerlink" title="1 核心概念"></a>1 核心概念</h3><p>Principle(User), Authority(Role)和Permission是Spring Security的3个核心概念。跟通常理解上Role和Permission之间一对多的关系不同,在Spring Security中,Authority和Permission是两个完全独立的概念,两者并没有必然的联系,但可以通过配置进行关联。</p><h3 id="2-基础配置"><a href="#2-基础配置" class="headerlink" title="2 基础配置"></a>2 基础配置</h3><p>首先在项目的pom.xml中引入spring-boot-starter-security依赖。</p><pre><code><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency></code></pre><p>和其余Spring框架一样,XML Configuration和Java Configuration是Spring Security的两种常用配置方式。Spring 3.2版本之后,Java Configuration因其流式API支持,强类型校验等特性,逐渐替代XML Configuration成为更广泛的配置方式。下面是一个示例Java Configuration。</p><pre><code>@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MyUserDetailsService detailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .and().formLogin().loginPage("/login").permitAll().defaultSuccessUrl("/", true) .and().logout().logoutUrl("/logout") .and().sessionManagement().maximumSessions(1).expiredUrl("/expired") .and() .and().exceptionHandling().accessDeniedPage("/accessDenied"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/**/favicon.ico"); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(detailsService).passwordEncoder(new BCryptPasswordEncoder()); }}</code></pre><ul><li>@EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置(需要扩展WebSecurityConfigurerAdapter)</li><li>@EnableGlobalMethodSecurity(prePostEnabled = true): 启用Security注解,例如最常用的@PreAuthorize</li><li>configure(HttpSecurity): Request层面的配置,对应XML Configuration中的<code><http></code>元素</li><li>configure(WebSecurity): Web层面的配置,一般用来配置无需安全检查的路径</li><li>configure(AuthenticationManagerBuilder): 身份验证配置,用于注入自定义身份验证Bean和密码校验规则</li></ul><h3 id="3-扩展配置"><a href="#3-扩展配置" class="headerlink" title="3 扩展配置"></a>3 扩展配置</h3><p>完成基础配置之后,下一步就是实现自己的UserDetailsService和PermissionEvaluator,分别用于自定义Principle, Authority和Permission。</p><pre><code>@Componentpublic class MyUserDetailsService implements UserDetailsService { @Autowired private LoginService loginService; @Autowired private RoleService roleService; @Override public UserDetails loadUserByUsername(String username) { if (StringUtils.isBlank(username)) { throw new UsernameNotFoundException("用户名为空"); } Login login = loginService.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在")); Set<GrantedAuthority> authorities = new HashSet<>(); roleService.getRoles(login.getId()).forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName()))); return new org.springframework.security.core.userdetails.User( username, login.getPassword(), true,//是否可用 true,//是否过期 true,//证书不过期为true true,//账户未锁定为true authorities); }}</code></pre><blockquote><p>创建GrantedAuthority对象时,一般名称加上ROLE_前缀。</p></blockquote><figure class="highlight plain"><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">@Component</span><br><span class="line">public class MyPermissionEvaluator implements PermissionEvaluator {</span><br><span class="line"></span><br><span class="line">@Autowired</span><br><span class="line">private LoginService loginService;</span><br><span class="line"></span><br><span class="line">@Autowired</span><br><span class="line">private RoleService roleService;</span><br><span class="line"></span><br><span class="line">@Override</span><br><span class="line"> public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {</span><br><span class="line"> String username = authentication.getName();</span><br><span class="line"> Login login = loginService.findByUsername(username).get();</span><br><span class="line"> return roleService.authorized(login.getId(), targetDomainObject.toString(), permission.toString());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {</span><br><span class="line"> // not supported</span><br><span class="line"> return false;</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ul><li>hasPermission(Authentication, Object, Object)和hasPermission(Authentication, Serializable, String, Object)两个方法分别对应Spring Security中两个同名的表达式。</li></ul><h3 id="4-业务集成"><a href="#4-业务集成" class="headerlink" title="4 业务集成"></a>4 业务集成</h3><p>Spring Security提供了注解,Servlet API,JSP Tag,系统API等多种方式进行集成,最常用的是第一种方式,包含@Secured, @PreAuthorize, @PreFilter, @PostAuthorize和@PostFilter五个注解。@Secure是最初版本中的一个注解,自3.0版本引入了支持Spring EL表达式的其余四个注解之后,就很少使用了。</p><pre><code>@RequestMapping(value = "/hello", method = RequestMethod.GET)@PreAuthorize("authenticated and hasPermission('hello', 'view')")public String hello(Model model) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); model.addAttribute("message", username); return "hello";}</code></pre><ul><li>@PreAuthorize(“authenticated and hasPermission(‘hello’, ‘view’)”): 表示只有当前已登录的并且拥有(“hello”, “view”)权限的用户才能访问此页面</li><li>SecurityContextHolder.getContext().getAuthentication().getName(): 获取当前登录的用户,也可以通过HttpServletRequest.getRemoteUser()获取</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上就是Spring Security的一般集成步骤,更多细节和高级特性可参考官方文档。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/" target="_blank" rel="noopener">http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/</a></li><li><a href="http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/" target="_blank" rel="noopener">http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-boot-security/#disqus_thread</comments>
</item>
<item>
<title>【Spring】如何在单个Boot应用中配置多数据库?</title>
<link>http://emacoo.cn/backend/spring-boot-multi-db/</link>
<guid>http://emacoo.cn/backend/spring-boot-multi-db/</guid>
<pubDate>Sat, 20 Feb 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="为什么需要多数据库?"><a href="#为什么需要多数据库?" class="headerlink" title="为什么需要多数据库?"></a>为什么需要多数据库?</h2><p>默认情况下,Spring Boot使用的是单数据库配置(通过spring.d
</description>
<content:encoded><![CDATA[<h2 id="为什么需要多数据库?"><a href="#为什么需要多数据库?" class="headerlink" title="为什么需要多数据库?"></a>为什么需要多数据库?</h2><p>默认情况下,Spring Boot使用的是单数据库配置(通过spring.datasource.*配置具体数据库连接信息)。对于绝大多数Spring Boot应用,这是符合其使用场景的,因为Spring Boot提倡的是微服务理念,每个应用对应一个单独的业务领域。但在某些特殊情况下,一个应用对应多个数据库又是无法避免的,例如实施数据库分库后原本单个数据库变为多个数据库。本文就结合实际代码介绍如何在单个Boot应用中配置多数据库,以及与之相关的Druid,jOOQ,Flyway等数据服务框架的配置改造。</p><h2 id="配置示例"><a href="#配置示例" class="headerlink" title="配置示例"></a>配置示例</h2><p><img src="QQ20160221-0.png" alt></p><ul><li>DB1,DB2: 两个示例数据库</li><li>ServiceA, ServiceB: 分别使用DB1和DB2的服务类</li></ul><h3 id="连接池Druid"><a href="#连接池Druid" class="headerlink" title="连接池Druid"></a>连接池Druid</h3><p><a href="https://github.com/alibaba/druid" target="_blank" rel="noopener">Druid</a>是阿里巴巴开源的数据库连接池,提供了强大的监控支持,号称Java语言中最好的连接池。</p><p>创建两个配置类分别注册对应DB1和DB2的DataSource Bean和TransactionManager Bean。以DB1为例:</p><blockquote><p>Tip: 可以把其中一个配置类中注册的DataSource Bean和DataSourceTransactionManager Bean加上@Primary注解,作为默认装配实例。</p></blockquote><pre><code>// DB1@Configurationpublic class Db1Config { @Bean(initMethod = "init", destroyMethod = "close") @ConfigurationProperties(prefix = "db.db1") public DataSource dataSource1() { return new DruidDataSource(); } @Bean public DataSourceTransactionManager transactionManager1() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource1()); return transactionManager; }}</code></pre><p>application.conf中的配置:</p><pre><code># DB1db.db1.url=jdbc:mysql://127.0.0.1:3306/db1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=truedb.db1.username=rootdb.db1.password=</code></pre><h3 id="ORM框架jOOQ"><a href="#ORM框架jOOQ" class="headerlink" title="ORM框架jOOQ"></a>ORM框架jOOQ</h3><p><a href="http://www.jooq.org/" target="_blank" rel="noopener">jOOQ</a>是一个开源ORM框架,最大特点是提供类型安全的流式API,支持代码生成。</p><p>参照Boot自带的JooqAutoConfiguration,不难写出如下配置类:</p><pre><code>@Configurationpublic class JooqConfig { // DB1 @Bean public DataSourceConnectionProvider dataSourceConnectionProvider1( @Qualifier("dataSource1") DataSource dataSource1) { return new DataSourceConnectionProvider( new TransactionAwareDataSourceProxy(dataSource1)); } @Bean public SpringTransactionProvider transactionProvider1( @Qualifier("transactionManager1") DataSourceTransactionManager txManager1) { return new SpringTransactionProvider(txManager1); } // DB2 // ... @Configuration public static class DslContextConfig { @Autowired(required = false) private RecordMapperProvider recordMapperProvider; @Autowired(required = false) private Settings settings; @Autowired(required = false) private RecordListenerProvider[] recordListenerProviders; @Autowired private ExecuteListenerProvider[] executeListenerProviders; @Autowired(required = false) private VisitListenerProvider[] visitListenerProviders; // DSLContext for DB1 @Bean public DefaultDSLContext dslContext1(@Qualifier("dataSourceConnectionProvider1") DataSourceConnectionProvider connectionProvider1, @Qualifier("transactionProvider1") SpringTransactionProvider transactionProvider1) { return new DefaultDSLContext(configuration(connectionProvider1, transactionProvider1)); } // DSLContext for DB2 // ... private DefaultConfiguration configuration(ConnectionProvider connectionProvider, TransactionProvider transactionProvider) { DefaultConfiguration configuration = new DefaultConfiguration(); configuration.setSQLDialect(SQLDialect.MYSQL); configuration.set(connectionProvider); configuration.set(transactionProvider); if (this.recordMapperProvider != null) { configuration.set(this.recordMapperProvider); } if (this.settings != null) { configuration.set(this.settings); } configuration.set(this.recordListenerProviders); configuration.set(this.executeListenerProviders); configuration.set(this.visitListenerProviders); return configuration; } }}</code></pre><h3 id="服务类"><a href="#服务类" class="headerlink" title="服务类"></a>服务类</h3><p>配置好DataSource,TransacationManager和DSLContext之后,服务类的配置就比较简单了,直接引用即可。注意由于存在多套Beans,需要通过@Qualifier注解指定装配实例。</p><pre><code>@Transactional("TransactionManager1")public class ServiceA { @Autowired @Qualifier("dslContext1") protected DSLContext dsl;}</code></pre><h3 id="数据库迁移框架Flyway"><a href="#数据库迁移框架Flyway" class="headerlink" title="数据库迁移框架Flyway"></a>数据库迁移框架Flyway</h3><p><a href="https://flywaydb.org/" target="_blank" rel="noopener">Flyway</a>是一个轻量级的开源数据库迁移框架,使用非常广泛。</p><p>参照Boot自带的FlywayAutoConfiguration,同样可以写出如下配置类:</p><pre><code>@Configurationpublic class FlywayConfig { @Bean(initMethod = "migrate") @ConfigurationProperties(prefix = "fw.db1") public Flyway flyway(@Qualifier("dataSource1") DataSource dataSource1) { Flyway clinic = new Flyway(); clinic.setDataSource(dataSource1); return clinic; } // DB2 // ... /** * @see FlywayAutoConfiguration */ @Bean @ConfigurationPropertiesBinding public StringOrNumberToMigrationVersionConverter stringOrNumberMigrationVersionConverter() { return new StringOrNumberToMigrationVersionConverter(); } /** * Convert a String or Number to a {@link MigrationVersion}. * @see FlywayAutoConfiguration */ private static class StringOrNumberToMigrationVersionConverter implements GenericConverter { private static final Set<ConvertiblePair> CONVERTIBLE_TYPES; static { Set<ConvertiblePair> types = new HashSet<ConvertiblePair>(2); types.add(new ConvertiblePair(String.class, MigrationVersion.class)); types.add(new ConvertiblePair(Number.class, MigrationVersion.class)); CONVERTIBLE_TYPES = Collections.unmodifiableSet(types); } @Override public Set<ConvertiblePair> getConvertibleTypes() { return CONVERTIBLE_TYPES; } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String value = ObjectUtils.nullSafeToString(source); return MigrationVersion.fromVersion(value); } }}</code></pre><p>application.conf中的配置:</p><pre><code># DB1fw.db1.enabled=true</code></pre><h2 id="关于事务"><a href="#关于事务" class="headerlink" title="关于事务"></a>关于事务</h2><p>有经验的同学马上会问,多数据库下事务会不会有问题?需要改造成分布式事务吗?只要为每个数据库创建独立的TransactionManager,就不会有问题,Spring会自动处理好事务的提交和回滚,就像单数据库一样。至于分布式事务,大可不必,因为虽然有多个数据库,但仍然属于Local Transaction范畴。以后有时间我会再写篇文章展开阐述一下。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>由上可见,无论是基础的DataSource和TransactionManager,还是Spring之外的第三方框架,在Boot中基本都可以找到相应的AutoConfiguration配置类。参照这些配置类,就不难根据实际需要写出自己的扩展版本。对于那些找不到AutoConfiguration配置类的,可结合框架的官方文档,使用@Configuration和@Bean注解自行进行配置。</p>]]></content:encoded>
<comments>http://emacoo.cn/backend/spring-boot-multi-db/#disqus_thread</comments>
</item>
<item>
<title>【翻译】Scala的起源 —— Martin Odersky访谈(一)</title>
<link>http://emacoo.cn/coding/scala-origins/</link>
<guid>http://emacoo.cn/coding/scala-origins/</guid>
<pubDate>Sun, 31 Jan 2016 16:00:00 GMT</pubDate>
<description>
<p>Scala是一种运行在JVM之上的通用的,面向对象的函数式语言。它的作者是Martin Odersky,一位来自洛桑联邦理工大学(EPFL)的教授。作为本系列访谈的第一部分,Martin Odersky和Artima网站的Bill Venners一起讨论了Scala的历史和
</description>
<content:encoded><![CDATA[<p>Scala是一种运行在JVM之上的通用的,面向对象的函数式语言。它的作者是Martin Odersky,一位来自洛桑联邦理工大学(EPFL)的教授。作为本系列访谈的第一部分,Martin Odersky和Artima网站的Bill Venners一起讨论了Scala的历史和起源。</p><h2 id="结缘编译器"><a href="#结缘编译器" class="headerlink" title="结缘编译器"></a>结缘编译器</h2><p><strong>Bill Venners</strong>: 让我们回到最初。你是如何接触编程语言的?</p><blockquote><p><strong>Martin Odersky</strong>: 编译器和编程语言从来都是我最爱的课题。1980年,那时我还在读本科,当我第一次搞明白什么是编译器,我立刻就想自己构建一个。我当时唯一能买的起的电脑就是拥有1K内存的Sinclair ZX 80。我差点就能一试身手,不过幸运的是,很快我接触到一台更强大的电脑,Osborne-1。它是世界上第一台便携式电脑,从远处看起来就像一台倾斜了90度的缝纫机。它拥有5英寸屏幕,每行显示52个字符。最令人印象深刻的是它拥有56K内存以及两个90K的软盘驱动器。</p><p>那段时间,我经常和学校里一名叫Peter Sollich的学生在一起。我们通过阅读了解到一门新的被称为Modula-2的语言,我们发现这门语言不仅非常优雅而且设计也很棒。于是我们就产生了为8位的Z80电脑设计Modula-2编译器的念头。不过Osborne有一个小问题,它只自带微软Basic这一门语言。这跟我们的预想完全不相符,因为Basic语言只支持全局变量,甚至不支持带参数的过程。而当时其它的编译器对我们而言都太贵了。于是我们决定采用经典的引导技术(bootstrapping technique)。Peter曾经用Z80的汇编语言写过一个支持部分Pascal语法的编译器。我们使用这个编译器编译了更大一点的语言,然后再更大一点,迭代数次后,直至我们能够编译所有Modula-2的语法。这个新的编译器能够生成用于解释执行的字节码以及相应的Z80上的二进制文件。这些字节码是当时任何系统上所能生成的最紧凑的字节码,相应的二进制版本也是8位电脑上运行最快的。这个编译器在当时可以说是相当棒的。</p><p>就在我们快完成我们的编译器时,Borland推出了Turbo Pascal,并且准备进军Modula-2市场。实际上,Borland决定购买我们的Modula-2编译器,重新冠以Turbo Module-2(CP/M版本)的名称和另一款他们打算开发的IBM PC版本一起搭售。我们提出为他们开发这个IBM PC版本,但是他们告诉我们他们已经有人在着手做了。三或四年之后,当这款编译器最终面世时,实现它的团队已经从Borland分离出来,并且为这款编译器起了一个新的名字,TopSpeed Module-2。在缺少IBM PC版本的情况下,Borland从来没有进行过任何Turbo Modula-2的市场推广,因此它也一直默默无名。</p><p>当我们完成Modula-2编译器时,Borland立刻提出聘请Peter和我。Peter加入了他们。我差点也去了,但是我还有一年的课程以及研究生计划。我当时对从大学辍学的念头也很动心。最后,我还是决定留在学校。在我读研期间(研究课题是增量解析),我发现我非常喜欢做研究。所以最终,我放弃了去Borland写编译器的念头,转而去ETH Zurich跟随Pascal和Module-2的发明者Niklaus Wirth攻读博士学位。</p></blockquote><h2 id="更好的Java"><a href="#更好的Java" class="headerlink" title="更好的Java"></a>更好的Java</h2><p><strong>Bill Venners</strong>: Scala是如何产生的?它的历史是什么样的?</p><blockquote><p><strong>Martin Odersky</strong>: 1988、1989年之间,当我即将结束在Zurich的学业,我对函数式编程产生了极大的兴趣。所以我留下来继续做研究,最后成为了德国Karlsruhe大学的一名教授。一开始我专注于偏理论的编程领域,比如按需调用(call-by-need)的λ演算。这部分工作是和当时在Glasgow大学的Phil Wadler一起进行的。一天,Phil告诉我他组里的一个助教听说有一门新的语言正在兴起,目前处于Alpha版本,名字叫Java。它是可迁移的,能够产生字节码,运行在web端,并且有垃圾回收机制。这门语言将颠覆你的工作。你准备怎么办?Phil说。好吧,他也许是对的。</p><p>回答是Phil Wadler和我决定将一部分函数式编程的概念植入到Java世界中。这部分工作诞生了一门叫Pizza的语言,它拥有3个函数式编程的特性,泛型,高阶函数和模式匹配。Pizza首次发布于1996年,在Java面世后的一年。它成功的证明了在JVM平台上是能够实现函数式语言特性的。</p><p>于是我们联系了Sun公司核心开发团队的Gilad Bracha和David Stoutamire。他们回应说,“我们对你们正在做的泛型那部分工作很感兴趣。我们可以开始一个新的专注于此的项目。”那个项目被称为GJ(Generic Java)。1997、1998年之间,我们完成了GJ的开发。六年后,加上一些之前我们没做的特性,它成为了Java 5中的泛型。值得一提的是,Java泛型中的通配符是在我们之后由Gilad Bracha和Aarhus大学的其他人一起独立完成的。</p><p>虽然我们的泛型扩展延期了六年才推出,Sun对我写的GJ编译器产生了更强烈的兴趣。它被证明比Sun最初版本的Java编译器更稳定也更易维护。所以他们决定从2000年发布的Java 1.3版本起,使用GJ编译器作为标准的Java编译器。</p></blockquote><h2 id="超越Java"><a href="#超越Java" class="headerlink" title="超越Java"></a>超越Java</h2><blockquote><p><strong>Martin Odersky</strong>: 在开发Pizza和GJ期间,我有时感到绝望,因为Java是一门限制性很强的语言,很多事情不能按我自认为正确的方式去做。因此,在那之后,当我实质性的工作目标转移到让Java更好上时,我决定是时候后退一步了。我想从头来过,看看我能否设计出一门比Java更好的语言。但同时我知道我不能从零开始,我必须借助于已有的基础架构,没有任何类库、工具的支持去启动这样一个项目是不现实的。所以我决定虽然我的目的是设计一门与Java不同的语言,但这门语言仍然可以与Java的基础架构(JVM和类库)互连。这就是我(设计Scala语言)的初衷。也是在那个时候,我成为了EPFL的一名教授,这为我提供了一个绝佳的进行独立研究的环境。我得以成立一个研究小组,无需成天申请外部津贴。</p><p>起初我们非常激进。我们想创建一门基于一个被称为join calculus的非常完美的并发模型上的语言。我们创建了一个被称为Functional Nets的面向对象版本的join calculus实现,以及一门基于其上的语言Funnal。但没过多久,我们发现作为一门很纯粹的语言,Funnel并没有太多实际意义。Funnal有一个很小的内核。很多大家认为理所应当包含的东西(比如类,模式匹配)只能通过编码的方式植入到内核中。从学术的角度,这是一种很优雅的技巧。但从实践角度,并不是太好。初学者发现这类编码非常困难,而专家们又认为过于重复和单调。</p><p>结果是我们决定重新开始,在非常纯粹的学术性语言Funnal和非常实用但又在一定程度上带有限制性的GJ之间寻找出路。我们想创造一门不仅实用同时又比Java更高级的语言。大约在2002年,我们开始开发这门后来被我们称为Scala的语言,并于2003年发布第一版本。2006年初,又发布了一个相对比较大的重构版本。在此之后,就进入一个稳定的迭代期。</p></blockquote><h2 id="挑战"><a href="#挑战" class="headerlink" title="挑战"></a>挑战</h2><p><strong>Bill Venners</strong>: 你曾说有时候为了保证与Java的兼容性让你非常抓狂。你能给一些具体的起初由于兼容性限制你做不了,但后来从源代码兼容性降为二进制兼容性之后又可以做的例子吗?</p><blockquote><p><strong>Martin Odersky</strong>: 在泛型设计中,存在很多非常非常困难的限制。最强的也是最难解决的限制是需要与没有泛型支持的Java版本兼容。这个问题的背景是Java 1.2版本发布了没有泛型支持的集合类库,那时泛型刚出现,Sun公司还没准备好发布一个全新的基于泛型的集合类库。</p><p>那就是为什么存在这么多非常丑陋的设计的原因。你总是要同时面对非泛型类型和泛型类型(也被成为原始类型)。同样你也不能改变数组的行为,于是你就不得不接受未受检警告。最重要的是,在操作数组时很多你想做的事情是做不了的,比如生成一个拥有未知类型T的数组。后来在Scala中我们实际上发现了一种实现的方式,但那是由于我们给数组加入了协变的特性。</p></blockquote><p><strong>Bill Venners</strong>: 你能够详细说明一下有关Java中协变数组的问题吗?</p><blockquote><p><strong>Martin Odersky</strong>: 最初Java刚发布时,Bill Joy和James Gosling以及其他Java组成员都认为Java应该支持泛型,只是他们没有时间去完善设计并加入进去。正因为最初版本的Java不支持泛型,他们感觉数组应该是协变的。举例来说,那就意味着一个String数组是一个Object数组的子类型。背后的原因是他们想实现一个类泛型的排序方法,接受一个Object数组和一个排序类然后对这个数组进行排序,同时允许你传入一个String数组。结果发现这个设计通常是有缺陷的。那就是为什么在Java中你会得到一个数组存储异常。实际上这个设计最后也阻止了实现一个优雅的泛型数组。那就是为什么Java泛型中根本不支持数组。你不能创建一个类型为String列表的数组,这根本不可能。你永远不得不使用丑陋的原始类型,一个列表类型的数组。所以某种程度上这就是一个原罪。他们完成的很快,认为这只不过是一次快速改造。但实际上这毁掉了之后每一个设计决定。因此,为了避免掉入同样的陷阱,我们不得不停下来宣布,现在我们不再和Java保持向前兼容,我们想做一些不同的事。</p></blockquote><p><em>查看英文原文:<a href="http://www.artima.com/scalazine/articles/origins_of_scala.html" target="_blank" rel="noopener">The Origins of Scala</a></em></p><p>该系列访谈的其余部分:</p><ul><li><a href="http://www.infoq.com/cn/articles/Scala-Design" target="_blank" rel="noopener">Scala的设计目标——Martin Odersky访谈(二)</a></li><li><a href="http://www.infoq.com/cn/articles/scala-type-system" target="_blank" rel="noopener">Scala类型系统的目的——Martin Odersky访谈(三)</a></li><li><a href="http://www.infoq.com/cn/articles/Scala-PatternMatching" target="_blank" rel="noopener">Scala模式匹配的亮点——Martin Odersky访谈(四)</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/coding/scala-origins/#disqus_thread</comments>
</item>
<item>
<title>【Web】CDN加速效果浅析</title>
<link>http://emacoo.cn/arch/web-cdn-benchmark/</link>
<guid>http://emacoo.cn/arch/web-cdn-benchmark/</guid>
<pubDate>Sun, 17 Jan 2016 16:00:00 GMT</pubDate>
<description>
<h2 id="1-什么是CDN?"><a href="#1-什么是CDN?" class="headerlink" title="1. 什么是CDN?"></a>1. 什么是CDN?</h2><blockquote>
<p>CDN的全称是Content Delivery Net
</description>
<content:encoded><![CDATA[<h2 id="1-什么是CDN?"><a href="#1-什么是CDN?" class="headerlink" title="1. 什么是CDN?"></a>1. 什么是CDN?</h2><blockquote><p>CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的CACHE(缓存)层,将网站的内容发布到最接近用户的网络”边缘”的节点,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等原因,提高用户访问网站的响应速度。<br><a href="http://www.51know.info/system_performance/cdn/cdn.html" target="_blank" rel="noopener">http://www.51know.info/system_performance/cdn/cdn.html</a></p></blockquote><p>可以认为,CDN就是加上了智能DNS和缓存层的反向代理集群。由于智能DNS能够根据请求的来源定位到离用户较近的缓存服务器,因此有效的缩短了连接时间,而缓存层的存在极大的提高了下载速度,并且不再受限于源站的带宽大小。注意上述第二点仅针对静态资源有意义,对于动态内容(比如POST请求,WebSocket连接),CDN仍然需要将请求发回源站再将结果返回,并不能起到加速作用。下面就针对上述分析进行实验验证。</p><h2 id="2-环境准备"><a href="#2-环境准备" class="headerlink" title="2. 环境准备"></a>2. 环境准备</h2><ul><li>源站,提供静态页面和WebSocket服务。</li><li>CDN,使用腾讯云CDN服务,仅对静态页面设置缓存。</li></ul><h2 id="3-基准测试"><a href="#3-基准测试" class="headerlink" title="3. 基准测试"></a>3. 基准测试</h2><h3 id="3-1-静态页面"><a href="#3-1-静态页面" class="headerlink" title="3.1 静态页面"></a>3.1 静态页面</h3><p>如下图所示,使用CDN之后,无论是连接时间还是下载时间都明显缩短,下载速度也有<strong>5倍</strong>以上的提速。</p><p><img src="web-cdn-benchmark-static.png" alt></p><p>测速网站:<a href="http://www.17ce.com/" target="_blank" rel="noopener">http://www.17ce.com/</a></p><h3 id="3-2-动态内容"><a href="#3-2-动态内容" class="headerlink" title="3.2 动态内容"></a>3.2 动态内容</h3><p>选择一台远离源站的服务器,运行Node基准测试,先后与源站和CDN站建立WebSocket连接,发送消息,计算总耗时。测试结果显示,与直连源站相比,使用CDN并没有起到加速效果,反而有所下降。不难理解,这是因为去掉缓存之后,CDN平白在用户和源站之间多加了一层链路。</p><p><img src="web-cdn-benchmark-ws.png" alt></p><h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4. 参考"></a>4. 参考</h2><ul><li><a href="http://www.51know.info/system_performance/cdn/cdn.html" target="_blank" rel="noopener">http://www.51know.info/system_performance/cdn/cdn.html</a></li><li><a href="http://www.qcloud.com/wiki/CDN%E4%BB%8B%E7%BB%8D" target="_blank" rel="noopener">http://www.qcloud.com/wiki/CDN%E4%BB%8B%E7%BB%8D</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/arch/web-cdn-benchmark/#disqus_thread</comments>
</item>
<item>
<title>既然选择了远方,就只顾风雨兼程</title>
<link>http://emacoo.cn/notes/xr-ruijin/</link>
<guid>http://emacoo.cn/notes/xr-ruijin/</guid>
<pubDate>Sat, 09 Jan 2016 16:00:00 GMT</pubDate>
<description>
<p>周六,早上8:30,这个城市大多数年轻人仍在温暖的被窝里,XR上海23名顾问已准时集结在大会议室,待早会之后便奔赴各自的“战场”。早会里面绩效评比这个环节很有特色,顾问的四项核心绩效指标(活跃数,拜访量,日报,医院照片)分别被比喻成早、中、晚饭加夜宵,让枯燥的数字变得充满关
</description>
<content:encoded><![CDATA[<p>周六,早上8:30,这个城市大多数年轻人仍在温暖的被窝里,XR上海23名顾问已准时集结在大会议室,待早会之后便奔赴各自的“战场”。早会里面绩效评比这个环节很有特色,顾问的四项核心绩效指标(活跃数,拜访量,日报,医院照片)分别被比喻成早、中、晚饭加夜宵,让枯燥的数字变得充满关怀。</p><p>小何,上海顾问团队第三组小组长,和两位战狼配合打瑞金医院,是我今天的跟访对象。薄夹克,公文包,显得很利落。我问他冷不冷,他说不冷,一天医院跑下来,常常一身汗,不能穿太多。去医院的路上,小何主动跟我聊了一些移动医疗的话题,虽然10月份刚从一家传统的面向企业的软件公司跳槽到我们公司,但他对于互联网和医疗行业已经非常熟悉,对公司的产品和理念也非常认同。</p><p>到了医院,我们先在门诊大楼跟战狼小张会合,然后坐电梯上19楼住院部。昨天小张在那里拿下了一批新的认证医生,今天过去发放台卡。因为是周六,医生办公室里只有一个女医生,见了小何和小张显得很客气。小张发放台卡的时候,我才注意到他除了一个背包,肩上还跨了一个背袋,里面装满了台卡,大概有十个左右。我们的台卡除了纸质日历本,还配有一个木头底座和一块木板,不难想象这一袋台卡着实是有些分量的。一天背下来,也是件体力活。Hunter说去年打北京的时候,20吨礼品生生的被战狼们背进了各家医院,真不是玩笑话。</p><p>在医院做地推其实就是门诊室蹲点和住院部扫楼。像瑞金这样的知名三甲医院,患者多医生多楼也多,大医生们神出鬼没,除了门诊,其他地方难见踪影。而门诊一般都是人满为患,一天中最有效的时间只有中午11点到12点,下午4点到5点这两个小时,战狼和顾问们需要争分夺秒,逐个敲开没人排队的门诊室,给医生们介绍我们的产品和帮助他们掌握基本的操作。一般的流程是,头天战狼帮医生安装好App,第二天顾问上门教会医生使用App。在这之后,顾问会有节奏的持续拜访这些医生,从中筛选出最有可能成为高活跃的医生并提供更有针对性的服务。</p><p>出了门诊大楼,小何带我扫了一遍12层的6号楼,那里是瑞金的一个住院部。坐电梯到12楼,然后逐层往下,每一层有两个医生办公室。临近中午,加上又是周末,办公室都很冷清,一般1到2位,半数都空着。医生们或者在操作电脑,或者在休息,或者在跟患者聊天,这些医生顾问都不会去打扰,他们会找那些正在闲聊或者刷手机的医生。好不容易找到一个,小何赶紧凑上去,三言两句就顺利的拿到那名医生的手机开始过权威认证,演示加患者流程,告之如何开通收费服务等。一圈聊完,我看了一下时钟大概花了10分钟左右。“她成不了高活跃医生。”出了办公室,小何摇了摇头。</p><p>扫完6号楼已经过了12点,小何请我在瑞金食堂吃了午饭。“下午要抓紧了,不然一天又过去了。”小何对自己说,又像是对我说。是啊,要抓紧了,在通往梦想的路上,每一天都蕴含着无限的可能。</p><p>既然选择了远方,就只顾风雨兼程。</p>]]></content:encoded>
<comments>http://emacoo.cn/notes/xr-ruijin/#disqus_thread</comments>
</item>
<item>
<title>【Play】热部署是如何工作的?</title>
<link>http://emacoo.cn/backend/play-hotdeploy/</link>
<guid>http://emacoo.cn/backend/play-hotdeploy/</guid>
<pubDate>Sat, 26 Dec 2015 16:00:00 GMT</pubDate>
<description>
<h2 id="1-什么是热部署"><a href="#1-什么是热部署" class="headerlink" title="1.什么是热部署"></a>1.什么是热部署</h2><blockquote>
<p>所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用
</description>
<content:encoded><![CDATA[<h2 id="1-什么是热部署"><a href="#1-什么是热部署" class="headerlink" title="1.什么是热部署"></a>1.什么是热部署</h2><blockquote><p>所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件。– <a href="http://baike.baidu.com/view/5036687.htm" target="_blank" rel="noopener">百度百科</a></p></blockquote><p>对于Java应用,有三种常见的实现热部署的方式:</p><ul><li>JPDA: 利用JVM原生的JPDA接口,参见<a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/enhancements1.4.html#hotswap" target="_blank" rel="noopener">官方文档</a></li><li>Classloader: 通过创建新的Classloader来加载新的Class文件。OSGi就是通过这种方式实现Bundle的动态加载。</li><li>Agent: 通过自定义Java Agent实现Class动态加载。JRebel,hotswapagent使用的就是这种方式。</li></ul><p>Play console自带的auto-reload功能正是基于上述第二种方式实现的。</p><h2 id="2-Auto-reload机制"><a href="#2-Auto-reload机制" class="headerlink" title="2.Auto-reload机制"></a>2.Auto-reload机制</h2><p>Play console是Typesafe封装的一种特殊的的sbt console,主要增加了activator new和activator ui两个命令。其auto-reload功能是以sbt插件(”com.typesafe.play” % “sbt-plugin”)的形式提供的,sbt-plugin通过sbt-run-support类库连接到play开发模式下的启动类(play.core.server.DevServerStart)。每当应用收到请求时,play会通过sbt-plugin检查是否有源文件被修改,如果存在,则调用sbt命令进行编译,然后依次停止老的play应用,创建新的classloader,然后启动新的play应用,在此过程中运行sbt的JVM并没有被重启,只是play应用完成了重启。</p><h2 id="3-源码分析"><a href="#3-源码分析" class="headerlink" title="3.源码分析"></a>3.源码分析</h2><p>以下分别从sbt-plugin,sbt-run-support和play-server挑选3个核心类对上述流程进行简单梳理。</p><h3 id="play-sbt-run-PlayRun"><a href="#play-sbt-run-PlayRun" class="headerlink" title="play.sbt.run.PlayRun"></a>play.sbt.run.PlayRun</h3><p>定义play run task,通过Reloader传递sbt回调函数引用给DevServerStart。</p><figure class="highlight scala"><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="type">Line</span> <span class="number">73</span><span class="number">-93</span>: <span class="type">PlayRun</span>#playRunTask]</span><br><span class="line"><span class="keyword">lazy</span> <span class="keyword">val</span> devModeServer = <span class="type">Reloader</span>.startDevMode(</span><br><span class="line"> runHooks.value,</span><br><span class="line"> (javaOptions in <span class="type">Runtime</span>).value,</span><br><span class="line"> dependencyClasspath.value.files,</span><br><span class="line"> dependencyClassLoader.value,</span><br><span class="line"> reloadCompile,# sbt回调函数引用</span><br><span class="line"> reloaderClassLoader.value,</span><br><span class="line"> assetsClassLoader.value,</span><br><span class="line"> playCommonClassloader.value,</span><br><span class="line"> playMonitoredFiles.value,</span><br><span class="line"> fileWatchService.value,</span><br><span class="line"> (managedClasspath in <span class="type">DocsApplication</span>).value.files,</span><br><span class="line"> playDocsJar.value,</span><br><span class="line"> playDefaultPort.value,</span><br><span class="line"> playDefaultAddress.value,</span><br><span class="line"> baseDirectory.value,</span><br><span class="line"> devSettings.value,</span><br><span class="line"> args,</span><br><span class="line"> runSbtTask,</span><br><span class="line"> (mainClass in (<span class="type">Compile</span>, <span class="type">Keys</span>.run)).value.get</span><br><span class="line"> )</span><br></pre></td></tr></table></figure><h3 id="play-runsupport-Reloader"><a href="#play-runsupport-Reloader" class="headerlink" title="play.runsupport.Reloader"></a>play.runsupport.Reloader</h3><p>通过反射启动play应用,将Reloader自身作为参数传入。</p><figure class="highlight scala"><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="type">Line</span> <span class="number">203</span><span class="number">-212</span>: <span class="type">Reloader</span>#startDevMode]</span><br><span class="line"><span class="keyword">val</span> server = {</span><br><span class="line"> <span class="keyword">val</span> mainClass = applicationLoader.loadClass(mainClassName)</span><br><span class="line"> <span class="keyword">if</span> (httpPort.isDefined) {</span><br><span class="line"> <span class="keyword">val</span> mainDev = mainClass.getMethod(<span class="string">"mainDevHttpMode"</span>, classOf[<span class="type">BuildLink</span>], classOf[<span class="type">BuildDocHandler</span>], classOf[<span class="type">Int</span>], classOf[<span class="type">String</span>])</span><br><span class="line"> mainDev.invoke(<span class="literal">null</span>, reloader, buildDocHandler, httpPort.get: java.lang.<span class="type">Integer</span>, httpAddress).asInstanceOf[play.core.server.<span class="type">ServerWithStop</span>]</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">val</span> mainDev = mainClass.getMethod(<span class="string">"mainDevOnlyHttpsMode"</span>, classOf[<span class="type">BuildLink</span>], classOf[<span class="type">BuildDocHandler</span>], classOf[<span class="type">Int</span>], classOf[<span class="type">String</span>])</span><br><span class="line"> mainDev.invoke(<span class="literal">null</span>, reloader, buildDocHandler, httpsPort.get: java.lang.<span class="type">Integer</span>, httpAddress).asInstanceOf[play.core.server.<span class="type">ServerWithStop</span>]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="play-core-server-DevServerStart"><a href="#play-core-server-DevServerStart" class="headerlink" title="play.core.server.DevServerStart"></a>play.core.server.DevServerStart</h3><p>从注释可以清楚的看到stop-and-start的重启逻辑。</p><figure class="highlight scala"><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></pre></td><td class="code"><pre><span class="line">[<span class="type">Line</span> <span class="number">113</span><span class="number">-180</span>: <span class="type">DevServerStart</span>#mainDev]</span><br><span class="line"><span class="keyword">val</span> reloaded = buildLink.reload <span class="keyword">match</span> {</span><br><span class="line"> <span class="keyword">case</span> <span class="type">NonFatal</span>(t) => <span class="type">Failure</span>(t)</span><br><span class="line"> <span class="keyword">case</span> cl:</span><br><span class="line"> <span class="type">ClassLoader</span> => <span class="type">Success</span>(<span class="type">Some</span>(cl))</span><br><span class="line"> <span class="keyword">case</span> <span class="literal">null</span> => <span class="type">Success</span>(<span class="type">None</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">reloaded.flatMap {</span><br><span class="line"> maybeClassLoader =></span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> maybeApplication: <span class="type">Option</span>[<span class="type">Try</span>[<span class="type">Application</span>]] = maybeClassLoader.map {</span><br><span class="line"> projectClassloader =></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (lastState.isSuccess) {</span><br><span class="line"> println()</span><br><span class="line"> println(play.utils.<span class="type">Colors</span>.magenta(<span class="string">"--- (RELOAD) ---"</span>))</span><br><span class="line"> println()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> reloadable = <span class="keyword">this</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// First, stop the old application if it exists</span></span><br><span class="line"> lastState.foreach(<span class="type">Play</span>.stop)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create the new environment</span></span><br><span class="line"> <span class="keyword">val</span> environment = <span class="type">Environment</span>(path, projectClassloader, <span class="type">Mode</span>.<span class="type">Dev</span>)</span><br><span class="line"> <span class="keyword">val</span> sourceMapper = <span class="keyword">new</span> <span class="type">SourceMapper</span> {</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">sourceOf</span></span>(className: <span class="type">String</span>, line: <span class="type">Option</span>[<span class="type">Int</span>]) = {</span><br><span class="line"> <span class="type">Option</span>(buildLink.findSource(className, line.map(_.asInstanceOf[java.lang.<span class="type">Integer</span>]).orNull)).flatMap {</span><br><span class="line"> <span class="keyword">case</span> <span class="type">Array</span>(file: java.io.<span class="type">File</span>, <span class="literal">null</span>) => <span class="type">Some</span>((file, <span class="type">None</span>))</span><br><span class="line"> <span class="keyword">case</span> <span class="type">Array</span>(file: java.io.<span class="type">File</span>, line: java.lang.<span class="type">Integer</span>) => <span class="type">Some</span>((file, <span class="type">Some</span>(line)))</span><br><span class="line"> <span class="keyword">case</span> _ => <span class="type">None</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> webCommands = <span class="keyword">new</span> <span class="type">DefaultWebCommands</span></span><br><span class="line"> currentWebCommands = <span class="type">Some</span>(webCommands)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> newApplication = <span class="type">Threads</span>.withContextClassLoader(projectClassloader) {</span><br><span class="line"> <span class="keyword">val</span> context = <span class="type">ApplicationLoader</span>.createContext(environment, dirAndDevSettings, <span class="type">Some</span>(sourceMapper), webCommands)</span><br><span class="line"> <span class="keyword">val</span> loader = <span class="type">ApplicationLoader</span>(context)</span><br><span class="line"> loader.load(context)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">Play</span>.start(newApplication)</span><br><span class="line"></span><br><span class="line"> <span class="type">Success</span>(newApplication)</span><br><span class="line"> } <span class="keyword">catch</span> {</span><br><span class="line"> <span class="keyword">case</span> e:</span><br><span class="line"> <span class="type">PlayException</span> => {</span><br><span class="line"> lastState = <span class="type">Failure</span>(e)</span><br><span class="line"> lastState</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">case</span> <span class="type">NonFatal</span>(e) => {</span><br><span class="line"> lastState = <span class="type">Failure</span>(<span class="type">UnexpectedException</span>(unexpected = <span class="type">Some</span>(e)))</span><br><span class="line"> lastState</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">case</span> e:</span><br><span class="line"> <span class="type">LinkageError</span> => {</span><br><span class="line"> lastState = <span class="type">Failure</span>(<span class="type">UnexpectedException</span>(unexpected = <span class="type">Some</span>(e)))</span><br><span class="line"> lastState</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> maybeApplication.flatMap(_.toOption).foreach {</span><br><span class="line"> app =></span><br><span class="line"> lastState = <span class="type">Success</span>(app)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> maybeApplication.getOrElse(lastState)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="4-Gotcha"><a href="#4-Gotcha" class="headerlink" title="4. Gotcha"></a>4. Gotcha</h2><p>上述的实现看上去并不复杂,那为什么老牌的Tomcat,JBoss容器却始终没有提供类似的机制呢?原因很简单,Play是stateless的,而其余的不是。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://jto.github.io/articles/play_anatomy_part2_sbt/" target="_blank" rel="noopener">http://jto.github.io/articles/play_anatomy_part2_sbt/</a></li></ul>]]></content:encoded>
<comments>http://emacoo.cn/backend/play-hotdeploy/#disqus_thread</comments>
</item>
</channel>
</rss>