-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlocal-search.xml
More file actions
578 lines (278 loc) · 320 KB
/
local-search.xml
File metadata and controls
578 lines (278 loc) · 320 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>结构化并发与响应式并发</title>
<link href="/2025/11/12/architect/%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%93%8D%E5%BA%94%E5%BC%8F%E5%B9%B6%E5%8F%91/"/>
<url>/2025/11/12/architect/%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%93%8D%E5%BA%94%E5%BC%8F%E5%B9%B6%E5%8F%91/</url>
<content type="html"><![CDATA[<h1 id="结构化并发与响应式并发:现代并发编程的两种核心范式"><a href="#结构化并发与响应式并发:现代并发编程的两种核心范式" class="headerlink" title="结构化并发与响应式并发:现代并发编程的两种核心范式"></a>结构化并发与响应式并发:现代并发编程的两种核心范式</h1><blockquote><p>本文深入探讨了“结构化并发”与“响应式操作库的并发”在思想、实现与适用场景上的核心区别,旨在帮助开发者理解并选择合适的并发模型。</p></blockquote><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在现代软件开发中,高效、安全地处理并发任务是提升应用性能与用户体验的关键。随着技术演进,两种主流的并发范式逐渐脱颖而出:<strong>结构化并发</strong> 与 <strong>响应式操作库的并发</strong>。它们源于不同的设计哲学,解决了不同层面的核心问题。理解它们的区别,对于架构设计和代码编写至关重要。</p><h2 id="核心思想类比"><a href="#核心思想类比" class="headerlink" title="核心思想类比"></a>核心思想类比</h2><p>在深入技术细节前,我们可以通过两个生动的比喻来建立直觉理解。</p><h3 id="结构化并发:严谨的团队项目"><a href="#结构化并发:严谨的团队项目" class="headerlink" title="结构化并发:严谨的团队项目"></a>结构化并发:严谨的团队项目</h3><p>想象你在组织一个严谨的团队项目:</p><ul><li>你有一个明确的总任务(<strong>父作用域</strong>)。</li><li>你将总任务分解为几个子任务(<strong>子协程/线程</strong>)。</li><li><strong>关键规则是</strong>:所有子任务的生命周期都必须严格限定在总任务的生命周期之内。</li><li>如果总任务被取消,所有子任务必须立即停止。</li><li>如果某个子任务失败,它可能会导致整个总任务失败,并连带取消所有其他子任务。</li><li>你必须等待所有子任务都完成后,总任务才算最终完成。</li></ul><p><strong>核心特质</strong>:<strong>不会有无家可归的“孤儿任务”</strong>,管理严格,权责清晰。</p><h3 id="响应式并发:高度自动化的流水线"><a href="#响应式并发:高度自动化的流水线" class="headerlink" title="响应式并发:高度自动化的流水线"></a>响应式并发:高度自动化的流水线</h3><p>想象一条高度自动化的工业流水线:</p><ul><li>数据如同零件,在流水线上流动。</li><li>每个工作站(<strong>操作符</strong>)对流经的数据进行处理、转换、组合或过滤。</li><li>并发是通过将工作分发到不同的并行流水线(<strong>线程</strong>)来实现的。</li><li>你关注的是数据流的变换规则和组合方式,以及当上游生产速度超过下游处理能力时的<strong>背压</strong>管理。</li></ul><p><strong>核心特质</strong>:关注<strong>数据流</strong>本身,而非单个任务的生死。</p><hr><h2 id="技术实现深度对比"><a href="#技术实现深度对比" class="headerlink" title="技术实现深度对比"></a>技术实现深度对比</h2><table><thead><tr><th align="left">维度</th><th align="left">结构化并发</th><th align="left">响应式操作库的并发</th></tr></thead><tbody><tr><td align="left"><strong>核心范式</strong></td><td align="left"><strong>命令式 / 过程式</strong></td><td align="left"><strong>声明式 / 函数式</strong></td></tr><tr><td align="left"><strong>关注点</strong></td><td align="left"><strong>任务的生命周期和资源管理</strong>,防止任务泄漏。</td><td align="left"><strong>数据流和变换</strong>,组合异步操作。</td></tr><tr><td align="left"><strong>并发单位</strong></td><td align="left"><strong>协程 / 轻量级线程</strong> (如 <code>Job</code>, <code>StructuredTaskScope</code>)</td><td align="left"><strong>数据流 / 发布者</strong> (如 <code>Flux</code>, <code>Observable</code>, <code>Flow</code>)</td></tr><tr><td align="left"><strong>并发控制</strong></td><td align="left">通过 <strong>父作用域</strong> 的取消信号自动传播和协作。</td><td align="left">通过 <strong>操作符</strong> (如 <code>subscribeOn</code>, <code>publishOn</code>) 指定执行上下文。</td></tr><tr><td align="left"><strong>错误处理</strong></td><td align="left"><strong>使用异常机制</strong>。未捕获异常会取消父作用域及所有兄弟任务。</td><td align="left"><strong>错误作为数据流事件</strong>。通过 <code>onError</code> 信号在流中传递和处理。</td></tr><tr><td align="left"><strong>取消机制</strong></td><td align="left"><strong>协作式取消</strong>。任务定期检查取消标志或调用可取消的挂起函数。</td><td align="left"><strong>通过取消订阅</strong>。调用 <code>dispose()</code> 或 <code>cancel()</code> 来中断流的处理。</td></tr><tr><td align="left"><strong>资源安全</strong></td><td align="left"><strong>核心优势</strong>。利用作用域块,确保退出时自动释放资源(如 Kotlin 的 <code>use</code>)。</td><td align="left"><strong>需显式管理</strong>。通过 <code>doFinally</code> 等操作符处理,易因忘记取消订阅而泄漏。</td></tr><tr><td align="left"><strong>典型代表</strong></td><td align="left">Kotlin Coroutines, Java 21+ <code>StructuredTaskScope</code></td><td align="left">Project Reactor, RxJava, Kotlin Flow</td></tr></tbody></table><hr><h2 id="代码实战:对比两种实现"><a href="#代码实战:对比两种实现" class="headerlink" title="代码实战:对比两种实现"></a>代码实战:对比两种实现</h2><p>假设我们有一个需求:<strong>并发地从两个网络源获取数据,然后将结果合并。如果任何一个请求失败,整个操作应立即失败并取消另一个仍在进行的请求。</strong></p><h3 id="1-结构化并发实现(Kotlin-Coroutines)"><a href="#1-结构化并发实现(Kotlin-Coroutines)" class="headerlink" title="1. 结构化并发实现(Kotlin Coroutines)"></a>1. 结构化并发实现(Kotlin Coroutines)</h3><p>kotlin</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs kotlin"><span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">fetchUserData</span><span class="hljs-params">()</span></span>: UserData = coroutineScope {<br> <span class="hljs-comment">// 在同一个结构化作用域内启动两个子协程</span><br> <span class="hljs-keyword">val</span> userProfileAsync = async { api.fetchUserProfile() }<br> <span class="hljs-keyword">val</span> userPostsAsync = async { api.fetchUserPosts() }<br><br> <span class="hljs-comment">// 等待结果,任何异常都会向外传播</span><br> <span class="hljs-keyword">try</span> {<br> UserData(<br> profile = userProfileAsync.await(), <span class="hljs-comment">// 若此处失败,userPostsAsync 会被自动取消</span><br> posts = userPostsAsync.await()<br> )<br> } <span class="hljs-keyword">catch</span> (e: Exception) {<br> <span class="hljs-comment">// 无需手动取消,结构化并发已自动处理</span><br> Log.e(<span class="hljs-string">"Fetch failed"</span>, e)<br> <span class="hljs-keyword">throw</span> e<br> }<br> <span class="hljs-comment">// 当作用域退出时,所有子协程必然已完成/取消,无资源泄漏。</span><br>}<br></code></pre></td></tr></table></figure><p><strong>实现解析</strong>:</p><ul><li><code>coroutineScope</code> 构建器创建了一个结构化的并发作用域。</li><li>两个 <code>async</code> 启动的子任务的生命周期严格绑定于此作用域。</li><li>任一 <code>await()</code> 调用抛出异常,另一个并发任务会<strong>自动被取消</strong>。</li><li>代码风格是线性的、命令式的,非常符合人类的直觉思维。</li></ul><h3 id="2-响应式操作库实现(Project-Reactor)"><a href="#2-响应式操作库实现(Project-Reactor)" class="headerlink" title="2. 响应式操作库实现(Project Reactor)"></a>2. 响应式操作库实现(Project Reactor)</h3><p>java</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-keyword">public</span> Mono<UserData> <span class="hljs-title">fetchUserData</span><span class="hljs-params">()</span> </span>{<br> <span class="hljs-comment">// 定义两个异步数据源,并指定其执行线程</span><br> Mono<UserProfile> profileMono = api.<span class="hljs-built_in">fetchUserProfile</span>().<span class="hljs-built_in">subscribeOn</span>(Schedulers.<span class="hljs-built_in">parallel</span>());<br> Mono<List<Post>> postsMono = api.<span class="hljs-built_in">fetchUserPosts</span>().<span class="hljs-built_in">subscribeOn</span>(Schedulers.<span class="hljs-built_in">parallel</span>());<br><br> <span class="hljs-comment">// 使用 zip 操作符组合这两个源</span><br> <span class="hljs-keyword">return</span> Mono.<span class="hljs-built_in">zip</span>(profileMono, postsMono)<br> .<span class="hljs-built_in">map</span>(tuple -> <span class="hljs-keyword">new</span> <span class="hljs-built_in">UserData</span>(tuple.<span class="hljs-built_in">getT1</span>(), tuple.<span class="hljs-built_in">getT2</span>())) <span class="hljs-comment">// 转换结果</span><br> .<span class="hljs-built_in">doOnCancel</span>(() -> Log.<span class="hljs-built_in">d</span>(<span class="hljs-string">"Operation cancelled"</span>)); <span class="hljs-comment">// 可选的取消回调</span><br>}<br></code></pre></td></tr></table></figure><p><strong>实现解析</strong>:</p><ul><li>定义了两个 <code>Mono</code>(代表产生单个结果的异步序列)。</li><li>使用 <code>subscribeOn</code> 操作符来指定它们在各自的线程上并发执行。</li><li><code>zip</code> 操作符是核心,它订阅所有输入的 <code>Mono</code>,等待它们都完成后,将结果组合成一个元组。如果其中任何一个 <code>Mono</code> 发出错误信号,<code>zip</code> 会<strong>自动取消对其余 <code>Mono</code> 的订阅</strong>。</li><li>代码风格是声明式的、链式的,通过组合操作符来定义数据流的处理管道。</li></ul><hr><h2 id="范式融合与选型建议"><a href="#范式融合与选型建议" class="headerlink" title="范式融合与选型建议"></a>范式融合与选型建议</h2><h3 id="融合趋势:Kotlin-Flow"><a href="#融合趋势:Kotlin-Flow" class="headerlink" title="融合趋势:Kotlin Flow"></a>融合趋势:Kotlin Flow</h3><p>值得注意的是,这两种范式并非水火不容,而是可以优雅地融合。<strong>Kotlin Flow</strong> 就是一个典范:</p><p>kotlin</p><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-comment">// 定义一个响应式流</span><br><span class="hljs-keyword">val</span> userDataFlow: Flow<UserData> = flow {<br> <span class="hljs-comment">// ...</span><br>}.map { ... } <span class="hljs-comment">// 响应式变换</span><br>.flowOn(Dispatchers.IO) <span class="hljs-comment">// 在IO线程上执行上游操作</span><br><br><span class="hljs-comment">// 在结构化并发作用域中收集流</span><br><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onUserAction</span><span class="hljs-params">()</span></span> {<br> viewModelScope.launch { <span class="hljs-comment">// 结构化并发</span><br> userDataFlow.collect { userData -> <span class="hljs-comment">// 响应式收集</span><br> updateUi(userData)<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>在这个例子中:</p><ul><li><code>Flow</code> 提供了丰富的响应式操作符,用于处理数据流。</li><li><code>viewModelScope</code> 是一个结构化并发作用域,当 ViewModel 清除时,它会自动取消其内部启动的所有协程,包括正在收集流的这个协程。</li><li>这样就实现了<strong>用响应式风格声明数据流,用结构化并发管理生命周期</strong>,兼具两者的优势。</li></ul><h3 id="如何选择?"><a href="#如何选择?" class="headerlink" title="如何选择?"></a>如何选择?</h3><table><thead><tr><th align="left">场景</th><th align="left">推荐范式</th><th align="left">理由</th></tr></thead><tbody><tr><td align="left"><strong>UI生命周期相关的后台任务</strong></td><td align="left"><strong>结构化并发</strong></td><td align="left">与UI组件(Activity, ViewModel)的生命周期天然绑定,避免内存泄漏和无效更新。</td></tr><tr><td align="left"><strong>实现一个可取消的复杂业务逻辑</strong></td><td align="left"><strong>结构化并发</strong></td><td align="left">任务的组织、依赖和取消逻辑清晰直观,资源安全有保障。</td></tr><tr><td align="left"><strong>处理事件流(如点击去抖)</strong></td><td align="left"><strong>响应式并发</strong></td><td align="left">提供了 <code>debounce</code>, <code>filter</code>, <code>map</code> 等专用操作符,处理此类问题得心应手。</td></tr><tr><td align="left"><strong>构建高吞吐、背压敏感的数据管道</strong></td><td align="left"><strong>响应式并发</strong></td><td align="left">强大的背压支持和流组合能力,是构建数据管道的首选。</td></tr><tr><td align="left"><strong>服务端并发请求处理</strong></td><td align="left"><strong>两者皆可,常结合使用</strong></td><td align="left">可用结构化并发管理请求上下文,内部使用响应式流处理数据。</td></tr></tbody></table><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li><strong>结构化并发</strong> 是一种 <strong>“任务生命周期”的管理范式</strong>。它通过作用域的嵌套,为并发任务带来了类似于结构化编程的纪律性,核心解决了<strong>任务泄漏</strong>和<strong>取消传播</strong>的问题,使并发代码更易编写、推理和维护。</li><li><strong>响应式操作库的并发</strong> 是一种 <strong>“异步数据流”的处理范式</strong>。它通过声明式的操作符组合,优雅地处理流动的数据,核心解决了<strong>复杂的异步变换</strong>和<strong>背压控制</strong>的问题。</li></ul><p>在技术选型上,不应将其视为二选一的对立项。理解它们各自的精髓,并在合适的场景运用合适的技术,甚至将两者结合(如 Kotlin Flow 所做的那样),才能打造出既高效又健壮的现代并发应用。</p>]]></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>架构</tag>
<tag>结构化并发</tag>
<tag>响应式并发</tag>
</tags>
</entry>
<entry>
<title>结构化并发</title>
<link href="/2025/11/11/architect/%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91/"/>
<url>/2025/11/11/architect/%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91/</url>
<content type="html"><![CDATA[<h1 id="结构化并发:让并发编程重回简单与可靠"><a href="#结构化并发:让并发编程重回简单与可靠" class="headerlink" title="结构化并发:让并发编程重回简单与可靠"></a>结构化并发:让并发编程重回简单与可靠</h1><h2 id="什么是结构化并发?"><a href="#什么是结构化并发?" class="headerlink" title="什么是结构化并发?"></a>什么是结构化并发?</h2><p><strong>结构化并发</strong>是一种编程范式,它要求并发任务(如线程、协程)必须在明确的<strong>作用域</strong>内开始和结束,其中外部作用域的生命周期决定其内部所有并发子任务的生命周期。</p><p><strong>核心思想</strong>:父作用域在所有子任务完成之前不会结束,任何子任务都不会超出父作用域的生命周期。</p><h2 id="为什么需要结构化并发?"><a href="#为什么需要结构化并发?" class="headerlink" title="为什么需要结构化并发?"></a>为什么需要结构化并发?</h2><h3 id="传统并发编程的问题"><a href="#传统并发编程的问题" class="headerlink" title="传统并发编程的问题"></a>传统并发编程的问题</h3><p>先看一个典型的非结构化并发示例:</p><p>java</p><figure class="highlight scss"><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><code class="hljs scss"><span class="hljs-comment">// 问题代码:任务泄漏</span><br>void <span class="hljs-built_in">processOrder</span>() {<br> <span class="hljs-comment">// 启动后台线程发送通知</span><br> new <span class="hljs-built_in">Thread</span>(() -> {<br> <span class="hljs-built_in">sendEmailNotification</span>(); <span class="hljs-comment">// 可能执行很长时间</span><br> })<span class="hljs-selector-class">.start</span>();<br> <br> <span class="hljs-built_in">processPayment</span>();<br> <span class="hljs-built_in">updateInventory</span>();<br> System<span class="hljs-selector-class">.out</span><span class="hljs-selector-class">.println</span>("Order processed!");<br> <span class="hljs-comment">// 方法结束,但邮件线程可能仍在运行!</span><br>}<br></code></pre></td></tr></table></figure><h3 id="非结构化并发的主要缺陷"><a href="#非结构化并发的主要缺陷" class="headerlink" title="非结构化并发的主要缺陷"></a>非结构化并发的主要缺陷</h3><ol><li><strong>资源泄漏</strong>:无人管理的线程持续消耗系统资源</li><li><strong>任务泄漏</strong>:父任务结束后,子任务像”幽灵”一样继续运行</li><li><strong>难以维护</strong>:无法清晰追踪系统中存在的并发任务</li><li><strong>取消困难</strong>:需要手动管理线程中断,容易出错</li><li><strong>调试困难</strong>:并发流程不清晰,问题难以定位</li></ol><h2 id="结构化并发如何解决问题?"><a href="#结构化并发如何解决问题?" class="headerlink" title="结构化并发如何解决问题?"></a>结构化并发如何解决问题?</h2><h3 id="核心机制"><a href="#核心机制" class="headerlink" title="核心机制"></a>核心机制</h3><p>结构化并发通过<strong>作用域绑定</strong>来解决这些问题:</p><ul><li><strong>父作用域</strong>作为管理者</li><li><strong>子任务</strong>作为工作者</li><li>严格的父子关系确保完整的生命周期管理</li></ul><h3 id="生动比喻:幼儿园老师"><a href="#生动比喻:幼儿园老师" class="headerlink" title="生动比喻:幼儿园老师"></a>生动比喻:幼儿园老师</h3><ul><li><strong>非结构化并发</strong>:老师下班回家,孩子们在操场无人看管</li><li><strong>结构化并发</strong>:老师全程监督,确保所有孩子集合后才一起离开</li></ul><h2 id="实际代码示例"><a href="#实际代码示例" class="headerlink" title="实际代码示例"></a>实际代码示例</h2><h3 id="Java-中的结构化并发(JDK-19-)"><a href="#Java-中的结构化并发(JDK-19-)" class="headerlink" title="Java 中的结构化并发(JDK 19+)"></a>Java 中的结构化并发(JDK 19+)</h3><p>java</p><figure class="highlight livescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><code class="hljs livescript"><span class="hljs-keyword">import</span> java.util.concurrent.*;<br><br>public <span class="hljs-keyword">class</span> <span class="hljs-title class_">StructuredConcurrencyExample</span> {<br> <br> public OrderResult processOrderStructured(Order order) <br> throws ExecutionException, InterruptedException {<br> <br> <span class="hljs-regexp">// 创建明确的作用域</span><br><span class="hljs-regexp"> try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {</span><br><span class="hljs-regexp"> </span><br><span class="hljs-regexp"> //</span> 在作用域内启动并发任务<br> Future<PaymentResult> paymentFuture = <br> scope.fork<span class="hljs-function"><span class="hljs-params">(() -> processPayment(order))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">InventoryUpdate</span>> <span class="hljs-title">inventoryFuture</span> = </span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> updateInventory(order))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">NotificationResult</span>> <span class="hljs-title">notificationFuture</span> = </span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> sendNotification(order))</span>;</span><br><span class="hljs-function"></span><br><span class="hljs-function"> // 等待所有任务完成或任何一个失败</span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">throwIfFailed</span><span class="hljs-params">()</span>; // 如果有任务失败,抛出异常</span><br><span class="hljs-function"></span><br><span class="hljs-function"> // 安全地获取结果</span><br><span class="hljs-function"> <span class="hljs-title">PaymentResult</span> <span class="hljs-title">payment</span> = <span class="hljs-title">paymentFuture</span>.<span class="hljs-title">resultNow</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> <span class="hljs-title">InventoryUpdate</span> <span class="hljs-title">inventory</span> = <span class="hljs-title">inventoryFuture</span>.<span class="hljs-title">resultNow</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> <span class="hljs-title">NotificationResult</span> <span class="hljs-title">notification</span> = <span class="hljs-title">notificationFuture</span>.<span class="hljs-title">resultNow</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">return</span> <span class="hljs-title">new</span> <span class="hljs-title">OrderResult</span><span class="hljs-params">(payment, inventory, notification)</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> } // 作用域结束,自动确保所有任务完成</span><br><span class="hljs-function"> }</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h3 id="Kotlin-协程示例"><a href="#Kotlin-协程示例" class="headerlink" title="Kotlin 协程示例"></a>Kotlin 协程示例</h3><p>kotlin</p><figure class="highlight stylus"><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><code class="hljs stylus">suspend fun <span class="hljs-built_in">processOrder</span>(<span class="hljs-attribute">order</span>: Order): OrderResult = coroutineScope {<br> <span class="hljs-comment">// 所有并发任务都在这个作用域内</span><br> val paymentDeferred = async { <span class="hljs-built_in">processPayment</span>(<span class="hljs-attribute">order</span>) }<br> val inventoryDeferred = async { <span class="hljs-built_in">updateInventory</span>(<span class="hljs-attribute">order</span>) }<br> val notificationDeferred = async { <span class="hljs-built_in">sendNotification</span>(<span class="hljs-attribute">order</span>) }<br><br> <span class="hljs-comment">// 自动等待所有任务完成</span><br> <span class="hljs-built_in">OrderResult</span>(<br> payment = paymentDeferred<span class="hljs-selector-class">.await</span>(),<br> inventory = inventoryDeferred<span class="hljs-selector-class">.await</span>(),<br> notification = notificationDeferred<span class="hljs-selector-class">.await</span>()<br> )<br> <span class="hljs-comment">// 作用域结束,所有子协程自动结束</span><br>}<br></code></pre></td></tr></table></figure><h2 id="关键特性与优势"><a href="#关键特性与优势" class="headerlink" title="关键特性与优势"></a>关键特性与优势</h2><h3 id="1-自动生命周期管理"><a href="#1-自动生命周期管理" class="headerlink" title="1. 自动生命周期管理"></a>1. 自动生命周期管理</h3><p>java</p><figure class="highlight livescript"><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><code class="hljs livescript"><span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> scope = <span class="hljs-keyword">new</span> StructuredTaskScope<>()) {<br> Future<<span class="hljs-built_in">String</span>> task1 = scope.fork<span class="hljs-function"><span class="hljs-params">(() -> doWork1())</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">String</span>> <span class="hljs-title">task2</span> = <span class="hljs-title">scope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> doWork2())</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> // 明确的任务边界 - 不会有意外的后台任务</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h3 id="2-可靠的错误处理"><a href="#2-可靠的错误处理" class="headerlink" title="2. 可靠的错误处理"></a>2. 可靠的错误处理</h3><ul><li><strong>自动传播</strong>:子任务失败自动向上传播</li><li><strong>协同取消</strong>:一个任务失败,自动取消相关任务</li><li><strong>统一处理</strong>:集中的异常处理点</li></ul><h3 id="3-资源安全"><a href="#3-资源安全" class="headerlink" title="3. 资源安全"></a>3. 资源安全</h3><p>java</p><figure class="highlight haxe"><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><code class="hljs haxe"><span class="hljs-comment">// 使用 try-with-resources 确保资源清理</span><br><span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> scope = <span class="hljs-keyword">new</span> <span class="hljs-type">StructuredTaskScope</span>.ShutdownOnFailure()) {<br> <span class="hljs-comment">// 并发任务...</span><br>} <span class="hljs-comment">// 自动清理所有资源</span><br></code></pre></td></tr></table></figure><h3 id="4-可观测性"><a href="#4-可观测性" class="headerlink" title="4. 可观测性"></a>4. 可观测性</h3><ul><li>清晰的调用链路</li><li>明确的任务关系</li><li>简化的监控和调试</li></ul><h2 id="不同语言的实现"><a href="#不同语言的实现" class="headerlink" title="不同语言的实现"></a>不同语言的实现</h2><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3><ul><li><strong>机制</strong>:<code>StructuredTaskScope</code> (JDK 19+)</li><li><strong>特点</strong>:显式作用域管理,强类型</li><li><strong>适用</strong>:服务端应用,大型系统</li></ul><h3 id="Kotlin"><a href="#Kotlin" class="headerlink" title="Kotlin"></a>Kotlin</h3><ul><li><strong>机制</strong>:协程 + <code>coroutineScope</code> 构建器</li><li><strong>特点</strong>:语言级原生支持,语法简洁</li><li><strong>适用</strong>:Android,全栈开发</li></ul><h3 id="Go"><a href="#Go" class="headerlink" title="Go"></a>Go</h3><ul><li><strong>机制</strong>:goroutine + <code>context</code> + <code>sync.WaitGroup</code></li><li><strong>特点</strong>:轻量级,需手动实现模式</li><li><strong>适用</strong>:网络服务,CLI工具</li></ul><h3 id="Swift"><a href="#Swift" class="headerlink" title="Swift"></a>Swift</h3><ul><li><strong>机制</strong>:Async/Await + <code>TaskGroup</code></li><li><strong>特点</strong>:Apple生态系统,类型安全</li><li><strong>适用</strong>:iOS/macOS应用</li></ul><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="1-合理设计作用域"><a href="#1-合理设计作用域" class="headerlink" title="1. 合理设计作用域"></a>1. 合理设计作用域</h3><p>java</p><figure class="highlight livescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs livescript"><span class="hljs-regexp">// 根据业务逻辑划分作用域</span><br><span class="hljs-regexp">public class OrderService {</span><br><span class="hljs-regexp"> public CompletableFuture<OrderResult> processComplexOrder(Order order) {</span><br><span class="hljs-regexp"> try (var outerScope = new StructuredTaskScope.ShutdownOnFailure()) {</span><br><span class="hljs-regexp"> </span><br><span class="hljs-regexp"> //</span> 第一阶段:验证和基础处理<br> <span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> validationScope = <span class="hljs-keyword">new</span> StructuredTaskScope<>()) {<br> Future<<span class="hljs-built_in">Boolean</span>> stockCheck = validationScope.fork<span class="hljs-function"><span class="hljs-params">(() -> checkStock(order))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">Boolean</span>> <span class="hljs-title">fraudCheck</span> = <span class="hljs-title">validationScope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> fraudDetection(order))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">validationScope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> }</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> // 第二阶段:执行核心业务</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">Payment</span>> <span class="hljs-title">payment</span> = <span class="hljs-title">outerScope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> processPayment(order))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">Shipping</span>> <span class="hljs-title">shipping</span> = <span class="hljs-title">outerScope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> arrangeShipping(order))</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">outerScope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> <span class="hljs-title">return</span> <span class="hljs-title">CompletableFuture</span>.<span class="hljs-title">completedFuture</span><span class="hljs-params">(combineResults(payment, shipping))</span>;</span><br><span class="hljs-function"> }</span><br><span class="hljs-function"> }</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h3 id="2-合理的超时和取消"><a href="#2-合理的超时和取消" class="headerlink" title="2. 合理的超时和取消"></a>2. 合理的超时和取消</h3><p>java</p><figure class="highlight livescript"><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><code class="hljs livescript"><span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> scope = <span class="hljs-keyword">new</span> StructuredTaskScope.ShutdownOnFailure()) {<br> Future<<span class="hljs-built_in">String</span>> result = scope.fork<span class="hljs-function"><span class="hljs-params">(() -> callExternalService())</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> // 设置超时</span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">joinUntil</span><span class="hljs-params">(Instant.now().plusSeconds(<span class="hljs-number">30</span>))</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">if</span> <span class="hljs-params">(!result.isDone())</span> {</span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">shutdown</span><span class="hljs-params">()</span>; // 触发取消</span><br><span class="hljs-function"> <span class="hljs-title">throw</span> <span class="hljs-title">new</span> <span class="hljs-title">TimeoutException</span><span class="hljs-params">(<span class="hljs-string">"Operation timed out"</span>)</span>;</span><br><span class="hljs-function"> }</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h3 id="3-错误处理策略"><a href="#3-错误处理策略" class="headerlink" title="3. 错误处理策略"></a>3. 错误处理策略</h3><ul><li><strong>快速失败</strong>:使用 <code>ShutdownOnFailure</code></li><li><strong>首个成功</strong>:使用 <code>ShutdownOnSuccess</code></li><li><strong>自定义策略</strong>:根据业务需求定制</li></ul><h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><h3 id="1-Web-请求处理"><a href="#1-Web-请求处理" class="headerlink" title="1. Web 请求处理"></a>1. Web 请求处理</h3><p>java</p><figure class="highlight livescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs livescript">@RestController<br>public <span class="hljs-keyword">class</span> <span class="hljs-title class_">OrderController</span> {<br> <br> @PostMapping(<span class="hljs-string">"/orders"</span>)<br> public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {<br> <span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> scope = <span class="hljs-keyword">new</span> StructuredTaskScope.ShutdownOnFailure()) {<br> Future<ValidationResult> validation = scope.fork<span class="hljs-function"><span class="hljs-params">(() -> validateRequest(request))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">PriceCalculation</span>> <span class="hljs-title">pricing</span> = <span class="hljs-title">scope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> calculatePrice(request))</span>;</span><br><span class="hljs-function"> <span class="hljs-title">Future</span><<span class="hljs-title">InventoryCheck</span>> <span class="hljs-title">inventory</span> = <span class="hljs-title">scope</span>.<span class="hljs-title">fork</span><span class="hljs-params">(() -> checkInventory(request))</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">throwIfFailed</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">return</span> <span class="hljs-title">ResponseEntity</span>.<span class="hljs-title">ok</span><span class="hljs-params">(buildResponse(validation, pricing, inventory))</span>;</span><br><span class="hljs-function"> } <span class="hljs-title">catch</span> <span class="hljs-params">(Exception e)</span> {</span><br><span class="hljs-function"> <span class="hljs-title">return</span> <span class="hljs-title">ResponseEntity</span>.<span class="hljs-title">badRequest</span><span class="hljs-params">()</span>.<span class="hljs-title">build</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> }</span><br><span class="hljs-function"> }</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h3 id="2-批量数据处理"><a href="#2-批量数据处理" class="headerlink" title="2. 批量数据处理"></a>2. 批量数据处理</h3><p>java</p><figure class="highlight livescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs livescript">public <span class="hljs-keyword">class</span> <span class="hljs-title class_">BatchProcessor</span> {<br> public <span class="hljs-literal">void</span> processBatch(List<DataItem> items) {<br> <span class="hljs-keyword">try</span> (<span class="hljs-keyword">var</span> scope = <span class="hljs-keyword">new</span> StructuredTaskScope.ShutdownOnFailure()) {<br> <br> List<Future<ProcessResult>> futures = items.stream()<br> .<span class="hljs-keyword">map</span><span class="hljs-function"><span class="hljs-params">(item -> scope.fork(() -> processItem(item)))</span></span><br><span class="hljs-function"> .<span class="hljs-title">collect</span><span class="hljs-params">(Collectors.toList())</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">scope</span>.<span class="hljs-title">join</span><span class="hljs-params">()</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> <span class="hljs-title">List</span><<span class="hljs-title">ProcessResult</span>> <span class="hljs-title">results</span> = <span class="hljs-title">futures</span>.<span class="hljs-title">stream</span><span class="hljs-params">()</span></span><br><span class="hljs-function"> .<span class="hljs-title">map</span><span class="hljs-params">(Future::resultNow)</span></span><br><span class="hljs-function"> .<span class="hljs-title">collect</span><span class="hljs-params">(Collectors.toList())</span>;</span><br><span class="hljs-function"> </span><br><span class="hljs-function"> // 处理结果...</span><br><span class="hljs-function"> }</span><br><span class="hljs-function"> }</span><br><span class="hljs-function">}</span><br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>结构化并发通过简单的原则——<strong>将并发任务的生命周期与代码作用域绑定</strong>,解决了传统并发编程中的根本问题:</p><h3 id="带来的变革"><a href="#带来的变革" class="headerlink" title="带来的变革"></a>带来的变革</h3><ul><li>✅ <strong>消除任务泄漏</strong> - 没有意外的后台任务</li><li>✅ <strong>自动化资源管理</strong> - 无需手动清理</li><li>✅ <strong>简化错误处理</strong> - 统一的异常传播机制</li><li>✅ <strong>提升代码可读性</strong> - 清晰的并发结构</li><li>✅ <strong>增强系统可靠性</strong> - 确定性的行为</li></ul><h3 id="核心价值"><a href="#核心价值" class="headerlink" title="核心价值"></a>核心价值</h3><p>结构化并发让并发编程重新变得<strong>简单</strong>和<strong>可靠</strong>,使开发者能够像编写顺序代码一样自然地编写并发代码,同时享受并发带来的性能优势。</p><blockquote><p><strong>关键洞见</strong>:通过约束带来自由。结构化的约束反而让复杂的并发编程变得简单可控。</p></blockquote>]]></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>架构</tag>
<tag>结构化并发</tag>
</tags>
</entry>
<entry>
<title>响应式核心,命令式调用</title>
<link href="/2025/11/04/architect/%E5%93%8D%E5%BA%94%E5%BC%8F%E6%A0%B8%E5%BF%83-%E5%91%BD%E4%BB%A4%E5%BC%8F%E8%B0%83%E7%94%A8/"/>
<url>/2025/11/04/architect/%E5%93%8D%E5%BA%94%E5%BC%8F%E6%A0%B8%E5%BF%83-%E5%91%BD%E4%BB%A4%E5%BC%8F%E8%B0%83%E7%94%A8/</url>
<content type="html"><![CDATA[<div class="hbe hbe-container" id="hexo-blog-encrypt" data-wpm="抱歉, 这个密码看着不太对, 请再试试." data-whm="抱歉, 这个文章不能被校验, 不过您还是能看看解密后的内容."> <script id="hbeData" type="hbeData" data-hmacdigest="a49bd9cb985c3ee46addd4313377e2d95837fe342d1f32e6c1619801e5e366fa">4630436162ade97ba2718b7d0c4b3b6351ade09e539dfebeb5402828e29e64b0741d921722ed4e63bf714fd90f83efe9d73711b4d0c46608fd1bb550079e6ee894a905577ad751e3177faca1b63df443ae41b812b353b318f0530633b3c89db36ae3a30f59953a354aad0e76d1ec84addabb663f7117c2a1fd49444e95d8c8c35c2cdd97f9af0b2d89ee340524428cfe5289340989021b6f80c8493f4cf9e0354c733fee99ab049a965a7d2cc7f84e01237ed6e00ed420cc4e17e2628e3569732445968068143b697c194dcf53c2219b4c4f14c4273bc70c4296ac269b84fbd4c7528879b85b881b8d969791ac38bb11d6838ef3a404dc6b178d8f51a67546d05c8d7c73320e5d9fbc2db0c15e0239cc0fa3a4fbb71336dd0493a1f03f780b9165f5e5f193ff972479b39a57f61faf245fe05d796ba11f1fdea9ca239dfc3a1b4a00c834c5d6596280ee1de0cb27ea5607b561c0d1946437099ba978479ef30e4b9ae9afa5d1a1acfa3ea2c876001e20dd7e8f51461bb2480a8dd0ffa9b0963d39abcd4a4fe71f0e30ab2180df9a1c2516bf7d6ffff1de0c3602c815907d1c9bf61fa83683700173a5cd961ca82f773d8ce93794f1bb7eeba6e8a19a3dd0f0fd8b12b6f858152d64604befebef0e9618e18c4bae1b5d7a157b0d3fb8e439b69726dfaab4d3cc163a966fe0feff6678fdcee8bd5483e040a456a3ed48b86b2b52ec070b51f3b9659d6781dfb37abf4f71d3733cebc4b0a9d9cf02e181b78cf72c2cfa6b0c1fc239ac1a86387afdae774797eb3b21ef7780d0d2b6ed02b8c3241b811ceef9b771332e1c05be6aa2525bbb158afa0ae1315a76bb58e3aa7b772b143aafeda396f0b68c45279762e2ce7783359ab78c4601747b9d210cd8ee193c4c521e2a418ccd835eef1533f4aff92445e4d1f7c5d781924d71c88abadc4532c4523372816e59f296069b25cabc8792ca97898353b17e44781842d620927bbde4b68d1eefdc1b555b98a4da901ffd47ba9e4484e635a0219c3fbdc3aac797b2a6d98a230f2db2597af81e2e5ff5246e969ff01f32424fc5c80c87848fc1e061df17c7a3c9ffd745880fe1d001a55f3e582353ee2e0fe43585a8a228efab1f941a1126b742e8f3c441e51d3d9e3f21767dab72fcca8757472a7ef44c0ef96411c5e4552747ac5c3073253670a44cc6944327c35e504cd1bcd15b591e9c43c3d76a48372655550b7810d6e85cecb07b9da251d057178409c44e5be7307ed181fd46d5e94d252c47f7e492b227e326d7ca9321aa1a7b20402b2171c1d2830b9e437201d77d9ac8f2e610ae67dbc1fb43bdf8c18e2f066f8066dbb76359758b6fcb1681b701783898dac9bbe7d58b23ad7f43d3d78669d50604a0cd849d69a40c9e691b42c89fb819319dad913d6e21230df7853b1ac6a58e43076b6aa7df6899e16ab00534a24d5a5058d2c3af2cc3747fc5155c1e8333e184aad4f1dcc72fbe47164e79850b14e35da46b94c4531744ee0eeb7af2182b750825fd1c5a1283e54f19d8073edc14b8e630ce569d4573fbaa27f926b0a4e303ebd1e0663336f1a7a59dca7aa7bc6da48e874d02f59ab4e9eba3dba5e3d7380f8249ec05014b01ef598b4a36a2f148deab3d2f55f1c262c0cb9a2d5aeacb1776f48dde8e7057d574484d98a6778f842b28f11fc2968b9950903d1880ea1cb8d8562b1775dafd58a6697a78fd5285dab64463478a4a8381ebf21381f4e62c4f3a031bd31259c41a8c1a64a8a0d3a26d559a183520dfa45d2f74be633dd59925f4eb984e6f8d0619e8a91ec054d4e33d75a9eedb2639989a14816bc4914c3f1b670a556496a6861de979af03835cef4827d91ee7f143829fc8f1a3bd984372b31e6f78cf8ea72b7411e8935560e8537391402c9a2c97f860aeaad544cb4e60717ce4bcc94cb11af4a6adfb5426f5008a8a58578bebcf01bafc0287ae3a121d244519e9a2b79d81bc741a1838419e94dd4e73090105d93465983770445c2224427c0d9a33da154154060b839797a16db4a611559de9e8202f985f81e6988e0eae05b2e19be562e4658ef7632196e49d2395bd3eedcbf8d6448eafb9942e58b6469b13d7cd2f8f1e47250b33d63a20964ef485ac608632c713867edcdb7a544605333d75ed3d7fa8d6226daf785821bac616d220f4e4416bed80d9ff2046e8eb44999ca0b690c5f411fbd0768f3ad14b2b8baa40e984418123c42bb5b64f5fece104257aeb4ce98ca8ca471b5b102a6fd08882d05ccc8e6297d404676c534e73079a86730175b52a9b09f5508356ded38e72453e26dfb5f55826da5b769c46881909f62eb5aedf589d27c9d58d8ded527dea0fef1eb9e9c02af0ea2a5a98824b704690123d681d9bc8dacb3a4a8dbd1ed3429acb34ffa51a8f7ad21e5ab5e2432cd63ce532785772012465b2318653366742980fac7ff9d58a6fd62ac9fbf5a842ea1150647e35178f99db9b09871d3da4a36f66610c1ac0b4b0391d1628344939db78138f2c7b950fa33ddb62b10163d03092241c2498c188aa4283cc57a0e4a919d1dd300d69fe65f7b8af140060b8889fd383e01bda04a4113f9cd19b678d5880f51b44e94f0eaeed060ef00d1d50cc0c56dd91b8282bb32aaa8f1b477e5176892c99a50035398e51293878350828dc010e378185a4fb23d05f8148cb705647a6ad91656fe2bff9f8e24ad960a6ccf8285e64db337b0f1ebdbd886d8f95834eedf6b94b5934facbb01daa55c589a78b75d646ae23f291a9f17c8f8c6ea13389e576d897d7bf6d440da4da429426b0b837a179339c274f51fb0791369af10bd77e2030b389c39f510bfa7380ae1875cf93fa676e241de326da13d0a72afec214059609e59d26106782b20a379c5ac28fea47947881ecc33091e002a6af33c56285264e0d578cf88788c2dd24a806042d8c44efb2c45be2dca1705e2617152b56a906c6020a58cd768d9d71e4a8b5ab5426fe9edaac12e9af5b6311652105cd5b8b383b5b5b77a2ef907cebbb14c3785f4c4f6ce39a284decf946c50aa627e852f9685efffb9937b530f1b1881f4326c836a89c70ca4d3630476c52a0017aac529026dedf55dc763269d5f2f12907bb1f1bc55163102d38cee2eb8d44b9e2ba27af59997c5bda912ba5549ba19181cc96fd393bb1e95f387791d80fd3de8f05eb5fa65c69426ec861244f0703e6fc50ef94dfad7b1a9ee5986654305ad37c6f8770c7d89fb27aa571603331c7fa8b2f0eb08c487851539ded2f1e2598904eb7f385bee2f99bea8d2372b6300e0c19a9584b9371f08d7731f8732547334196de03c2d9f3ac4b415ec91111f07a5724c60ff0b1bfc8110322732efd5e91931ae841978cafc36cd1e8fb856aef2446d19ae7e53543ee9cbee3d6b84443bd4e0a3cc036b11dd9ef4dd37b6b459ae99931c6529577bd9fd675946f5242eec69b5d793b41a4d514dd4eb8ecaf10582d97a3546e7e2a31ad5c20578d3e4ec87c505b34ee78cd3a8558451689dba0302f0532384bfd418575e81b18f1ff221929c1b525b35845ff10c72ccfef7f328f78190b10a2d73ed518d528339bf97942556b909ace651bef54eaf48be68f319f67f9c2e3651938edf02e299586e91f49e024dd68feec69b7cb9b4f088631cfdbeb7e025241951ab3692c1cc53296fbb28d76005545ecbc3410c716a141c36cf909d6260273d107abd577bfd2a0c44495197a0f8c4d08248d61a7897375243c656acb97f86b32472f569625e9fc4d517d1ce4b8b3c7bcadc5ba06decd069d71c3d88d30a9d4ebbaa52706657eb217c32dfe435a995ab2ec777c07c774d17fd7f1e8251057e09b730f008374e5d939f14ffe4056c8b9d75881586732c201fe41855548f44a03a104c016b2d07af7376a09d5e1a93e6133ec0e0e324fecd8aaba3637b80f138160a3b355b7bde096e3673e041eed019dbe7dbe5d73481230868797af150b2e47618ac649a2d6a8915fbbdbf85dbc29d8f2a3d086e6685b3e55e3182e080bed0a045bdb8b2430dcc691414748d77c66391497f6626e30506959609f7627c226a166947ee00fcac3d086cccf4363328ce9ad3c61300a1882c0318ebed5716dffb3af47e64472664b747573808fe2e3bec2e0f1f8dfe184c9e72a9c817dba924cf8ddf195eb1e2ab6199b0099436ece9e22c0caee5792898fda2b45e26895d4712e44f75c8d6d4ce5659cbb8736c03bb531fd00daa931c0174dc8823901b537c516e9b1584f6623ae2376c1bd2144bb049819aad87844da91f047967d412d818fc40a98528997d3953123b8019918f6d360e09761eb78485312eca8c08acbf27593a022579b713f01dbb34688fcf692aedb3240655cd119ade647f600f5f8521678ce742eb6e05fc75587d33165574298752c2bce5008f4596ca59bc6ba562b7e265b6e10191a7c9b2e47ba3978c14eaa3da3ccafe1de10676bed5da5a7d5f0767e33a9bb6a1ae7cdc44ad0f83bb135984ff530dade891aa5b5ac093ff92ce5e811b9ab52a215f738b3e5c9f7ec21cfe6c0b4b5d23e0c3bfe29e29d5b514eb35cffe434b5a5ef2f186c2f6651fc1fff0e50b763b6c47a486c7d23e0e0dfcbf6bb517fc444b798222245d5ed1c42bfcb3e6ad8d371f174113d372b20979f78d8959b50a275414640e78498b3f0055ced6b3b9929f5cf259f41b5ea1180e2e51926c6f84bfce21191c650ad6db77eb8abfba6066915c69ad64f7e0cb76ac48e3002dd57241d714f6ab754fd44e5b7e565de08bb2dd02544d02a5c382688dbc7f70791c29a4707507e0c6ff0c09158fffc7695e884c1e9ccdf071e27ca408026f267d92c101225c4c5db045d0147ad465e266a29f8827c6b3bda411126216e1bd3a3e9927f668b5410201c338857e8abd555e88dca4fe0f575d092de9299169a5d9f6067c595a18868317ea670517e766f6dae3a317b09a59ce8ca71c1e488aba3b9a9570be14db24c6b1e0c13b14ca4e3b16b5bcbe0f7593a513a1d0cca8935ed0f23fd143c2d5429350ba84a22826bc45b8db6a2366fc2bf7c6a47b81d145ff2a2b03c6301658fc9d02beb65d6e72c0ce1a891d2b39060a2dc84f27d4e954620ab27d87adb0199af287b21a861c63c2df70b996a5c44232ec73f17df6970ccb436f1d1fd434ee19dd4cdc62c48ec6f1efa8b69f27001a277bf645a719ebc62477897ac005e133523c70090120ce3fc84c9f7dcf3b375ec24343754da37e248ce5655efc43fc55fa5e61f9d29340d674be03217c690aba24365fd157e8de5504536c20e21c9f6a2b9de64e1f8cca3f60ee28b21a986d4d425f0e7b9dbe664dbcbf32658351dc8695f6a07a0a6add34bf3e8dad807f4122fb01f1b1cd53843486b08816aab5b47460d0eb4829c6790c6b4b06e5f1da85352462f305e498ddc87b50a9c7673ad658c9731d5daa4e1a512825f2acb6eddd13016f6ea5510b07a0614f016ac978f5e00290226b495a8fb13c98353bd6b0a8009f8839ea8b269fa234a28e6696683fa3106d07d1b3a4f189355a2b8adfb8ee0d925cda4d1e5e144eb843133678e87510b41af437c3d12e36b599c94f766524e7969e45cb0135cccec690e805d4514c0f87669bc1448adbfcab98d0991745bbcc58a6e38e43235bc83165bf141743ccc855ea16d395e5a0908a8d4f6c6229526454bb5b37fb48a2543ac4ed05c72e744bd58d00e15a682bd26daa31493d193788cc0526d3cd9a844cd670f61a47d8c4c1e19eada8e0a8ec8a4c27fc35f8605b6ca9088ed64e062bb7057997e0306acc9ac4885821d39b35351d7c40295df49e11870f8d0d156743b3b30def8b4a74682b9d5d79210d30ab50d2a9eb5ea7e889fcc4b8a21d87c379492932b03eb186de0f1e58aecc3af993d71587990e5fc03d6c137dd34a8662e6ef878fa327ec79f587953e09e3ba20d0e05aa9cf9bcc6b36f9476eb7bb2ca2a0cbbd3e4877f67079400f3eb6f604e08b5b24772750b9c50f4fd7d904fc8fd054aee510172be216526288076392d6c7cb7166dbebc93cdb5d5f5878dd19c9fe80e13f93360a145672b1c8a54bf82c210678e0587eeaa6600d5add3bebd25702795aa4af0944e8c2c8a9ce5756cc1fe6bec20dd82c2cdd6126375fdc2a42836e3b4aebb5e3a0fb29461805c49c3d758a8f6fd0596d6ff2fc9a67f202527ddbb0e5fdc3c59ac3e9a6d33000045473f2be0c1734e27478355cd7204211d9f76978a3ab6b787982aa2b5b121eded8aaac3b3b66a918d0cf277b80670e0731d8512fa0779c862dbde08c6f64250bde641edc84e9eafd427c9b45a2617254aafd2c61244301c7ad39722c7e9ff4db914406a643f9ad621709168f34935a9a281af21aacdbdf7283fb86514a87ec18eb2e06a4e19e98bc3550799dc545ae87fcddbdecfb0e1c8686d907e3f7d80d54469dfc819b39faf4c2ccd3a4413ef71982f7f7303e97a499dfb5b2ed8d099ece15737fc2485ccf954b1c3952aa2e85caafb048d51de3d3cff3869e56e22ca74df8550dfb5b8cb0da9d2e811069024932884a12c12501337c244d7b490e517a69f01e7d1132353e325661027cae552994bd85e0159d0c0c864ad2bf4dbdc820099b97e8585e27ba498bb17106997c13d450655ee34d48ab30b3944cd0e7f2b783d68f096ccd210e0871728a145fde6e81df0f173b34598d801ffe7349b6cb6b9ef7a19a814252dab9f844ed04bd93821c837129d42cb1006acc2dfe10d5843c7ddc0e58b4fb383767c24c4e425574b2cda791c0ec7fda72e23262aec99153bed5cc7c3bff1ae338e9960cbf9a33e32db1c07597ba46a7a194e9adae8e54db547191ef0219e7e502fdd60522788738f11b11599a4860b213895dd12a9dcc82d2c59823c235530ca8ee23b5d92ff80fdc96dae68a34250f84e3a5e796e769f58b9b4b1b70ac610170b63f65d8ebb2db6ffbc0ca414e4e9d2ad6d44ec9a2c4c0adc209fa0e74eb94fdda0889f697cd08b61c204aa5a6f4693275250f3e95c93237ff45f52fff52756a4a8aea8d3078a82028d065d31d94804508b16ddc89c9c50c282565fec6873551327692d85cf800ab3e8613b935752b0e65f63e77734c0eb05407ff82d8c1d84a09a591a651890d7cd589f9fbc50cb0b8c43800b8b11cc30ccda87b99aef8e445859c922156a391fe23c4e5ebf3fc4f82933424cf008c9ffe42305c1b48ab83885533f69aac48bd3e2abc93a39c8d5fcefdfc97764260cebb502921900f61854f0722ee0c998fcfb7ead4f885cc179e3b7279cd3641d96dad99823e3c06256556e603eab1a946ee9dbf13dc0053c44dc30789941bb3e8d7015130ed7f4b1b8f9627dd5c895d209856b3d350029ffb4e61423ce98da7193d659449d8bc3f8edf01a3fb908bf2798a532d911fd93427da79b7284fb1060b7e1335364a835ca0ad737c25b62fe23a07c6dcbba5b97f94ddbd15f9cfd2b2120f18ea1a1be4aed24bd64a18650b921a90118c11c06e9a2e69bd94f3748cc3157437efe967901e94424a1818a962a14304f0a695a1714665a4ec56e1c5e924f25238c9a5acf31728fe9fd69d731d44c0567196dbe7cce0bdb031e93a2895166e39d6ca8c521b622edea4fbb3099c3b8904aaa611cd20e8a00cf50ee9062611f9cc85db5d405f8751bcee0fba2e20d8fc072d55564f7d9bdb40817ea9f06133370bd645ce53c62895738ffc95eab966baabe705bba333718f3c52f5f9a1ead50456930f782d673dc93dbdfc8a15bbe9166843d974285ab40cec4655a2ec6aa4cd9746ee750432184a95c205d60bd640be9ff75ae07c1c8c4195d56b8e539a54a50cc67f6574724693e1fcb4236b62ee14309b0b5a8fae5c5e6b8ae0262e630b75f731c73b1dc179e92994d93562b92b20473f75b29ad3f7585d082bf569dd04ae77ccc235be9691337c4cdadd126124b7933037a697f31d5a2363e2a65800470d733d7227d09518de6e70842dd0a75300920bda783960269169ef9cc5876a61fb7ecc6051e15da1d18763e945a228cc3057e396ea20004790e2984c6bc6ec84d08ec4a9605d954ddf12fc6c6fc4c52af66c0edf860be6d39b7a2bdf99981a8735f2da48f08932c3b24cb1765676408b736c0fa3aa44c0e804a54b1d38f0cf8dc7c69a7a083e21fdce3d3ceecb517bc32d1a8bba5755d2121567342d0d67cade8313f9bd940f811184420287e3e198fddd6e25c577fac4554136d3a3d6f32f7a8bc6e302b1b64ef377b20f85ed158926f657a1207f23cb16849769a3fd2323c2d884c12b415ee1e8ee01941990ac65f784d46277fca757293faa8612dc78dee41f4add16ae39fbdbe4c436d07720f094d3f4d87ee3955b66f8d726e0a4ec48a572455b2f573a0d24f0ddbfe5a87363fd2f9795fd5e790293225b1160846624eb5ab552d7010f7af9412aa2309a487bdc7db7c16b5d445e3173f9cc96dd385f8f0b313524bf2cae54908e02e1d387d2a3d0b134b902075f6ceb08b2adaa38c511cdf132bd1794f1fb614cd093fd2d491e481c39543cff45ce1d1d034a8fcde72af200d96f953469c83335f9ef653d0c156937e42f9b3eb770e59ee031d87eb7bb74c0ad7ba0f415e64593e123c11c16d9a69b0bfd67b523a5036e8900992a0bf2ba3e1340f10853a772a47d50948520fdb98a9a5223abdf78a1a854263a562b5881030a8f162c76b8da9ce89802d0b7e452a94b4b6fab4e7645644caf842e62e9d699ce02b5b2bb34caf0b684cfb4c7a1079f79e7e72b675de63185834d0330e6f6b545a8009df74b706d832aae6cf1fbc75472d23aa224786de2a45cd291b58a029f49450cf356b85879e0b8ef51a2547f65124d71af1f54b6df8b2016c40ff85f3003e0ef56adddc7b670c8e62477665b3a1b5ea3c59518b14e6be0c1b0c362af18d66e8bea3bc3c5b0dbfaf6af86480ab03b9489000c4fb5b039c7ca79c925422419ad05c2b7c5e5f084f20edb350f00c5c8a8e0deb60d3fa018a91daabf15a0c0f93d85deb1a84e9e66b2020c7fe8a3aaa2d29ad34329890007c9081a064e097974bf864de4bdcb57c8baeff7acb78f86479a64770409ed1b89167de8f4e50f4c48d7397e7e50912ab2a1a469d4d0ef938d21022ed675f87f6934fdf22a8d3345bd52a3a9d7b542209dc98874cc13970356fd4453754d3a9486fe5343065b091f7e4b16fa4e6878e3265f9e6ffb94f8cde36ed6bc820886d0e3aa77a4ad2c81b33010726094ff5e9bb698c0bf0896ac88bda98a75c3d67c2b5558b9a3d6f1f63c66b6d2874f4783bcedf3bce4a56a92119034e32934f081acfe86b1249a44851130e8907d5b89f811d8c8cc7174868c28986792865fa342fee22c64fc30eaa38e51e31e8a42b303088bc3e2a3c9a5e5a2b5895de739b594768181a0e1df4f57a552252470b5a51193a82753fd26ab91f7cc9ea33a0d3160f8f5355c1708be48bd4f4ccf588c32c943d0a47401501fb56945f59b9aa77b2668a1fcf1408736f1149fd34c9332fecb2634225659d62bd97a237e61987bc4d79b0e6cd1db8ca7a24f9fa05a039b0fb460874c91dcdf87b7aa3de48cfbf45e7e1b1b402ed026960fe6162d4ab4b3c8165344e415eac492e33f1b180c962dfcf2421759d6032e4c63ac34ce4240d34319f1772e24909a18e70384df3241c7a0a6ba794682edf5cb72502e81a1fac6290709bcb90d08bb50923aac459c63a2310babe8e63ad9d9d963aead7a80bd11aa9643da1d74bac2ada25f95c2b3519fcd17ee8480c39011ee0fa9530487eec3c5d7e0a0bac30854714cc78995ac2b9eb4ffa3a31b8323b9b2ba9a2ea687ef192f2d8cd2f93fe748f17b903288496840121981820cf5ed22b59457cd51aa9de4c421221c70d3e8433fd0c454ffc74d413fe2d79aed50a6b716a732cd3819ab2c6a6099185c9400051806027e9c805de5e169c095388b235b146f1d288212b83f9cb48dde5678a46ad974b733947d4b4da881f993a351b981623ab3795d7dbd97f0f150417121f72597b151dc315f4d56676c29838ee283b56004ef86dc3d286645707b810c04427defcf6548a03032b177ca083874f6ff2464722b2a1fbc224d291502e47e71139e577a94b9ce7ec77e6ef0593b4e763a62ed5704d560810e99ecdb47a1dcea7d580aa57f810f167123264d1e3a40ef2e296dcb5e32a5bf8455f9d37e495bb61acab8fe64a3b0d124784aaaefca9f2ca77e335a64abfb95f3bc95e4e1f970c17b614b014bf9ad766ac5959b3b8e4e19dae5bf0bc07ea41ff0c86c448c6b7c46adc7ff962e8fa9092205d8041543f25e57cb255e0ff75fd3838525fa95e8ef656b9d8ebd4991241eed00e240e7f0bdf49cb3fe27127bc022bbefb58ed128fcd4b8a70d209d535eb49d18fca15d5d9029c399318b4e1c81bf68cefb531f457ca3401697278c589461caec86a8f3cf03f915c3cc51b8af6f7ffcc923bf6d2aee9349d6647ac54eb5c659cfa56fee0e0e7ebbc162d30e495511c7cd8b771cb4c68ee8120f23367580cee46d9ab31f5d52808d8568b44d77cc3d717e653692f67a18240d7752f11edcd2cde4ae8b05e76d5a115c7e2adab48c7d8598c47644b46e78f318931928b4185dca7218be7773b627e90b97e817f5396f3121759a75918fc041e4365c394aa46cd367781a16af8d744fc95e4324f6c5faa32147b2aa3719d270140e8e5a7f3614b5349830570266b3ed1b4ca0591fd74b826af396b5beb2ad5a18d252eb8b4322420f01e661488e220b810ad26f0de0c9887e9621be5578a5bb2dd3ac1671e2b157691cee71f3c1adf5b42a9a6dac3734093f3c440fd72bf90951d389079feddd4f1eea5b16bfad8da0120297ef2a7b85286689dbd6cf40f089ce6e589bc4db839818e4b0719f406ae87874911af28d09ed5572749a60cce99a743729f1a89b7ab2f698de09115654a5ae459fbf95d50ff479fbbb8e6c03afcf98a3d266ab3577f9e345f91b27f40b964476b2b4a596918d71a91deb8ab0f3fe5afca41f54f7fa99900ddb1257bc0512d822a10b839cddb49a478e46d876afc75833a9e70b5b13119fd0e3a39590613120175014b9acbb2e43a159a892f7155aa7394036e64efa13443508ed496c9a6d2311b8d99af39fd0544a7b99466cd8dfdad3573aa6218e74f3b828479e9f36de21f606869720e8be3cb9341d6a9c9f066e68c568e09f4c216cdf758c95edff8692ddb43fb410ffaf767cb76b4323d6588ed2860a77f8b6a2f2e1b94e92cf5e998936c46cd0312a0488fd4dffc22087bf5ba2ffd6afb36365cf9e0bcbf90f46bc7513b562fbd98c3bef032ac4c1e15274a2a01fad5509342ef4a15d60b1745b60c8f0ac0af3f4099e0524b242601924776f76b459f37d750c42f946dcf42b4a2e3cf2ed9c45b1f70dae2181995ae931ae968c30e3e76c49eaa39f65d765129d302f4351bf298ba1d03b79ac66078409604eedfc1b52dcc387dc9dc28e18685106929a03cb1a2822a3106192b160133171e27a435f4249c5e7806a2dc6db3aa08ede61531a6f8996a3879773a8b847674cb105c483ecf7123db387ebcda05b0d4d4f2ee3eb71060198fb7371686ed481dd8d1ebb126289b6c59f4c3268bcf636c89c48b86ea179e87984b40e40777fb2cfedd5abdb2ba6fb756c5c542508c322172c244c2e2e7ce17dc9d0361ef3b5d22c60235d5e71b510213b34f97eeb424d9cd95b20683dbb96402ec55c32acd20aceb39d6b40941a2a36f365d53b37b18d8879b0c863d53fdfd962415e67fbbaa582f68d9b75f63c6a84c5c79bbe8077b83e7860a5305af1390b124dff49f797bb2919027662640b49771d34ee4b14a93b94d91a7d361bf67b0d004d721a3dd1603a3aa5c6e03ceec86687a94f67644fdb58e9bcf549b050e45de596195063060b12357625d4105a2cf284d2fcefab4056698123e22e0d8305b5e650d1809a694bca512039ec441b6539c9e1e2e60f2645ff266dfb3c29cf924fbee154e7cd6cbf29c285d4fc60aa260bbe27bc48a2a07f20638243252b0a32469610704d39079a23689758872a58e9e506ddd88b260ad8ae707ae82348d1f8d2256e8a6db474b51cd8149a6cea2fde9d004906a24eb7ed5271de0c0108ddf579c0a9f0c78debb3b57f2aecf053cc5164bb33492ca7e549e783ecc534925d13dfb1a5e671a2ed721c936dee40b3ebb15beb57e8ec6e1b5acede6283a2a2149f22b27129d197dbaab91e843c6389a21d9f797418d7c1d8b486a6e68aac1daea4ad445c8365cd088228edb2ad994745a8c3aea02230bf9a4a61092709abb4e6e3e523d26253cfe8e67b903f7695634715ce5e8bb6204ed2b8828e7e652ebcc32bc1ae9b229e7ee25e9deb2c7699bc4c181da359dc282af5ae5b295c2a27f077b6879f7e8d8c868b2326ce4097147970918955620fddebe49f7c9e2414b704980798cfd7a3c7d9eb4c8904eaa377e2117c64fd771d41f15bed2e4bb26c6dc0e854c867eb37777b75b074189635a12974d4b331eee112a58c9c11d4f3312393d3dd6e8409022ba0b57a2779c1328c99e7e38a96c668358a3e0b4f26d2bd26db18c1760bb2c5fc8931b74aa9c43ee014be42a6d6add89893a0cc1f15099adbdf60696aac1a41437dbf8b311cb73ac1c3e078668a7dc152fb4e41a37a45e18aa50893be34e241f17d977fe29089ac7b802865d8b3a2af2f24257f98cc28746808063b37d2c3c89be49508f40207793c8cea6697f8fded165f5d5e2e3bdc9646524f85e77479127b041a94325b33bfb696e960c2de1fd4d3c787bfcbf310ccba6dad2ab0692cbb8264c1f1b90fbe6ade15acd76b20c841c337e5074c90313f4e03b49506068e96dcdac5848812120b52bcb8ec2fb3631904b57e6a107b33fdf0a2aeb56bbe2172d337fd3e61c1858ecef8261576cc77e5f2e4b819fe736ee5e6341dbf2549bbeb06cb019239c737ae44bf53bf2ba700ab8ced78382147c4e7116db1b5ea18f7774450f12b16b05492b312653bf3f438605744fc9045da8a979dc16e961214fd95ef471d14109ac611b1adbd47c4183be3a456fc2e74dd47408d451ba1f53960bfa87254856d136d2c96328f130db890c851cc425b4920ff22d739d232e635812d17ec8479db82c15a24938dca99383214a6241d0d2bba7db123fabcb72b51452347894e8aabccf2401ed907f35c27309b866f38c9cf3ab96f0221d8285acc09be9a484fc4e1fc631c2df43eb9a001eb9344c84f5fff94d1b208ca8fe635504c0b6039fb6becdf2fa04703cafff112aa9ed14353a46d730cf239551c38aaff1960e634a889991743eb82ab3f1df5750187a18f6faa7972af1046716cf008761e7e884dc4bd54306775c35ee9d321ba63e01394d0ff5e7b43b93085ac5531e0a43ef6e9f845b1aa8d6a08ebeced392b358b62e1a6c9df6c51d0ada34762ce412df7a02648d281be2282356c12d59fd2825cc6f348a1833d8740db55f76a8c64be22e80e4107b738d0ef05ec0eec5860ef750aca3597dd0438f2e057357c8ff1caf8adada7bd0c33e816fe26cfe2e2769b448c38f27dc90de2e1510925458b599bddb58c01bf77e311d7350feaf7fc9ac5640e5d0a9f7e92d2045b8696336aaa27df0f016c9675d9e2b4fe907acd95b0cd3a806d40a8fc8896f0b9f3cdafea69b2104d25a37c8a8cbb2e9165f62d061ace560f046bd6475fc9d0285e6de96bd3acd0eb8da09e31013c8aef84d6bdd7738271f6104c662aa4df925f61df84097aeb32870c54a3e2929bdf9f2e83e5d0495d1d06261505098c863f0cb56c23e75b41e77ee2f0b114bef1444909ab0c33e09818de371b553298da0bb4ac86794e61884ace4c4ce573f6a69ce63121473b881d2a9851525d482b9d4d6cae05bd1e4d5660dc578beb42641b6e4819473ec92945095247a05f38163e9161ee36145fa782b063388c0264af5e6aba46205012d2f0ce128bd61bee875c9e8795558eecc641bde7e05a2713b1f587b2424c4f2591ddf34aa8fa019d24b9ac9c75446110b67e18a2dd99f1fc05579017c1b9e18d6478bcb5ee20da339a6b2e9a0f933ad19b048efff1fd716d299308c61695f52f96212a39b3aeb07e2c463eabd295c039290af3bdfa34c3c2d5b77626d154f5d75266a1259b43d1e83b82b9291bad9f00d25879eca3123b175055f55d05b5daf40f0d82090df5cea784060d42d91064a4c142163787bd0538da9691087155cf3f5afed8a143d1702cc9e3a35c935490a5d5baa9e2af1b0d77d94a9104e3a27a6f2620997ac2eaaf8617830e7c72bcf50012424ea947af386d5b0096ad7c1d98e53484b1e46f662a720e1fd606252f0107dc281b96ce9aec845477a644f5823e14f803131dd7663b30b317ffa95b1ecc05f114171d05b926dc4ca9803e271111de13b98440f5826d3b499f797dc57857cd32a4b78ab3e160851784284f49413b1102ec070992c9cdd3ec8d96750eb30c52f03267dd5081dfbfc798e6fe6500cfd7e496860e829b7022f7378a5d7051dfe2be1ac85c00388762f5d05bcca3466ed551483d78638d28601be44cc64505b2f642df13548c2cef7ce68f8eb0b2f90dd4e92ee17c62e7e82066f8755668fd03462377fce369dc73bbf735a37faf506d909b9e86f6b3b4dc2e1894994242b2738aee0e63d6bc61eeaadcd7fcd17b94f2eba8c3052955779d23ec1f7cafb49ee7e8e886f1d363a7f6799c0dcfb43296e43c32ac244f43a77fa042b800a11db16ec1423d4ef59f2974e6585b5fccb6774e2f6675275bf20c2e68d868365fd2d0bf05a3bf74775f5b416afc9a3778077cdf5a5b3d6003bd070d2ab24dd8e41ac1b6fd0bcd65cdc47a1d7ae81dc90d5b5eecf7aea527b2c26a5cd0b92821d58ac3aaa60f70043e28c318e5a6bf24d09a3b60771f45cb9e8a771cb52de9100f09fd256f207db95a330500fb5665db47a8cf8b0988920d7e4f220823e95041892853d23c998d72744bdaaaffcf64fc6198d334eb9a27488ae30ff911028c69b8b5a8156e624535da35e806d5b2f70997c9dd9c70d52c17a02de0e03816f7df2cdcdd8dca7332c579d4da1e3b2562d9604efbf92bad8715bba8b3cae8ba0b3d6b5ceb9f02c0ae078292b6af77143a66c3a80a095e2be02598fe9bfc0e84edd9eac136a1037d2d4b73b5c36836179f815f5f0babea9c62579136e7f6d9232f0b6f4640caf51092a0ec84b29e48936806b97b19d31cf9f2ce4d575753ea5c0e5d5a133a81059092a834008363387591583b0a738da8ebfbec8cb86de18d35f8758d7298932bfcc0c8be128f2f973e960aa90a43411ff991e53cd5fd120ed4e295164c7ef78edd8b1e8ef3e0b47378a4ffb43492558dc6fcb3c7ce7d20e9c1fd3208a619b09f843a7b2ef454a0a9a00d98df0ca750340be90df019d264395d6c97ecf5ba711024d92cc658884e9301ce502733c35a2483a617f79bfe19bcbfc7c93571c6ec50cc369b7b00d0ccb0925114e33ee0d1ff60e2894309275f890a127fdc4060265221d4a1db6d1702bc53c95987b106a9deb297daba287952af5a87fdbd2f224fcca5db9af3ed9882aa5e7671e6d52d48ce97b50c95175b6ecb451d4ac41a9492f71f3372140872c19cce78ae512657250088f7dea61bfbba28f0c204070bfa2aa61f5f0ca6c1477dd976fec0493d96a2ec06d5e8e027c3b86721fa6d2c4cd902b4c127b580e568de1e41dc4bf2984faf34a56a726aaa5e8d77ebc6f982df6fb570ae6d2849d380975bd61792f205e99920a5d54d8818ddfdde23fff107a0fbdd11ea62cd21daccca56c696c974d22eb8ab5ed5e485b1dc96595ffd4191c36ebdd64fbc139a3bdf5cfbe60b194c1399549c399804d426ea1c53474a05864fd9eb5983f4d57b8ba26edd4c2d5cec3e5a3b6e2fb21db7e273bd4ce47ea59eb4e75265465fe7590221c8d0b5297a63de547e33a4db745a8b113970add0e2be583187eeb9bb639a93db9d606bbe6e12e3fbbd39372ef3943cebd5273d439ed197ac7f1b4aaa7732d48e9f93c26745661f1097e70a54350ded8d7c5e3cadcf485aab56fb486cd876758b32744f0dab742aa13c271097a0866cb36d8b903321318417c751d95396940a76326420be46c57e2a5163bd8f1263b2623841a8b05e9470c1673a05fe2d54df10b4d5c964f443eb2ed1569b05319fd41ab7c6a11f5e30d4d8f0218e4da7977edc47f033dbfb4116fb44a66dee26acb13fdd6e981daf911f416fd990401012060558c4554644bd6867a96a6a76797c13cebc9c2593bb85c015217491d2af12244d0db53306fc7fa14073331ecaf41ee2d5e7c5830ce30b8620f335511159ab32a3e0edd2474389e4ec45b57bb10646e487adbe2656e5cc3ac07541c81b08a76611fe7190aac55ad663fff000f19bd04fb9533029c1f6250fd5277d80d2889ae981d08cab0e1982b333bdf89a0c0326c7956b0e2bd625a146782b01927dcb8267d46554f3a35abe13c3b87599962f22ef102f39300b040946f7be0a5196f06ec35a406a0abbe922a2525f464b0ecf3d3179466344584f683c8cbef2e6dd4fed4c6d7f37d5add3fade1fa6b9f5676d66637c97476cb7aff9afaad85d3b0228a132d622e74f5fb206f8335204acee1ebcd9796557008066d6c6ad3c0c22fc2838df56ecb23093dd06f3449aee2522ff740f689eb1e5859723c2fd427c4c36f9c0bc91571fe4376eca7786f583e11a85eb0c5ed0ec9c6ff197c02d5f28c730cf8b4f0be4e2a646b693e95140f71e1943e226c3887dc68e6694ce2d391cd50faaac69f816e1cc8794077520eeb565e902d0e331750918409b072282051ec3b9c170741595675ec9371adad1509704e0523289b52b25c0293c34a70de8249c3f6dcc1ea9c26dfbf89bfbf9a6e6d0b49c14538862f5dd0cd4ce476addee763129657a8c7c4a6ef75107b33a131637e12d8bbcf60da8b98867d87ce221ab181fdeddfe78154e9a82722474401686239e07d4515092fb38b9f9946bcab17be9078183f6d6384a868ca41ea40cfcceeceee7316638e13423bb014ea8dfe96f53dee5726de7621c11d966fd93d1dd6fd24697218274dda6d11f00ab0355e199be054e40a0ee9443972931ec4ccbe8f5fa81c273cbe3a3d7f18fa69c14aec24f501f25522330dadf04fe3676987b846ef9c274abe30a9c26e62f4ee156dcd9e5a7f3a5efd38b774c4bcfdbd5adb0c6096172a008f8a2bc7288fe3416af28f6c86c236916eb32a7a161da74b2b9bda28f32c09d900398e73f4cdfcb6943e337c67fcce039ad8ba7ab5fea4bca7821a1b9f6f3daa13d64845a1ce49d0dddbd182015a8527814dea056bf82a5757a13f6471c61ad31ff13bd5b855715a9aaee8b700c3366184d26d7b9d377859d17f6d5d550797678c078b2af18f6fd3a41849897cf36441137d4fd5af585e508fd792c13ec762fb92079472324cc2b020e85ed9d05251adfda3a608383f25c156c815a3f6a4aaa7cc2b78b68fe92e01e4818358b9d7a55365ca4fbb64a5d26f355160b69b6ef3ce30b668b46fcdf5b310002b2d73de2b1d460e810f959928618fe81ee4c94772dd761f5f34b522e21605671c9ed7f2435460ae4c0b1860a02d8777ba6fe147c689ec9eaef402c456b7007cc3dc69d3f3c41d4a1949b16e2babba8dd82c2ccdfffa98f395272591687cb6b9da19efe7d97db85430cd3cf6563e12d4c67fd8bef721bc798a36b9bd69473928aa98c8e4b74d0a27cf12d847cd9abc485f20738fa6f4c84da704df3df4058f81e727ee48ee376ab0c5e6b596664ea9fbd541b05bdb97dca673e5b73c3fef3c7511b76c3fa679469edf9deb4b530c18bab1f053761d3383de33c784dcc46208a46320b284cc38cba9505ce396f1e104cafc56d82e3644862c9f5564c6113aaee4c8ac1e8ff47221375eee3e2067bcf944020963acf6d6646815b97bee448fbe70d4a87d6c343ae2704b3907c6262ec4fe41bc3890595116f8be7f96ef873e3cd57632a91266022697b2bd7f0eee4aa07bc6e2496ab7da656a7db905979c196ed49a1998008433a5c95f2ee7a0cb7f030c488b7defb39df5e84f97a0cbd2d11eb21cdffd2eb2a0c436ebd6e83cdb56388527c7466534979e66b4f71ea59a850afe350fde883ca02512700b104a826ca05335bd42ea4a02c44ea8d15483e73dcbc3530b68586ed41487f5d148aecf2584f31cb08b675799e058ca306103f66d3e5187c35973431b4fd4a1c03ccf001bd79150cb364e0dd72cec4ebaf898152209a506843da66f6da2f13c213313703d8bc085208992be8d50115a6ed493c1ff3aa57eadc3ba2300ea8b1e4ef0510a052bd53dfd3929bd42e606c617c5b877b4584e7259f7e40a2bf47a9f283c4e58c6a1e3794908e04b64fbab7a20d9f487012a1f0583dfe2a6354a2a3a12149fe9f35c1bc10f00c38165125cdaa4d018b36acbc57b7ca557e72f65dbb5e63c099a627c6ffaceb9a9573dbc05262a7be21937e0d0c3ab418a3e2713977fcda8efdb46418f59d1df4efcaf792ee1e1a93f1ce601b1525eedc0987d2286b98565bb8b0a7e7f04052a5df27ae4476ca64869a547974ab1578babe299481ac3796c029e2cbed0660b2be1dc25a79b4297c2689650a58ae9b4f4f780b4f3aefe1e1eac09aaddbe40c2f6e5ec91313a16ae05228b2cf9af3c194a8a367a6864965c2a66ff8104bfd1d323da854c8cac551678a27a217ad4cb149b77fbb1e0047e32131105ee7d94ae9581d64db843d1b1742135ad0d1a410c39828433359a3993ab08b76377feb7125a7687e5171edaf551225e218a02190f9a3fb37a1b73891b35d97ea89e09a0d683ca67418dd08465b88af89bf92a59fdcc48e54f30dfda02d01b34f4d4d24c4ffa87dff595060f796176d16791800c122db83ac26b9d29e34612fdda60893462177745a991ab75cd5aaa197718a57718c9e09c13667988172e33d618b6017e71778773fa3a7d9aeb30117022396d1e92e8884654cd7f99f1084e89622a762330ed5cf2eaf3b77cd6fec2bd978b96cb8325c4b8e3ee7482f05f754c0bd337b84f12c5e8167e115013f5391e3a5bb4c38a8881c3b72faf9bef86612a262718bce7f0254f0bfe64f0ff049c5006ed6c0b65ff135cbf490b8c87b11c0b3c5c424df6b43dfbec0dc1b5710b386d0cc0f6c534ceaa68257f9c7e167fc62e476edba4a4458f623407724446535ede641aad4bd6f3e70d31388e2ceccac7b46a3f7788883394d441bbbcb6f664481dbc590f59face2e312b75fbc418ec4ae32a8974fc1f7f6c53829dd738555eca5be9f73492488a9a50dcd0000bbad6f93a23ecc8952719b3b41f492b8a979a245a85d921c28ee65d1bc9aefce9604b442d429b297c856ee80c901966ca345a3d2c8204e51f3fa88bc674029fa73e5d29a9e26b0447e4bfcd62f5b3802ff567a4396e07804aaca8c88fc88c62f8f09dac2beeed5254c2d554c19e95f313135425abcc125fdb5b1f425996744218cda79b16b202d00b9255600e38fb8d0fdc250664149e4e0a1bd3548c83349d515ffab69dbb1a8472b9ff38c33d29d3d2722ecf656f4b05cce9b9869c42a7e19e6b9b1812eb94840b983742f9719bdeebd2c94865a1415fa55accfd742088857d334ad15a75c39bc733b7b1ed3f133989ec39cf8779f7bf5b127c0182505efd46ac8fb08ff2e67b9e14fb586cb892dd33519424225bf11d94a1c61d56cefe20d2aa180db6b60028374b18f3f545a6cde6b5c017405041512a559a78a672989b16ef51c1c69a9071f4fe46df774eb815b4ffe6c7bef8fd16b4f25d3d17ecf91a10b3c6e93e117e857d76f492ed5e68c5028eea8a6b4645399b36588386662822427c1072c1da26f746ade1bdfcb0a54d3fd4c1719b7ce177445fa93db209f1980ef8998022a6291b2fd2dedbc8ddaa013d1ee55df77840f6653d7b3940f14e25092507bb63beaf9f99bda67acc0427ff8e689e6f7afdc1f5d7289628f544510af6804042cf839bc2db864a70a75f5a0adf55c3f34fdbd20a22cdcdfce4b7e42e9ce4c989432693c54dc9639788fd778e5393d76297e46d538f297d52a5309046cdde1aee21fafb2a20377f1d46c10c587a3c8ef1c644aa9fa83daae548c58c9e1e3f6dcda06c5240639690f5ee9ddad67168e664a01efd976ebcbbb63f713752386f8025c424a5d61196897c503918816440958c432b29d85a21730cfc08c89eeddaa8e855454e37704a4e7fa32dcc7248da8599a20dfd28ee8fcfddc8174cf3c20806a20cd87305e98fecaca8f553e381cad61b4d77a40dbea8cf8e33f1a53600b43c87e4d6b3278e16e9776733d420ece94df3e132ba64496219cebea7d55fd09ef2efbb6104e2727791b0094fb75e017b36e5c1d68b4f5b32608baf17ac18c6cbaea08efa80cd3b4b1159451ca30d410478f5c9c4b39ef69a732a7c2d2602f7b8819c6db73f244d9ae5d4566f218ba4b35d061bbed0fddfe04227b0bfc5193543a299f4e1ff7fb9c01b9db3b18439675c067c58a422d9074c16548ec3de61645d1a4a2bb92fa478e2ec4b5dc3f8d958aa8902106bfa282c2cdef3bda358c0fd97f867f21ad046341dffc6e4c31673f47809932a1fa104e98d3e22da4f212bc56c5ba2095caba7bcb0a9ace638ff0ca7cd5ece828153ab93ae795df6945962016cd00240cff3b9b974d9c3a92f348db9ff6e3f01444c0f79cf08f7d7986e1a55028219b678710ab7a8ea33e308a8bbcbb9e9acae26a53d771b133fd17d7ca35902d70b7253ec304a27ed643135f9a43b4dceb9bc29fc57099454339d97da174faec489d56214426fc487174cbfe6ceeec975984cb8fe87961b3337a4839172d3d798cf897aa7901c8efb9f947be5a6e32028c64bdd9d815457e17ce6a3882d92eddd47313569b741a6899bc1ca5dd10ee7be16e477675b51275925f70b8dcb47c76b7a3a60b6256e0a7d38c2c8b15bb4d9313531e543e59170029f846520472670cc68a124bccfa0346c2820ccac67d44bf73e247f2e3821a3ca7c616b991423abd6a684d3e1ed77cfaf51a3a35de7297267319249438fad206e5146154c3909204205431dd1fcb492bc6023510a5193b36bf994e8d940fe706745d593dea4cbeea4ce43a0728ee86bf03a54fb99bdf52212724103362eb7a8f5ac9426db7a5de7e901ab988981d429a9136662ac01125a7704d4069535c2593070510bcf89f46ab6f37769281b1231747adac3e59076a2742d320cfb964e3f26cdcb5f492a05c62aa46cf3472b83e44b99e4d75c4ed93d47290af26fc936852bbad64b67081658dbb4d914aa2f9c510fb24c776ade457898b348eefbf1da141b24cda010192f31a6e25760abe533eb3bc3374cddb7d24a670595887fe7772ee55800b6433dca943bb23b84db46a8769190b28ec81c2c0c7288d886a8f61d73e8ca092be73fa9d23703adc89fc4773688dd7709e4900d16b5db83070d0c516c6b5989fa12776bbb8bc700c52177c03ad1a40179281e9a1550af9c7630427cccdb9b68938470bdec3fe8ad2001e3679c2be55ee95bde4a81e2c617a71793dc12ecc246521f2703b886f6697bccdcacc09a589eba71bc55a5a4433445a7a8ca89f154f413bd96c866fd32da506b901f811092200380cbf81ea4d3d4a247951c09fba07fc4a59be85141a07df735cfb9963e69b5f00eb0aec90118f118130dcd227bf7e7de3c3d0f805e34d7481636de9976f79cf2b96419158b1657924880be434db4847bbd414483661d84c42a5a80229a821da659b97c3ddf0a97639dd3285f3e12d750d0dad330fb2625928fa5ad0aed5623991f4681d6ae73ccd0b6ba3ff30291b404901e4744ab48e877d9660813140b176ade7218a91522f55b4b60a8c9758960f52bd89f52302b0ec291840cd53664c761c4a161e8324382b9d9c646e9b2852851532d777ef59cf39a22081a849dafb4f4a622b3bf9df6fb10fd2cab049f4f1dd91a2a8685418de40bc42a7cbb58a7eb695273218851824f9d353786c6dc227bf69894d54a032e4d6d1c2c25d114e65601a4a527a60eb17a48f31718be016b3e65e5f2da1acc1ee53b89204ba6344b2f44fffb85819795fc7390a703d546067429ccd064a37bd8491a198acb4ad896bfafe78e3ea94a4295f07a474f45114c48aaf186c8a46573b0dd66acb451bb82a8d3f20f739167bec948c8e243d042b9ae89de9a2d7cc4112c3cd9febe6939a234a58ab78f5bbf05bb0e01af6ac8eb2b179772417a3ded647e1fac54ac51041f187b094513240215f9525399c7275a2904d49bde470929c0f4971282eee0de5f28cafc76e17e8fa38e35ce897fde2fc5857832071799413b8b25bb35a86e7968863399fded0f706419d1e3a85a712ccd105649b44d4d5946207cd41559ee7f880c35083af95b1087bb762c0e30b81179860205d87fb0807f6aa155a386116e13ee194ff5bfde923f0409abc019f773e0cab48f76e9eb29af72853571f04b1b99541360b3b7f2e5cd6ca76f46eab39fb7d3fffe698ada9fd0be5d4d945d228cf2f1f91bd5fc976c6f155c3092507cdf37e2d7d93893224ceede2b04a533d975093adec395fc1d78982c252a98231e21e397ecd65d6b06ce515cee45445b174bf940ef93e60005e4bdad0f9c26874fdc986f94ce5df97283dcc6d7e39dc13af6adc4c35c72356b591345bb68ff2f6799c0f6e80ca910d328b7db6debbeae79fa8a026db375e9606795db66d9dbd630a35ee15e08193ed58674fe870a5dbb08f84ca1ce820f324ebc1edfefdc021b5cc43f90bad69e82ac9fc029162e6a7bbfb3c8f18103627b8b47cc7931d88b1323f5dd2f8a2c19d4cce9e507bbd9e54e9a03da5f7bb11c9b2b54fa2faa63838ff43ecad8183de8e26fb8a140cc0c8c1c7bb47e9e84ea92b6b37c440b9f47bdddb9ddbf6add2d718868cdfb96a29bc5906b6a4bab96a7d0378fa0144ddfef161ffe4e1985d8681d7a4c0ea4df70dcbf17aa0e2dccd89619be313ff92a00cd926a4113f21f98e4643d49ce505025c8ddff5481251ce905766e9c97d9b8ecb461aac7bad8ffc6506d05c782b5ef2243401793f3b3ca67e2b724f7ae04368bf43eedd6771c784378b6aabd5a5b7a3ec70a5b9df58c1f3890cee723064bd14d8f8bb567e4bc2169c29902b919ba0547b53b3cd3b06d072cee0c78571c33832c639673e58e4b4600fc8c0135302873e2d183b8a02510c13e1482f3b0d5946f3d96537edc6ed7c593475a0f05998fc1b1028de464e4bb9324390bdeff23f33b46236abb1b6bdd12a8518e35ed3fce297272e707c0402f981745865aa7b79c3973e00416922232425d3caa1f45295a655ee7224dfa77291209c54c315daaaa3fb25f12e482a991e212a8eaa8bd7896c751ae1090eab8c81388c0b89f6d56d39c5b8f82b250003ac070850c87e882b2e35cb650d5b89d0459c783e93eb45329c84e694413a29837c9b308ac0378c9232a861a91bb6b8e994b02b6d730f5b7741abb20c4f71d087f9d6d033e205cdf38990a372b6209da7368bf766968e8183b6157fe0e3193f46c7f3634d40d107787a080b7dbe985d0af67fd34bd3f018dea5302de38a01d2d4e86bd3de98d94d35d9a78534062bc2727b22280c3e3640470cc52b1ee9d46865fed4d097f98de5597f7b51deddb042045c26a74576c8bd0bcd9488336980ad5343e94a81743397eea738c122229ffb4a2a535ed89cc6cec3cbf4712716ed0379f611c2b89617076507e9f793329a88fce179895161809bfbaf3781a05f1d5fb515c0192004d3f573d6034e88aec0009e017db81696d9f7943a38859b688f5d6b6856f9fee0a6e0a186129fe39a10cebd508bc8223d816402206911fb992526ee8af061cd494cb1c52c8538bcbe81c01e2bd5f1665eabef4d57846e647132fc57cac9c556d7e989ae7a898d4ed261291dc09aa25da55d7aedeae6a28ddd7b29c523fc04c472f7e0ef3eba17fbf1f02e98e461a5c91c5813334ba49fd72e67589d883bbfcfa878aff56e566816e443e374c75ec8eb3da9df628b5ecad62dc72492bbfa2069c88ccb01b6baa49fd2e88205d45c7b3be0a48c35544ef78573fba24cfc7992b5113acd12c203db2000e24f9a9958effa20bd678cda884747e3ebcb871e605fb2f5e2037ecb578737853f42159694140735970c4308b43e35a24aa04fd71bd7b17378ea306211afa7630053fbb951616b733b9b8f1e53373d2c9a9984c1141f3d63811bf91bda508f69fae7701c5131cb4faec74ed7f5dac40351e58fa44b9d8f82b5a94f1044f64460bb923cb2b017f105ab06c874aa551a8e8eb7d4696bd4f8b46def27cc0e1e1fe2a926cc527705ad3b3b51a4ae4375e70a47f73f9a9458b4f8516e2a36a9c340572bb2787e8bf76f45caa955f8a3a8422b14ab7523152e8500518d55c384c2d37ff9ca16be00bba2d37ecc6cd6eacb38dab9458e086559efeb2f675b640596287ff1f3f02d5a559818ea1e2c1272629ee917caf7acadadc3bc318bc026e5fc6a19dcc77c299bccdbd14522fe93c006c41e8a38110696cc00f096ef3827d4b24551d55955067ff523a80a145145866420a091cf2e7bdf119786abf54c9bbe60ac974c1aaae6ae6ff308afbde1508066b7c690a772aceb2a6c4a2884e62a616d1f3a350a5fde54a37dc4135ce83719d513ea522e5ac4334b178c2f6cd5d125163ea7d155e255d44edd6ef0dd77496c258d98adf78eb100a95f16f3a797ad9a671338bc083303d9dd209271d414033857a2923e8e3bdc6d28abe45f82028c60b6381cec9fef394108ee6db9b664851f78369ae5bbe72492c1184ff5e27ec0e62190cd0b871205e24b0c18d5da0e12d33415a3bf27944d86630bf93522d19c02447d157a0e5dabbb464a5511c6a81a122bc4d3c7c5152b559dea9c3f6ee46ddcb60b6776512f459fe93914a1fddefb39711792b44aaaec4e5c919924386f54323d778631fc10cbf69f7ca6aa4dc36577c5775e4a77bdf1f0ae5a0726639c9289eb320e65f4ae3669c366cfd2e48a6144c63b8885741b03f21c0110c4ff21d2d82a9f6120389af93f73cc7d4a61869034b4d1ba786167d04d46ced11488925e1524e72e0fd9b4d99b3fb54af73d5611dd7fe512bf34ea79e08b03e61dc516e87329703fe263e1d608007d56b8e0d386b42c4fa719783a97b9ff4f9cb78df5fce6e460a26451bae04e847ba4e31c88d8b7d9edcc73b89913cd6765f68dc3d84d6622afc2f8ea8e6eda1959c2b95dfc134b671ce886f68c7dd12dfb441a5c034d1511937984e3b31603c0b19f61862817b5d671376a773784e3ec556512ff2e0dc4a177cbc9a75f51e09723a12a52b86211acced34bf60fe00bf24081cff65253253e3d130208906f5a0291ace3d6b0b54856991a0437a29b5011d05ae8c77f3a21432406c1bf934ffee36a767c6a7c1f564f190f4fd158356eb1ac8caf39bb6291aaecbdf47cb6bb54278ce0f9e1ed250ae346a0e14b5909c2ed748582211da079600478c42f1f9868f9e9129b6170d46e8a411cb82bf09c425d73ec8c0810f0927bac7e2e3526db46bbfd8a01c4ca13d75fea8b04c9cc58b9cb931eae66030ebd420095522735334a09e84ff79daf524fe9d1a40a64364f4aaaf45ff5c7dab1db28682ff89c024a4b3e906b0679b3d794656dda4a4e5dee5afbaa3384628bb4d159ffac0a68d05227cefcd5159a7ac265ddb84b89f31d6e823dd5d7f764db0d778d659248ac2ef1821d57fae070903fa53a492d8ea1deeb1a433814cef9e46945d2b4c9014576e35d90f45dcb5973015e6ddab8e89e89a4e19d89a3c1e975dd09d20ab0d0a056c7d3360de6b0b93c8d10b5dac3aa00ff4ab64f6a05ae566c88690a711f959ef3106e80005390edd7bf6c920dfae554edd7b91ff35c1038ac8a32b10169601c43e15af3d358cda12b654f6cfc9553bf684d3db3b85f787cf5a2d76aa4567b0208628bedec07b5bd77e4890d3e85e5013155a7945</script> <div class="hbe hbe-content"> <div class="hbe hbe-input hbe-input-default"> <input class="hbe hbe-input-field hbe-input-field-default" type="password" id="hbePass"> <label class="hbe hbe-input-label hbe-input-label-default" for="hbePass"> <span class="hbe hbe-input-label-content hbe-input-label-content-default">您好, 这里需要密码.</span> </label> </div> </div></div><script data-pjax src="/lib/hbe.js"></script><link href="/css/hbe.style.css" rel="stylesheet" type="text/css">]]></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title>批量操作性能优化 rewriteBatchedStatements 特性</title>
<link href="/2025/10/25/database/%E6%89%B9%E9%87%8F%E6%93%8D%E4%BD%9C%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%9ArewriteBatchedStatements%20%E7%89%B9%E6%80%A7/"/>
<url>/2025/10/25/database/%E6%89%B9%E9%87%8F%E6%93%8D%E4%BD%9C%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%9ArewriteBatchedStatements%20%E7%89%B9%E6%80%A7/</url>
<content type="html"><![CDATA[<h1 id="JDBC-批量操作性能优化-rewriteBatchedStatements-特性总结"><a href="#JDBC-批量操作性能优化-rewriteBatchedStatements-特性总结" class="headerlink" title="JDBC 批量操作性能优化 rewriteBatchedStatements 特性总结"></a>JDBC 批量操作性能优化 rewriteBatchedStatements 特性总结</h1><p><code>rewriteBatchedStatements</code> 是 MySQL JDBC 驱动 (Connector/J) 提供的一个<strong>非标准</strong>但极其重要的连接参数。它通过在应用层重写 SQL 语句,来显著减少网络往返和数据库端的 SQL 解析次数,从而大幅提高批量 DML 操作的性能。</p><h3 id="一、核心作用原理:减少网络往返和-SQL-执行次数"><a href="#一、核心作用原理:减少网络往返和-SQL-执行次数" class="headerlink" title="一、核心作用原理:减少网络往返和 SQL 执行次数"></a>一、核心作用原理:减少网络往返和 SQL 执行次数</h3><p>在默认 JDBC 批处理中,即使使用了 <code>addBatch()</code> 和 <code>executeBatch()</code>,数据库服务器仍可能将 $ N $ 条语句视为 $ N $ 个独立的执行请求。启用 <code>rewriteBatchedStatements=true</code> 后,驱动程序会进行以下优化:</p><h4 id="1-对-INSERT-语句的优化(效果最佳)"><a href="#1-对-INSERT-语句的优化(效果最佳)" class="headerlink" title="1. 对 INSERT 语句的优化(效果最佳)"></a>1. 对 INSERT 语句的优化(效果最佳)</h4><p>这是该特性的<strong>最大优势</strong>。驱动程序会将多条独立的 INSERT 语句重写成一条包含多组 <code>VALUES</code> 的<strong>单条 <strong>SQL</strong> 语句</strong>。</p><table><thead><tr><th align="left">客户端 Batch 代码(逻辑上)</th><th align="left">开启重写后数据库实际接收的 SQL</th><th align="left">性能提升来源</th></tr></thead><tbody><tr><td align="left"><code>INSERT INTO T VALUES(?);</code> ($ \times N $)</td><td align="left"><code>INSERT INTO T VALUES(?), (?), (?), ...;</code> (一条 SQL 语句)</td><td align="left"><strong>极高。</strong> 数据库只需<strong>解析和执行一次</strong>,效率是默认批处理的数倍。</td></tr></tbody></table><h4 id="2-对-UPDATE-和-DELETE-语句的优化"><a href="#2-对-UPDATE-和-DELETE-语句的优化" class="headerlink" title="2. 对 UPDATE 和 DELETE 语句的优化"></a>2. 对 UPDATE 和 DELETE 语句的优化</h4><p>驱动程序通常<strong>不会</strong>对 UPDATE 和 DELETE 语句进行多值语句重写。但它会通过以下方式优化:</p><ul><li><strong>多查询合并(<strong>Multi-Query</strong>):</strong> 驱动程序将 $ N $ 条 UPDATE 或 DELETE 语句用分号(<code>;</code>)连接起来,形成一个<strong>多查询字符串</strong>,在单次网络往返中发送给数据库。</li><li><strong>性能提升:</strong> 提升主要来自将 $ N $ 次网络往返(Round Trips)减少到 $ 1 $ 次(或少数几次)。数据库端仍然会依次执行 $ N $ 条语句。</li></ul><h3 id="二、使用注意事项和潜在风险"><a href="#二、使用注意事项和潜在风险" class="headerlink" title="二、使用注意事项和潜在风险"></a>二、使用注意事项和潜在风险</h3><table><thead><tr><th align="left">方面</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left"><strong>推荐性</strong></td><td align="left"><strong>强烈推荐</strong>在所有批量 INSERT、UPDATE 和 DELETE 场景下使用。</td></tr><tr><td align="left"><strong>错误处理</strong></td><td align="left">启用该特性后,批处理中的任何一条 SQL 失败,可能会导致<strong>后续的语句全部停止执行</strong>。 executeBatch() 返回的受影响行数数组可能不精确。</td></tr><tr><td align="left"><code>Statement</code></td><td align="left">虽然对 <code>Statement</code> 对象也有效,但可能会引入 SQL 注入风险。<strong>推荐搭配 <strong>PreparedStatement</strong> 使用。</strong></td></tr><tr><td align="left"><strong>最佳实践</strong></td><td align="left"><strong>对于批量 <strong>DELETE</strong>:</strong> 手动将多个 ID 合并成一条带有 IN 子句的 SQL 语句,性能通常比依赖 <code>rewriteBatchedStatements</code> 更高。</td></tr></tbody></table><h3 id="三、主流数据库对批量操作的支持概览"><a href="#三、主流数据库对批量操作的支持概览" class="headerlink" title="三、主流数据库对批量操作的支持概览"></a>三、主流数据库对批量操作的支持概览</h3><table><thead><tr><th align="left">数据库</th><th align="left">JDBC 批量优化机制</th><th align="left">是否有类似 rewriteBatchedStatements 的驱动参数</th></tr></thead><tbody><tr><td align="left">MySQL</td><td align="left">JDBC 批处理 + SQL 重写</td><td align="left"><strong>有:</strong> <code>rewriteBatchedStatements=true</code></td></tr><tr><td align="left">PostgreSQL</td><td align="left">JDBC 批处理 + Extended Query Protocol</td><td align="left"><strong>有:</strong> <code>reWriteBatchedInserts=true</code></td></tr><tr><td align="left">Oracle</td><td align="left">JDBC 批处理(标准)</td><td align="left"><strong>无。</strong> 依赖 JDBC 标准批处理,驱动内部进行高效的数据传输。</td></tr><tr><td align="left">SQL Server</td><td align="left">JDBC 批处理(标准)</td><td align="left"><strong>无。</strong> 可使用 Bulk Copy 机制进行高效批量导入。</td></tr></tbody></table><p><strong>总结:</strong></p><p><code>rewriteBatchedStatements</code> 是一个强大的优化工具,尤其对于 MySQL 的批量 INSERT 性能提升是革命性的。在配置 JDBC 连接时,应当始终考虑开启此参数(或 PostgreSQL 的类似参数)来充分利用批处理带来的性能优势。</p>]]></content>
<categories>
<category>RDBMS</category>
</categories>
<tags>
<tag>关系型数据库</tag>
<tag>RDBMS</tag>
</tags>
</entry>
<entry>
<title>深度解析 SQL 预处理语句(Prepared Statements):性能与生命周期</title>
<link href="/2025/10/24/database/%E7%AE%80%E5%8D%95%E5%88%86%E6%9E%90%20SQL%20%E9%A2%84%E5%A4%84%E7%90%86%20Prepared%20Statements%20%E5%8E%9F%E7%90%86/"/>
<url>/2025/10/24/database/%E7%AE%80%E5%8D%95%E5%88%86%E6%9E%90%20SQL%20%E9%A2%84%E5%A4%84%E7%90%86%20Prepared%20Statements%20%E5%8E%9F%E7%90%86/</url>
<content type="html"><![CDATA[<h1 id="深度解析-SQL-预处理语句(Prepared-Statements):性能与生命周期"><a href="#深度解析-SQL-预处理语句(Prepared-Statements):性能与生命周期" class="headerlink" title="深度解析 SQL 预处理语句(Prepared Statements):性能与生命周期"></a>深度解析 SQL 预处理语句(Prepared Statements):性能与生命周期</h1><p>在任何使用关系型数据库的应用程序中,正确使用预处理语句(Prepared Statements)是实现高性能数据库操作的关键。本文将为您整理预处理语句的核心机制、批量操作的区别以及其在连接池环境下的生命周期。</p><h2 id="1-预处理语句的核心价值:预编译与安全"><a href="#1-预处理语句的核心价值:预编译与安全" class="headerlink" title="1. 预处理语句的核心价值:预编译与安全"></a>1. 预处理语句的核心价值:预编译与安全</h2><p>预处理语句通过将 SQL 文本与参数值分离,实现了两大核心优势:</p><h3 id="A-性能优化(预编译)"><a href="#A-性能优化(预编译)" class="headerlink" title="A. 性能优化(预编译)"></a>A. 性能优化(预编译)</h3><p>当连接库首次向数据库发送一个预处理语句时,会触发以下流程:</p><ul><li><strong>数据库行为:</strong> 数据库服务器对 SQL 文本进行解析、验证,并生成一个高效的<strong>执行计划(Execution Plan)</strong>。</li><li><strong>缓存与句柄:</strong> 数据库将这个执行计划缓存起来,并为该连接返回一个**句柄(Handle)**或引用。</li><li><strong>后续执行:</strong> 在后续执行中,连接库只需发送这个句柄和具体的<strong>参数值</strong>,数据库即可直接执行,跳过了重复的昂贵解析和计划生成步骤。</li></ul><h3 id="B-参数绑定(绑定变量)与类型支持"><a href="#B-参数绑定(绑定变量)与类型支持" class="headerlink" title="B. 参数绑定(绑定变量)与类型支持"></a>B. 参数绑定(绑定变量)与类型支持</h3><p>预处理语句使用<strong>占位符</strong>(通常是 <code>?</code> 或 <code>$1, $2, ...</code>)来代替 SQL 语句中的实际值,这些占位符被称为<strong>绑定变量</strong>。</p><h4 id="示例:参数绑定"><a href="#示例:参数绑定" class="headerlink" title="示例:参数绑定"></a><strong>示例:参数绑定</strong></h4><p>假设我们要执行以下插入操作:</p><table><thead><tr><th><strong>原始 SQL (不安全/低效)</strong></th><th><strong>预处理语句 SQL (使用占位符)</strong></th></tr></thead><tbody><tr><td><code>INSERT INTO users (name, age) VALUES ('Alice', 30)</code></td><td><code>INSERT INTO users (name, age) VALUES (?, ?)</code></td></tr></tbody></table><p>在代码中,连接库会将占位符与应用程序变量进行绑定:</p><p>Java</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 假设使用 Java/Kotlin 风格的连接库</span><br><span class="hljs-type">PreparedStatement</span> <span class="hljs-variable">ps</span> <span class="hljs-operator">=</span> connection.prepareStatement(<br> <span class="hljs-string">"INSERT INTO users (name, age) VALUES (?, ?)"</span><br>);<br><br><span class="hljs-comment">// 绑定第一个参数 (String -> VARCHAR)</span><br>ps.setString(<span class="hljs-number">1</span>, <span class="hljs-string">"Bob"</span>); <br><span class="hljs-comment">// 绑定第二个参数 (Integer -> INT)</span><br>ps.setInt(<span class="hljs-number">2</span>, <span class="hljs-number">25</span>); <br><span class="hljs-comment">// 执行操作</span><br>ps.execute(); <br></code></pre></td></tr></table></figure><h4 id="参数类型支持:"><a href="#参数类型支持:" class="headerlink" title="参数类型支持:"></a><strong>参数类型支持:</strong></h4><p>主流的 SQL 连接库和数据库驱动支持将几乎所有编程语言类型映射到相应的数据库类型,常见的支持类型包括:</p><table><thead><tr><th><strong>编程语言类型 (示例)</strong></th><th><strong>对应的数据库类型 (示例)</strong></th><th><strong>说明</strong></th></tr></thead><tbody><tr><td><strong>数值</strong> (<code>Int</code>, <code>Long</code>, <code>Double</code>, <code>BigDecimal</code>)</td><td><code>INTEGER</code>, <code>BIGINT</code>, <code>NUMERIC</code>, <code>DECIMAL</code></td><td>用于精确或非精确的数值操作。</td></tr><tr><td><strong>字符串</strong> (<code>String</code>)</td><td><code>VARCHAR</code>, <code>TEXT</code>, <code>CHAR</code></td><td>用于文本数据。</td></tr><tr><td><strong>日期/时间</strong> (<code>LocalDate</code>, <code>Instant</code>)</td><td><code>DATE</code>, <code>TIME</code>, <code>TIMESTAMP</code></td><td>用于存储时间点、日期或时间间隔。</td></tr><tr><td><strong>二进制</strong> (<code>byte[]</code>, <code>ByteBuffer</code>)</td><td><code>BLOB</code>, <code>BYTEA</code></td><td>用于存储文件、图片等二进制大对象。</td></tr><tr><td><strong>布尔值</strong> (<code>Boolean</code>)</td><td><code>BOOLEAN</code> 或映射到 <code>TINYINT</code></td><td>用于逻辑真/假判断。</td></tr><tr><td><strong>数组/集合</strong> (<code>List</code>, <code>Array</code>)</td><td>仅部分数据库(如 PostgreSQL)支持 <code>ARRAY</code> 类型。</td><td>用于传递集合数据。</td></tr><tr><td><strong>空值</strong> (<code>null</code>)</td><td><code>NULL</code></td><td>通过调用 <code>setNull()</code> 或绑定 <code>null</code> 对象实现。</td></tr></tbody></table><h4 id="重要限制:绑定变量的用途"><a href="#重要限制:绑定变量的用途" class="headerlink" title="重要限制:绑定变量的用途"></a><strong>重要限制:绑定变量的用途</strong></h4><p>绑定变量<strong>只能</strong>用于替换 SQL 语句中的<strong>数据值(Value)</strong>。它们<strong>不能</strong>用于替换以下 SQL 元素:</p><ol><li><strong>SQL 关键字或函数:</strong> 例如,不能使用 <code>?</code> 来替代 <code>SELECT * FROM table WHERE column = ?</code> 中的 <code>column</code> 名称,也不能替代 <code>DEFAULT</code> 关键字或 <code>NOW()</code> 等函数。</li></ol><ul><li>错误示例:<code>INSERT INTO users (name, created_at) VALUES (?, ?)</code>,并试图绑定 <code>DEFAULT</code> 或 <code>NOW()</code> 到第二个问号。<strong>这不会成功。</strong></li></ul><ol start="2"><li><strong>表名、列名或 SQL 结构:</strong> 语句的结构必须在预编译时完全固定。</li></ol><p><strong>核心优势:</strong> 连接库负责将您应用中的数据类型正确地转换为数据库所需的格式,并在底层通过<strong>二进制协议</strong>传输,这通常比传输文本字符串更高效。</p><h3 id="C-安全性(防止-SQL-注入)"><a href="#C-安全性(防止-SQL-注入)" class="headerlink" title="C. 安全性(防止 SQL 注入)"></a>C. 安全性(防止 SQL 注入)</h3><p>预处理语句强制将 SQL 结构和数据值分离,参数值在发送到数据库时不会作为可执行的 SQL 代码的一部分。这从根本上杜绝了最常见的 <strong>SQL 注入</strong>攻击。</p><h2 id="2-批量操作的效率:批量执行-vs-单次执行"><a href="#2-批量操作的效率:批量执行-vs-单次执行" class="headerlink" title="2. 批量操作的效率:批量执行 vs 单次执行"></a>2. 批量操作的效率:批量执行 vs 单次执行</h2><p>现代连接库通常提供两种执行相同语句的方法:</p><table><thead><tr><th><strong>方法</strong></th><th><strong>用途</strong></th><th><strong>参数数量</strong></th><th><strong>网络效率</strong></th><th><strong>性能推荐</strong></th></tr></thead><tbody><tr><td><strong>批量执行(Batch Execute)</strong></td><td>批量操作,多组参数</td><td>$N$ 组参数</td><td>高(一次或极少次网络往返)</td><td><strong>多行插入/更新</strong></td></tr><tr><td><strong>单次执行(Single Execute)</strong></td><td>单次操作,一组参数</td><td>1 组参数</td><td>低(一次操作一次往返)</td><td><strong>单行插入/更新</strong></td></tr></tbody></table><p><strong>关键点:</strong></p><ul><li>如果需要执行 $N$ 个操作(例如插入 $N$ 行数据),应将所有参数打包成一个<strong>批量执行</strong>请求。这远优于 $N$ 次循环调用单次执行,因为它大幅减少了网络延迟和数据库处理事务的次数。</li><li>对于只执行一次的操作,应使用清晰明确的单次执行方法。</li></ul><h2 id="3-预处理语句的生命周期和连接池重用"><a href="#3-预处理语句的生命周期和连接池重用" class="headerlink" title="3. 预处理语句的生命周期和连接池重用"></a>3. 预处理语句的生命周期和连接池重用</h2><p>预处理语句的有效性范围是理解其性能优势的关键。</p><h3 id="有效范围:单次数据库连接-会话"><a href="#有效范围:单次数据库连接-会话" class="headerlink" title="有效范围:单次数据库连接/会话"></a>有效范围:<strong>单次数据库连接/会话</strong></h3><ul><li><strong>绑定机制:</strong> 预编译后的执行计划通常是绑定在创建它的那个<strong>数据库连接</strong>所对应的数据库<strong>会话</strong>上的。</li><li><strong>失效条件:</strong> 一旦该连接被<strong>关闭</strong>(或与数据库断开),数据库会清除该会话关联的执行计划。</li></ul><h3 id="连接池(Connection-Pool)环境下的重用(核心优势:软解析)"><a href="#连接池(Connection-Pool)环境下的重用(核心优势:软解析)" class="headerlink" title="连接池(Connection Pool)环境下的重用(核心优势:软解析)"></a>连接池(Connection Pool)环境下的重用(核心优势:软解析)</h3><p>连接池是预处理语句性能最大化的场景:</p><ol><li><strong>连接复用与缓存(应用层):</strong></li></ol><ul><li>许多高性能 <strong>SQL 连接库</strong>(如连接池或驱动层)在应用层维护着一个<strong>预处理语句缓存</strong>。</li><li>当一个连接从池中取出时,如果它需要执行的 SQL 语句已经在该连接上被预编译过,<strong>连接库可以直接从应用层的缓存中获取已有的句柄</strong>,并跳过向数据库发送完整的“预编译请求”。这极大地减少了网络往返次数。</li></ul><ol start="2"><li><strong>软解析(Soft Parse)与会话状态(数据库层):</strong></li></ol><ul><li>当连接返回池中时,它与数据库的物理连接和会话保持开放。数据库服务器为该连接维护的会话状态(包括已预编译的 SQL)得以保留。</li><li>当连接被再次使用时,即使客户端需要再次发送预编译请求(即应用层缓存失效),数据库服务器也能够识别出这是来自<strong>同一个会话</strong>的相同语句。</li><li>数据库会直接重用缓存的执行计划,跳过昂贵的 SQL <strong>解析、验证、优化</strong>(硬解析)步骤,仅执行<strong>软解析</strong>。</li></ul><p><strong>总结:</strong> 在连接池环境中,预处理语句通过<strong>应用层的句柄缓存</strong>(减少网络往返)和数据库层的<strong>执行计划重用</strong>(实现软解析),共同实现了跨请求的高效执行。因此,在任何高性能应用中,<strong>始终</strong>建议使用预处理语句来执行 DML(增删改)和 Select 查询操作,以确保高效的数据库资源利用。</p>]]></content>
<categories>
<category>RDBMS</category>
</categories>
<tags>
<tag>关系型数据库</tag>
<tag>RDBMS</tag>
<tag>SQL</tag>
</tags>
</entry>
<entry>
<title>关系型数据库外键的一些见解</title>
<link href="/2025/10/22/database/%E5%A4%96%E9%94%AE%E8%A7%81%E8%A7%A3/"/>
<url>/2025/10/22/database/%E5%A4%96%E9%94%AE%E8%A7%81%E8%A7%A3/</url>
<content type="html"><![CDATA[<div class="hbe hbe-container" id="hexo-blog-encrypt" data-wpm="抱歉, 这个密码看着不太对, 请再试试." data-whm="抱歉, 这个文章不能被校验, 不过您还是能看看解密后的内容."> <script id="hbeData" type="hbeData" data-hmacdigest="9ee7de1de9eb01d35bccf61cda6c0d7e41fe813eb765bff852d524e5df8e95a3">4630436162ade97ba2718b7d0c4b3b63cb3a3f659c5ca08b4bca0f5845928186c343f7a7bd2665e468d6b0b56d2098398e6989b156c0d5623a7cea67a1ed7e95e9b40bd86b0ea890faa5c508850306a3bbcb9452c01cdc784d7878dd2f8fd846cec3c55d4298aaeba08445d67c408ea79954ed295c594db57114fb00c4e113e6160b2c4402dc94915877ffa8de12437ea5e86a88f7be7e7de6bd1768a7918467bfebf26776e90e83b1f5ddae70d6d2bf0bd4364bc9e30c9beb7f7e75a138bba230f813889c826134c7f2474a3e2901da8ad642b359ce9aca01126259df56c988fe968aef75a7f4ca366fddd45c69109b1a7637aa9a0ee4b5c46e1570992a2bc2c2eae7a7e6044cc2973e5aa18c2577b25d470229be6afd84f0a68df1efb589fbb4669af0ed4dff6b434058493ebae7f0ed180702e90c48509c363120f0c75acd3a48abb0ceaeaa7e097a5eefe4076f45ea63a20684e3521bed64f7a6d94bbe6eccece8da7e9fedd6e844f1912853f92060af81f573f11e00997c0be0a49c1302af7a2807688a751f1be316af433ca6227a04cef1f73e0bd15afa6322263e4de32cfb0279f467a4a3247d1f7dbbc6c3b60ffe46cc2792f68a630dddb3b87db009b68bbbcc2ab4c2b4452d9b0e9e21370edf1bf5313e47855c1302e26613b3caa1c8023eeabe9a7002424635342af24f2f406054392f361f4cca55ceef1eb8071c6251b84c46cf96f13fb44f084c269030275c1617f7c7083d17f8b0bc937bd32c8cc4dd6f176efa7133b54a4212c7c9f479af5894048184e9a343f19427a56e0406da51a462b14553fc6547cc4375ea53be4ed01990b5a34b946ad9c205579e3ed8594efaab2ea4432a011e9a4c252bc854353c6629a88272924451679e012249390fb5627e049dcbaff9c9560ac0897877a2e7d57998a6d2537f2cbe7cabbafa13c881dda7d423fa7e9929612bc23354076896f27be50dbf7ca83a5d1221745b8df26a98adab6873ca06fc2e91dfe899185c9c5ac8d956c7ffddbc21249eaf7531d7e3a7988bc584a047bc92b57ee63f8dc27653ba08a6e189099375c859c1f4ec3db82135b5f59fd53ebace07b178b172f64d81c29793d068deafbc1a5cc885d7b08063b5e3eacf7456a864c445b1f7eadb575ae38a82d34c1bb71b49aaa3edaf17492a3d78f10fd88acb4fdd36d62c92d2cbb0856d160cd34486d941423979b6628886614c33fd7f0fd8a9a97857bdd007e6cbf3928200ef9925abd3203a014064c88072a38d27f255a17d3f9ed9c003a964c30bec77380391bc108232da908cbde19bf55a1fa10ca9cdb837157b59b30746f63dd2987e3b080d9fd177a9f3efca5348ac60daaaeeb9f964236371f0b9e09ef71323d0a4f65f5bd18824193915edc651d7bd41160e54ee3d4434fda552108b2156de681e27bd8fa83e1b0027701b2f76a023fad4e4a7fc9d1608faf26f9c5b952dde802817a6af0b32500cebcd12322a0f097ed5c0e752f4a0ebc4ff21a2ae05b0748da745fe3a1ba624ce2842713ce3b641a7bc1c94cd185c60ddf9c8848f877e923377fd23d5da9ef0014af45a521c61fcc87d35fbb51d83ba67912bca5c67cb92591e5c6e984100e04430596dfbf0860fd2bb559a56592ff34bc0ece29a8b8cd5df220bb05702e525474f832890e4c92b4eec076545e21766ab927b35b01b1306ec242fa6816975e820c6946d7e2b88c27b99df4ce01b3290ea0914d217647e89e9e18f43e5c663aa66fe9b0842c0ef72dd6ff9e482dc2af13d58ef58748c62dc28fbcdd667f26a35097a89bab7fe60106fc87353e63742df7b595d0fcba74cf6a49f999d524c6d1d6e39fec9f45f83aaac3d0a96a102690ddc41a1f7c0914a3d31ccc417bbf48e0aafe8ef129745f74a7b8326a02eeb20342df0fff15cb6ee9a90935c970f74fd24cc212c25c51181fc0a0be11e09f6266fe11428fddb6da11c0e6d071027bc175f04e10cff278dcb60f85a25a3aee15fd94e43345131a960f4cbc1e708df1738c8e774cf4452144b52f48d3ba898783fea6aaf9321d8d9f4ba9d23d05662a3a149efd5d3dc2447671db3a315e26e026658a5cb1dbf3a196b533abb97996dcc3f952592393e94d021ca4b2121d15a1b6bf45b7cbeb194d1c5a97c246ffd86406f7b7e7b13c33261ebf3a961d4c501048ea5181e240fefa42f5d39e0846bef15dda69bd30ab86d4b48fe2c84eebfe3c2786f842bb10e84a0d60ce43e0484d931363516af20bcd93d799b902abbf6990c47419cb6debf226e3953761c8059b575e69461238ae5aed6c19cf0ccc66dbb4c910e117f1bd133f5c547bc6706ce8d7b063fd6e02151b9b282144152b8d486eb6aebbc07211289501aad7a2e8e2ae858883d0e9e5ac5b5c90b2e89600bf9cb19f677b7f08eac524375c4a375f1c704e28effc74c92ccb17236b18e90149c9434758e9c43369ed6db9d0c3f4efb2e053e9ed141575356f871a6e83e57deeb4e91396acdf20e68351714684a55b6e363b75c28161462bf5a07964047c704ba0425a49f2dfcc352ea5b539cb26c09e0d57c44641dee9b207e9c436bcbc949c88ef39206c031eeb634c9c5cbe48ab89ce41984b2b9888157b0185c6988de443565009f16fed6e62d22157216dc20119a562a1eb5543a931a79fc73ee0a64cc6950f08931f7f5db9246ae0daa008a6d5b9bf8b16197625f9c46f6016176f3b6e2b343fa8470f5e124e82107451565566a11138e1baf82422599b94356c01a485c7795e57f98e317e898949f595e9f514102b4e17994482c02e8515b7019b6a5d06a77246f4d38f6ca0ffa7434f012c9f66fc5de27ae57a28a54095e09dcdfe1865e9ce1e42b82c9986af1ef0d2f942c24163e13fb2af90bfb60da4e570adad7823c2ee05965e022d0659e7a3dc928a8472f6dba3dbc8e0fc284e44292a9cb1decf0b7489e07932a526853e5a47029c0c4526383f5e6cb7461c0eaa9e022a487d01101776fdf8fc83605688077f58c2e34abdb7ef1f31fceea4817aa56c8bfcdf91ed6e14dfbeb66ac064150956fdfe75c1b4a9e0be8101a675f14ea8154e3411873fbc94f841b7d0d109f426780c794b0fca21cdd991abec4cbb01bc3d89ecb7ce267a3f195c8f46da12ed1fb10b13a036ee47f0375fcc9daa7695fc3840634eee6abd8f78be5252d0463a105fc441af8f3b3842251fb18165f527a49bdc788cd73f42c732f6304c6400062c24cb08f1044ec8d2785f5260251e17ede7770766538d9fd0c42e8b436a4c91eae9ca608564f8802a8ab937230fc91315e0e8b05934e7c9d8f26795062c6a6d8cec0b6a5f290a399c243ceff4c935011783ea3541e0d0be6638875cde33db6a4615c8a172085329761e992a47f99ff288b121d63cc6e6759e903d1f415ba6fa291f337d60614b8297070626d28bb9083c1346232bab442fff11a112f71e018fa3ce78b0d8555cc6eb256e8fb4c05f1a160df0d5fb6d7876c467abbe9d943c3e591c1fc7254d4a7608a20aa4bf4b61621d8544a089bb14703e2fccaaab60d6c1ec0d85258834ecd25d4a2bc008f8c3d90640246ff194c6308a7b242c16d4e1eb819f47f342de610f97c4b5aae8ea2d321df2e934b283a95c728d963ca32fa753b8ce9911de520cc1a895406ab3d21b7446454a0620f1f32e92d95af8894b3c53ad59fa3fecba026513e49e70ea3fe8dfed4bd675f4be92cf0b71fe180d41ab312efc11158b3b77b5c4906c31e65211fadcd1e62279b0d0139f3f10cbb1f4bdddea0f8ad6e3201c53d2beb09ebc80661d17225adab95695e4bead864b0a6af8fe03d1c46058fe7e1ba06b44d758e6950cd99c3ea71b2f156b592a7ddae44eb86a51d0f6cc32374588fac0644217d82044b0abb3e4dcbb1c2bf5e8c7268640696e39816db679e30de531847ffda731fde8d0f21287a5be8f0bb411ef9c6236113e8a9bf66579916e53c7fd36d841caa6a72b44d96c3267b4fa4d95f207a424e3e760209cdc75a4a72e49d60e82f28d8f6484b9787e5e4df3beddb5dfe8cb9193a552ae210c551c006bb2789da9360e953fc725e77b20fa5c8e9cfd73c045e9a94c40c5869eb754276b488dd2080b51b313d34f0ec6aac146f0f36e93f75045de2ed81b641bbb40b371a1a7bf9bed6834da5e187ff229d33106578d6e12193c846f6c8babaa8c03a9dedf6ed0dae268d1061dad0a6d2a31066c6e1e02b27a9d1394924f1aa45a4a0f22aed6b9b27f96727b036bbf93784416e3e60821f1cf4d15a1d5be4c303cccdf02fe41d849798863f4e2fe4f2dfab8268596e461e51973190c848e057b0266ec30e74282568bfbc2a92cba5b7d8b144ec9be7921cbf09d1f0b65d20c1ed6703e697586ba41248744eacb69a51685200c61ca5bc7cf35f6d4333b4200b5947bb223f201d26cdd66223ee8aea5152800187f99a9d6f705250cd4656e2450fd03bac527a4f35e2a032986c86950df9b30b5a3fb65c88c1c408bc3245492fece0b0725a70294b221cde05e3cb5a8edcf5606805b77b75090c7080b343892ef230e060cde525bbdfd4c9379d307daf62eabcea405f7f644eaf6ea2a15c8919cf43d84f543c59f66809ae27025e3c46b41dcd31d6272a33785908975818e65c849c52ee06b4561951da55cb851a39e487a36b843b6d5bba9b138acbe5305aa343f42b6894478c9f5120d04d1606da98e694848e7f5fc29be7f8c5a9540322ae8b656400132458fbf54c75457f66e5d79785c5d3068970f7ebf0381023ab773a11b8c82a231f26185ba774e82ad25a00f444ebd9c2945e00a6e48e7e5cf087c2aec9d6c8ecf29537186e2277fb6a6faa76770bd48885e51e1ffb8b1836831a8dec9fee20983c611438846aa09e1cbf1c29e61d8d4643cde7be183c7e5940575ce047d3bf9c57c3efccce3336736b01d05016288980a1833f29cd82ec9b41a81d9bf1f77415d369b4606e7c5e7a0d65863c43da47b917116ba77723bea7831441ba8cb0d579dabd06f3f3c0989bfe6e870f7eb087284f300ccd23975ee42d3c75ba19ecd37ca1bc42ba5f097c8b3974b6bab1bdc0930d9b8c743ce1609be350bb9ff4bf466de65e47e4d92692fa9563e4d7f5aa135db373a01bd048f930815d04df8478aced02c7e9ddad064356260be2e9d1f33f5161015f675a776611d55047ea3bdac4f8a00753a94c1904eddaefd45aaad590a684da233f4dd939ac7bf686f778ead96673686e7ad676c9eb45d2749a74a105e286c25d710533c9015c0b16888e4fe546ae73f71858fabd10e33c14dd501fe011f6233a083c175b4c4e5e763a9ff0b2432b5d08c0de2e2481f6db8650f0fe1cf423048e96d5e87000d99b98d82c8d75315d5b00888b349267c31007ebc30994d6bfb2abcf25bd1656647eb894abda6f2d02732d00991c833dbbd2cfd91c48482af408594c98ce377d96209257a0532f0bb8d921be75f377ef25e6707024d79d056c036c8bce2b3e18a2e7090df2d024c93919c2c736051f8ac52aeb192e46a659bee87e6e50613a0f12b7b1c6bdd7125ec1db33924ece34f40ee1b268989ba05447423e7a01dddfa993e3f34fbe17a52c2ef6b25df02afaea030e64cf712e3cb31082b6829b25c2c1525cf5807087e725577af67a3c56653398497c4ed4c5407a3a7011f2a27985a18491cc6d57120dc6af0efedcc9e32318065db32d289a2b31da0408f03f8345b77e04b30f4290ee1aaf9fe73e26b2e66cde7ad25167d0456fa517f72c19b0080c8f3121fdc93e592688c00ab6606e012a6a691f262d8abee6e98f2b583a53ea01b4bfe50a4e55c8718be9e04a55500f31db6f1bc2cc1a077285a7a07e761fbfd1c2374071cd6a473763372bfa4295a9b6436a96da0772d17763968da9ec5f3d23939a4064605d28d15d94498c7bd5931e4d0abd19c66e9c03055205add60e4aa86739133d051bb41ecd3bc22fbcbf11222f54fd1c65cb7ce8446e5a89c34f505f7f87fa2ea5254a9ec2ad1bfd54adc1b3e94a27c53489016d900682d1534c0093f1bca3d74f84346fd4f030c7c93311bdccc1f52c2f75976c4950c32528afeb51df8763362f4e89f93eebcd9d8e13f88ce9974a3ecbc2aa07f3948bb9cbe9368627cab1b6dcbbb41ad571bc044286de312ec449941ff0e50d934b8a64236ec8753fef81c2f4e7e6f5a356710ed54f6548012cf96a8dcd10bf39b41fcdfd702b5d38a8e6da0dc87cbd971731c2f1a5907ba11a8089c147e4019842a219ca3604a8a511de1a95410077062304a338e72519be466e0568f0a126ca185527da80ee410b9cd1bbc1d9b4c95deebfde396f3f937d51c5028ce9c33a535d8e1a2769e6137c369f13018b71cacd414691430b9b6ab2c1bb4172235711da3fe9c7dce3ebee0e82c0cb59f2e14ac8f3fe977dcb247ddf79439ac4110c60bb9ce5697505a82ecfb482f6e788dd6edd6159d39724311377c03f68aeeea00fd5d295c51dd0a73b565aac77aaf20b700976e94a222ae74805a56cd327720495edde0d0d01e7a552eff76339676176a3cf00cabf97376e06157c292059870194c206d7f5250c01980632269af54ee1aee7323703c93f86bf22edc1a2e46342873255021d3930b04ffd13d2aa27038d038d5bf95804e05d628fe79ecdc11d81c64847b8c882d436461a9f8cf4a00c99ba4e7b892a76af1ec38a899ecffe09d36c6e31ff09a3e097220228298fabd13bee2c4ec6e2697103c5862a4503a73862f192dc0326e3fd069e299bac51a87ce459ad05c8cddd6f343d6a5466a3b36aff125b30c0259bd6576846dcce31ee56a559a13e180e4e552fb24ef0793ead4fe1cad0cdadbd862201341feb46003703bed42c0d79f40497e2b9f832d409d4dad17f8a73c4abd7ebd694589a47f3c689d36b68fdb392de67c2f664069cc0b793d004b53d7000a7aaa041d0a2159d26361d3ae405183032053c24aaea34504a2f927a8f415adbd3c16cb137bc9f83e6835cf6baa83c05e0b8af94f65b7250a29cf4d7b27a3eb9da97a091e8fdcc7651c3811135b4c99650f14f2161995c85e5b6b03f518c3eb09c4d9d0b384caa21ce0f6c5122a0a9458d7984628152f732e16e5366c42a4544590e6ae713fdaaaea141040e42147eb3bddadc7d865f80e42b6d428a32ee8bc066dde457df7fd16f70440b62cd671a0f356671993c9de18dc3167408c4727f773871d5ecc75f7badff3484c6b40f96df5cfb9515c3ad61ddbea31e20890a5c8df4b0c600b1a90f67efd94cacbe99615727c6df67af4d36a115fdcf187cfc5c137b8324750205cb0b0b05fc29bd3c041d118a78041769736d9b0e94fbb28b9290ec90c484a109dd76d021d9cb47d6e4051c1e29fb0537a110eaf22aa0956eb76bc202c293652063181869b04c35f4a8369da3854c293c554779318bb7dc24d256d8ebb07c1ad24e08711ee352f2490060240ce510dd2ae33d4feb363c6c626d482185b8ec11a31a2da37411761c6ca66a064816bc7ce409815c3e33bcf02bad6da017b89e6372e2aaf5a3f3e64321861075075392fb6d3cf81e76be9cc1614a7ff3afc14b046df7b09cf3f322be23dfd8937b04619f32bb4632130851651460fdca761c290c675d64c04d86c777878759d7a55922d77e6f973515e9c1e8179d978b10c69c10a5c106c988ff5a3aaf1c15edd2e6524ed2e524906e87afe8441e28843c5bbc1cc8b72fce458d3984b83f76020023b68aa3b7c52a628522fd0c1cacbe34f7ea2862ceddfb4bda7cfe0cd4cafa9c142d40aea573da56363256dde8988d4c75bfee9843fd40932eca80cc3d222b1130a7e4d6e8780464956e521fc4d8b7875ff0da97618ae8771167e30b79cefc6160e16ebe0e6beb7a59f133f38bc3eafe16f47c226b27e4a8b606d7433cfcc2ae9e241d86e72daa67b83cffe7d60535064bc3848dd15939566c41710195b5bf35d4b33c7cec287e9afd1443cc8595577fcc1526c22a6672c86e6aacc3000c62f09f4c79dc63fccb618a62b40cded8748c2dfcf9db3e9a5f40c3a35c1ace067b77f27be409e1d2e4ccc1b4b6956f3e686dabdf8ac9c49f1e4c0ccc9d309356fe8457d83a4b88e5f4e25eca5a336b13200a713372bfb30ea0ad24ae7c7646f8d632d60c2b38a678620b0962f6a3bfba8ccf2c92356957c25bedd5fb53c2bf128100fa4bc3437f79eaf9d5ca3c7787e2897c786bfe47f5b9726c229388af4e2efc717592e72050403dcc065ee9f01c0b1b75af23b6a7a80a0ca3392a2d5f3eeecc03850fc013c3107604119954b405237e307286690d01823c77e9ac451559c0d30224c46cc6eba174c8c033265ac509f6102f73f92679dd196958c47993c45824b0c40f11d8a8d262229d2cb7fb1be1f9bf4ca70cad40118e9ff044a7a5fa4e7159efb1cd0098857f415723701c5d1bcf0df038f33c4dbc9361aa4274828125d73c7565f13a1fefa4445c96c9e51930a9058a339f0b967734e5f12aad501ae877afb7ce8477d9c8e29030ecf054beb934c5b07e5680b465432417c59e1f59b7760e5db196a52199ebd2b683e3014fc92c44c1f80e0214f2fca7600ff044d0a155cbe7ccdde20184f8293ea61bc50302724cf6c08759ad79b0d27172fe3351557120da4bb10810af61d99f7d971995c3a51123d339d3268abad1c578989d999b0409237899ef43feb1da81f75f226c67b5a2e5f56991c7814d8c06f3b9abf07bee8add90245645205e550da76d4ccd4fbc113fbeea2bc4a8b230e7daac36c93ab2ec20cbfb8576133c8b3ac7b2b6a8633945fea031855490635adca63ccd86144d3609e2ce8a7eadd0a63f1b48d0246ddd1133b6de19ade411b27f026030d2e34da4d1686c726d2c11560928b7ef2684e91f6d77fbf4483ce8567775f5388ebef76d116d9b44228a4155b45714d1ccba698534ee72c4ca40957fb50ff6182468d745dfb10969826c66481e09531a1d809c9b00a839783bb3ab835f703edc174647d9d20bf46da1db53c208fa67d869c987bf9b882a1dcec2b51c43c5f5218b78ef5393e247e83c2548fb2f19129796d26822ca9c10b0776ad99e8fca55baacfe66c5c2b5606f5b85f5063d7c97006f4fe2c05f0cf07093dbbaa280bc064f040c048aef30390b223df0911ab6264088c5494f4a03f2909dac76a7353d0ff1967a06c42ea0bd924894c291d3bba4058c6ae3a2117fd88bc5a50e0bd5e95250fb81d33ffb94fe4257f0c1b532cabb35e9b9067550dbc2bb721b8ea5c0fd390bfacd847ac43108017d2419c7c509ae5caf68c397133b232ea7a6f40dff889ff1beaad0f996dfb9f4871115c35616934eccb2c95bfb1fc0c18b320ddd71813a407258a7c6b21b22ce2b9b34bdcfcc1f37cdbcf4b0a37363cc1b42ce59c1a5ed90796c2d5bcf3ebba7b5f2553e554e5d9448adfc1807f60f749e20eab6ef34d24136b62e7a710da395f581a7174a10478c6752dc500923d84084a41d888ce4295109d4c3b65d4d35b2ce5839543563925ea487108b58cbbe0edb774fa912d17f4b207172a293f85c50c3647f34d645d843deeb1d8a131836379a84976421f50acdd14c85b820c7b268a04e8ddfc98c750bc4489bea7b2143faaa9bb6837ffa440efa115286f9ab161f5e37fcb73a0641113319bc10dad63ebcc3d9e1c56387c806d4f48f8b0c89049b46ba79988cedc7e8863f61dfcf922c970151928d09e384ecd3c92ee75192f107843bfee7479c6533c70e85d228f31ccaef90ceb0787cde54f3528625b97110b6e8c1f5e90f7304e740854e5c4cebaeed11797b72af85cc180fd8a4c414353993a258b2f9f71e0af05aae016a7227c7e77900f9a32a3370ad804251f5e0752364d649b8858dca39121cc41adfe3d3f3a0da645f2e5e28449f5e23be3cb12587d6f5a7104826bf0f130e164ced009bc7f148cd244b7d9fd122a4cf128c0f148182ca9c329acf5e798b7a253a79082b19df91c70f1b3c5552694366ee8136e8d307d96ef8924f46febe37ebfde23d1b0e4674e1fe65351efa5c394437f6fc068666dcd5dbda21ee1596b27ee5b6d8945e4d8dddaf667e297898f75fe6b6c4bb4af1b48378cb64225dd63cdfa79bc4b3e0af621f2b8453f2b18956dc624623559d4db92d60e273d8b4e5c152c8bd010815537c88b0711861d1704493d2b15878732cfd48d556486b8d5703d1a44710647605fc869828aa80c840ea</script> <div class="hbe hbe-content"> <div class="hbe hbe-input hbe-input-default"> <input class="hbe hbe-input-field hbe-input-field-default" type="password" id="hbePass"> <label class="hbe hbe-input-label hbe-input-label-default" for="hbePass"> <span class="hbe hbe-input-label-content hbe-input-label-content-default">您好, 这里需要密码.</span> </label> </div> </div></div><script data-pjax src="/lib/hbe.js"></script><link href="/css/hbe.style.css" rel="stylesheet" type="text/css">]]></content>
<categories>
<category>RDBMS</category>
</categories>
<tags>
<tag>关系型数据库</tag>
<tag>RDBMS</tag>
</tags>
</entry>
<entry>
<title>Gradle 项目中 MapStruct 配置指南</title>
<link href="/2025/10/21/mapstruct/Gradle%20%E9%A1%B9%E7%9B%AE%20MapStruct%20%E9%85%8D%E7%BD%AE%E6%8C%87%E5%8D%97/"/>
<url>/2025/10/21/mapstruct/Gradle%20%E9%A1%B9%E7%9B%AE%20MapStruct%20%E9%85%8D%E7%BD%AE%E6%8C%87%E5%8D%97/</url>
<content type="html"><![CDATA[<h1 id="Gradle-项目中-MapStruct-配置指南:Java-与-Kotlin-完整实践"><a href="#Gradle-项目中-MapStruct-配置指南:Java-与-Kotlin-完整实践" class="headerlink" title="Gradle 项目中 MapStruct 配置指南:Java 与 Kotlin 完整实践"></a>Gradle 项目中 MapStruct 配置指南:Java 与 Kotlin 完整实践</h1><h2 id="什么是-MapStruct?"><a href="#什么是-MapStruct?" class="headerlink" title="什么是 MapStruct?"></a>什么是 MapStruct?</h2><p>MapStruct 是一个<strong>代码生成器</strong>,它基于<strong>约定优于配置</strong>的原则,极大地简化了 Java Bean 类型之间的映射实现。通过定义 Mapper 接口,MapStruct 在编译时自动生成映射实现代码,这种方式既保证了类型安全,又避免了手动编写繁琐映射代码的痛苦。</p><h3 id="MapStruct-的核心优势:"><a href="#MapStruct-的核心优势:" class="headerlink" title="MapStruct 的核心优势:"></a>MapStruct 的核心优势:</h3><ul><li><strong>编译时生成代码</strong> - 无运行时性能损耗</li><li><strong>类型安全</strong> - 编译时检查映射错误</li><li><strong>易于调试</strong> - 生成的代码可读性强</li><li><strong>高度可配置</strong> - 支持复杂映射场景</li></ul><h2 id="文章概述"><a href="#文章概述" class="headerlink" title="文章概述"></a>文章概述</h2><p>本文将全面介绍在 Gradle 项目中配置 MapStruct 的完整方案,涵盖:</p><ol><li><strong>Java 项目配置</strong> - 使用 annotationProcessor 的基本和进阶配置</li><li><strong>Kotlin 项目配置</strong> - 使用 kapt 的详细设置和常见问题解决</li><li><strong>实用技巧</strong> - 组件模型配置、映射策略优化等实战经验</li><li><strong>故障排查</strong> - 解决代码未生成、性能优化等常见问题</li></ol><p>无论你是纯 Java 项目、纯 Kotlin 项目还是混合语言项目,本文都将为你提供可靠的配置方案。</p><hr><h2 id="🔧-Java-项目配置"><a href="#🔧-Java-项目配置" class="headerlink" title="🔧 Java 项目配置"></a>🔧 Java 项目配置</h2><p>在 Java 项目中,主要使用 <code>annotationProcessor</code> 来引入 MapStruct 处理器。</p><h3 id="1-基本依赖配置"><a href="#1-基本依赖配置" class="headerlink" title="1. 基本依赖配置"></a>1. 基本依赖配置</h3><p>在 <code>build.gradle</code> 中添加依赖:</p><p>gradle</p><figure class="highlight clean"><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><code class="hljs clean">plugins {<br> id <span class="hljs-string">'java'</span><br>}<br><br>dependencies {<br> <span class="hljs-keyword">implementation</span> <span class="hljs-string">'org.mapstruct:mapstruct:1.5.5.Final'</span><br> annotationProcessor <span class="hljs-string">'org.mapstruct:mapstruct-processor:1.5.5.Final'</span><br> <br> <span class="hljs-comment">// 如果使用 Spring</span><br> <span class="hljs-keyword">implementation</span> <span class="hljs-string">'org.springframework.boot:spring-boot-starter:2.7.0'</span><br>}<br></code></pre></td></tr></table></figure><h3 id="2-组件模型设置"><a href="#2-组件模型设置" class="headerlink" title="2. 组件模型设置"></a>2. 组件模型设置</h3><p>如果你想将映射器实例作为 Spring Bean 注入,可以通过 <code>compilerArgs</code> 设置组件模型:</p><p>gradle</p><figure class="highlight puppet"><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><code class="hljs puppet"><span class="hljs-keyword">compileJava</span> {<br> <span class="hljs-literal">options</span>.compilerArgs += [<br> <span class="hljs-string">"-Amapstruct.defaultComponentModel=spring"</span>,<br> <span class="hljs-string">"-Amapstruct.unmappedTargetPolicy=IGNORE"</span><br> ]<br>}<br></code></pre></td></tr></table></figure><h3 id="3-完整-Java-配置示例"><a href="#3-完整-Java-配置示例" class="headerlink" title="3. 完整 Java 配置示例"></a>3. 完整 Java 配置示例</h3><p>gradle</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs bash">plugins {<br> <span class="hljs-built_in">id</span> <span class="hljs-string">'java'</span><br> <span class="hljs-built_in">id</span> <span class="hljs-string">'org.springframework.boot'</span> version <span class="hljs-string">'2.7.0'</span><br>}<br><br>dependencies {<br> implementation <span class="hljs-string">'org.mapstruct:mapstruct:1.5.5.Final'</span><br> annotationProcessor <span class="hljs-string">'org.mapstruct:mapstruct-processor:1.5.5.Final'</span><br> testAnnotationProcessor <span class="hljs-string">'org.mapstruct:mapstruct-processor:1.5.5.Final'</span><br> <br> implementation <span class="hljs-string">'org.springframework.boot:spring-boot-starter'</span><br>}<br><br>compileJava {<br> options.compilerArgs += [<br> <span class="hljs-string">"-Amapstruct.defaultComponentModel=spring"</span>,<br> <span class="hljs-string">"-Amapstruct.unmappedTargetPolicy=WARN"</span><br> ]<br>}<br></code></pre></td></tr></table></figure><h2 id="⚙️-Kotlin-项目配置"><a href="#⚙️-Kotlin-项目配置" class="headerlink" title="⚙️ Kotlin 项目配置"></a>⚙️ Kotlin 项目配置</h2><p>在 Kotlin 项目中,由于 <code>annotationProcessor</code> 对 Kotlin 代码不生效,需要使用 <strong>kapt</strong> (Kotlin Annotation Processing Tool)。</p><h3 id="1-应用-kapt-插件"><a href="#1-应用-kapt-插件" class="headerlink" title="1. 应用 kapt 插件"></a>1. 应用 kapt 插件</h3><p>确保在 <code>build.gradle.kts</code> 中应用了 kapt 插件:</p><p>kotlin</p><figure class="highlight d"><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><code class="hljs d">plugins {<br> kotlin(<span class="hljs-string">"jvm"</span>) <span class="hljs-keyword">version</span> <span class="hljs-string">"1.9.0"</span><br> kotlin(<span class="hljs-string">"kapt"</span>) <span class="hljs-keyword">version</span> <span class="hljs-string">"1.9.0"</span> <span class="hljs-comment">// 关键:kapt 插件</span><br>}<br></code></pre></td></tr></table></figure><h3 id="2-添加依赖和配置"><a href="#2-添加依赖和配置" class="headerlink" title="2. 添加依赖和配置"></a>2. 添加依赖和配置</h3><p>kotlin</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs stylus">dependencies {<br> <span class="hljs-built_in">implementation</span>(<span class="hljs-string">"org.mapstruct:mapstruct:1.5.5.Final"</span>)<br> <span class="hljs-built_in">kapt</span>(<span class="hljs-string">"org.mapstruct:mapstruct-processor:1.5.5.Final"</span>)<br> <br> <span class="hljs-comment">// 如果使用 Spring</span><br> <span class="hljs-built_in">implementation</span>(<span class="hljs-string">"org.springframework.boot:spring-boot-starter:2.7.0"</span>)<br>}<br><br>kapt {<br> correctErrorTypes = true<br> arguments {<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.defaultComponentModel"</span>, <span class="hljs-string">"spring"</span>)<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.unmappedTargetPolicy"</span>, <span class="hljs-string">"IGNORE"</span>)<br> }<br>}<br></code></pre></td></tr></table></figure><h3 id="3-配置参数详解"><a href="#3-配置参数详解" class="headerlink" title="3. 配置参数详解"></a>3. 配置参数详解</h3><ul><li><strong>correctErrorTypes = true</strong> - 防止 kapt 将生成的类识别为不存在的类型</li><li><strong>mapstruct.defaultComponentModel</strong> - 设置生成的 Mapper 组件模型<ul><li><code>spring</code> - 作为 Spring Bean</li><li><code>cdi</code> - 作为 CDI Bean</li><li><code>jsr330</code> - 使用 <code>@Named</code> 注解</li></ul></li><li><strong>mapstruct.unmappedTargetPolicy</strong> - 未映射字段处理策略<ul><li><code>IGNORE</code> - 忽略</li><li><code>WARN</code> - 警告</li><li><code>ERROR</code> - 编译错误</li></ul></li></ul><h3 id="4-完整-Kotlin-配置示例"><a href="#4-完整-Kotlin-配置示例" class="headerlink" title="4. 完整 Kotlin 配置示例"></a>4. 完整 Kotlin 配置示例</h3><p>kotlin</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs stylus">plugins {<br> <span class="hljs-built_in">kotlin</span>(<span class="hljs-string">"jvm"</span>) version <span class="hljs-string">"1.9.0"</span><br> <span class="hljs-built_in">kotlin</span>(<span class="hljs-string">"kapt"</span>) version <span class="hljs-string">"1.9.0"</span><br> <span class="hljs-built_in">id</span>(<span class="hljs-string">"org.springframework.boot"</span>) version <span class="hljs-string">"2.7.0"</span><br>}<br><br>dependencies {<br> <span class="hljs-built_in">implementation</span>(<span class="hljs-string">"org.mapstruct:mapstruct:1.5.5.Final"</span>)<br> <span class="hljs-built_in">kapt</span>(<span class="hljs-string">"org.mapstruct:mapstruct-processor:1.5.5.Final"</span>)<br> <span class="hljs-built_in">implementation</span>(<span class="hljs-string">"org.springframework.boot:spring-boot-starter"</span>)<br>}<br><br>kapt {<br> correctErrorTypes = true<br> keepJavacAnnotationProcessors = true<br> arguments {<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.defaultComponentModel"</span>, <span class="hljs-string">"spring"</span>)<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.unmappedTargetPolicy"</span>, <span class="hljs-string">"WARN"</span>)<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.verbose"</span>, <span class="hljs-string">"true"</span>)<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="🎯-Mapper-接口示例"><a href="#🎯-Mapper-接口示例" class="headerlink" title="🎯 Mapper 接口示例"></a>🎯 Mapper 接口示例</h2><h3 id="Java-Mapper-示例"><a href="#Java-Mapper-示例" class="headerlink" title="Java Mapper 示例"></a>Java Mapper 示例</h3><p>java</p><figure class="highlight pf"><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><code class="hljs pf">@Mapper(componentModel = <span class="hljs-string">"spring"</span>)<br>public interface UserMapper {<br> <br> @Mapping(source = <span class="hljs-string">"username"</span>, target = <span class="hljs-string">"name"</span>)<br> @Mapping(source = <span class="hljs-string">"createdAt"</span>, target = <span class="hljs-string">"registrationDate"</span>)<br> UserD<span class="hljs-keyword">to</span> <span class="hljs-keyword">to</span>D<span class="hljs-keyword">to</span>(User <span class="hljs-keyword">user</span>);<br> <br> User <span class="hljs-keyword">to</span>Entity(UserD<span class="hljs-keyword">to</span> dto);<br> <br> List<span class="hljs-variable"><UserDto></span> <span class="hljs-keyword">to</span>D<span class="hljs-keyword">to</span>List(List<span class="hljs-variable"><User></span> users);<br>}<br></code></pre></td></tr></table></figure><h3 id="Kotlin-Mapper-示例"><a href="#Kotlin-Mapper-示例" class="headerlink" title="Kotlin Mapper 示例"></a>Kotlin Mapper 示例</h3><p>kotlin</p><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-meta">@Mapper(componentModel = <span class="hljs-string">"spring"</span>)</span><br><span class="hljs-keyword">interface</span> <span class="hljs-title class_">UserMapper</span> {<br> <br> <span class="hljs-meta">@Mapping(source = <span class="hljs-string">"username"</span>, target = <span class="hljs-string">"name"</span>)</span><br> <span class="hljs-meta">@Mapping(source = <span class="hljs-string">"createdAt"</span>, target = <span class="hljs-string">"registrationDate"</span>)</span><br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toDto</span><span class="hljs-params">(user: <span class="hljs-type">User</span>)</span></span>: UserDto<br> <br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toEntity</span><span class="hljs-params">(dto: <span class="hljs-type">UserDto</span>)</span></span>: User<br> <br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toDtoList</span><span class="hljs-params">(users: <span class="hljs-type">List</span><<span class="hljs-type">User</span>>)</span></span>: List<UserDto><br>}<br><br><span class="hljs-comment">// 数据类定义</span><br><span class="hljs-keyword">data</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span>(<br> <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>,<br> <span class="hljs-keyword">val</span> username: String,<br> <span class="hljs-keyword">val</span> email: String,<br> <span class="hljs-keyword">val</span> createdAt: java.time.Instant<br>)<br><br><span class="hljs-keyword">data</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserDto</span>(<br> <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>,<br> <span class="hljs-keyword">val</span> name: String,<br> <span class="hljs-keyword">val</span> email: String,<br> <span class="hljs-keyword">val</span> registrationDate: java.time.Instant<br>)<br></code></pre></td></tr></table></figure><h2 id="🚨-常见问题与解决方案"><a href="#🚨-常见问题与解决方案" class="headerlink" title="🚨 常见问题与解决方案"></a>🚨 常见问题与解决方案</h2><h3 id="1-MapStruct-未生成代码"><a href="#1-MapStruct-未生成代码" class="headerlink" title="1. MapStruct 未生成代码"></a>1. MapStruct 未生成代码</h3><p><strong>解决方案:</strong></p><p>bash</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><code class="hljs bash"><span class="hljs-comment"># 清理并重新构建</span><br>./gradlew clean build<br><br><span class="hljs-comment"># 或者只运行注解处理任务</span><br>./gradlew kaptKotlin <span class="hljs-comment"># Kotlin 项目</span><br>./gradlew compileJava <span class="hljs-comment"># Java 项目</span><br></code></pre></td></tr></table></figure><h3 id="2-IDE-中无法识别生成的代码"><a href="#2-IDE-中无法识别生成的代码" class="headerlink" title="2. IDE 中无法识别生成的代码"></a>2. IDE 中无法识别生成的代码</h3><p><strong>IntelliJ IDEA 设置:</strong></p><ul><li><strong>Settings</strong> → <strong>Build, Execution, Deployment</strong> → <strong>Compiler</strong> → <strong>Annotation Processors</strong></li><li>勾选 <strong>Enable annotation processing</strong></li><li>或者委托给 Gradle:<strong>Settings</strong> → <strong>Build Tools</strong> → <strong>Gradle</strong> → <strong>Runner</strong></li><li>勾选 <strong>Delegate IDE build/run actions to Gradle</strong></li></ul><h3 id="3-与-Lombok-一起使用"><a href="#3-与-Lombok-一起使用" class="headerlink" title="3. 与 Lombok 一起使用"></a>3. 与 Lombok 一起使用</h3><p>对于 Java 项目,需要确保 Lombok 在 MapStruct 之前处理:</p><p>gradle</p><figure class="highlight roboconf"><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><code class="hljs roboconf">dependencies {<br> <span class="hljs-attribute">compileOnly 'org.projectlombok</span>:lombok:1.18.24'<br> annotationProcessor 'org<span class="hljs-variable">.projectlombok</span>:lombok:1.18.24'<br> implementation 'org<span class="hljs-variable">.mapstruct</span>:mapstruct:1.5.5<span class="hljs-variable">.Final</span>'<br> annotationProcessor 'org<span class="hljs-variable">.mapstruct</span>:mapstruct-processor:1.5.5<span class="hljs-variable">.Final</span>'<br>}<br></code></pre></td></tr></table></figure><h3 id="4-性能优化配置"><a href="#4-性能优化配置" class="headerlink" title="4. 性能优化配置"></a>4. 性能优化配置</h3><p>在 <code>gradle.properties</code> 中添加:</p><p>properties</p><figure class="highlight ini"><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><code class="hljs ini"><span class="hljs-comment"># 启用增量注解处理</span><br><span class="hljs-attr">kapt.incremental.apt</span>=<span class="hljs-literal">true</span><br><span class="hljs-comment"># 使用 Worker API 并行执行</span><br><span class="hljs-attr">kapt.use.worker.api</span>=<span class="hljs-literal">true</span><br><span class="hljs-comment"># 启用编译规避</span><br><span class="hljs-attr">kapt.include.compile.classpath</span>=<span class="hljs-literal">false</span><br></code></pre></td></tr></table></figure><h2 id="💡-实用技巧"><a href="#💡-实用技巧" class="headerlink" title="💡 实用技巧"></a>💡 实用技巧</h2><h3 id="1-自定义映射方法"><a href="#1-自定义映射方法" class="headerlink" title="1. 自定义映射方法"></a>1. 自定义映射方法</h3><p>kotlin</p><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-meta">@Mapper(componentModel = <span class="hljs-string">"spring"</span>)</span><br><span class="hljs-keyword">interface</span> <span class="hljs-title class_">UserMapper</span> {<br> <br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toDto</span><span class="hljs-params">(user: <span class="hljs-type">User</span>)</span></span>: UserDto<br> <br> <span class="hljs-comment">// 自定义映射逻辑</span><br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toDetailedDto</span><span class="hljs-params">(user: <span class="hljs-type">User</span>)</span></span>: UserDetailedDto {<br> <span class="hljs-keyword">return</span> UserDetailedDto(<br> id = user.id,<br> displayName = <span class="hljs-string">"<span class="hljs-subst">${user.firstName}</span> <span class="hljs-subst">${user.lastName}</span>"</span>,<br> email = user.email<br> )<br> }<br>}<br></code></pre></td></tr></table></figure><h3 id="2-使用表达式进行复杂映射"><a href="#2-使用表达式进行复杂映射" class="headerlink" title="2. 使用表达式进行复杂映射"></a>2. 使用表达式进行复杂映射</h3><p>java</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Mapper(componentModel = "spring")</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">ProductMapper</span> {<br> <br> <span class="hljs-meta">@Mapping(target = "priceWithTax", </span><br><span class="hljs-meta"> expression = "java(product.getPrice().multiply(BigDecimal.valueOf(1.19)))")</span><br> ProductDto <span class="hljs-title function_">toDto</span><span class="hljs-params">(Product product)</span>;<br>}<br></code></pre></td></tr></table></figure><h3 id="3-注入其他服务到-Mapper"><a href="#3-注入其他服务到-Mapper" class="headerlink" title="3. 注入其他服务到 Mapper"></a>3. 注入其他服务到 Mapper</h3><p>kotlin</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs kotlin"><span class="hljs-meta">@Mapper(componentModel = <span class="hljs-string">"spring"</span>)</span><br><span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserMapper</span> {<br> <br> <span class="hljs-meta">@Autowired</span><br> <span class="hljs-keyword">protected</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> passwordEncoder: PasswordEncoder<br> <br> <span class="hljs-meta">@Mapping(target = <span class="hljs-string">"passwordHash"</span>, ignore = true)</span><br> <span class="hljs-keyword">abstract</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toEntity</span><span class="hljs-params">(dto: <span class="hljs-type">CreateUserDto</span>)</span></span>: UserEntity<br> <br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">toEntityWithPassword</span><span class="hljs-params">(dto: <span class="hljs-type">CreateUserDto</span>)</span></span>: UserEntity {<br> <span class="hljs-keyword">val</span> user = toEntity(dto)<br> user.passwordHash = passwordEncoder.encode(dto.password)<br> <span class="hljs-keyword">return</span> user<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="🔄-混合项目配置"><a href="#🔄-混合项目配置" class="headerlink" title="🔄 混合项目配置"></a>🔄 混合项目配置</h2><p>对于同时包含 Java 和 Kotlin 代码的项目:</p><p>kotlin</p><figure class="highlight stylus"><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><code class="hljs stylus">plugins {<br> <span class="hljs-built_in">kotlin</span>(<span class="hljs-string">"jvm"</span>) version <span class="hljs-string">"1.9.0"</span><br> <span class="hljs-built_in">kotlin</span>(<span class="hljs-string">"kapt"</span>) version <span class="hljs-string">"1.9.0"</span><br> <span class="hljs-built_in">id</span>(<span class="hljs-string">"java"</span>)<br>}<br><br>dependencies {<br> <span class="hljs-built_in">implementation</span>(<span class="hljs-string">"org.mapstruct:mapstruct:1.5.5.Final"</span>)<br> <br> <span class="hljs-comment">// 处理 Java 代码</span><br> <span class="hljs-built_in">annotationProcessor</span>(<span class="hljs-string">"org.mapstruct:mapstruct-processor:1.5.5.Final"</span>)<br> <br> <span class="hljs-comment">// 处理 Kotlin 代码 </span><br> <span class="hljs-built_in">kapt</span>(<span class="hljs-string">"org.mapstruct:mapstruct-processor:1.5.5.Final"</span>)<br>}<br><br>kapt {<br> arguments {<br> <span class="hljs-built_in">arg</span>(<span class="hljs-string">"mapstruct.defaultComponentModel"</span>, <span class="hljs-string">"spring"</span>)<br> }<br>}<br><br>tasks<span class="hljs-selector-class">.compileJava</span> {<br> options<span class="hljs-selector-class">.compilerArgs</span><span class="hljs-selector-class">.addAll</span>(<span class="hljs-built_in">listOf</span>(<br> <span class="hljs-string">"-Amapstruct.defaultComponentModel=spring"</span><br> ))<br>}<br></code></pre></td></tr></table></figure><h2 id="✅-验证配置"><a href="#✅-验证配置" class="headerlink" title="✅ 验证配置"></a>✅ 验证配置</h2><p>创建测试验证映射器是否正常工作:</p><p>kotlin</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs kotlin"><span class="hljs-meta">@SpringBootTest</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">UserMapperTest</span> {<br> <br> <span class="hljs-meta">@Autowired</span><br> <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> userMapper: UserMapper<br> <br> <span class="hljs-meta">@Test</span><br> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">testUserMapping</span><span class="hljs-params">()</span></span> {<br> <span class="hljs-keyword">val</span> user = User(<br> id = <span class="hljs-number">1L</span>,<br> username = <span class="hljs-string">"john_doe"</span>, <br> email = <span class="hljs-string">"john@example.com"</span>,<br> createdAt = Instant.now()<br> )<br> <br> <span class="hljs-keyword">val</span> dto = userMapper.toDto(user)<br> <br> assertThat(dto.name).isEqualTo(user.username)<br> assertThat(dto.registrationDate).isEqualTo(user.createdAt)<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="💎-总结"><a href="#💎-总结" class="headerlink" title="💎 总结"></a>💎 总结</h2><p>通过本文的配置指南,你应该能够:</p><ul><li>✅ 在 Java 项目中正确配置 MapStruct</li><li>✅ 在 Kotlin 项目中解决 kapt 配置问题</li><li>✅ 根据需求配置合适的组件模型和映射策略</li><li>✅ 解决常见的代码生成问题</li><li>✅ 优化构建性能</li></ul><p>记住关键点:<strong>Java 项目用 annotationProcessor,Kotlin 项目用 kapt</strong>,这是大多数配置问题的根源。合理使用组件模型和映射策略,可以大大提升开发效率和代码质量。</p><p>MapStruct 的强大之处在于它的编译时代码生成,既保证了性能又提供了类型安全。希望这份指南能帮助你在项目中顺利使用 MapStruct!</p>]]></content>
<categories>
<category>Gradle</category>
<category>MapStruct</category>
</categories>
<tags>
<tag>Gradle</tag>
<tag>MapStruct</tag>
</tags>
</entry>
<entry>
<title>JOOQ 常用操作</title>
<link href="/2025/10/17/jooq/JOOQ%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/"/>
<url>/2025/10/17/jooq/JOOQ%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/</url>
<content type="html"><![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>JOOQ 常用操作</p><h4 id="SQL"><a href="#SQL" class="headerlink" title="SQL"></a>SQL</h4><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-comment">// 初始化不带连接的上下文操作</span><br><span class="hljs-keyword">val</span> dslContext = DSL.using(<br> SQLDialect.MYSQL,<br> Settings()<br> <span class="hljs-comment">// SQL渲染库名。禁用库名</span><br> .withRenderSchema(<span class="hljs-literal">false</span>)<br> <span class="hljs-comment">// SQL渲染表名。当字段不明确,再添加表名</span><br> .withRenderTable(RenderTable.WHEN_AMBIGUOUS_COLUMNS)<br>)<br> <br><span class="hljs-comment">// 查询sql</span><br><span class="hljs-keyword">val</span> select = dslContext<br> .select()<br> .from(User.USER)<br> .<span class="hljs-keyword">where</span>()<br> <span class="hljs-comment">// 可以添加更多查询条件</span><br> .limit(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>)<br><br><span class="hljs-comment">// 渲染sql</span><br><span class="hljs-keyword">val</span> sql0 = dslContext.render(select)<br><span class="hljs-keyword">val</span> sql1 = dslContext.renderInlined(select)<br><br><br><span class="hljs-comment">// record 映射</span><br><span class="hljs-keyword">val</span> userRecord = dslContext.newRecord(User.USER)<br><span class="hljs-comment">// map to record</span><br>userRecord.from(mapOf(<span class="hljs-string">"id"</span> to <span class="hljs-number">1</span>, <span class="hljs-string">"name"</span> to <span class="hljs-string">"name"</span>))<br><span class="hljs-comment">// record to map</span><br><span class="hljs-keyword">val</span> map = userRecord.intoMap()<br><br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>JOOQ</category>
</categories>
<tags>
<tag>JOOQ</tag>
</tags>
</entry>
<entry>
<title>使用 Gradle Kotlin DSL 配置 jOOQ 代码生成(Quarkus & Kotlin)</title>
<link href="/2025/10/16/jooq/Gradle%20Kotlin%20%E9%85%8D%E7%BD%AE%20JOOQ%20%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90/"/>
<url>/2025/10/16/jooq/Gradle%20Kotlin%20%E9%85%8D%E7%BD%AE%20JOOQ%20%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90/</url>
<content type="html"><![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>本文档记录了在一个基于 <strong>Quarkus</strong> 和 <strong>Kotlin</strong> 的项目中使用 <strong>Gradle Kotlin DSL</strong> 配置 **jOOQ 代码生成(Codegen)**的步骤和关键配置项。jOOQ 代码生成能够将数据库 Schema 转换为类型安全的 Kotlin/Java 代码,极大地提升开发效率和代码质量。</p><h2 id="1-核心依赖和插件配置"><a href="#1-核心依赖和插件配置" class="headerlink" title="1. 核心依赖和插件配置"></a>1. 核心依赖和插件配置</h2><p>在 <code>build.gradle.kts</code> 文件中,需要引入 jOOQ Gradle 插件、Quarkus 依赖以及 jOOQ Codegen 所需的 JDBC 驱动。</p><h3 id="1-1-插件"><a href="#1-1-插件" class="headerlink" title="1.1 插件"></a>1.1 插件</h3><p>Kotlin</p><figure class="highlight fsharp"><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><code class="hljs fsharp"><span class="hljs-keyword">plugins</span> {<br> kotlin(<span class="hljs-string">"jvm"</span>) version <span class="hljs-string">"2.2.20"</span><br> <span class="hljs-built_in">id</span>(<span class="hljs-string">"io.quarkus"</span>)<br><br> <span class="hljs-comment">// jOOQ Codegen Gradle 插件。gradlePluginPortal() 查不到,可能发布到 mavenCentral()</span><br> <span class="hljs-built_in">id</span>(<span class="hljs-string">"org.jooq.jooq-codegen-gradle"</span>) version <span class="hljs-string">"3.19.23"</span><br>}<br></code></pre></td></tr></table></figure><h3 id="1-2-依赖配置"><a href="#1-2-依赖配置" class="headerlink" title="1.2 依赖配置"></a>1.2 依赖配置</h3><p><strong>关键点:</strong> jOOQ 代码生成所需的依赖(<code>jooq-codegen</code> 和 JDBC 驱动)需要添加到 <code>jooqCodegen</code> 配置中,而不是常规的 <code>implementation</code>。</p><p>Kotlin</p><figure class="highlight scss"><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><code class="hljs scss">dependencies {<br> <span class="hljs-comment">// ... Quarkus 运行依赖,生成可不添加 ...</span><br> <span class="hljs-built_in">implementation</span>("io.quarkus:quarkus-jdbc-mysql")<br> <span class="hljs-built_in">implementation</span>("io.quarkiverse.jooq:quarkus-jooq:<span class="hljs-number">2.1</span>.<span class="hljs-number">0</span>") <span class="hljs-comment">// Quarkus jOOQ 集成</span><br><br> <span class="hljs-comment">// ----------------------------------------------------</span><br> <span class="hljs-comment">// jOOQ 代码生成 (Codegen) 依赖</span><br> <span class="hljs-comment">// ----------------------------------------------------</span><br> <span class="hljs-built_in">jooqCodegen</span>("org.jooq:jooq-codegen:<span class="hljs-number">3.19</span>.<span class="hljs-number">23</span>")<br> <span class="hljs-built_in">jooqCodegen</span>("org.jooq:jooq-meta:<span class="hljs-number">3.19</span>.<span class="hljs-number">23</span>")<br> <span class="hljs-comment">// **关键:用于代码生成的 MySQL JDBC 驱动**</span><br> <span class="hljs-built_in">jooqCodegen</span>("com.mysql:mysql-connector-j:<span class="hljs-number">8.3</span>.<span class="hljs-number">0</span>")<br> <span class="hljs-comment">// ... 其他依赖 ...</span><br>}<br></code></pre></td></tr></table></figure><h2 id="2-jOOQ-代码生成配置-jooq-块"><a href="#2-jOOQ-代码生成配置-jooq-块" class="headerlink" title="2. jOOQ 代码生成配置 (jooq 块)"></a>2. jOOQ 代码生成配置 (<code>jooq</code> 块)</h2><p>使用 <code>jooq</code> 块来定义代码生成器的详细配置。默认情况下,插件会创建一个名为 <code>jooqCodegenMain</code> 的任务。</p><p>Kotlin</p><figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><code class="hljs fsharp"><span class="hljs-keyword">jooq</span> {<br> <span class="hljs-keyword">configuration</span> {<br> <span class="hljs-comment">// JDBC 连接配置</span><br> <span class="hljs-keyword">jdbc</span> {<br> driver <span class="hljs-operator">=</span> <span class="hljs-string">"com.mysql.cj.jdbc.Driver"</span><br> <span class="hljs-comment">// 注意:完整的连接 URL,包含必要的时区和编码参数</span><br> url <span class="hljs-operator">=</span> <span class="hljs-string">"jdbc:mysql://localhost:4407/dev-xxx?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"</span><br> user <span class="hljs-operator">=</span> <span class="hljs-string">"root"</span><br> password <span class="hljs-operator">=</span> <span class="hljs-string">"123456"</span><br> }<br><br> <span class="hljs-comment">// 代码生成器配置</span><br> <span class="hljs-keyword">generator</span> {<br> <span class="hljs-comment">// **关键:使用 KotlinGenerator 来生成 Kotlin 代码**</span><br> name <span class="hljs-operator">=</span> <span class="hljs-string">"org.jooq.codegen.KotlinGenerator"</span><br><br> <span class="hljs-keyword">database</span> {<br> name <span class="hljs-operator">=</span> <span class="hljs-string">"org.jooq.meta.mysql.MySQLDatabase"</span><br> <br> <span class="hljs-comment">// 要生成代码的模式 (Schema)。MYSQL 为数据库名称</span><br> inputSchema <span class="hljs-operator">=</span> <span class="hljs-string">"dev-xxx"</span><br> <br> <span class="hljs-comment">// 包含的表、视图等对象的正则表达式 (.* 表示包含所有)</span><br> <span class="hljs-comment">//includes = ".*"</span><br> includes <span class="hljs-operator">=</span> <span class="hljs-string">"user|account"</span><br> <br> <span class="hljs-comment">// 排除的对象,可选</span><br> excludes <span class="hljs-operator">=</span> <span class="hljs-string">"flyway_schema_history"</span><br> <br> <span class="hljs-comment">// 可选:强制类型转换,例如将数据库的 JSONB 列转换为 Java 的 String 或自定义类型</span><br> <span class="hljs-comment">// forcedTypes {</span><br> <span class="hljs-comment">// forcedType {</span><br> <span class="hljs-comment">// name = "json"</span><br> <span class="hljs-comment">// includeTypes = "JSONB?"</span><br> <span class="hljs-comment">// userType = "java.lang.String"</span><br> <span class="hljs-comment">// }</span><br> <span class="hljs-comment">// }</span><br><br> <span class="hljs-comment">// 可选:禁用 jOOQ 对所有无符号整数的特殊处理</span><br> <span class="hljs-comment">// 如果设置为 false, jOOQ 会使用 JDBC 默认类型 (通常是 Long 或 Integer)</span><br> <span class="hljs-comment">// 推荐同时使用 forcedTypes 来确保类型的一致性</span><br> unsignedTypes <span class="hljs-operator">=</span> <span class="hljs-literal">false</span><br> }<br><br> <span class="hljs-comment">// 生成内容配置 (Generate)(选配)</span><br> <span class="hljs-keyword">generate</span> {<br> <span class="hljs-comment">// 启用生成 Kotlin 数据类作为 POJO</span><br> isPojos <span class="hljs-operator">=</span> <span class="hljs-literal">true</span><br> isPojosAsKotlinDataClasses <span class="hljs-operator">=</span> <span class="hljs-literal">true</span> <br> isPojosEqualsAndHashCode <span class="hljs-operator">=</span> <span class="hljs-literal">false</span><br> isPojosToString <span class="hljs-operator">=</span> <span class="hljs-literal">false</span><br><br> <span class="hljs-comment">// 生成 DAO 类 (Data Access Objects)</span><br> isDaos <span class="hljs-operator">=</span> <span class="hljs-literal">true</span><br> isSpringDao <span class="hljs-operator">=</span> <span class="hljs-literal">true</span> <span class="hljs-comment">// 启用 Spring/Quarkus 兼容的 DAO</span><br><br> <span class="hljs-comment">// 生成 jOOQ Record 类</span><br> isRecords <span class="hljs-operator">=</span> <span class="hljs-literal">true</span><br><br> <span class="hljs-comment">// 添加注解,如 @Generated, @NotNull</span><br> isGeneratedAnnotation <span class="hljs-operator">=</span> <span class="hljs-literal">true</span><br> isNullableAnnotation <span class="hljs-operator">=</span> <span class="hljs-literal">true</span> <br> }<br> <br> <span class="hljs-comment">// 目标配置 (Target)</span><br> <span class="hljs-keyword">target</span> {<br> <span class="hljs-comment">// 生成的 Java 类的包名</span><br> packageName <span class="hljs-operator">=</span> <span class="hljs-string">"com.example.jooq.generated"</span><br> <span class="hljs-comment">// 生成的代码输出目录</span><br> directory <span class="hljs-operator">=</span> <span class="hljs-string">"build/generated-src/jooq/main"</span><br> }<br><br> <span class="hljs-comment">// 策略配置,可选,用于自定义类名、方法名等</span><br> <span class="hljs-comment">//strategy {</span><br> <span class="hljs-comment">// name = "org.jooq.codegen.DefaultGeneratorStrategy"</span><br> <span class="hljs-comment">//}</span><br> }<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="3-源码集和任务依赖配置"><a href="#3-源码集和任务依赖配置" class="headerlink" title="3. 源码集和任务依赖配置"></a>3. 源码集和任务依赖配置</h2><p>生成的代码必须被 Gradle 识别并加入到编译路径中。同时,要确保代码生成任务在编译之前运行。</p><h3 id="3-1-添加源码目录"><a href="#3-1-添加源码目录" class="headerlink" title="3.1 添加源码目录"></a>3.1 添加源码目录</h3><p>将 jOOQ 生成的目录添加到 <code>main</code> 源码集的 Kotlin 路径。</p><p>Kotlin</p><figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs fsharp"><span class="hljs-keyword">sourceSets</span> {<br> <span class="hljs-keyword">main</span> {<br> <span class="hljs-keyword">kotlin</span> {<br> <span class="hljs-comment">// 将生成的代码输出目录添加到 Source Set</span><br> srcDir(<span class="hljs-string">"build/generated-src/jooq/main"</span>)<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><h3 id="3-2-任务依赖"><a href="#3-2-任务依赖" class="headerlink" title="3.2 任务依赖"></a>3.2 任务依赖</h3><p>确保在 Kotlin 编译 (<code>KotlinCompile</code>) 和 Quarkus 构建 (<code>quarkusBuild</code>) 之前,强制运行 jOOQ 代码生成任务(默认任务名为 <code>jooqCodegen</code> 或 <code>jooqCodegenMain</code>)。</p><p>Kotlin</p><figure class="highlight isbl"><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><code class="hljs isbl"><span class="hljs-variable"><span class="hljs-class">tasks</span>.withType</span><<span class="hljs-variable">KotlinCompile</span>>().configureEach {<br> <span class="hljs-comment">// 确保编译前 jOOQ 代码已生成</span><br> <span class="hljs-function"><span class="hljs-title">dependsOn</span>(<span class="hljs-variable"><span class="hljs-class">tasks</span>.named</span>(<span class="hljs-string">"jooqCodegen"</span>))</span><br>}<br><br><span class="hljs-variable"><span class="hljs-class">tasks</span>.named</span>(<span class="hljs-string">"quarkusBuild"</span>) {<br> <span class="hljs-comment">// 确保构建前 jOOQ 代码已生成</span><br> <span class="hljs-function"><span class="hljs-title">dependsOn</span>(<span class="hljs-variable"><span class="hljs-class">tasks</span>.named</span>(<span class="hljs-string">"jooqCodegen"</span>))</span><br>}<br></code></pre></td></tr></table></figure><h2 id="4-运行代码生成"><a href="#4-运行代码生成" class="headerlink" title="4. 运行代码生成"></a>4. 运行代码生成</h2><p>配置完成后,您可以通过运行 Gradle 任务来执行代码生成:</p><p>Bash</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></pre></td><td class="code"><pre><code class="hljs bash">./gradlew jooqCodegen<br><span class="hljs-comment"># 或</span><br>./gradlew jooqCodegenMain<br></code></pre></td></tr></table></figure><p>执行成功后,生成的 Kotlin 代码(包括 Records, POJOs, DAOs, Tables 等)将位于 <code>build/generated-src/jooq/main</code> 目录下。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://www.jooq.org/doc/latest/manual/code-generation/codegen-execution/codegen-gradle/">JOOQ - Running the code generator with Gradle</a></p>]]></content>
<categories>
<category>JOOQ</category>
</categories>
<tags>
<tag>Gradle</tag>
<tag>Kotlin</tag>
<tag>Quarkus</tag>
<tag>JOOQ</tag>
<tag>代码生成</tag>
<tag>codegen</tag>
</tags>
</entry>
<entry>
<title>Docker 安装 Redis</title>
<link href="/2019/10/03/docker/Docker-%E5%AE%89%E8%A3%85-Redis/"/>
<url>/2019/10/03/docker/Docker-%E5%AE%89%E8%A3%85-Redis/</url>
<content type="html"><![CDATA[<p>Docker 安装 Redis 教程</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>Centos 7 ,SELinux 启动</p><p>Docker 19.03.1</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>启动容器</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><code class="hljs bash">docker run \<br>-p 6379:6379 \<br>--name redis \<br>-d redis \<br>redis-server<br></code></pre></td></tr></table></figure><p>自定义路径,保存数据和配置文件,<a href="../../static/redis/redis.conf">redis.conf</a> 请提前复制到 <code>/program/redis/conf</code>(注意:<code>$PWD</code>表示当前目录)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run \<br>-p 6379:6379 \<br>-v /program/redis/data:/data:Z \<br>-v /program/redis/conf:/usr/local/etc/redis:Z \<br>--name redis \<br>-d redis \<br>redis-server /usr/local/etc/redis/redis.conf<br></code></pre></td></tr></table></figure><p>防火墙</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><code class="hljs bash">firewall-cmd --zone=public --add-port=6379/tcp --permanent<br>firewall-cmd --reload<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title>Docker 安装 Nacos</title>
<link href="/2019/09/18/docker/Docker-%E5%AE%89%E8%A3%85-Nacos/"/>
<url>/2019/09/18/docker/Docker-%E5%AE%89%E8%A3%85-Nacos/</url>
<content type="html"><![CDATA[<p><a href="https://nacos.io/zh-cn/">Nacos</a> 是阿里的一个开源中间件,用于服务的注册以及发现,配置和服务管理。</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>CentOS release 7.4.1708</p><p>Docker 19.03.1</p><p>Docker Compose 1.24.1</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><ol><li><p>安装<code>Git</code></p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmake">yum <span class="hljs-keyword">install</span> git -y<br></code></pre></td></tr></table></figure></li><li><p>安装 <code>Docker</code>,<a href="https://docs.docker.com/install/linux/docker-ce/centos/">官网</a>,<a href="https://holylcd.gitee.io/2019/01/30/5fd50d278d504636b6d32603d5018429.html">个人教程</a></p></li><li><p>克隆项目</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> --depth 1 https://github.com/nacos-group/nacos-docker.git<br></code></pre></td></tr></table></figure></li><li><p>Standalone Derby</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker-compose -f example/standalone-derby.yaml up<br></code></pre></td></tr></table></figure></li><li><p>Standalone Mysql</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker-compose -f example/standalone-mysql.yaml up<br></code></pre></td></tr></table></figure></li><li><p>Cluster</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker-compose -f example/cluster-hostname.yaml up <br></code></pre></td></tr></table></figure></li><li><p>查询服务状态</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker ps -a<br></code></pre></td></tr></table></figure></li></ol><h1 id="防火墙"><a href="#防火墙" class="headerlink" title="防火墙"></a>防火墙</h1><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">firewall-cmd --zone=public --add-port=8848/tcp --permanent<br></code></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://hub.docker.com/r/nacos/nacos-server">Nacos</a></p>]]></content>
<categories>
<category>Nacos</category>
</categories>
<tags>
<tag>Spring Cloud Alibaba</tag>
<tag>Nacos</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title>修改SSH端口 禁用root登录</title>
<link href="/2019/09/17/linux/centos/%E4%BF%AE%E6%94%B9SSH%E7%AB%AF%E5%8F%A3-%E7%A6%81%E7%94%A8root%E7%99%BB%E5%BD%95/"/>
<url>/2019/09/17/linux/centos/%E4%BF%AE%E6%94%B9SSH%E7%AB%AF%E5%8F%A3-%E7%A6%81%E7%94%A8root%E7%99%BB%E5%BD%95/</url>
<content type="html"><![CDATA[<p>Linux 新机修改 SSH 端口,禁用 root 登录</p><span id="more"></span><h1 id="启用-SELinux"><a href="#启用-SELinux" class="headerlink" title="启用 SELinux"></a>启用 SELinux</h1><p>检查状态</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sestatus<br></code></pre></td></tr></table></figure><p>安装</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">yum install selinux-policy-targeted<br></code></pre></td></tr></table></figure><p>开启</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></pre></td><td class="code"><pre><code class="hljs bash">vim /etc/selinux/config<br>或<br>setenforce 1<br></code></pre></td></tr></table></figure><p>设置SELinux下次重启仍然保持开启,【重要】否则无法正常启动系统</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">touch</span> /.autorelabel<br></code></pre></td></tr></table></figure><p>重启</p><h1 id="修改SSH端口"><a href="#修改SSH端口" class="headerlink" title="修改SSH端口"></a>修改SSH端口</h1><p>修改 sshd 配置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim /etc/ssh/sshd_config<br></code></pre></td></tr></table></figure><figure class="highlight vhdl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs vhdl"><span class="hljs-keyword">Port</span> <span class="hljs-number">22</span> //旧 <span class="hljs-keyword">Port</span>,先保留,防止翻车导致无法远程登录<br><span class="hljs-keyword">Port</span> <span class="hljs-number">2222</span> //新 <span class="hljs-keyword">Port</span><br></code></pre></td></tr></table></figure><p>开放端口</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><code class="hljs bash"><span class="hljs-comment"># 启动防火墙</span><br>systemctl start firewalld<br><span class="hljs-comment"># 添加开放端口</span><br>firewall-cmd --zone=public --add-port=2222/tcp --permanent<br><span class="hljs-comment"># 重载端口配置</span><br>firewall-cmd --reload<br></code></pre></td></tr></table></figure><p>查看 SSH 端口</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">semanage port -l | grep ssh<br></code></pre></td></tr></table></figure><p>添加 SSH 端口,之后可以再次查看端口,确认是否添加成功</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">semanage port -a -t ssh_port_t -p tcp 2222<br></code></pre></td></tr></table></figure><p>如果出现 semanage command not found,请执行 </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></pre></td><td class="code"><pre><code class="hljs bash">yum provides /usr/sbin/semanage<br>或者<br>yum whatprovides /usr/sbin/semanage<br>yum -y install policycoreutils-python<br></code></pre></td></tr></table></figure><p>重启服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl restart sshd<br></code></pre></td></tr></table></figure><p>旧的远程连接不会离线,可以先保留窗口,新开窗口测试 SSH 端口是否正常</p><p>服务正常之后,回头注释 <code>/etc/ssh/sshd_config</code> 的 22 Port</p><h1 id="禁用-root-远程登录"><a href="#禁用-root-远程登录" class="headerlink" title="禁用 root 远程登录"></a>禁用 root 远程登录</h1><p>修改配置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim /etc/ssh/sshd_config<br></code></pre></td></tr></table></figure><figure class="highlight gams"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs gams">#PermitRootLogin <span class="hljs-keyword">yes</span> #去掉注释,<span class="hljs-keyword">yes</span>-允许登录,<span class="hljs-keyword">no</span>-不允许登录<br></code></pre></td></tr></table></figure><p>重启服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl restart sshd<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>CentOS</category>
<category>Linux</category>
</categories>
<tags>
<tag>CentOS</tag>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title>Spring Cache Redis实现 (Spring Boot + Redis)</title>
<link href="/2019/09/12/spring-boot/Spring-Cache-Redis%E5%AE%9E%E7%8E%B0-Spring-Boot-Redis/"/>
<url>/2019/09/12/spring-boot/Spring-Cache-Redis%E5%AE%9E%E7%8E%B0-Spring-Boot-Redis/</url>
<content type="html"><![CDATA[<p>基于Spring Data Redis、Spring Cache实现后端缓存,提高并发。可实现 <code>@Cacheable</code>有效期、固定时间过期。</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>Spring Boot 2.1.7.RELEASE</p><h1 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h1><ol><li><p>已经开启 Spring Cache</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@EnableCaching</span><br></code></pre></td></tr></table></figure></li></ol><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><ol><li>自定义 redis 缓存管理</li><li><code>@Cacheable</code> 指定缓存管理</li></ol><h1 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h1><ol><li><p>自定义 redis 缓存管理</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean("customCacheManager")</span><br> <span class="hljs-keyword">public</span> CacheManager <span class="hljs-title function_">commonCacheManager</span><span class="hljs-params">(RedisConnectionFactory redisConnectionFactory)</span> {<br> <span class="hljs-comment">// 设置缓存有效期5分钟</span><br> <span class="hljs-type">RedisCacheConfiguration</span> <span class="hljs-variable">redisCacheConfiguration</span> <span class="hljs-operator">=</span> RedisCacheConfiguration<br> .defaultCacheConfig()<br> .entryTtl(Duration.ofMinutes(<span class="hljs-number">5L</span>));<br> <span class="hljs-keyword">return</span> RedisCacheManager<br> .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))<br> .cacheDefaults(redisCacheConfiguration).build();<br> }<br></code></pre></td></tr></table></figure></li><li><p><code>@Cacheable</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></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Cacheable(</span><br><span class="hljs-meta"> value = "redis:key:xxx",</span><br><span class="hljs-meta"> cacheManager = "customCacheManager"</span><br><span class="hljs-meta">)</span><br><span class="hljs-meta">@RequestMapping(value = "index", method = RequestMethod.GET)</span><br><span class="hljs-keyword">public</span> ResponseEntity<IndexVO> <span class="hljs-title function_">index</span><span class="hljs-params">()</span> {<br> ...<br></code></pre></td></tr></table></figure></li><li><p>@CacheEvict 主动清理缓存</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@CacheEvict(</span><br><span class="hljs-meta"> value = "redis:key:xxx",</span><br><span class="hljs-meta"> cacheManager = "customCacheManager"</span><br><span class="hljs-meta">)</span><br><span class="hljs-meta">@RequestMapping(value = "index/clear", method = RequestMethod.GET)</span><br><span class="hljs-keyword">public</span> ResponseEntity <span class="hljs-title function_">clearIndex</span><span class="hljs-params">()</span> {<br> ...<br></code></pre></td></tr></table></figure></li></ol><h1 id="案例"><a href="#案例" class="headerlink" title="案例"></a>案例</h1><p><a href="https://gitee.com/holylcd/spring-boot-quick">案例</a>还有其他集成,本章读者可以直接阅读一下代码(跪求 Star……)</p><ol><li><p>缓存管理</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">org.holy.spring.boot.quick.config.redis.RedisConfig#commonCacheManager<br></code></pre></td></tr></table></figure></li><li><p>@Cacheable注解</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">org.holy.spring.boot.quick.controller.v1.IndexController#banner<br></code></pre></td></tr></table></figure></li><li><p>@CacheEvict注解</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">org.holy.spring.boot.quick.controller.v1.IndexController#clearBanner<br></code></pre></td></tr></table></figure></li></ol>]]></content>
<categories>
<category>Spring Boot</category>
</categories>
<tags>
<tag>Redis</tag>
<tag>Spring Boot</tag>
<tag>Cache</tag>
</tags>
</entry>
<entry>
<title>接口文档管理系统-小幺鸡</title>
<link href="/2019/09/12/api-doc/%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F-%E5%B0%8F%E5%B9%BA%E9%B8%A1/"/>
<url>/2019/09/12/api-doc/%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F-%E5%B0%8F%E5%B9%BA%E9%B8%A1/</url>
<content type="html"><![CDATA[<p>小幺鸡在线文档管理,支持在线测试,支持json,txt,xml,html,js,流,和 WebSocket</p><p>前后端分离开发环境下,前端开发人员和后端开发人员主要通过接口文档对接,规范的、好用的接口文档管理系统能够大大提高开发人员效率。本章主要介绍《小幺鸡在线文档管理》,支持在线测试,支持json,txt,xml,html,js,流,和 WebSocket。个人觉得这是一款非常棒的接口管理系统。系统开发者还提供了,读者可以看下效果哦<a href="http://www.xiaoyaoji.cn/doc/1n6N0lgsSmW">演示系统</a></p><span id="more"></span><h1 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h1><ol><li>可设置不同环境,方便切换开发、测试环境</li><li>可设置全局请求头、响应头、请求体、响应体,满足多种场景</li><li>可在线测试</li></ol><h1 id="搭建"><a href="#搭建" class="headerlink" title="搭建"></a>搭建</h1><p><strong>环境</strong></p><p>Linux</p><p>Tomcat</p><p><strong>搭建</strong></p><p>下载源码</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> https://gitee.com/zhoujingjie/apiManager.git<br></code></pre></td></tr></table></figure><p>创建数据库,数据库脚本位于项目 <code>./doc/xiaoyaoji.sql</code>,自行建库即可</p><p>打包,项目跟目录执行maven指令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">mvn package<br></code></pre></td></tr></table></figure><p>复制 <code>xiaoyaoji-web/target/xiaoyaoji-2.1.7.war</code>至tomcat服务器<code>webapps</code>目录,改名 <code>ROOT.war</code></p><p>解压 <code>ROOT.war</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">tar -zxvf ./ROOT.war<br></code></pre></td></tr></table></figure><p>修改数据库配置</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim ./ROOT/WEB-INF/classes/platform.properties<br></code></pre></td></tr></table></figure><p>填写正确的数据库地址、名称、登录账号、密码即可,其他配置可后期自行了解</p><p>启动 Tomcat,为简单说明问题,这里直接启动 Tomcat,对外线上服务请不要这样启动项目</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sh ./bin/startup.sh<br></code></pre></td></tr></table></figure><p>大功告成!</p><p><strong>Google chrome 插件</strong></p><ol><li><p>可翻墙的同学,直接到应用商店安装即可 <code>小幺鸡</code></p></li><li><p>不能翻墙的同学,这里提供一种目前(2019/09/12)可行的办法</p><p>开发者提供的插件源码位于项目 <code>./浏览器插件/</code></p><p>打开 chrome-拓展程序,右上角开启开发者模式</p><p>加载已解压的拓展程序,选择文件夹 <code>./浏览器插件</code>,现在可以看到插件已经成功加载至 chrome!</p></li></ol><p><strong>使用</strong></p><ol><li>全部访客都可以注册账号</li><li>那个账号创建的项目,创建人即管理员,可以将其他用户拉到项目里</li></ol>]]></content>
<categories>
<category>Doc</category>
<category>API</category>
</categories>
<tags>
<tag>Doc</tag>
<tag>API</tag>
</tags>
</entry>
<entry>
<title>Spring Boot 部署</title>
<link href="/2019/09/11/spring-boot/Spring-Boot-%E9%83%A8%E7%BD%B2-Linux-%E6%9C%8D%E5%8A%A1%E5%8C%96/"/>
<url>/2019/09/11/spring-boot/Spring-Boot-%E9%83%A8%E7%BD%B2-Linux-%E6%9C%8D%E5%8A%A1%E5%8C%96/</url>
<content type="html"><![CDATA[<p>简单介绍三种Spring Boot应用的部署方式,分别是:nohub、Linux 服务化、Docker,nohub方式和Linux服务化建议用于单体应用,分布式应用建议使用 Docker。教程建立在Spring Boot已经完成打包并且可用情况下。</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>CentOS 7,SELinux开启</p><p>Spring Boot应用程序(路径为<code>/program/app.jar</code>)</p><h1 id="nohub启动"><a href="#nohub启动" class="headerlink" title="nohub启动"></a>nohub启动</h1><p>直接启动,这种方式其实就是 <code>java -jar</code>,只是通过 nohub 使程序能够以守护进程继续运行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">nohup</span> java -jar /program/app.jar &<br></code></pre></td></tr></table></figure><p>查询程序进程</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ps aux|grep report-0.0.1-SNAPSHOT.jar<br></code></pre></td></tr></table></figure><p>停止程序进程</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">kill</span> -9 xxxx上面查询到的pid<br></code></pre></td></tr></table></figure><h1 id="Linux-服务化"><a href="#Linux-服务化" class="headerlink" title="Linux 服务化"></a>Linux 服务化</h1><p>为 Spring Boot 应用建立一个简单的 Linux 服务</p><p>新增服务文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim /etc/systemd/system/myapp.service<br></code></pre></td></tr></table></figure><p>编辑内容</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-section">[Unit]</span><br><span class="hljs-comment">#服务描述</span><br><span class="hljs-attr">Description</span>=My Spring Boot Application<br><span class="hljs-attr">After</span>=network.target<br><br><span class="hljs-section">[Service]</span><br><span class="hljs-attr">Type</span>=forking<br><span class="hljs-attr">TimeoutStartSec</span>=<span class="hljs-number">0</span><br><span class="hljs-comment">#这里改为机器的JVM路径</span><br><span class="hljs-attr">ExecStart</span>=/opt/jdk1.<span class="hljs-number">8.0</span>_191/bin/java -jar /program/app.jar<br><span class="hljs-attr">ExecStop</span>=/opt/jdk1.<span class="hljs-number">8.0</span>_191/bin/java -jar /program/app.jar<br><span class="hljs-comment">#这里可以使用非root用户,记得给用到的路径授权哦</span><br><span class="hljs-comment">#User=root</span><br><span class="hljs-comment">#Group=root</span><br><br><span class="hljs-section">[Install]</span><br><span class="hljs-comment">#多用户</span><br><span class="hljs-attr">WantedBy</span>=multi-user.target<br></code></pre></td></tr></table></figure><p>启动服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl start myapp<br></code></pre></td></tr></table></figure><p>停止服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl stop myapp<br></code></pre></td></tr></table></figure><p>重启服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl restart myapp<br></code></pre></td></tr></table></figure><p>自启服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl <span class="hljs-built_in">enable</span> myapp<br></code></pre></td></tr></table></figure><p>关闭自启</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl <span class="hljs-built_in">disable</span> myapp<br></code></pre></td></tr></table></figure><h1 id="Docker-部署"><a href="#Docker-部署" class="headerlink" title="Docker 部署"></a>Docker 部署</h1><ol><li><p><a href="https://holylcd.gitee.io/2019/01/30/5fd50d278d504636b6d32603d5018429.html">Docker 安装</a></p></li><li><p>准备镜像</p><p>新建 Dockerfile</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim /program/Dockerfile<br></code></pre></td></tr></table></figure><p>边界 Dockerfile</p><figure class="highlight dockerfile"><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><code class="hljs dockerfile"><span class="hljs-keyword">FROM</span> openjdk:<span class="hljs-number">8</span>-jre-alpine<br><span class="hljs-keyword">VOLUME</span><span class="language-bash"> [<span class="hljs-string">"/tmp"</span>]</span><br><span class="hljs-keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="hljs-string">"java"</span>,<span class="hljs-string">"-Djava.security.egd=file:/dev/./urandom"</span>,<span class="hljs-string">"-jar"</span>]</span><br><span class="hljs-keyword">CMD</span><span class="language-bash"> [<span class="hljs-string">"/program/app.jar"</span>]</span><br></code></pre></td></tr></table></figure><p>构建镜像</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker build -f ./Dockerfile -t myapp .<br></code></pre></td></tr></table></figure></li><li><p>启动 Spring Boot 服务</p><p>为了简单说明 Docker 部署,这里先使用 host 网络模式</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run -d \<br>--net=host \<br>--restart=unless-stopped \<br>--name myapp \<br>-v /etc/localtime:/etc/localtime \<br>-v /program/app.jar:/program/app.jar:ro \<br>myapp<br></code></pre></td></tr></table></figure><p>net:网络模式</p><p>restart:重启策略。unless-stopped,服务异常时总是重启,除非主动停止</p><p>name:容器名</p><p>第一个-v:同步物理机的时间至容器</p><p>第二个-v:映射物理机文件<code>/program/app.jar</code>至容器<code>/program/app.jar</code></p></li></ol>]]></content>
<categories>
<category>Spring Boot</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>微服务</tag>
<tag>Spring Boot</tag>
<tag>nohub</tag>
</tags>
</entry>
<entry>
<title>自选组件搭建邮件服务器 CentOS</title>
<link href="/2019/03/28/Roundcube-WebMail/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/"/>
<url>/2019/03/28/Roundcube-WebMail/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/</url>
<content type="html"><![CDATA[<p>自选组件搭建邮件服务器 CentOS</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>CentOS 7</p><h1 id="组件"><a href="#组件" class="headerlink" title="组件"></a>组件</h1><p>Postfix</p><p>Dovecot</p><p>MySQL 5.7</p><p>OpenDKIM</p><p>Nginx</p><p>PHP</p><p>Roundcube WebMail</p><p>Policyd</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><h2 id="MySQL"><a href="#MySQL" class="headerlink" title="MySQL"></a>MySQL</h2><p>从<a href="https://dev.mysql.com/downloads/mysql/">mysql</a>下载离线包</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.25-1.el7.x86_64.rpm-bundle.tar<br></code></pre></td></tr></table></figure><p>解压包</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">tar -zxvf mysql-5.7.xx.rpm-bundle.tar<br></code></pre></td></tr></table></figure><p>包的作用</p><blockquote><p>mysql-community-client(*客户端程序和工具)<br>mysql-community-server(*服务器程序和工具)<br>mysql-community-libs(*LIB库)<br>mysql-community-libs-compat(*LIB共享兼容库)<br>mysql-community-common(*公共文件)<br>mysql-community-devel(开发MySQL必备的头文件和库)<br>mysql-community-embedded(嵌入式库)<br>mysql-community-embedded-compat(嵌入式共享兼容库)<br>mysql-community-embedded-dev(嵌入式开发库)<br>mysql-community-test(测试套件)</p></blockquote><p>安装需要的包</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install mysql-community-{server,client,common,libs}-*<br></code></pre></td></tr></table></figure><p>安装生成文件</p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/mysql.jpg" alt="mysql.jpg"></p><p>启动</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl start mysqld<br></code></pre></td></tr></table></figure><p>设置root密码</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> /var/log/mysqld.log<br></code></pre></td></tr></table></figure><p>SET PASSWORD FOR ‘root‘@’localhost’ = PASSWORD(‘123’);</p><p>SHOW VARIABLES LIKE ‘validate_password%’;</p><p>validate_password_dictionary_file<br>插件用于验证密码强度的字典文件路径。</p><p>validate_password_length<br>密码最小长度,参数默认为8,它有最小值的限制,最小值为:validate_password_number_count + validate_password_special_char_count + (2 * validate_password_mixed_case_count)</p><p>validate_password_mixed_case_count<br>密码至少要包含的小写字母个数和大写字母个数。</p><p>validate_password_number_count<br>密码至少要包含的数字个数。</p><p>validate_password_policy<br>密码强度检查等级,0/LOW、1/MEDIUM、2/STRONG。有以下取值:<br>Policy Tests Performed<br>0 or LOW Length<br>1 or MEDIUM Length; numeric, lowercase/uppercase, and special characters<br>2 or STRONG Length; numeric, lowercase/uppercase, and special characters; dictionary file<br>默认是1,即MEDIUM,所以刚开始设置的密码必须符合长度,且必须含有数字,小写或大写字母,特殊字符。</p><p>validate_password_special_char_count</p><p>密码至少要包含的特殊字符数。</p><p>set global validate_password_policy=0;</p><p>set global validate_password_mixed_case_count=0;</p><p>set global validate_password_number_count=3;</p><p>set global validate_password_special_char_count=0;</p><p>set global validate_password_length=3;</p><h2 id="创建虚拟数据"><a href="#创建虚拟数据" class="headerlink" title="创建虚拟数据"></a>创建虚拟数据</h2><p>创建用户</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE USER 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';<br></code></pre></td></tr></table></figure><p>创建数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE DATABASE mail_sys;<br></code></pre></td></tr></table></figure><p>授权</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">GRANT SELECT ON mail_sys.* TO 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';<br></code></pre></td></tr></table></figure><p>刷新权限</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">FLUSH PRIVILEGES;<br></code></pre></td></tr></table></figure><p>切换数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">USE mail_sys;<br></code></pre></td></tr></table></figure><p>域名表</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE TABLE `domains` ( `id` int(20) NOT NULL auto_increment, `name` varchar(100) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;<br></code></pre></td></tr></table></figure><p>用户表</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE TABLE `users` ( `id` int(20) NOT NULL auto_increment, `domain_id` int(20) NOT NULL, `password` varchar(200) NOT NULL, `email` varchar(200) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`), FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;<br></code></pre></td></tr></table></figure><p>别名表</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE TABLE `aliases` ( `id` int(20) NOT NULL auto_increment, `domain_id` int(20) NOT NULL, `source` varchar(200) NOT NULL, `destination` varchar(200) NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;<br></code></pre></td></tr></table></figure><p>添加域名</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mysql">INSERT INTO `mail_sys`.`domains` (`id` ,`name`) <br>VALUES (1, 'example.com');<br></code></pre></td></tr></table></figure><p>添加用户</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs mysql">INSERT INTO `mail_sys`.`users`<br> (`id`, `domain_id`, `password` , `email`) VALUES<br> ('1', '1', ENCRYPT('123qwe', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'admin@example.com'),<br> ('2', '1', ENCRYPT('123qwe', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'user1@example.com'),<br> ('3', '1', ENCRYPT('123qwe', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'user2@example.com');<br></code></pre></td></tr></table></figure><p>添加别名</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs mysql">INSERT INTO `mail_sys`.`aliases`<br> (`id`, `domain_id`, `source`, `destination`) VALUES<br> ('1', '1', 'admin1@example.com', 'admin@example.com'),<br> ('2', '1', 'user11@example.com', 'user1@example.com'),<br> ('3', '1', 'user22@example.com', 'user2@example.com');<br></code></pre></td></tr></table></figure><p>检查数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">SELECT * FROM mail_sys.domains;<br></code></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">SELECT * FROM mail_sys.users;<br></code></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">SELECT * FROM mail_sys.aliases;<br></code></pre></td></tr></table></figure><h2 id="邮件系统专用用户"><a href="#邮件系统专用用户" class="headerlink" title="邮件系统专用用户"></a>邮件系统专用用户</h2><ol><li>所有的邮件用户都映射到系统的同一个真实的用户上。</li><li>不建议直接使用系统自带的 mail 账户。</li></ol><p>创建专用组</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">groupadd -g 2000 mail_sys<br></code></pre></td></tr></table></figure><p>创建专用用户</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">useradd -g mail_sys -u 2000 mail_sys -d /var/spool/mail -s /sbin/nologin<br></code></pre></td></tr></table></figure><p>修改邮件目录所有者</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">chown</span> -R mail_sys:mail_sys /var/spool/mail<br></code></pre></td></tr></table></figure><h2 id="生成数字签名"><a href="#生成数字签名" class="headerlink" title="生成数字签名"></a>生成数字签名</h2><p>私钥</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">openssl genrsa -des3 -out privkey.pem 2048<br><br></code></pre></td></tr></table></figure><p>自签证书</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1095<br><br></code></pre></td></tr></table></figure><h2 id="Postfix"><a href="#Postfix" class="headerlink" title="Postfix"></a>Postfix</h2><h3 id="安装-1"><a href="#安装-1" class="headerlink" title="安装"></a>安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install postfix<br><br></code></pre></td></tr></table></figure><h3 id="备份原版配置文件"><a href="#备份原版配置文件" class="headerlink" title="备份原版配置文件"></a>备份原版配置文件</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cp</span> -r /etc/postfix /etc/postfix.bak<br><br></code></pre></td></tr></table></figure><h3 id="修改-main-cf"><a href="#修改-main-cf" class="headerlink" title="修改 main.cf"></a>修改 main.cf</h3><p> 该文件配置 Postfix 的全局参数。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/main.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>请按实际情况以及注释提示<strong>修改以下内容</strong>,完成去除 # 号和后面的注释,然后粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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></pre></td><td class="code"><pre><code class="hljs sh">myhostname = example.com<span class="hljs-comment">#本机域名,需要修改</span><br>alias_maps = <span class="hljs-built_in">hash</span>:/etc/aliases<br>alias_database = <span class="hljs-built_in">hash</span>:/etc/aliases<br>myorigin = /etc/mailname<br>mydestination = localhost<br>relayhost =<br>mynetworks = 127.0.0.0/8<br>mailbox_size_limit = 0<br>recipient_delimiter = +<br>inet_interfaces = all<br>smtpd_banner = <span class="hljs-variable">$myhostname</span> ESMTP <span class="hljs-variable">$mail_name</span><br>biff = no<br>append_dot_mydomain = no<br>readme_directory = no<br>smtpd_tls_cert_file=/etc/pki/tls/certs/cert.pem<span class="hljs-comment"># example.com 证书文件位置,需要修改</span><br>smtpd_tls_key_file=/etc/pki/tls/certs/key.pem<span class="hljs-comment"># example.com 证书私钥文件位置,需要修改</span><br>smtpd_use_tls=<span class="hljs-built_in">yes</span><br>smtpd_tls_auth_only = no<br>smtpd_tls_security_level = may<br>smtp_tls_security_level = may<br>virtual_transport = lmtp:unix:private/dovecot-lmtp<br>smtpd_sasl_type = dovecot<br>smtpd_sasl_path = private/auth<br>smtpd_sasl_auth_enable = <span class="hljs-built_in">yes</span><br>virtual_mailbox_domains = mysql:/etc/postfix/mysql_mailbox_domains.cf<br>virtual_mailbox_maps = mysql:/etc/postfix/mysql_mailbox_maps.cf<br>virtual_alias_maps = mysql:/etc/postfix/mysql_alias_maps.cf, mysql:/etc/postfix/mysql_email2email.cf<br>smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination<br>virtual_uid_maps = static:2000<br>virtual_gid_maps = static:2000<br>message_size_limit = 102400000<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-master-cf"><a href="#修改-master-cf" class="headerlink" title="修改 master.cf"></a>修改 master.cf</h3><p>该文件配置 Postfix 中各模块的参数。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/master.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">smtp inet n - n - - smtpd<br>submission inet n - n - - smtpd<br> -o syslog_name=postfix/submission<br> -o smtpd_tls_security_level=encrypt<br> -o smtpd_sasl_auth_enable=<span class="hljs-built_in">yes</span><br> -o smtpd_tls_auth_only=no <br> -o smtpd_client_restrictions=permit_sasl_authenticated,reject<br> -o milter_macro_daemon_name=ORIGINATING<br>pickup unix n - n 60 1 pickup<br>cleanup unix n - n - 0 cleanup<br>qmgr unix n - n 300 1 qmgr<br>tlsmgr unix - - n 1000? 1 tlsmgr<br>rewrite unix - - n - - trivial-rewrite<br>bounce unix - - n - 0 bounce<br>defer unix - - n - 0 bounce<br>trace unix - - n - 0 bounce<br>verify unix - - n - 1 verify<br>flush unix n - n 1000? 0 flush<br>proxymap unix - - n - - proxymap<br>proxywrite unix - - n - 1 proxymap<br>smtp unix - - n - - smtp<br>relay unix - - n - - smtp<br> -o smtp_helo_timeout=120 -o smtp_connect_timeout=120<br>showq unix n - n - - showq<br>error unix - - n - - error<br>retry unix - - n - - error<br>discard unix - - n - - discard<br><span class="hljs-built_in">local</span> unix - n n - - <span class="hljs-built_in">local</span><br>virtual unix - n n - - virtual<br>lmtp unix - - n - - lmtp<br>anvil unix - - n - 1 anvil<br>scache unix - - n - 1 scache<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="与-MySQL-对接"><a href="#与-MySQL-对接" class="headerlink" title="与 MySQL 对接"></a>与 MySQL 对接</h3><p><strong>指定域名数据表</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/mysql_mailbox_domains.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">user = mail_sys<br>password = mail_sys<br>hosts = localhost<br>dbname = mail_sys<br>query = SELECT 1 FROM domains WHERE name=<span class="hljs-string">'%s'</span><br>EOF<br><br></code></pre></td></tr></table></figure><p><strong>指定用户数据表</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/mysql_mailbox_maps.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">user = mail_sys<br>password = mail_sys<br>hosts = localhost<br>dbname = mail_sys<br>query = SELECT 1 FROM <span class="hljs-built_in">users</span> WHERE email=<span class="hljs-string">'%s'</span><br>EOF<br><br></code></pre></td></tr></table></figure><p><strong>指定别名数据表</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/mysql_alias_maps.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">user = mail_sys<br>password = mail_sys<br>hosts = localhost<br>dbname = mail_sys<br>query = SELECT destination FROM aliases WHERE <span class="hljs-built_in">source</span>=<span class="hljs-string">'%s'</span><br>EOF<br><br></code></pre></td></tr></table></figure><p><strong>指定用户收件目标数据表</strong></p><p>(这个有点懵,Linode 上面的教程要求添加这个,懂的大神麻烦解释下)</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/postfix/mysql_email2email.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">user = mail_sys<br>password = mail_sys<br>hosts = localhost<br>dbname = mail_sys<br>query = SELECT email FROM <span class="hljs-built_in">users</span> WHERE email=<span class="hljs-string">'%s'</span><br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="测试数据库读取"><a href="#测试数据库读取" class="headerlink" title="测试数据库读取"></a>测试数据库读取</h3><p>启动 Postfix 服务</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl start postfix<br><br></code></pre></td></tr></table></figure><p>测试域名数据表的读取</p><p>下面的 example.com 请替换为自己设定的域名。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">postmap -q example.com mysql:/etc/postfix/mysql_mailbox_domains.cf<br><br></code></pre></td></tr></table></figure><p>若返回 1 ,则说明设置正确。</p><p>测试用户名数据表的读取</p><p>下面的 <a href="mailto:user1@example.com">user1@example.com</a> 请替换为自己设定的用户名其中一个。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">postmap -q user1@example.com mysql:/etc/postfix/mysql_mailbox_maps.cf<br><br></code></pre></td></tr></table></figure><p>若返回 1 ,则说明设置正确。</p><p>测试别名数据表的读取(可选)</p><p>下面的 <a href="mailto:user11@example.com">user11@example.com</a> 请替换为自己设定的别名其中一个。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">postmap -q user11@example.com mysql:/etc/postfix/mysql_alias_maps.cf<br><br></code></pre></td></tr></table></figure><p>若返回别名所对应的真实用户名 ,则说明设置正确。</p><p>停止 Postfix 服务</p><p>等全部配置完成后再启动。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl stop postfix<br><br></code></pre></td></tr></table></figure><h3 id="日志"><a href="#日志" class="headerlink" title="日志"></a>日志</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs"><br></code></pre></td></tr></table></figure><h2 id="Dovecot"><a href="#Dovecot" class="headerlink" title="Dovecot"></a>Dovecot</h2><h3 id="安装-2"><a href="#安装-2" class="headerlink" title="安装"></a>安装</h3><p>卸载 sendmail</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum remove sendmail<br><br></code></pre></td></tr></table></figure><p>安装 Dovecot</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install dovecot<br><br></code></pre></td></tr></table></figure><p>注意:CentOS 6 需要安装 managesieve</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install dovecot-pigeonhole<br><br></code></pre></td></tr></table></figure><h3 id="备份原版配置文件-1"><a href="#备份原版配置文件-1" class="headerlink" title="备份原版配置文件"></a>备份原版配置文件</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cp</span> -r /etc/dovecot /etc/dovecot.bak<br><br></code></pre></td></tr></table></figure><h3 id="修改-dovecot-conf"><a href="#修改-dovecot-conf" class="headerlink" title="修改 dovecot.conf"></a>修改 dovecot.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置 Dovecot 的全局参数。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/dovecot.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">protocols = imap lmtp<br>dict {<br>}<br>!include conf.d/*.conf<br>!include_try local.conf<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-10-mail-conf"><a href="#修改-conf-d-10-mail-conf" class="headerlink" title="修改 conf.d/10-mail.conf"></a>修改 conf.d/10-mail.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置邮箱文件存储的位置和命名空间。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/10-mail.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">namespace inbox {<br> inbox = <span class="hljs-built_in">yes</span><br>}<br>first_valid_uid = 1000<br>mbox_write_locks = fcntl<br>mail_location = maildir:/var/spool/mail/%d/%n<br>mail_privileged_group = mail<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-15-mailboxes-conf"><a href="#修改-conf-d-15-mailboxes-conf" class="headerlink" title="修改 conf.d/15-mailboxes.conf"></a>修改 conf.d/15-mailboxes.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置邮箱内部的目录结构。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/15-mailboxes.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">namespace inbox {<br> mailbox Drafts {<br> auto = create<br> special_use = \Drafts<br> }<br> mailbox Trash {<br> auto = create<br> special_use = \Trash<br> }<br> mailbox Sent {<br> auto = create<br> special_use = \Sent<br> }<br>}<br>EOF<br><br></code></pre></td></tr></table></figure><p>注意</p><p>dovecot2.3以下放入空内容就好</p><figure class="highlight sh"><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><code class="hljs sh">namespace inbox {<br>}<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-10-auth-conf"><a href="#修改-conf-d-10-auth-conf" class="headerlink" title="修改 conf.d/10-auth.conf"></a>修改 conf.d/10-auth.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置用户身份认证流程。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/10-auth.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">auth_mechanisms = plain login<br>!include auth-sql.conf.ext<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-auth-sql-conf-ext"><a href="#修改-conf-d-auth-sql-conf-ext" class="headerlink" title="修改 conf.d/auth-sql.conf.ext"></a>修改 conf.d/auth-sql.conf.ext</h3><blockquote><p><strong>说明</strong><br>• 该文件配置数据库认证的参数。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/auth-sql.conf.ext << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs sh">passdb {<br> driver = sql<br> args = /etc/dovecot/dovecot-sql.conf.ext<br>}<br>userdb {<br> driver = static<br> args = uid=mail_sys gid=mail_sys home=/var/spool/mail/%d/%n<br>}<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-dovecot-sql-conf-ext"><a href="#修改-dovecot-sql-conf-ext" class="headerlink" title="修改 dovecot-sql.conf.ext"></a>修改 dovecot-sql.conf.ext</h3><blockquote><p><strong>说明</strong><br>• 该文件配置验证用户名密码所用的数据表以及认证方法。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/dovecot-sql.conf.ext << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">driver = mysql<br>connect = host=localhost dbname=mail_sys user=mail_sys password=mail_sys<br>default_pass_scheme = SHA512-CRYPT<br>password_query = SELECT email as user, password FROM <span class="hljs-built_in">users</span> WHERE email=<span class="hljs-string">'%u'</span>;<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-10-ssl-conf"><a href="#修改-conf-d-10-ssl-conf" class="headerlink" title="修改 conf.d/10-ssl.conf"></a>修改 conf.d/10-ssl.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置 SSL 加密参数。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/10-ssl.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>请按实际情况以及注释提示<strong>修改以下内容</strong>,完成去除 # 号和后面的注释,然后粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs sh">ssl = required<br>ssl_cert = </etc/pki/tls/certs/cert.pem<span class="hljs-comment"># example.com 证书文件位置,需要修改</span><br>ssl_key = </etc/pki/tls/certs/key.pem<span class="hljs-comment"># example.com 证书私钥文件位置,需要修改</span><br>ssl_protocols = !SSLv3<br>ssl_cipher_list = ALL:!LOW:!SSLv3:!EXP:!aNULL<br>ssl_prefer_server_ciphers = no<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="修改-conf-d-10-master-conf"><a href="#修改-conf-d-10-master-conf" class="headerlink" title="修改 conf.d/10-master.conf"></a>修改 conf.d/10-master.conf</h3><blockquote><p><strong>说明</strong><br>• 该文件配置 Dovecot 中各服务的参数。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/dovecot/conf.d/10-master.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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></pre></td><td class="code"><pre><code class="hljs sh">service imap-login {<br> inet_listener imap {<br> port = 143<br> }<br> inet_listener imaps {<br> port = 993<br> ssl = <span class="hljs-built_in">yes</span><br> }<br>}<br><br>service lmtp {<br> unix_listener /var/spool/postfix/private/dovecot-lmtp {<br> mode = 0600<br> user = postfix<br> group = postfix<br> }<br>}<br><br>service imap {<br><br>}<br><br>service auth {<br> unix_listener /var/spool/postfix/private/auth {<br> mode = 0666<br> user = postfix<br> group = postfix<br> }<br><br> unix_listener auth-userdb {<br> mode = 0600<br> user = mail_sys<br> }<br> user = dovecot<br>}<br><br>service auth-worker {<br> user = mail_sys<br>}<br><br>service dict {<br> unix_listener dict {<br> mode = 0660<br> user = mail<br> group = mail<br> }<br>}<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="日志-1"><a href="#日志-1" class="headerlink" title="日志"></a>日志</h3><figure class="highlight arcade"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs arcade">/<span class="hljs-keyword">var</span>/<span class="hljs-built_in">log</span>/maillog<br><br></code></pre></td></tr></table></figure><h2 id="配置-OpenDKIM"><a href="#配置-OpenDKIM" class="headerlink" title="配置 OpenDKIM"></a>配置 OpenDKIM</h2><p>安装</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install opendkim<br><br></code></pre></td></tr></table></figure><h3 id="修改-OpenDKIM-配置文件"><a href="#修改-OpenDKIM-配置文件" class="headerlink" title="修改 OpenDKIM 配置文件"></a>修改 OpenDKIM 配置文件</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> > /etc/opendkim.conf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>请按实际情况以及注释提示<strong>修改以下内容</strong>,完成去除 # 号和后面的注释,然后粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs sh">Syslog <span class="hljs-built_in">yes</span><br>UMask 002<br>OversignHeaders From<br>Socket inet:8891@127.0.0.1<br>Domain example.com <span class="hljs-comment">#本机域名,需要修改</span><br>KeyFile /etc/opendkim/keys/mail.private<br>Selector mail<br>RequireSafeKeys no<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="生成私钥"><a href="#生成私钥" class="headerlink" title="生成私钥"></a>生成私钥</h3><p>下面的 example.com 请替换成您的域名。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">opendkim-genkey -D /etc/opendkim/keys/ -d example.com -s mail && chown -R opendkim:opendkim /etc/opendkim/keys/<br><br></code></pre></td></tr></table></figure><h3 id="配置-Postfix-的-main-cf"><a href="#配置-Postfix-的-main-cf" class="headerlink" title="配置 Postfix 的 main.cf"></a>配置 Postfix 的 main.cf</h3><p>配置发件增加 DKIM 签名。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> >> /etc/postfix/main.cf << <span class="hljs-string">EOF</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>以下内容直接粘贴到命令行窗口中按回车即可。</p><figure class="highlight sh"><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><code class="hljs sh">milter_protocol = 2<br>milter_default_action = accept<br>smtpd_milters = inet:localhost:8891<br>non_smtpd_milters = inet:localhost:8891<br>EOF<br><br></code></pre></td></tr></table></figure><h3 id="启动所有服务"><a href="#启动所有服务" class="headerlink" title="启动所有服务"></a>启动所有服务</h3><p>CentOS 7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl start postfix dovecot opendkim<br><br></code></pre></td></tr></table></figure><p>CentOS 6</p><figure class="highlight crmsh"><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><code class="hljs crmsh">service postfix <span class="hljs-literal">start</span><br>service dovecot <span class="hljs-literal">start</span><br>service opendkim <span class="hljs-literal">start</span><br><br></code></pre></td></tr></table></figure><p>如需开机启动,请执行以下命令。</p><p>CentOS 7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl <span class="hljs-built_in">enable</span> postfix dovecot opendkim mariadb<br><br></code></pre></td></tr></table></figure><p>CentOS 6</p><figure class="highlight sh"><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><code class="hljs sh">chkconfig postfix on<br>chkconfig dovecot on<br>chkconfig opendkim on<br>chkconfig mariadb on<br><br></code></pre></td></tr></table></figure><h2 id="配置域名参数"><a href="#配置域名参数" class="headerlink" title="配置域名参数"></a>配置域名参数</h2><h3 id="A-记录的设定"><a href="#A-记录的设定" class="headerlink" title="A 记录的设定"></a>A 记录的设定</h3><p>在域名控制面板中添加一条记录,<strong>主机记录</strong>填 <strong>@</strong> ,<strong>记录类型</strong>选 <strong>A</strong> <strong>记录值</strong>填 <strong>1.1.1.1</strong> ,其他保持默认,保存即可。</p><h3 id="MX-记录的设定"><a href="#MX-记录的设定" class="headerlink" title="MX 记录的设定"></a>MX 记录的设定</h3><p>在域名控制面板中添加一条记录,<strong>主机记录</strong>填 <strong>@</strong> ,<strong>记录类型</strong>选 <strong>MX</strong> ,<strong>记录值</strong>填 <strong><a href="http://mail.example.com/">http://mail.example.com</a></strong>,其他保持默认;<br>再添加一条记录,<strong>主机记录</strong>填 <strong>mail</strong> ,<strong>记录类型</strong>选 <strong>A</strong> ,<strong>记录值</strong>填 <strong>1.1.1.1</strong>,其他保持默认,保存即可。</p><h3 id="SPF-记录的设定"><a href="#SPF-记录的设定" class="headerlink" title="SPF 记录的设定"></a>SPF 记录的设定</h3><p>在域名控制面板中添加一条记录,<strong>主机记录</strong>填 <strong>@</strong> ,<strong>记录类型</strong>选 <strong>TXT</strong> ,<strong>记录值</strong>填 <strong>v=spf1 mx -all</strong>,其他保持默认,保存即可。</p><h3 id="DMARC-记录的设定"><a href="#DMARC-记录的设定" class="headerlink" title="DMARC 记录的设定"></a>DMARC 记录的设定</h3><p>在域名控制面板中添加一条记录,<strong>主机记录</strong>填 <strong>_dmarc</strong> ,<strong>记录类型</strong>选 <strong>TXT</strong> ,<strong>记录值</strong>填 <strong>v=DMARC1; p=none</strong>,其他保持默认,保存即可。</p><h3 id="DKIM-记录的设定"><a href="#DKIM-记录的设定" class="headerlink" title="DKIM 记录的设定"></a>DKIM 记录的设定</h3><p>(以我的密钥文件为例)<br>执行以下命令</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cat</span> /etc/opendkim/keys/mail.txt <br><br></code></pre></td></tr></table></figure><p>会出现以下结果</p><figure class="highlight sh"><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><code class="hljs sh">mail._domainkeyINTXT( <span class="hljs-string">"v=DKIM1; k=rsa; "</span><br> <span class="hljs-string">"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDU5nkxbS36hOa2FCJqotvahTbxb83CvLt5XqV4WAPzJQmfaA1eHyvOz9XiZgE8vWRgP2jJFlL+J4yEroB3YV/8EBjAM8lFTi31DVgRsoHMwH6f3GuLAfcuVofymDfRxHxPzIlm7rgzfWwrGcPrIzt64NLuZG4yusTWp8MTfWZxvQIDAQAB"</span> ) ; ----- DKIM key default <span class="hljs-keyword">for</span> example.com<br><br></code></pre></td></tr></table></figure><p>把括号内的值复制出来,去掉所有引号并整理成一行,形如:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDU5nkxbS36hOa2FCJqotvahTbxb83CvLt5XqV4WAPzJQmfaA1eHyvOz9XiZgE8vWRgP2jJFlL+J4yEroB3YV/8EBjAM8lFTi31DVgRsoHMwH6f3GuLAfcuVofymDfRxHxPzIlm7rgzfWwrGcPrIzt64NLuZG4yusTWp8MTfWZxvQIDAQAB<br><br></code></pre></td></tr></table></figure><p>在域名控制面板中添加一条记录,<strong>主机记录</strong>填 <strong>mail._domainkey</strong> ,<strong>记录类型</strong>选 <strong>TXT</strong> ,<strong>记录值</strong>填 <strong>v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDU5nkxbS36hOa2FCJqotvahTbxb83CvLt5XqV4WAPzJQmfaA1eHyvOz9XiZgE8vWRgP2jJFlL+J4yEroB3YV/8EBjAM8lFTi31DVgRsoHMwH6f3GuLAfcuVofymDfRxHxPzIlm7rgzfWwrGcPrIzt64NLuZG4yusTWp8MTfWZxvQIDAQAB</strong>,其他保持默认,保存即可。</p><p>最终效果如图</p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/dns_settings.png" alt="dns_settings.png"></p><h3 id="IP-反向解析的设定"><a href="#IP-反向解析的设定" class="headerlink" title="IP 反向解析的设定"></a>IP 反向解析的设定</h3><p>这个……普通家庭宽带用户就别想了,企业宽带和国内云主机用户需要网站通过备案才能设置。如果用国外云主机的话,有些可以支持。为您的域名配置 IP 地址反向解析可以大幅降低外发邮件被拒收的几率。</p><h2 id="过滤沟通-Amavisd-new"><a href="#过滤沟通-Amavisd-new" class="headerlink" title="过滤沟通 Amavisd-new"></a>过滤沟通 Amavisd-new</h2><p><a href="https://blog.51cto.com/freeloda/1246350">安装并配置病毒扫描与垃圾邮件过滤</a></p><h2 id="病毒扫描-ClamAV"><a href="#病毒扫描-ClamAV" class="headerlink" title="病毒扫描 ClamAV"></a>病毒扫描 ClamAV</h2><h2 id="垃圾邮件过滤-SpamAssassin"><a href="#垃圾邮件过滤-SpamAssassin" class="headerlink" title="垃圾邮件过滤 SpamAssassin"></a>垃圾邮件过滤 SpamAssassin</h2><h2 id="Policyd-白名单"><a href="#Policyd-白名单" class="headerlink" title="Policyd 白名单"></a>Policyd 白名单</h2><p><a href="https://wiki.policyd.org/download">官网</a>下载</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">wget https://download.policyd.org/v2.0.14/cluebringer-v2.0.14.tar.gz<br><br></code></pre></td></tr></table></figure><p>解压</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">tar -zxvf cluebringer-v2.0.14.tar.gz<br><br></code></pre></td></tr></table></figure><p>整合初始化 sql</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd ./cluebringer-v2.0.14/database<br><br></code></pre></td></tr></table></figure><p>执行shell</p><figure class="highlight shell"><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><code class="hljs shell">for i in core.tsql access_control.tsql quotas.tsql amavis.tsql checkhelo.tsql checkspf.tsql greylisting.tsql<br>do<br> ./convert-tsql mysql $i<br>done > policyd.sql<br><br></code></pre></td></tr></table></figure><p>使用 vim 替换错误 SQL 语法</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">%s/TYPE=InnoDB/ENGINE=InnoDB/g<br><br></code></pre></td></tr></table></figure><p>建库</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">mysql -u root -p -e <span class="hljs-string">'CREATE DATABASE policyd'</span><br><br></code></pre></td></tr></table></figure><p>初始化数据库</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">mysql -u root -p policyd < policyd.sql<br><br></code></pre></td></tr></table></figure><p>复制文件 cbp/ & awitpt/</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">mkdir</span> /usr/local/lib/cbpolicyd-2.0<br><br></code></pre></td></tr></table></figure><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-built_in">cp</span> -r cbp /usr/local/lib/cbpolicyd-2.0/<br><span class="hljs-built_in">cp</span> -r awitpt/awitpt /usr/local/lib/cbpolicyd-2.0/<br><br></code></pre></td></tr></table></figure><p>复制文件 cbpolicyd 和 cbpadmin</p><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-built_in">cp</span> cbpadmin /usr/local/bin/<br><span class="hljs-built_in">cp</span> cbpolicyd /usr/local/sbin/<br><br></code></pre></td></tr></table></figure><p>设置系统目录</p><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-built_in">mkdir</span> /var/log/cbpolicyd<br><span class="hljs-built_in">mkdir</span> /var/run/cbpolicyd<br><br></code></pre></td></tr></table></figure><p>在 /etc/postfix/main.cf 中配置</p><figure class="highlight apache"><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><code class="hljs apache"><span class="hljs-comment"># policyd</span><br><span class="hljs-attribute">smtpd_recipient_restrictions</span> = check_policy_service inet:<span class="hljs-number">127.0.0.1</span>:<span class="hljs-number">10031</span>,permit_mynetworks,...,...<br><br></code></pre></td></tr></table></figure><h2 id="日志-Awstats"><a href="#日志-Awstats" class="headerlink" title="日志 Awstats"></a>日志 Awstats</h2><h2 id="防爆-fail2ban"><a href="#防爆-fail2ban" class="headerlink" title="防爆 fail2ban"></a>防爆 fail2ban</h2><h3 id="安装-3"><a href="#安装-3" class="headerlink" title="安装"></a>安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install <br><br></code></pre></td></tr></table></figure><p>添加过滤配置 <code>/etc/fail2ban/filter.d/dovecot-pop3imap.conf</code></p><figure class="highlight coq"><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><code class="hljs coq">[<span class="hljs-keyword">Definition</span>]<br>failregex = (?: pop3-login|<span class="hljs-type">imap</span>-login): .*(?:Authentication failure|<span class="hljs-type">Aborted</span> login \(auth failed|<span class="hljs-type">Aborted</span> login \(tried to use disabled|<span class="hljs-type">Disconnected</span> \(auth failed|<span class="hljs-type">Aborted</span> login \(\d+ authentication attempts).*rip=`<HOST>`<br><br></code></pre></td></tr></table></figure><p>添加配置 <code>/etc/fail2ban/jail.conf</code></p><figure class="highlight ini"><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><code class="hljs ini"><span class="hljs-section">[dovecot-pop3imap]</span><br><span class="hljs-attr">enabled</span> = <span class="hljs-literal">true</span><br><span class="hljs-attr">filter</span> = dovecot-pop3imap<br><span class="hljs-attr">action</span> = iptables-multiport[name=dovecot-pop3imap, port=<span class="hljs-string">"pop3,imap"</span>, protocol=tcp]<br><span class="hljs-attr">logpath</span> = /var/log/maillog<br><span class="hljs-attr">maxretry</span> = <span class="hljs-number">20</span><br><span class="hljs-attr">findtime</span> = <span class="hljs-number">1200</span><br><span class="hljs-attr">bantime</span> = <span class="hljs-number">1200</span><br><br></code></pre></td></tr></table></figure><h3 id="运行服务"><a href="#运行服务" class="headerlink" title="运行服务"></a>运行服务</h3><p>启动</p><figure class="highlight sh"><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><code class="hljs sh">service fail2ban start<br><br>systemctl start fail2ban<br><br></code></pre></td></tr></table></figure><p>自启</p><figure class="highlight sh"><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><code class="hljs sh">chkconfig fail2ban on<br><br>systemctl <span class="hljs-built_in">enable</span> fail2ban<br><br></code></pre></td></tr></table></figure><h2 id="配置-Web-邮箱"><a href="#配置-Web-邮箱" class="headerlink" title="配置 Web 邮箱"></a>配置 Web 邮箱</h2><h3 id="安装-Nginx、PHP"><a href="#安装-Nginx、PHP" class="headerlink" title="安装 Nginx、PHP"></a>安装 Nginx、PHP</h3><p>Nginx</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install nginx<br><br></code></pre></td></tr></table></figure><p>PHP</p><p>检查当前版本</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum list installed | grep php<br><br></code></pre></td></tr></table></figure><p>确保可以卸载旧版本</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum remove php*<br><br></code></pre></td></tr></table></figure><p>添加第三方yum源</p><p>CentOs 5.x</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">rpm -Uvh <http://mirror.webtatic.com/yum/el5/latest.rpm<br><br></code></pre></td></tr></table></figure><p>CentOs 6.x</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">rpm -Uvh <http://mirror.webtatic.com/yum/el6/latest.rpm<br><br></code></pre></td></tr></table></figure><p>CentOs 7.X</p><figure class="highlight sh"><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><code class="hljs sh">rpm -Uvh <https://mirror.webtatic.com/yum/el7/epel-release.rpm><br>rpm -Uvh <https://mirror.webtatic.com/yum/el7/webtatic-release.rpm><br><br></code></pre></td></tr></table></figure><p>安装php及其组件</p><figure class="highlight smali"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs smali">yum install php56w-mysql php56w-fpm php56w-xml php56w-mbstring<br><br></code></pre></td></tr></table></figure><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install -y php70w.x86_64 php70w-cli.x86_64 php70w-common.x86_64 php70w-gd.x86_64 php70w-ldap.x86_64 php70w-mbstring.x86_64 php70w-mcrypt.x86_64 php70w-mysql.x86_64 php70w-pdo.x86_64 php70w-fpm.x86_64 php70w-xml.x86_64<br><br></code></pre></td></tr></table></figure><h3 id="下载并安装-Roundcube"><a href="#下载并安装-Roundcube" class="headerlink" title="下载并安装 Roundcube"></a>下载并安装 Roundcube</h3><p>下载</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">wget https://github.com/roundcube/roundcubemail/releases/download/1.3.0/roundcubemail-1.3.0-complete.tar.gz<br><br></code></pre></td></tr></table></figure><p>解压 & 安装</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">tar -xf roundcubemail-1.3.0-complete.tar.gz && <span class="hljs-built_in">mv</span> roundcubemail-1.3.0 /usr/share/roundcube && <span class="hljs-built_in">chown</span> -R apache:apache /usr/share/roundcube<br><br></code></pre></td></tr></table></figure><h3 id="配置-Nginx-、PHP"><a href="#配置-Nginx-、PHP" class="headerlink" title="配置 Nginx 、PHP"></a>配置 Nginx 、PHP</h3><p>配置 Nginx</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">vim /etc/nginx/conf.d/mail.conf<br><br></code></pre></td></tr></table></figure><p>请按实际情况以及注释提示<strong>修改以下内容</strong>,在命令行窗口按下 i ,将内容直接粘贴到命令行窗口中,再按下 ESC ,最后输入 :wq 按回车。</p><figure class="highlight nginx"><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><code class="hljs nginx"><span class="hljs-section">server</span> {<br> <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;<br> <span class="hljs-attribute">server_name</span> mail.example.com; <span class="hljs-comment"># 本机域名前面加上mail. 需要修改</span><br> <span class="hljs-attribute">return</span> <span class="hljs-number">301</span> https://<span class="hljs-variable">$server_name</span><span class="hljs-variable">$request_uri</span>;<br>}<br><br><span class="hljs-section">server</span> {<br> <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl http2;<br> <span class="hljs-attribute">server_name</span> mail.example.com; <span class="hljs-comment"># 本机域名前面加上mail. 需要修改</span><br> <span class="hljs-attribute">ssl_certificate</span> <span class="hljs-string">"/etc/pki/tls/certs/cert.pem"</span>; <span class="hljs-comment"># mail.example.com 证书文件位置,需要修改</span><br> <span class="hljs-attribute">ssl_certificate_key</span> <span class="hljs-string">"/etc/pki/tls/certs/key.pem"</span>; <span class="hljs-comment"># mail.example.com 证书私钥文件位置,需要修改</span><br> <span class="hljs-attribute">add_header</span> Strict-Transport-Security <span class="hljs-string">"max-age=15552000; includeSubDomains"</span>;<br> <span class="hljs-section">location</span> / {<br> <span class="hljs-attribute">root</span> /usr/share/roundcube;<br> <span class="hljs-attribute">index</span> index.php; <br> }<br><span class="hljs-section">location</span> <span class="hljs-regexp">~ .php$</span> {<br> <span class="hljs-attribute">root</span> /usr/share/roundcube;<br> <span class="hljs-attribute">fastcgi_pass</span> <span class="hljs-number">127.0.0.1:9000</span>;<br> <span class="hljs-attribute">fastcgi_index</span> index.php;<br> <span class="hljs-attribute">fastcgi_param</span> SCRIPT_FILENAME <span class="hljs-variable">$document_root</span><span class="hljs-variable">$fastcgi_script_name</span>;<br> <span class="hljs-attribute">include</span> fastcgi_params;<br> }<br><br>}<br><br></code></pre></td></tr></table></figure><p><strong>配置 PHP</strong></p><p>设置时区</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">echo</span> <span class="hljs-string">"date.timezone = Asia/Shanghai"</span> >> /etc/php.ini <br><br></code></pre></td></tr></table></figure><p>创建会话文件夹</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">mkdir</span> /var/lib/php/session && <span class="hljs-built_in">chown</span> apache:apache /var/lib/php/session<br><br></code></pre></td></tr></table></figure><h3 id="配置数据库"><a href="#配置数据库" class="headerlink" title="配置数据库"></a>配置数据库</h3><p>进入 MySQL 命令行界面</p><figure class="highlight text"><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><code class="hljs text">mysql -u root -p<br><br>Enter password: #输入密码按回车<br><br></code></pre></td></tr></table></figure><p>创建一个用户用于读写 Roundcube 数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';<br><br></code></pre></td></tr></table></figure><p>创建 Roundcube 数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mysql">CREATE DATABASE roundcube;<br><br></code></pre></td></tr></table></figure><p>为用户授予读写权限</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mysql">GRANT ALL ON roundcube.* TO 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';<br><br></code></pre></td></tr></table></figure><p>刷新权限表</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mysql">FLUSH PRIVILEGES;<br><br></code></pre></td></tr></table></figure><p>数据库设置完成</p><h3 id="启动服务"><a href="#启动服务" class="headerlink" title="启动服务"></a>启动服务</h3><p>CentOS 6</p><figure class="highlight crmsh"><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><code class="hljs crmsh">service nginx <span class="hljs-literal">start</span><br>service php-fpm <span class="hljs-literal">start</span><br><br></code></pre></td></tr></table></figure><p>CentOS 7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl start nginx php-fpm<br><br></code></pre></td></tr></table></figure><p>如需开机启动,请执行以下命令。</p><p>CentOS 6</p><figure class="highlight nginx"><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><code class="hljs nginx"><span class="hljs-attribute">chkconfig</span> nginx <span class="hljs-literal">on</span><br>chkconfig php-fpm <span class="hljs-literal">on</span><br><br></code></pre></td></tr></table></figure><p>CentOS 7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh">systemctl <span class="hljs-built_in">enable</span> nginx php-fpm<br><br></code></pre></td></tr></table></figure><h3 id="配置-Roundcube"><a href="#配置-Roundcube" class="headerlink" title="配置 Roundcube"></a>配置 Roundcube</h3><p>打开浏览器,输入 <a href="https://mail.example.com/installer/?_step=1">https://mail.example.com/installer/?_step=1</a> ,回车打开。然后按图片提示进行配置。</p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_01.png" alt="roundcube_01.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_02.png" alt="roundcube_02.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_03.png" alt="roundcube_03.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_04.png" alt="roundcube_04.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_05.png" alt="roundcube_05.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_06.png" alt="roundcube_06.png"></p><p><img src="/../../static/image/%E8%87%AA%E9%80%89%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-CentOS/roundcube_07.png" alt="roundcube_07.png"></p><p>配置完成后,关闭浏览器页面。执行以下命令来使安装程序不可用。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">chmod -R 000 /usr/share/roundcube/installer/<br><br></code></pre></td></tr></table></figure><h3 id="登录测试"><a href="#登录测试" class="headerlink" title="登录测试"></a>登录测试</h3><p>打开浏览器,输入 <a href="http://link.zhihu.com/?target=https://mail.example.com/">https://mail.example.com</a> ,然后输入您的用户名密码登录,测试收发邮件</p><h3 id="日志-2"><a href="#日志-2" class="headerlink" title="日志"></a>日志</h3><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs awk"><span class="hljs-regexp">/usr/</span>share<span class="hljs-regexp">/roundcube/</span>logs/*<br><br></code></pre></td></tr></table></figure><h2 id="测试工具"><a href="#测试工具" class="headerlink" title="测试工具"></a>测试工具</h2><p>打开浏览器,输入 <a href="http://www.mail-tester.com/">http://www.mail-tester.com</a> ,回车打开。发一封邮件到它指定的邮箱里。然后过一分钟左右查看下结果,重点检查 SPF 记录、DMARC 记录和 DKIM 签名是不是有效的。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://zhuanlan.zhihu.com/p/28816035">在 CentOS 7 上搭建属于自己的 “完美” 邮件系统</a></p>]]></content>
<categories>
<category>Roundcube WebMail</category>
</categories>
<tags>
<tag>Roundcube WebMail</tag>
</tags>
</entry>
<entry>
<title>自建邮件服务器-开源集成方案iRedMail</title>
<link href="/2019/03/22/iRedMail/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/"/>
<url>/2019/03/22/iRedMail/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/</url>
<content type="html"><![CDATA[<p>自建邮件服务器-开源集成方案iRedMail</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>系统 CentOS 7</p><p>域名 example.com</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><ol><li><p>设置 hostname</p><p>RHEL/CentOS 6,<code>/etc/sysconfig/network</code></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">HOSTNAME=mail<br></code></pre></td></tr></table></figure><p>CentOS 7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">hostnamectl set-hostname mail<br></code></pre></td></tr></table></figure><p>例如完整域名是 mail.example.com,应该设置 mail</p><p>验证主机名</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">hostname -f<br></code></pre></td></tr></table></figure></li><li><p>设置 host</p><figure class="highlight accesslog"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs accesslog"><span class="hljs-number">127.0.0.1</span> mail.example.com mail localhost localhost.localdomain<br></code></pre></td></tr></table></figure><p>FQDN 主机名列在第一个,短主机名列在第二个</p></li><li><p>设置 SELinux</p><p>查看现状态</p><p>iRedMail 不支持 SELinux,所以需要在 /etc/selinux/config 文件里禁用它</p><figure class="highlight abnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs abnf"><span class="hljs-attribute">SELINUX</span><span class="hljs-operator">=</span>disabled<br></code></pre></td></tr></table></figure><p>如果不希望禁用 SELinux,可以设置为让它打印警告信息但不强制限制</p><figure class="highlight abnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs abnf"><span class="hljs-attribute">SELINUX</span><span class="hljs-operator">=</span>permissive<br></code></pre></td></tr></table></figure></li><li><p>上传最新版 iRedMail 安装包</p></li><li><p>解压 iRedMail</p><p>安装解压工具</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">yum install bzip2<br></code></pre></td></tr></table></figure><p>解压</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cd</span> /program<br></code></pre></td></tr></table></figure><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">tar xjf iRedMail-x.y.z.tar.bz2<br></code></pre></td></tr></table></figure></li><li><p>运行安装程序</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cd</span> /root/iRedMail-x.y.z/<br>bash iRedMail.sh<br></code></pre></td></tr></table></figure></li><li><p>安装过程</p><p>以下是官方安装过程</p><ul><li>欢迎和感谢使用</li></ul><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/welcome.png" alt="welcome.png"></p><ul><li>指定用于存储用户邮箱的路径。默认是 <code>/var/vmail/</code>。</li></ul><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/mail_storage.png" alt="mail_storage.png"></p><ul><li>选择用于存储邮件账号的数据库。</li></ul><p>Note</p><p>各个数据库之间没有太大区别,建议使用自己熟悉的数据库,便于后期维护。</p><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/backends.png" alt="backends.png"></p><ul><li>如果选择 OpenLDAP 数据库用于存储邮件账号,安装程序会要求你输入 LDAP 前缀:</li></ul><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/ldap_suffix.png" alt="ldap_suffix.png"></p><p>MySQL/MariaDB/PostgreSQL 用户</p><p>如果选择 MySQL/MariaDB/PostgreSQL 用于存储邮件账号, 安装程序会为数据库的 root 用户生成一个随机的强密码,安装完成后可以在 <code>iRedMail.tips</code> 文件里找到。</p><ul><li>添加第一个邮件域名</li></ul><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/first_domain.png" alt="first_domain.png"></p><p>这里填 example.com</p><ul><li>设置邮件管理员的密码</li></ul><p>Note</p><p>该账号即是邮件管理员,也是普通的邮件账号,可以登录管理后台和 webmail。</p><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/admin_pw.png" alt="admin_pw.png"></p><ul><li>可选的组件</li></ul><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/optional_components.png" alt="optional_components.png"></p><p>回答完上面的几个问题之后,安装程序给出本次安装的基本信息并要求确认是否实际 执行安装,请输入 <code>y</code> 或 <code>Y</code> 并按回车键确认,或 <code>n</code>, <code>N</code> 并按回车键中止安装。</p><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/review.png" alt="review.png"></p></li></ol><p>安装完成后你必须知道的几个重要事项</p><ul><li>邮件服务器最薄弱的环节是用户的弱密码,所以请一定强制你的用户使用强度高的密码。</li><li>阅读 /root/iRedMail-x.y.z/iRedMail.tips 文件,它包含了:<ul><li>各个 web 程序的访问地址(URL),用户名和密码。</li><li>各个组件的配置文件路径。除此之外还应该阅读文档:Locations of configuration and log files of major components.</li><li>以及其它一些重要和敏感信息</li></ul></li><li>设置 DNS 记录</li><li>如何配置邮件客户端程序</li><li>强烈建议获取 SSL 证书以避免每次访问 web 程序时烦人的自签名 SSL 证书警告, Let’s Encrypt 提供免费的 SSL 证书。可根据该文档 配置获取的证书:use a SSL certificate.</li><li>如果需要批量添加邮件账号,可以参考以下针对不同数据库的批量建账号的文档: OpenLDAP, MySQL/MariaDB/PostgreSQL。</li><li>如果这是一台繁忙的服务器,这里有一些提升性能的建议。</li></ul><p>访问 webmail 和其它 web 程序<br>安装完成后,可以通过以下 URL 访问相关程序。注意:请将 <server> 替换为实际的 服务器地址。</p><ul><li>Roundcube webmail: <a href="https://mail.example.com/mail/">https://mail.example.com/mail/</a></li><li>SOGo Groupware: <a href="https://mail.example.com/SOGo">https://mail.example.com/SOGo</a></li><li>Web 管理后台: httpS://mail.example.com/iredadmin/</li><li>Awstats: httpS://mail.example.com/awstats/awstats.pl?config=web (or <code>?config=smtp</code> for SMTP log)</li></ul><h1 id="DNS-设置"><a href="#DNS-设置" class="headerlink" title="DNS 设置"></a>DNS 设置</h1><p><img src="/../../static/image/%E8%87%AA%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8-%E5%BC%80%E6%BA%90%E9%9B%86%E6%88%90%E6%96%B9%E6%A1%88iRedMail/dns_settings.png" alt="dns_settings.png"></p><p>SPF</p><p>SPF 记录是一种域名服务 (DNS) 记录,可确定允许哪些邮件服务器代表您的域来发送电子邮件</p><p>这里说几个常用的:</p><ol><li>a:所有该域名的A记录都为通过,a不指定的情况下为当前域名</li><li>ip4:指定通过的IP</li><li>mx:mx记录域名的A记录IP可以发邮件</li><li>all:结束标志,“-”表示只允许设置的记录为通过,“~”表示失败,通常用于测试,“+”表示忽略SPF</li></ol><p>DKIM</p><p>centos6</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">amavisd showkeys<br></code></pre></td></tr></table></figure><p>centos7</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">amavisd -c /etc/amavisd/amavisd.conf showkeys<br></code></pre></td></tr></table></figure><p>复制结果,添加到DNS,主机记录为<code>dkim._domainkey</code>,<code>TXT</code>类别</p><p>DMARC</p><p>DMARC协议是有效解决信头From伪造而诞生的一种新的邮件来源验证手段,为邮件发件人地址提供强大保护,并在邮件收发双方之间建立起一个数据反馈机制。</p><ul><li>p:用于告知收件方,当检测到某邮件存在伪造我(发件人)的情况,收件方要做出什么处理,处理方式从轻到重依次为:none为不作任何处理;quarantine为将邮件标记为垃圾邮件;reject为拒绝该邮件。初期建议设置为none。</li><li>rua:用于在收件方检测后,将一段时间的汇总报告,发送到哪个邮箱地址。</li><li>ruf:用于当检测到伪造邮件时,收件方须将该伪造信息的报告发送到哪个邮箱地址。</li></ul><p>例如我设置的是v=DMARC1;p=reject;rua=<a href="mailto:xx@lxx.com">xx@lxx.com</a>,意思是拒绝伪造邮件,并且将一段时间的汇总报告发送给我。</p><p>添加到DNS</p><p>添加TXT记录,主机名:<code>_dmarc</code>,记录值:<code>v=DMARC1;p=reject;rua=xx@lxx.com</code></p><p>PTR</p><p>将 mail.xx.com 设置PTR</p><p>添加好了以后可以通过以下命令查看</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">dig -x IP<br></code></pre></td></tr></table></figure><p>如果看到了PTR记录为你的域名那就说明成功了。</p><p>测试工具</p><p><a href="http://www.mail-tester.com/">http://www.mail-tester.com/</a></p><p><strong>iRedmail 主要组件</strong></p><ul><li>Apache</li><li>Nginx </li><li>PHP</li><li>MySQL</li><li>OpenLDAP</li><li>Postfix</li><li>Dovecot</li><li>Amavisd</li><li>ClamAV</li><li>SpamAssassin</li><li>Policyd</li><li>Pysieved</li><li>Roundcube</li><li>Awstats 6.9</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://docs.iredmail.org/install.iredmail.on.rhel-zh_CN.html">iRedMail doc</a></p><p><a href="lomu.me/post/SPF-DKIM-DMARC-PTR">邮件服务器添加SPF、DKIM、DMARC、PTR提高送达率</a></p>]]></content>
<categories>
<category>iRedMail</category>
</categories>
<tags>
<tag>iRedMail</tag>
</tags>
</entry>
<entry>
<title>Docker 安装 GitLab 教程</title>
<link href="/2019/02/05/docker/Docker%20%E5%AE%89%E8%A3%85%20Gitlab/"/>
<url>/2019/02/05/docker/Docker%20%E5%AE%89%E8%A3%85%20Gitlab/</url>
<content type="html"><![CDATA[<p><a href="about.gitlab.com">GitLab</a> 是由GitLab Inc.开发,使用<a href="https://zh.wikipedia.org/wiki/MIT%E8%A8%B1%E5%8F%AF%E8%AD%89">MIT许可证</a>的基于<a href="https://zh.wikipedia.org/wiki/%E4%BA%92%E8%81%94%E7%BD%91">网络</a>的<a href="https://zh.wikipedia.org/wiki/Git">Git</a><a href="https://zh.wikipedia.org/wiki/%E4%BB%93%E5%BA%93_(%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6)">仓库</a>管理工具,且具有<a href="https://zh.wikipedia.org/wiki/Wiki">wiki</a>和<a href="https://zh.wikipedia.org/wiki/%E4%BA%8B%E5%8A%A1%E8%B7%9F%E8%B8%AA%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F">issue跟踪</a>功能。常用于企业内部代码管理。</p><span id="more"></span><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><p>操作系统:Centos7</p><p>SeLinux:开启</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>启动容器</p><figure class="highlight sh"><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><code class="hljs sh">docker run --detach \<br> --publish 2443:443 --publish 2280:80 --publish 2222:22 \<br> --name gitlab \<br> --hostname 10.10.18.155 \<br> --restart unless-stopped \<br> --volume /program/gitlab/config:/etc/gitlab:Z \<br> --volume /program/gitlab/logs:/var/log/gitlab:Z \<br> --volume /program/gitlab/data:/var/opt/gitlab:Z \<br> gitlab/gitlab-ce:latest<br></code></pre></td></tr></table></figure><h2 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h2><p>查看 <code>GitLab</code> 版本号</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">exec</span> -it gitlab <span class="hljs-built_in">cat</span> /opt/gitlab/embedded/service/gitlab-rails/VERSION<br></code></pre></td></tr></table></figure><p><strong>1. Docker volume 方式</strong></p><p>注意:该方法移动文件时,一定要保持原文件权限!!!</p><p>复制 volume</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">cp</span> -rfp /program/gitlab /program/gitlab.bak<br></code></pre></td></tr></table></figure><p>压缩/打包 volume</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">tar -czvp -f /program/gitlab.tar.gz /program/gitlab.bak<br></code></pre></td></tr></table></figure><p>解压/解包 volume</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">tar -xzvp -f /program/gitlab.tar.gz<br></code></pre></td></tr></table></figure><p><strong>2. GitLab备份方式</strong></p><p>备份数据</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">exec</span> gitlab gitlab-rake gitlab:backup:create<br></code></pre></td></tr></table></figure><p>拷贝到物理机</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">cp</span> gitlab:/xxx /program/<br></code></pre></td></tr></table></figure><p>还原</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker <span class="hljs-built_in">exec</span> -it <name of container> gitlab-rake gitlab:backup:restore<br></code></pre></td></tr></table></figure><h2 id="修复克隆地址"><a href="#修复克隆地址" class="headerlink" title="修复克隆地址"></a>修复克隆地址</h2><p>修改配置文件</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-attribute">vim</span> gitlab.rb<br></code></pre></td></tr></table></figure><p>修改正确的IP/地址</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-attribute">external_url</span> <span class="hljs-string">'http://git.taigu.org'</span><br></code></pre></td></tr></table></figure><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><ol><li><p>权限问题</p><p>执行修复指令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo docker <span class="hljs-built_in">exec</span> -it gitlab update-permissions<br></code></pre></td></tr></table></figure></li><li><p><code>gitaly.pid</code>错误</p><p>删除 <code>gitlab/data/gitaly/gitaly.pid</code></p></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://docs.gitlab.com/omnibus/docker/">docker install gitlab</a></p><p><a href="https://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-for-omnibus-installations">docker gitlab backup</a></p>]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>GitLab</tag>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title>Docker 安装教程</title>
<link href="/2019/01/30/docker/docker%20%E5%AE%89%E8%A3%85/"/>
<url>/2019/01/30/docker/docker%20%E5%AE%89%E8%A3%85/</url>
<content type="html"><![CDATA[<p><a href="www.docker.com">Docker</a> 是常用的软件容器平台,可快速创建、部署服务,提高生产效率。</p><span id="more"></span><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><p>操作系统:Centos7</p><p>SeLinux:开启</p><h2 id="存储库安装"><a href="#存储库安装" class="headerlink" title="存储库安装"></a>存储库安装</h2><p><strong>卸载旧</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo yum remove \<br> docker \<br> docker-client \<br> docker-client-latest \<br> docker-common \<br> docker-latest \<br> docker-latest-logrotate \<br> docker-logrotate \<br> docker-engine<br></code></pre></td></tr></table></figure><p><strong>安装需要的包</strong></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></pre></td><td class="code"><pre><code class="hljs bash">sudo yum install -y \<br> yum-utils \<br> device-mapper-persistent-data \<br> lvm2<br></code></pre></td></tr></table></figure><p><strong>设置稳定存储库</strong></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></pre></td><td class="code"><pre><code class="hljs bash">sudo yum-config-manager \<br> --add-repo \<br> https://download.docker.com/linux/centos/docker-ce.repo<br></code></pre></td></tr></table></figure><p><strong>安装新 Docker</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo yum install docker-ce docker-ce-cli containerd.io<br></code></pre></td></tr></table></figure><p><strong>国内加速</strong></p><p>阿里云搜索 <code>容器镜像加速</code>,进入,找到 <code>镜像加速器</code>,选择相应的操作系统,执行相关指令</p><p>例如:Centos7</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo <span class="hljs-built_in">mkdir</span> -p /etc/docker<br></code></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim <span class="hljs-built_in">tee</span> /etc/docker/daemon.json<br></code></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">{</span><br> <span class="hljs-attr">"registry-mirrors"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"https://xxxxxx.mirror.aliyuncs.com"</span><span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">}</span><br></code></pre></td></tr></table></figure><p>如果已经启动 docker,需要重启服务生效</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><code class="hljs bash">sudo systemctl daemon-reload<br>sudo systemctl restart docker<br></code></pre></td></tr></table></figure><p><strong>选择大容量分区</strong></p><p>查看分区空间使用情况</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">df</span> -h<br></code></pre></td></tr></table></figure><p>设置 docker 根目录</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">vim /etc/docker/daemon.json<br></code></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">{</span><br> <span class="hljs-attr">"graph"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"/home/docker"</span><br><span class="hljs-punctuation">}</span><br></code></pre></td></tr></table></figure><p>如果已经启动 docker,需要重启服务生效</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><code class="hljs bash">sudo systemctl daemon-reload<br>sudo systemctl restart docker<br></code></pre></td></tr></table></figure><p><strong>启动</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo systemctl start docker<br></code></pre></td></tr></table></figure><p><strong>自启</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo systemctl <span class="hljs-built_in">enable</span> docker<br></code></pre></td></tr></table></figure><p><strong>关闭自启</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">sudo systemctl <span class="hljs-built_in">disable</span> docker<br></code></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://docs.docker.com/install/linux/docker-ce/centos/">docker</a></p>]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>微服务</tag>
</tags>
</entry>
<entry>
<title>hexo 快速开始</title>
<link href="/2019/01/29/hexo-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B/"/>
<url>/2019/01/29/hexo-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B/</url>
<content type="html"><![CDATA[<p><a href="hexo.io">hexo</a> 是常用的静态网页构建工具,支持markdown,拥有丰富的插件。常用于构建个人博客。</p><span id="more"></span><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><p><code>OS</code> window 10 1809</p><p><code>node</code> 10.13.0</p><p><code>npm</code> 6.6.0</p><p><code>PowerShell</code> 5.1.17763.134</p><p><code>hexo</code> 3.8.0</p><p><code>hexo-cli</code> 1.1.0</p><h2 id="开始构建"><a href="#开始构建" class="headerlink" title="开始构建"></a>开始构建</h2><p><code>shift+右键</code> 启动 <code>PowerShell</code>,创建 hexo 目录(构建器项目)</p><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs arduino">mkdir hexo<br></code></pre></td></tr></table></figure><p>进入目录</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> hexo<br></code></pre></td></tr></table></figure><p>全局安装 <code>hexo-cli</code>,如果还没有安装<a href="nodejs.org">nodejs</a>和<a href="git-scm.com">git</a>,请参考<a href="hexo.io/zh-cn/docs">hexo文档</a>,此处不再重复记录</p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs avrasm">npm install -g hexo-<span class="hljs-keyword">cli</span><br></code></pre></td></tr></table></figure><p>初始化 hexo</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">hexo <span class="hljs-keyword">init</span><br></code></pre></td></tr></table></figure><p>安装依赖包</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmake">npm <span class="hljs-keyword">install</span><br></code></pre></td></tr></table></figure><p>生成静态资源</p><figure class="highlight verilog"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs verilog">hexo <span class="hljs-keyword">generate</span> 或 hexo g<br></code></pre></td></tr></table></figure><p>本地启动sever测试</p><figure class="highlight axapta"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs axapta">hexo <span class="hljs-keyword">server</span><br></code></pre></td></tr></table></figure><p>浏览器访问 <a href="localhost:4000">localhost:4000</a></p><p>可以看到一个简单的个人博客系统已经完成了!</p><p>参考 <a href="https://hexo.io/zh-cn/docs/">hexo docs</a></p><h2 id="更换主题"><a href="#更换主题" class="headerlink" title="更换主题"></a>更换主题</h2><p>这里以 <a href="https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/README.md">Next v6.0.x </a>为例子</p><p>下载主题</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs vim"><span class="hljs-keyword">cd</span> hexo<br>git clone https://github.<span class="hljs-keyword">com</span>/theme-<span class="hljs-keyword">next</span>/hexo-theme-<span class="hljs-keyword">next</span> themes/<span class="hljs-keyword">next</span><br></code></pre></td></tr></table></figure><p>修改主题,修改<code>./_config.yml</code>中的<code>theme</code>为<code>next</code></p><p>注意区分<code>./themes/next/_config.yml</code></p><p>为了方便区分,下面更换称呼</p><p><code>./_config.yml</code>称为系统配置</p><p><code>./themes/next/_config.yml</code>称为主题配置</p><figure class="highlight vbnet"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs vbnet"><span class="hljs-symbol">theme:</span> <span class="hljs-keyword">next</span><br><br></code></pre></td></tr></table></figure><p>保存</p><p>清理缓存</p><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">hexo clean</span><br><br></code></pre></td></tr></table></figure><p>重新构建</p><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">hexo g</span><br><br></code></pre></td></tr></table></figure><p>本地启动</p><figure class="highlight axapta"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs axapta">hexo <span class="hljs-keyword">server</span><br><br></code></pre></td></tr></table></figure><p>浏览器访问 <a href="localhost:4000">localhost:4000</a></p><p>成功更换主题!</p><h2 id="Hexo-优化"><a href="#Hexo-优化" class="headerlink" title="Hexo 优化"></a>Hexo 优化</h2><ol><li><p>基本设置<a href="hexo.io/zh-cn/docs/configuration.html">参考</a></p><p>title: xxx’s blog<br>author: xxx<br>language: zh-CN<br>timezone: Asia/Shanghai</p></li></ol><h2 id="主题优化"><a href="#主题优化" class="headerlink" title="主题优化"></a>主题优化</h2><ol><li><p>添加评论系统</p><p>此处使用 <a href="valine.js.org">valine</a></p><p>参考 <a href="valine.js.org/quickstart.html">快速申请</a> 申请 <code>leancloud</code> 账号,并获取 <code>APP ID</code> 和 <code>APP Key</code></p><p>打开主题配置<code>./themes/next/_config.yml</code></p><p>定位到 <code>valine</code></p><figure class="highlight vbnet"><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><code class="hljs vbnet"><span class="hljs-symbol">enable:</span> <span class="hljs-literal">true</span><br><span class="hljs-symbol">appid:</span> <APP ID><br><span class="hljs-symbol">appkey:</span> <APP <span class="hljs-keyword">Key</span>><br><br></code></pre></td></tr></table></figure></li><li><p>设置侧边栏</p><p>在主题配置中,找到<code>sidebar</code></p><p>例如修改显示方式 <code>display</code></p><figure class="highlight maxima"><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><code class="hljs maxima">#<span class="hljs-built_in">display</span>: post 默认<br>#<span class="hljs-built_in">display</span>: always 总是显示<br><span class="hljs-built_in">display</span>: hide 开始隐藏<br>#<span class="hljs-built_in">display</span>: <span class="hljs-built_in">remove</span> 不显示<br><br></code></pre></td></tr></table></figure></li><li><p>导航菜单</p><p>在主题配置中,找到<code>menu</code></p><p>注释<code>tags</code>等,读者可以自由选择</p><figure class="highlight subunit"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs subunit"><span class="hljs-keyword">tags:</span> /tags/ || tags<br><br></code></pre></td></tr></table></figure><p>在根目录执行指令</p><figure class="highlight haxe"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs haxe">hexo <span class="hljs-keyword">new</span> <span class="hljs-type">page</span> <span class="hljs-string">"tags"</span><br><br></code></pre></td></tr></table></figure><p>增加内容,关闭评论和指定页面类型</p><figure class="highlight vbnet"><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><code class="hljs vbnet"><span class="hljs-symbol">comments:</span> <span class="hljs-literal">false</span><br><span class="hljs-symbol">type:</span> <span class="hljs-string">"tags"</span><br><br></code></pre></td></tr></table></figure><p>执行后,会创建文件夹<code>./source/tags/</code>,并在里面初始化<code>tags</code>页面的md文件<code>index.md</code></p><p>重新构建后,可以看到页面多了一个导航栏<code>tags</code></p> <figure class="highlight axapta"><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><code class="hljs axapta">hexo clean<br>hexo g<br>hexo <span class="hljs-keyword">server</span><br></code></pre></td></tr></table></figure></li><li><p>文章阅读数leancloud_visitors</p><p>在主题配置中,找到<code>leancloud_visitors</code></p><p>填写 AppID 和 AppKey</p><figure class="highlight vbnet"><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><code class="hljs vbnet"><span class="hljs-symbol">appid:</span> <APP ID><br><span class="hljs-symbol">appkey:</span> <APP <span class="hljs-keyword">Key</span>><br><br></code></pre></td></tr></table></figure></li><li><p>社交</p><p>在主题配置中,找到<code>social</code></p><p>启用 <code>social</code></p><p>添加相应的社交工具以及账号</p></li><li><p>Next主题开启字数统计及阅读时长</p><p>安装 hexo-symbols-count-time</p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs applescript">npm i hexo-symbols-<span class="hljs-built_in">count</span>-<span class="hljs-built_in">time</span> -D<br><br></code></pre></td></tr></table></figure><p>hexo 配置文件增加</p><figure class="highlight yaml"><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><code class="hljs yaml"><span class="hljs-attr">symbols_count_time:</span><br> <span class="hljs-attr">symbols:</span> <span class="hljs-literal">true</span> <span class="hljs-comment"># 文章字数</span><br> <span class="hljs-attr">time:</span> <span class="hljs-literal">true</span> <span class="hljs-comment"># 阅读时长</span><br> <span class="hljs-attr">total_symbols:</span> <span class="hljs-literal">true</span> <span class="hljs-comment"># 所有文章总字数</span><br> <span class="hljs-attr">total_time:</span> <span class="hljs-literal">true</span> <span class="hljs-comment"># 所有文章阅读中时长</span><br><br></code></pre></td></tr></table></figure><p>Next 主题配置文件</p><figure class="highlight yaml"><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><code class="hljs yaml"><span class="hljs-attr">symbols_count_time:</span><br> <span class="hljs-attr">separated_meta:</span> <span class="hljs-literal">true</span><br> <span class="hljs-attr">item_text_post:</span> <span class="hljs-literal">true</span><br> <span class="hljs-attr">item_text_total:</span> <span class="hljs-literal">false</span><br> <span class="hljs-attr">awl:</span> <span class="hljs-number">4</span><br> <span class="hljs-attr">wpm:</span> <span class="hljs-number">275</span><br><br></code></pre></td></tr></table></figure></li><li><p>永久连接</p><p>安装uuid</p><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">npm install hexo-<span class="hljs-built_in">uuid</span> <span class="hljs-comment">--save</span><br><br></code></pre></td></tr></table></figure><p>修改hexo配置文件</p><figure class="highlight elixir"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs elixir"><span class="hljs-symbol">permalink:</span> <span class="hljs-symbol">:year</span><span class="hljs-symbol">:month</span><span class="hljs-symbol">:day</span><span class="hljs-symbol">:uuid/</span><br><br></code></pre></td></tr></table></figure></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="zhuanlan.zhihu.com/p/33616481">hexo+github博客搭建之路</a></p><p><a href="hexo.io">hexo.io</a></p>]]></content>
<tags>
<tag>hexo</tag>
<tag>blog</tag>
</tags>
</entry>
<entry>
<title>Making a round picture with PowerPoint 2016</title>
<link href="/2018/07/26/Making-a-round-picture-with-PowerPoint-2016/"/>
<url>/2018/07/26/Making-a-round-picture-with-PowerPoint-2016/</url>
<content type="html"><![CDATA[<p>Sometime we need to make a round image, but there are no tools like <a href="https://www.photoshop.com/">Photoshop</a> and <a href="https://www.gimp.org/">GIMP</a> on our computer. However, we don’t want to install these tools. Is there an easy way? Yes, we can do it with <a href="https://products.office.com/en-us/powerpoint">PowerPoint 2016</a>. </p><span id="more"></span><h3 id="Open-your-PowerPoint-2016-and-insert-an-image"><a href="#Open-your-PowerPoint-2016-and-insert-an-image" class="headerlink" title="Open your PowerPoint 2016 and insert an image"></a>Open your PowerPoint 2016 and insert an image</h3><p>Making-a-round-picture-with-PowerPoint-2016<img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0001.png" alt="0001.png"></p><h3 id="Insert-a-circle"><a href="#Insert-a-circle" class="headerlink" title="Insert a circle"></a>Insert a circle</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0002.png" alt="0002.png"></p><h3 id="Adjust-style"><a href="#Adjust-style" class="headerlink" title="Adjust style"></a>Adjust style</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0003.png" alt="0003.png"></p><h3 id="Adjust-the-position-of-the-circle-the-result-is-as-follows"><a href="#Adjust-the-position-of-the-circle-the-result-is-as-follows" class="headerlink" title="Adjust the position of the circle, the result is as follows:"></a>Adjust the position of the circle, the result is as follows:</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0004.png" alt="0003.png"></p><h3 id="Next-merge-the-circle-and-the-image-select-the-circle-and-image-note-be-sure-to-select-the-image-first"><a href="#Next-merge-the-circle-and-the-image-select-the-circle-and-image-note-be-sure-to-select-the-image-first" class="headerlink" title="Next merge the circle and the image, select the circle and image (note: be sure to select the image first)"></a>Next merge the circle and the image, select the circle and image (note: be sure to select the image first)</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0005.png" alt="0003.png"></p><h3 id="Then-merge"><a href="#Then-merge" class="headerlink" title="Then merge"></a>Then merge</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0006.png" alt="0003.png"></p><h3 id="Ok-now-we-have-the-picture-we-want-We-can-save-it-to-use-it"><a href="#Ok-now-we-have-the-picture-we-want-We-can-save-it-to-use-it" class="headerlink" title="Ok, now we have the picture we want. We can save it to use it"></a>Ok, now we have the picture we want. We can save it to use it</h3><p><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0007.png" alt="0003.png"><br><img src="/../static/image/Making-a-round-picture-with-PowerPoint-2016/0008.png" alt="0003.png"><br>In this way, we use <a href="https://products.office.com/en-us/powerpoint">PowerPoint 2016</a> to make a round image.</p>]]></content>
<tags>
<tag>PowerPoint2016</tag>
</tags>
</entry>
</search>