<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>叉叉哥的BLOG</title>
  
  <subtitle>Python毁一生，Java穷三代，两者皆不沾，必成高富帅！</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://xxgblog.com/"/>
  <updated>2026-07-14T13:16:26.687Z</updated>
  <id>https://xxgblog.com/</id>
  
  <author>
    <name>wucao</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>基于 embedding 模型和向量数据库搭建以图搜图应用</title>
    <link href="https://xxgblog.com/2026/07/14/image-vector-search/"/>
    <id>https://xxgblog.com/2026/07/14/image-vector-search/</id>
    <published>2026-07-14T03:14:16.000Z</published>
    <updated>2026-07-14T13:16:26.687Z</updated>
    
    <content type="html"><![CDATA[<p>搜索引擎提供的以图搜图功能，电商软件里拍照就能搜同款，音乐软件中的听歌识曲，视频网站上一张截图就能搜出来源于哪部电影或电视剧，乃至人脸识别和检索，以及智能体中常用的 RAG，底层都普遍采用同一项关键技术：向量检索。<br><img src="/img/image-vector-search-case.jpg" alt="电商“搜同款”功能"></p><h2 id="万物皆向量"><a href="#万物皆向量" class="headerlink" title="万物皆向量"></a>万物皆向量</h2><h3 id="什么是向量"><a href="#什么是向量" class="headerlink" title="什么是向量"></a>什么是向量</h3><p>在数学中，向量（vector）指具有大小和方向的量。它可以形象化地表示为带箭头的线段：</p><ul><li>箭头所指：代表向量的方向</li><li>线段长度：代表向量的大小（即模长）</li></ul><p><img src="/img/image-vector-search-vector.jpg" alt="一个二维向量"></p><p>在编程语言中，向量可以用一个数组来表示，向量的维度即为这个数组中元素的个数。例如上图是一个二维向量，可以用一个包含两个元素的数组表示：<code>v = [3, 4]</code>。</p><p>如果数组中有 512 个元素，那么向量就是 512 维的。虽然 512 维向量无法用图片直观画出来（因为我们所在的世界是三维空间，512 维对于三维生物来说太抽象了），但是，在数学中，它确实存在。以下是一个多维向量实例：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 用一个 512 个元素的数组表示一个 512 维向量</span></span><br><span class="line">v = [<span class="number">-0.013052909</span>, <span class="number">0.020387933</span>, <span class="number">-0.007869</span>, <span class="number">-0.11111383</span>,</span><br><span class="line">     <span class="number">-0.030188112</span>, <span class="number">-0.0053388323</span>, <span class="number">0.0010654867</span>, <span class="number">0.072027855</span>,</span><br><span class="line">     <span class="number">-0.04167721</span>, <span class="number">0.014839341</span>, <span class="number">-0.032948174</span>, <span class="number">-0.062975034</span>,</span><br><span class="line">     <span class="number">-0.024837125</span>, ...]</span><br></pre></td></tr></table></figure></p><h3 id="Embedding-模型"><a href="#Embedding-模型" class="headerlink" title="Embedding 模型"></a>Embedding 模型</h3><p>Embedding 模型（即嵌入模型）可将文本、图像、音频、视频等数据转换为数值向量，也就是所谓的“万物皆可向量化”。</p><p><img src="/img/image-vector-search-embedding-model.jpg" alt="Embedding 模型"></p><p>优秀的 embedding 模型能提取数据的特征，让内容相似的数据生成距离相近的向量。例如：</p><ul><li>“香蕉”和“Banana”在语义上接近，会被 embedding 模型转化为接近的向量；</li><li>“我爱苹果手机”和“我爱吃苹果”虽然都包含“苹果”，但是语义相差甚远，优秀的 embedding 模型会把二者区分开来，二者向量距离也会较远。</li></ul><p>在相似图片检索场景下，两只相似的猫在不同环境、角度、光线下的图片，其向量相似度远比同样环境下相同颜色的一只猫和一条狗的图片相似度更高。相对于传统的图片相似度算法（例如颜色直方图对比、明暗对比），embedding 模型能“看懂”图像里的事物，而非只对比像素级色彩明暗，匹配逻辑更贴近人类看待事物的方式。</p><p>此外，部分 embedding 模型支持将一张香蕉的图片和“香蕉”这个字符串转化为接近的向量，这就让“文搜图”更加精准。例如电商场景下搜索“红色的连衣裙”，更容易匹配到符合要求的图片。</p><p><img src="/img/image-vector-search-multimodal-embedding.png" alt="使用多模态 embedding 模型，语义接近的内容（无论是图片还是文本）向量化后其空间距离也非常接近，图片来源：https://odsc.medium.com/getting-started-with-vector-based-search-23c22da43266"></p><p>由于本文主题是“以图搜图”，所以需要用到多模态 embedding 模型（而非纯文本 embedding 模型），以下列出一些可选的模型：</p><ul><li>阿里云百炼 <code>qwen3-vl-embedding</code>、<code>tongyi-embedding-vision-plus</code> 等，参考：<a href="https://help.aliyun.com/zh/model-studio/multimodal-embedding-api-reference" target="_blank" rel="noopener">https://help.aliyun.com/zh/model-studio/multimodal-embedding-api-reference</a></li><li>火山方舟 <code>doubao-embedding-vision</code>，参考：<a href="https://www.volcengine.com/docs/82379/1409291" target="_blank" rel="noopener">https://www.volcengine.com/docs/82379/1409291</a></li><li>OpenAI 开源的 <code>CLIP</code>，参考：<a href="https://github.com/openai/CLIP" target="_blank" rel="noopener">https://github.com/openai/CLIP</a></li></ul><p>示例代码（使用 <code>qwen3-vl-embedding</code> 模型计算一张图片的向量）：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dashscope</span><br><span class="line"></span><br><span class="line">image = <span class="string">"https://xxgblog.com/img/image-vector-search-apple.jpg"</span></span><br><span class="line"></span><br><span class="line">input_data = [</span><br><span class="line">    &#123;<span class="string">"image"</span>: image&#125;</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line">resp = dashscope.MultiModalEmbedding.call(</span><br><span class="line">    api_key=<span class="string">"sk-xxx"</span>,</span><br><span class="line">    model=<span class="string">"qwen3-vl-embedding"</span>,</span><br><span class="line">    input=input_data,</span><br><span class="line">    <span class="comment"># 可选参数：指定向量维度</span></span><br><span class="line">    dimension=<span class="number">1024</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">embedding = resp.output[<span class="string">'embeddings'</span>][<span class="number">0</span>][<span class="string">'embedding'</span>]</span><br><span class="line"></span><br><span class="line">print(resp.output[<span class="string">'embeddings'</span>][<span class="number">0</span>][<span class="string">'embedding'</span>])</span><br><span class="line">print(<span class="string">"向量维度："</span> + str(len(embedding)))</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[0.00017150086932815611, -0.013744989410042763, 0.13095951080322266, ...]</span><br><span class="line">向量维度：1024</span><br></pre></td></tr></table></figure></p><h2 id="向量检索"><a href="#向量检索" class="headerlink" title="向量检索"></a>向量检索</h2><p>以图搜图应用分为离线入库与在线检索两大环节：</p><ul><li>离线环节将全量图片（如电商平台的商品图）通过多模态 embedding 模型转为向量并存储至向量数据库；</li><li>用户在线检索时，上传的图片经相同模型生成查询向量，在向量库中执行相似向量匹配，召回对应原图，实现相似图像检索。</li></ul><p><img src="/img/image-vector-search-process.jpg" alt="离线入库与在线检索流程"></p><h3 id="向量相似度计算"><a href="#向量相似度计算" class="headerlink" title="向量相似度计算"></a>向量相似度计算</h3><p>需要判断两个图片是否相似，就需要计算两个向量的相似度。通常有如下算法可选择：</p><ul><li>余弦相似度（cosine similarity）：计算两个向量的夹角，本质上只和向量的角度有关，和向量的长度无关；</li><li>内积（inner product, IP）或点积（dot product） ：当向量已归一化时，内积的计算结果与余弦相似度等价；</li><li>欧氏距离 (L2) ：测量两个向量之间线段的长度。</li></ul><p>这里引入一个概念：向量归一化。归一化本质上就是将向量的模长缩放为 1，方向不变。</p><p>很多 embedding 模型本身已经做过归一化（例如本文使用的 <code>qwen3-vl-embedding</code> 模型），则首选内积（IP）作为相似度计算算法，相对于余弦相似度更加简单，性能也会更好。</p><p>代码示例（使用内积计算向量相似度，从图库中获取最相似的图片）：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dashscope</span><br><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">embedding</span><span class="params">(image)</span>:</span></span><br><span class="line">    input_data = [</span><br><span class="line">        &#123;<span class="string">"image"</span>: image&#125;</span><br><span class="line">    ]</span><br><span class="line">    resp = dashscope.MultiModalEmbedding.call(</span><br><span class="line">        api_key=<span class="string">"sk-xxx"</span>,</span><br><span class="line">        model=<span class="string">"qwen3-vl-embedding"</span>,</span><br><span class="line">        input=input_data,</span><br><span class="line">        dimension=<span class="number">1024</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> resp.output[<span class="string">'embeddings'</span>][<span class="number">0</span>][<span class="string">'embedding'</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用内积/点积计算向量相似度</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">similarity</span><span class="params">(vector1, vector2)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> np.dot(vector1, vector2)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line">    <span class="comment"># 模拟离线环节：准备 5 张图片作为被检索图片，获取图片向量</span></span><br><span class="line">    image_data = &#123;</span><br><span class="line">        <span class="string">"苹果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-apple.jpg"</span>,</span><br><span class="line">        <span class="string">"香蕉"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-banana.jpg"</span>,</span><br><span class="line">        <span class="string">"蓝莓"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-blueberry.jpg"</span>,</span><br><span class="line">        <span class="string">"柠檬"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-lemon.jpg"</span>,</span><br><span class="line">        <span class="string">"芒果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-mango.jpg"</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    vectors = &#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> key, value <span class="keyword">in</span> image_data.items():</span><br><span class="line">        v = embedding(value)</span><br><span class="line">        vectors[key] = v</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 模拟在线检索：准备一张图片作为用户上传的图片，获取图片向量</span></span><br><span class="line">    user_image = <span class="string">"https://xxgblog.com/img/image-vector-search-user-image.jpg"</span></span><br><span class="line">    print(<span class="string">f"用户上传的图片：<span class="subst">&#123;user_image&#125;</span>"</span>)</span><br><span class="line">    user_image_vector = embedding(user_image)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 逐个对比相似度并检索出最相似的图片</span></span><br><span class="line">    top1 = &#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> key, value <span class="keyword">in</span> vectors.items():</span><br><span class="line">        image_similarity = similarity(user_image_vector, value)</span><br><span class="line">        print(<span class="string">f"<span class="subst">&#123;key&#125;</span>(<span class="subst">&#123;image_data[key]&#125;</span>) 和用户图片的相似度：<span class="subst">&#123;similarity(user_image_vector, value)&#125;</span>"</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> top1 <span class="keyword">or</span> image_similarity &gt; top1[<span class="string">"similarity"</span>]:</span><br><span class="line">            top1[<span class="string">"similarity"</span>] = image_similarity</span><br><span class="line">            top1[<span class="string">"image"</span>] = image_data[key]</span><br><span class="line">            top1[<span class="string">"name"</span>] = key</span><br><span class="line"></span><br><span class="line">    print(<span class="string">f"查找到最相似的图片是[<span class="subst">&#123;top1[<span class="string">'name'</span>]&#125;</span>]：<span class="subst">&#123;top1[<span class="string">'image'</span>]&#125;</span>，相似度：<span class="subst">&#123;top1[<span class="string">'similarity'</span>]&#125;</span>"</span>)</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">用户上传的图片：https://xxgblog.com/img/image-vector-search-user-image.jpg</span><br><span class="line">苹果(https://xxgblog.com/img/image-vector-search-apple.jpg) 和用户图片的相似度：0.20179038343485586</span><br><span class="line">香蕉(https://xxgblog.com/img/image-vector-search-banana.jpg) 和用户图片的相似度：0.5099913278397701</span><br><span class="line">蓝莓(https://xxgblog.com/img/image-vector-search-blueberry.jpg) 和用户图片的相似度：0.20387121238504502</span><br><span class="line">柠檬(https://xxgblog.com/img/image-vector-search-lemon.jpg) 和用户图片的相似度：0.2078659394638562</span><br><span class="line">芒果(https://xxgblog.com/img/image-vector-search-mango.jpg) 和用户图片的相似度：0.24791952668519704</span><br><span class="line">查找到最相似的图片是[香蕉]：https://xxgblog.com/img/image-vector-search-banana.jpg，相似度：0.5099913278397701</span><br></pre></td></tr></table></figure></p><h3 id="向量数据库"><a href="#向量数据库" class="headerlink" title="向量数据库"></a>向量数据库</h3><p>在大量向量的场景下，全表扫描式遍历所有向量并逐一计算相似度的方法效率极低。因此需要为向量数据构建专用索引，通过近似最近邻（ANN）算法大幅减少计算量，这套技术统称为向量索引算法，作用等价于关系数据库的 B+ 树索引。典型的索引实现方式有：</p><ul><li>HNSW (Hierarchical Navigable Small World)</li><li>IVF (Inverted File Index)</li></ul><p>支持向量检索的数据库就是所谓的向量数据库。目前流行的向量数据库有：</p><ul><li><a href="https://github.com/milvus-io/milvus" target="_blank" rel="noopener">Milvus</a></li><li><a href="https://github.com/qdrant/qdrant" target="_blank" rel="noopener">Qdrant</a></li><li><a href="https://weaviate.io/" target="_blank" rel="noopener">Weaviate</a></li><li><a href="https://github.com/chroma-core/chroma" target="_blank" rel="noopener">Chroma</a></li><li><a href="https://github.com/facebookresearch/faiss" target="_blank" rel="noopener">Faiss</a> （Faiss 是一个向量检索库，严格来说不算是向量数据库）</li></ul><p>此外，很多传统的数据库也扩展并支持向量检索：</p><ul><li><a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener">PostgreSQL（pgvector）</a></li><li><a href="https://www.elastic.co/elasticsearch" target="_blank" rel="noopener">Elasticsearch</a></li></ul><p>代码示例（使用 Chroma 作为向量数据库实现相似图片检索）：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dashscope</span><br><span class="line"><span class="keyword">import</span> chromadb</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">embedding</span><span class="params">(image)</span>:</span></span><br><span class="line">    input_data = [</span><br><span class="line">        &#123;<span class="string">"image"</span>: image&#125;</span><br><span class="line">    ]</span><br><span class="line">    resp = dashscope.MultiModalEmbedding.call(</span><br><span class="line">        api_key=<span class="string">"sk-xxx"</span>,</span><br><span class="line">        model=<span class="string">"qwen3-vl-embedding"</span>,</span><br><span class="line">        input=input_data,</span><br><span class="line">        dimension=<span class="number">1024</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> resp.output[<span class="string">'embeddings'</span>][<span class="number">0</span>][<span class="string">'embedding'</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line">    <span class="comment"># 模拟离线环节：准备 5 张图片作为被检索图片并入库</span></span><br><span class="line">    image_data = &#123;</span><br><span class="line">        <span class="string">"苹果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-apple.jpg"</span>,</span><br><span class="line">        <span class="string">"香蕉"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-banana.jpg"</span>,</span><br><span class="line">        <span class="string">"蓝莓"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-blueberry.jpg"</span>,</span><br><span class="line">        <span class="string">"柠檬"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-lemon.jpg"</span>,</span><br><span class="line">        <span class="string">"芒果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-mango.jpg"</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    vectors = []</span><br><span class="line">    metadatas = []</span><br><span class="line">    <span class="keyword">for</span> key, value <span class="keyword">in</span> image_data.items():</span><br><span class="line">        v = embedding(value)</span><br><span class="line">        vectors.append(v)</span><br><span class="line">        metadatas.append(&#123;<span class="string">"name"</span>: key, <span class="string">"image"</span>: value&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 获取图片向量写入向量数据库</span></span><br><span class="line">    <span class="comment"># 这里为了演示，使用内存数据库，实际使用中请使用持久化数据库：client = chromadb.PersistentClient(path="/path/to/save/to")</span></span><br><span class="line">    chroma_client = chromadb.Client()</span><br><span class="line">    collection = chroma_client.create_collection(</span><br><span class="line">        name=<span class="string">"my_products"</span>,</span><br><span class="line">        embedding_function=<span class="keyword">None</span>,  <span class="comment"># 由于图片向量是自行调用 embedding 模型获取，不需要 ChromaDB 提供 embedding 功能</span></span><br><span class="line">        configuration=&#123;</span><br><span class="line">            <span class="string">"hnsw"</span>: &#123;</span><br><span class="line">                <span class="string">"space"</span>: <span class="string">"ip"</span>  <span class="comment"># Chroma 默认使用 L2，这里改为使用内积（IP）</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line">    collection.add(</span><br><span class="line">        ids=[<span class="string">"1"</span>, <span class="string">"2"</span>, <span class="string">"3"</span>, <span class="string">"4"</span>, <span class="string">"5"</span>],</span><br><span class="line">        embeddings=vectors,</span><br><span class="line">        metadatas=metadatas,</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 模拟在线检索：准备一张图片作为用户上传的图片，获取图片向量</span></span><br><span class="line">    user_image = <span class="string">"https://xxgblog.com/img/image-vector-search-user-image.jpg"</span></span><br><span class="line">    print(<span class="string">f"用户上传的图片：<span class="subst">&#123;user_image&#125;</span>"</span>)</span><br><span class="line">    user_image_vector = embedding(user_image)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 从向量数据库中查询最相似的记录</span></span><br><span class="line">    results = collection.query(</span><br><span class="line">        query_embeddings=[user_image_vector],  <span class="comment"># 查询条件</span></span><br><span class="line">        n_results=<span class="number">3</span>  <span class="comment"># 查询几条记录</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(len(results[<span class="string">'metadatas'</span>][<span class="number">0</span>])):</span><br><span class="line">        print(<span class="string">f"[<span class="subst">&#123;i+<span class="number">1</span>&#125;</span>] <span class="subst">&#123;results[<span class="string">'metadatas'</span>][<span class="number">0</span>][i][<span class="string">'name'</span>]&#125;</span>：<span class="subst">&#123;results[<span class="string">'metadatas'</span>][<span class="number">0</span>][i][<span class="string">'image'</span>]&#125;</span>，相似度：<span class="subst">&#123;<span class="number">1</span> - results[<span class="string">'distances'</span>][<span class="number">0</span>][i]&#125;</span>"</span>)</span><br><span class="line"></span><br><span class="line">    top1 = results[<span class="string">'metadatas'</span>][<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">    top1_similarity = <span class="number">1</span> - results[<span class="string">'distances'</span>][<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">    print(<span class="string">f"查找到最相似的图片是[<span class="subst">&#123;top1[<span class="string">'name'</span>]&#125;</span>]：<span class="subst">&#123;top1[<span class="string">'image'</span>]&#125;</span>，相似度：<span class="subst">&#123;top1_similarity&#125;</span>"</span>)</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">用户上传的图片：https://xxgblog.com/img/image-vector-search-user-image.jpg</span><br><span class="line">[1] 香蕉：https://xxgblog.com/img/image-vector-search-banana.jpg，相似度：0.5099915862083435</span><br><span class="line">[2] 芒果：https://xxgblog.com/img/image-vector-search-mango.jpg，相似度：0.247919499874115</span><br><span class="line">[3] 柠檬：https://xxgblog.com/img/image-vector-search-lemon.jpg，相似度：0.2078658938407898</span><br><span class="line">查找到最相似的图片是[香蕉]：https://xxgblog.com/img/image-vector-search-banana.jpg，相似度：0.5099915862083435</span><br></pre></td></tr></table></figure></p><h2 id="进一步优化"><a href="#进一步优化" class="headerlink" title="进一步优化"></a>进一步优化</h2><p>目前我们已经基于 embedding 模型和向量数据库搭建了一个简易的以图搜图应用，但是在生产级应用中，通常还需要做更多优化，让检索结果更加精准。</p><h3 id="主体识别和图片裁切"><a href="#主体识别和图片裁切" class="headerlink" title="主体识别和图片裁切"></a>主体识别和图片裁切</h3><p>针对图片中杂乱的背景可能会影响检索结果的问题，通常会先将图片进行主体识别后裁切，再用 embedding 模型向量化后检索。很多公有云平台提供了图像主体检测 API，参考：</p><ul><li>百度图像主体检测：<a href="https://ai.baidu.com/tech/imagerecognition/object_detect" target="_blank" rel="noopener">https://ai.baidu.com/tech/imagerecognition/object_detect</a></li><li>腾讯云图像主体检测：<a href="https://cloud.tencent.com/document/product/460/97979" target="_blank" rel="noopener">https://cloud.tencent.com/document/product/460/97979</a></li></ul><h3 id="重排序（rerank）"><a href="#重排序（rerank）" class="headerlink" title="重排序（rerank）"></a>重排序（rerank）</h3><p>向量检索系统优先保证速度，因此结果的精确度可能不够理想。重排序模型对检索到的文档重新打分，可将最相关的结果排在前面，显著提升搜索精度。</p><p>Embedding 模型和重排序模型通常结合使用，互为补充：</p><ul><li>Embedding 模型对被检索的数据和用户实时数据分别独立计算向量，可基于被检索数据向量建立索引，便于快速检索，但结果往往没那么精准，可能更相似的结果反而排在后面；</li><li>重排序模型通常更加精确，但它需要将两个数据同时输入模型对比相似度并打分，无法建立索引快速检索，适合针对向量检索出的少量结果进行相似性排序。</li></ul><p>随着技术的发展，embedding 模型的结果也越来越精准，重排序模型也不是必选项，毕竟重排序过程会导致最终响应更慢，可根据实际情况来选择。</p><p>代码示例（使用 <code>qwen3-vl-rerank</code> 模型对向量数据库检索结果重排序）：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dashscope</span><br><span class="line"><span class="keyword">import</span> chromadb</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">embedding</span><span class="params">(image)</span>:</span></span><br><span class="line">    input_data = [</span><br><span class="line">        &#123;<span class="string">"image"</span>: image&#125;</span><br><span class="line">    ]</span><br><span class="line">    resp = dashscope.MultiModalEmbedding.call(</span><br><span class="line">        api_key=<span class="string">"sk-xxx"</span>,</span><br><span class="line">        model=<span class="string">"qwen3-vl-embedding"</span>,</span><br><span class="line">        input=input_data,</span><br><span class="line">        dimension=<span class="number">1024</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> resp.output[<span class="string">'embeddings'</span>][<span class="number">0</span>][<span class="string">'embedding'</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line">    <span class="comment"># 准备 5 张图片作为被检索图片</span></span><br><span class="line">    image_data = &#123;</span><br><span class="line">        <span class="string">"苹果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-apple.jpg"</span>,</span><br><span class="line">        <span class="string">"香蕉"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-banana.jpg"</span>,</span><br><span class="line">        <span class="string">"蓝莓"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-blueberry.jpg"</span>,</span><br><span class="line">        <span class="string">"柠檬"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-lemon.jpg"</span>,</span><br><span class="line">        <span class="string">"芒果"</span>: <span class="string">"https://xxgblog.com/img/image-vector-search-mango.jpg"</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 获取图片向量</span></span><br><span class="line">    vectors = []</span><br><span class="line">    metadatas = []</span><br><span class="line">    <span class="keyword">for</span> key, value <span class="keyword">in</span> image_data.items():</span><br><span class="line">        v = embedding(value)</span><br><span class="line">        vectors.append(v)</span><br><span class="line">        metadatas.append(&#123;<span class="string">"name"</span>: key, <span class="string">"image"</span>: value&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 获取图片向量写入向量数据库</span></span><br><span class="line">    chroma_client = chromadb.Client()</span><br><span class="line">    collection = chroma_client.create_collection(</span><br><span class="line">        name=<span class="string">"products"</span>,</span><br><span class="line">        embedding_function=<span class="keyword">None</span>,</span><br><span class="line">        configuration=&#123;</span><br><span class="line">            <span class="string">"hnsw"</span>: &#123;</span><br><span class="line">                <span class="string">"space"</span>: <span class="string">"ip"</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line">    collection.add(</span><br><span class="line">        ids=[<span class="string">"1"</span>, <span class="string">"2"</span>, <span class="string">"3"</span>, <span class="string">"4"</span>, <span class="string">"5"</span>],</span><br><span class="line">        embeddings=vectors,</span><br><span class="line">        metadatas=metadatas,</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 准备一张图片作为用户上传的图片，获取图片向量</span></span><br><span class="line">    user_image = <span class="string">"https://xxgblog.com/img/image-vector-search-user-image.jpg"</span></span><br><span class="line">    print(<span class="string">f"用户上传的图片：<span class="subst">&#123;user_image&#125;</span>"</span>)</span><br><span class="line">    user_image_vector = embedding(user_image)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 从向量数据库中查询最相似的记录</span></span><br><span class="line">    results = collection.query(</span><br><span class="line">        query_embeddings=[user_image_vector],</span><br><span class="line">        n_results=<span class="number">3</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    images = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> range(len(results[<span class="string">'metadatas'</span>][<span class="number">0</span>])):</span><br><span class="line">        images.append(&#123;<span class="string">"image"</span>: results[<span class="string">'metadatas'</span>][<span class="number">0</span>][i][<span class="string">'image'</span>]&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 使用 qwen3-vl-rerank 模型对向量数据库检索结果重排序（rerank）</span></span><br><span class="line">    resp = dashscope.TextReRank.call(</span><br><span class="line">        api_key=<span class="string">"sk-xxx"</span>,</span><br><span class="line">        model=<span class="string">"qwen3-vl-rerank"</span>,</span><br><span class="line">        query=&#123;<span class="string">"image"</span>: user_image&#125;,</span><br><span class="line">        documents=images,</span><br><span class="line">        top_n=<span class="number">3</span>,  <span class="comment"># 返回重排序后结果数量</span></span><br><span class="line">        return_documents=<span class="keyword">True</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> resp.output.results:</span><br><span class="line">        print(<span class="string">f"<span class="subst">&#123;item[<span class="string">'document'</span>][<span class="string">'image'</span>]&#125;</span>，相关度：<span class="subst">&#123;item.relevance_score&#125;</span>"</span>)</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">用户上传的图片：https://xxgblog.com/img/image-vector-search-user-image.jpg</span><br><span class="line">https://xxgblog.com/img/image-vector-search-banana.jpg，相关度：0.7338192024181801</span><br><span class="line">https://xxgblog.com/img/image-vector-search-mango.jpg，相关度：0.4842462474210706</span><br><span class="line">https://xxgblog.com/img/image-vector-search-lemon.jpg，相关度：0.4773846354376837</span><br></pre></td></tr></table></figure></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;搜索引擎提供的以图搜图功能，电商软件里拍照就能搜同款，音乐软件中的听歌识曲，视频网站上一张截图就能搜出来源于哪部电影或电视剧，乃至人脸识别和检索，以及智能体中常用的 RAG，底层都普遍采用同一项关键技术：向量检索。&lt;br&gt;&lt;img src=&quot;/img/image-vecto
      
    
    </summary>
    
      <category term="AI" scheme="https://xxgblog.com/categories/AI/"/>
    
    
      <category term="Python" scheme="https://xxgblog.com/tags/Python/"/>
    
      <category term="AI" scheme="https://xxgblog.com/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>软件架构中“弹性”的多种含义</title>
    <link href="https://xxgblog.com/2023/02/21/elastic-resilient/"/>
    <id>https://xxgblog.com/2023/02/21/elastic-resilient/</id>
    <published>2023-02-21T03:14:16.000Z</published>
    <updated>2024-03-16T12:19:19.428Z</updated>
    
    <content type="html"><![CDATA[<p>在软件架构领域的中文文档、书籍中，经常可以看到“弹性”这个专业术语，但在不同的语境下含义可能会不同。</p><p>在英语中，elastic 和 resilient 两个单词都可以翻译为“弹性的”，但是它们在软件架构中代表的含义却完全不同，需要避免混淆。</p><h2 id="Elastic"><a href="#Elastic" class="headerlink" title="Elastic"></a>Elastic</h2><p>Elastic 代表的“弹性”强调的是<strong>可伸缩性</strong>。</p><p>在《Designing Data-Intensive Applications》一书中，对 elastic 的定义：</p><blockquote><p>Some systems are elastic, meaning that they can automatically add computing resources when they detect a load increase, whereas other systems are scaled manually (a human analyzes the capacity and decides to add more machines to the system). An elastic system can be useful if load is highly unpredictable, but manually scaled systems are simpler and may have fewer operational surprises.</p></blockquote><blockquote><p>翻译：某些系统具有弹性，意味着它检测到负载增加时，可以自动化地增加计算资源。而非弹性的系统则需要手动扩展（人工分析性能并决定向系统中添加更多的机器）。当系统负载很难预测时，弹性系统会非常有用，而手动扩展的系统更加简单，并且可以减少操作上的意外。</p></blockquote><p>Elastic 表示系统可以根据负载情况和相关策略自动调整计算资源，所以也称为 Auto Scaling。例如一个电商应用会在大促时有更大的负载量，则需要自动添加更多的服务器等资源保障系统正常提供服务，而在平时负载量小的时候，则自动减少资源来控制成本。当我们看到“弹性伸缩”这个术语时，要知道这里的“弹性”代表的意思就是 elastic。</p><p>例如，Kubernetes 提供了 <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/" target="_blank" rel="noopener">HorizontalPodAutoscaler</a>，支持 Pod 水平自动扩缩容。阿里云等商业云平台也都提供了类似的<a href="https://help.aliyun.com/document_detail/25857.html" target="_blank" rel="noopener">弹性伸缩服务（Elastic Scaling Service）</a>，可根据负载情况和策略自动调整计算能力（即实例数量）。</p><h2 id="Resilient"><a href="#Resilient" class="headerlink" title="Resilient"></a>Resilient</h2><p>Resilient 代表的“弹性”强调的是<strong>复原能力</strong>。</p><p>在《Designing Data-Intensive Applications》一书中，对 resilient 的定义：</p><blockquote><p>The things that can go wrong are called faults, and systems that anticipate faults and can cope with them are called fault-tolerant or resilient.</p></blockquote><blockquote><p>翻译：可能出错的事情被称为故障，系统能够预测并应对故障的能力称为容错或弹性。</p></blockquote><p>Resilient 表示系统有容错和故障恢复能力，从而系统具有可靠性。</p><p>例如，Java 著名的开源库 <a href="https://github.com/Netflix/Hystrix" target="_blank" rel="noopener">Hystrix</a> 的介绍是这样的：</p><blockquote><p>Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable <strong>resilience</strong> in complex distributed systems where failure is inevitable.</p></blockquote><p>这里的 resilience 当然指的不是弹性伸缩能力，而是容错能力。</p><p>除 Hystrix 外，其它开源的容错库：</p><ul><li><a href="https://github.com/resilience4j/resilience4j" target="_blank" rel="noopener">Resilience4j</a>: Resilience4j is a fault tolerance library designed for Java8 and functional programming.</li><li><a href="https://github.com/alibaba/Sentinel" target="_blank" rel="noopener">Sentinel</a>: A powerful flow control component enabling reliability, resilience and monitoring for microservices.</li><li><a href="https://github.com/App-vNext/Polly" target="_blank" rel="noopener">Polly</a>: Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.</li><li><a href="https://github.com/eapache/go-resiliency" target="_blank" rel="noopener">go-resiliency</a>: Resiliency patterns for golang.</li><li><a href="https://github.com/Shopify/semian" target="_blank" rel="noopener">Semian</a>: Resiliency toolkit for Ruby for failing fast.</li></ul><p>实现 resilient 的策略通常包括断路器（Circuit Breaker）、限流器（Rate Limiter）、重试（Retry）、舱壁（Bulkhead）等，更多可参考：<a href="https://github.com/App-vNext/Polly#resilience-policies" target="_blank" rel="noopener">https://github.com/App-vNext/Polly#resilience-policies</a> 。</p><p>下面简单介绍常用的几种容错的策略：</p><ul><li>重试（Retry）：很多错误是短暂的并且可以自动恢复的，对这种问题采用重试策略。</li><li>断路器（Circuit Breaker）：类似于电路或股市中的“熔断”概念，当系统发生严重故障（大量超时或失败）时，为了避免后续持续不断的请求导致故障系统过载，超时导致网络、线程资源占用，最终产生雪崩，而在一段时间内直接 fail fast（快速失败，即直接返回错误而不再去请求故障的系统模块）。</li><li>舱壁（Bulkhead）：《泰坦尼克号》电影中有一段对船体的描述：船体包含 16 个相互隔离的水密舱，即使有 4 个水密舱受损进水也能保证船漂浮在海面上。架构设计中舱壁模式参考的就是这种方式，将资源进行隔离，例如可以为调用多个服务的消费者分配每个服务独立的连接池，从而保证一种故障只会影响到其对应的资源，而不会造成级联故障。</li><li>限流器（Rate Limiter）：通过限流算法（如令牌桶算法、漏桶算法），限制在特定时间段内的执行次数、数据量等指标，从而防止系统过载。</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在软件架构领域的中文文档、书籍中，经常可以看到“弹性”这个专业术语，但在不同的语境下含义可能会不同。&lt;/p&gt;
&lt;p&gt;在英语中，elastic 和 resilient 两个单词都可以翻译为“弹性的”，但是它们在软件架构中代表的含义却完全不同，需要避免混淆。&lt;/p&gt;
&lt;h2 i
      
    
    </summary>
    
      <category term="架构" scheme="https://xxgblog.com/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="架构" scheme="https://xxgblog.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>密码学基础：编码方式、消息摘要算法、加密算法总结</title>
    <link href="https://xxgblog.com/2021/11/30/cryptography/"/>
    <id>https://xxgblog.com/2021/11/30/cryptography/</id>
    <published>2021-11-30T02:31:00.000Z</published>
    <updated>2025-06-02T15:42:08.312Z</updated>
    
    <content type="html"><![CDATA[<h2 id="字节码转文本的编码方式"><a href="#字节码转文本的编码方式" class="headerlink" title="字节码转文本的编码方式"></a>字节码转文本的编码方式</h2><p>在计算机中，无论是内存、磁盘、网络传输，涉及到的数据都是以二进制格式来存储或传输的。</p><p>每一个二进制位（bit）只能是 0 或 1。二进制位不会单独存在，而是以 8 个二进制位组成 1 个字节（byte）的方式存在，即 1 byte = 8 bit。</p><p>字节码无法直接转为可打印的文本字符，有时想通过文本方式配置、存储、传输一段二进制字节码，比如配置文件、HTML/XML、URL、e-mail 正文、HTTP Header 等仅支持文本的场景下，就需要将二进制字节码转为文本字符串。</p><p>二进制字节码转文本字符有很多种方式，最简单的方式是直接用 0 和 1 来表示。但是这样的话，8 个 0/1 字符才能表示 1 个字节，长度太长很不方便。</p><p>下面介绍两种更加紧凑的方式：HEX 编码和 Base64 编码。</p><h3 id="HEX-编码"><a href="#HEX-编码" class="headerlink" title="HEX 编码"></a>HEX 编码</h3><p>HEX 是 16 进制的编码方式，所以又称为 Base16。</p><p>如果把一个字节中的二进制数值转为十六进制，使用 0-9 和 a-e（忽略大小写）这 16 个字符，那每个字符就可以表示 4 个二进制位（因为 2 的 4 次方等于 16），那么仅需要两个可打印字符就可以表示一个字节。</p><p>Java 中使用 HEX 编码（依赖 <a href="https://commons.apache.org/proper/commons-codec/" target="_blank" rel="noopener">Apache Commons Codec</a>）：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">String str = <span class="string">"相对论"</span>;</span><br><span class="line"><span class="keyword">byte</span>[] bytes = str.getBytes(<span class="string">"UTF-8"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Hex 编码</span></span><br><span class="line">String encodeString = Hex.encodeHexString(bytes);</span><br><span class="line">System.out.println(encodeString); <span class="comment">// 输出：e79bb8e5afb9e8aeba</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Hex 解码</span></span><br><span class="line"><span class="keyword">byte</span>[] decodeBytes = Hex.decodeHex(encodeString);</span><br><span class="line">System.out.println(<span class="keyword">new</span> String(decodeBytes, <span class="string">"UTF-8"</span>)); <span class="comment">// 输出：相对论</span></span><br></pre></td></tr></table></figure></p><p>HEX 编码使用场景非常多。下面介绍几种常见的使用场景：</p><h4 id="RGB-颜色码"><a href="#RGB-颜色码" class="headerlink" title="RGB 颜色码"></a>RGB 颜色码</h4><p>RGB 颜色通常用 HEX 方式表示。如橘红色可以用 #FF4500 来表示：<br><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.orangered</span> &#123; <span class="attribute">color</span>: <span class="number">#FF4500</span>; &#125;</span><br></pre></td></tr></table></figure></p><p>RGB 指红(red)绿(green)蓝(blue)三原色，这三种颜色按不同比例叠加后可以得到各式各样的颜色。三种颜色每种强度取值范围是 0~255，各需要 1 个字节来表示，共 3 个字节。</p><p>用 HEX 编码的表示某种 RGB 颜色，是一个长度为 6 位的字符串（通常还会加上 # 作为前缀，此时长度是 7 位）。例如 #FF4500 表示红绿蓝三原色的强度分别为 255、69、0。</p><h4 id="URL-编码"><a href="#URL-编码" class="headerlink" title="URL 编码"></a>URL 编码</h4><p>由于 URL 中仅允许出现字母、数字和一些特殊符号，当 URL 中有汉字，需要经过 URL 编码才可以。</p><p>例如百度百科”相对论”的页面 URL 是： <a href="https://baike.baidu.com/item/%E7%9B%B8%E5%AF%B9%E8%AE%BA/48750" target="_blank" rel="noopener">https://baike.baidu.com/item/%E7%9B%B8%E5%AF%B9%E8%AE%BA/48750</a></p><p>其中 %E7%9B%B8%E5%AF%B9%E8%AE%BA 实际上是将 ‘相对论’ 三个字用 UTF-8 编码后得到 9 个字节，再分别对这 9 个字节使用 HEX 编码并加上 ‘%’ 前缀得到的结果。</p><h4 id="IPv6-地址"><a href="#IPv6-地址" class="headerlink" title="IPv6 地址"></a>IPv6 地址</h4><p>由于 IPv4 的地址即将面临不够用的问题，取而代之的将会是 IPv6。IPv6 使用了 128 个二进制位的地址，通常会使用 HEX 编码方式来表示，例如：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2001:0db8:0000:0000:0000:ff00:0042:8329</span><br></pre></td></tr></table></figure></p><h3 id="Base64-编码"><a href="#Base64-编码" class="headerlink" title="Base64 编码"></a>Base64 编码</h3><p>如果觉得 HEX 编码不够紧凑，那么还有更加紧凑的编码方式：Base64 编码。</p><p>Base64 编码共使用了 64 个字符来表示二进制位：26 个大写的 A-Z、26 个小写的 a-z、10 个数字 0-9、2 个特殊符号 + 和 /。这意味着每个字符可以表示 6 个二进制位，因为 64 等于 2 的 6 次方。</p><p>由于每个字节是 8 个二进制位，而 Base64 编码每个字符表示 6 个二进制位，那么可以每凑够 3 个字节（即 24 个二进制位），可将其编码为 4 个字符。如果被 base64 编码的原数据字节数不是 3 的倍数，那么会在末尾补上 1 或 2 个值为 0 的字节，凑到 3 的倍数后再进行 Base64 编码，编码后会在末尾添加 1 或 2 个 = 符号，表示补了多少个字节，这个在解码时会用到。</p><p>Java 中使用 Base64 编码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">String str = <span class="string">"相对论"</span>;</span><br><span class="line"><span class="keyword">byte</span>[] bytes = str.getBytes(<span class="string">"UTF-8"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Base64 编码</span></span><br><span class="line">String encodeString = Base64.getEncoder().encodeToString(bytes);</span><br><span class="line">System.out.println(encodeString); <span class="comment">// 输出：55u45a+56K66</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Base64 解码</span></span><br><span class="line"><span class="keyword">byte</span>[] decodeBytes = Base64.getDecoder().decode(encodeString);</span><br><span class="line">System.out.println(<span class="keyword">new</span> String(decodeBytes, <span class="string">"UTF-8"</span>)); <span class="comment">// 输出：相对论</span></span><br></pre></td></tr></table></figure></p><p>Base64 编码的使用场景也有很多。例如，由于图片文件不是文本文件，没办法直接写入到 HTML 中，而将图片经过 Base64 编码后的结果是一串文本，可以直接放到 HTML 中：<br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">"data:image/jpg;base64,/9j/4QMZR..."</span> /&gt;</span></span><br></pre></td></tr></table></figure></p><p>需要注意的是，<strong>Base64 不是加密算法</strong>，有的开发人员把 Base64 当做加密算法来用，这是极其不安全的，因为 Base64 任何人都可以解码，不需要任何密钥。</p><h2 id="消息摘要算法"><a href="#消息摘要算法" class="headerlink" title="消息摘要算法"></a>消息摘要算法</h2><p>消息摘要算法（Message-Digest Algorithm），又称为密码散列函数（cryptographic hash function (CHF)），可以将任意长度的字节码数据通过哈希算法计算出一个固定大小的结果。常用的消息摘要算法有 MD5、SHA-1、SHA-2 系列（包括 SHA-256、SHA-512 等）。</p><p>以 MD5 为例，对任意一个数据进行 MD5 运算，结果是一个 128 个二进制位（16 个字节）的哈希值。而我们日常看到的 32 位 MD5 字符串，实际上是对 128 个二进制位的哈希值进行 HEX 编码后得到的结果。</p><p>例如，当使用 MD5 对 “相对论” 这个字符串进行运算，得到一个 32 位字符的 MD5 值，实际上是经过以下 3 个步骤（以下代码依赖 <a href="https://commons.apache.org/proper/commons-codec/" target="_blank" rel="noopener">Apache Commons Codec</a>）：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">String str = <span class="string">"相对论"</span>;</span><br><span class="line"><span class="comment">// 1. 将字符串通过 UTF-8 编码转为字节数组</span></span><br><span class="line"><span class="keyword">byte</span>[] bytes = str.getBytes(<span class="string">"UTF-8"</span>);</span><br><span class="line"><span class="comment">// 2. 对原始数组进行 MD5，得到一个 128 个二进制位（16 个字节）的哈希值</span></span><br><span class="line"><span class="keyword">byte</span>[] md5Bytes = DigestUtils.md5(bytes);</span><br><span class="line"><span class="comment">// 3. 将 128 位的哈希值 HEX 编码，得到一个长度为 32 的字符串</span></span><br><span class="line">String md5Hex = Hex.encodeHexString(md5Bytes);</span><br><span class="line">System.out.println(md5Hex); <span class="comment">// 输出：fa913fb181bc1a69513e3d05a367da49</span></span><br></pre></td></tr></table></figure></p><p>上面的代码仅仅是为了更清晰的看到计算一个字符串 MD5 值的整个过程。实际开发中可以使用更加便捷的 API，将上面的 3 个步骤合为 1 步：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">String str = <span class="string">"相对论"</span>;</span><br><span class="line"><span class="comment">// 使用默认的 UTF-8 编码将字符串转为字节数组计算 MD5 后再进行 HEX 编码</span></span><br><span class="line">String md5Hex = DigestUtils.md5Hex(str);</span><br><span class="line">System.out.println(md5Hex); <span class="comment">// 输出：fa913fb181bc1a69513e3d05a367da49</span></span><br></pre></td></tr></table></figure></p><p>除此之外，<a href="https://commons.apache.org/proper/commons-codec/" target="_blank" rel="noopener">Apache Commons Codec</a> 中的 <code>DigestUtils</code> 还提供了 SHA-1、SHA-256、SHA-384、SHA-512 等消息摘要算法。</p><p>消息摘要算法有以下特点：</p><ul><li>相同的消息通过消息摘要算法计算得到的结果总是相同的。</li><li>不同的消息通过消息摘要算法计算得到的结果要尽可能保证是不同的。如果两个不同的数据消息摘要后的结果相同，也就是发生了哈希碰撞，哈希碰撞出现的概率越大，那么这个消息摘要算法就越不安全。</li><li>不可逆，无法通过哈希结果反向推算出原始数据。所以，我们一般认为<strong>消息摘要算法并不算是加密算法</strong>，因为它无法解密。另外，这里的不可逆是指运算不可逆，但是攻击者通常会使用穷举法或彩虹表来找到哈希值对应的原始数据。</li></ul><p>下面列举一些典型的消息摘要算法的使用场景：</p><ul><li>对用户的登录密码使用消息摘要算法得到哈希值后再存储到数据库，即使数据库被黑客攻击，拿到所有的数据，也很难获得密码的原始值。这相对明文存储密码来说更加安全。当然，直接使用哈希值存储也是不安全的，特别是对于一些弱密码，黑客可以通过彩虹表轻松的查到对应的原始值。所以通常不会直接存储哈希值，而是经过一些处理，例如加盐、HMAC 等方式。</li><li>对比两个文件是否一致，只需要对比两个文件的消息摘要是否一致即可，无需按字节一个个去对比。例如百度网盘曾经就是用文件的 MD5 来判断新上传的文件是否已存在，如果已经存在则不需要重复上传和存储，达到节省空间的目的。</li><li>用于数字签名（Digital Signature），这个在本文后续会介绍。</li></ul><p>在安全性要求比较高的场景下，MD5、SHA-1 目前都已经不建议使用了，现在用的比较多的是 SHA-2 系列算法。</p><p>例如，MD5 相对来说很容易被碰撞攻击。下面两个完全不同的图片文件，分别计算它们的 MD5 值，会发现结果都是 253dd04e87492e4fc3471de5e776bc3d ：</p><p><img src="/img/cryptography-md5-ship.jpg" alt="两个 MD5 相同的图片-1"></p><p><img src="/img/cryptography-md5-plane.jpg" alt="两个 MD5 相同的图片-2"></p><p>大家有兴趣可以试试分别将两个图片传到百度网盘，可以发现有意思的事情：预览两个图片时可以发现变成同一个图片了。</p><h2 id="HMAC"><a href="#HMAC" class="headerlink" title="HMAC"></a>HMAC</h2><p>HMAC 全称是散列消息认证码（Hash-Based Message Authentication Code），它在消息摘要算法的基础上，加上了一个密钥（secret key）。</p><p>例如 HMAC-SHA256 就是在 SHA-256 算法基础上加了一个密钥。以下为代码示例（依赖 <a href="https://commons.apache.org/proper/commons-codec/" target="_blank" rel="noopener">Apache Commons Codec</a>）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">String str = <span class="string">"相对论"</span>;</span><br><span class="line">String key = <span class="string">"12345678"</span>; <span class="comment">// 密钥</span></span><br><span class="line">HmacUtils hmacUtils = <span class="keyword">new</span> HmacUtils(HmacAlgorithms.HMAC_SHA_256, key.getBytes(<span class="string">"UTF-8"</span>));</span><br><span class="line">String result = hmacUtils.hmacHex(str.getBytes(<span class="string">"UTF-8"</span>));</span><br><span class="line">System.out.println(result); <span class="comment">// 输出：3bd7bbf58159a6d0bff846016b346a617a588fc1e9c43ebbdf38be53d3fc455a</span></span><br></pre></td></tr></table></figure><p>相对于直接使用消息摘要算法，使用 HMAC 优势在于它可以对消息进行真实性（authenticity）和完整性（integrity）验证。</p><p>只要密钥没有泄露，那么只有持有密钥才可以计算和验证原始数据哈希值。攻击者在没有密钥的前提下，无法伪造消息，也无法篡改消息。</p><p>HMAC 可用于接口认证。例如一个暴露在网络环境中的 HTTP 接口，如果想要对调用方进行认证，可以将密钥发放给调用方，并要求调用方调用接口时，将所有请求参数用密钥通过 HMAC-SHA256 计算一个签名，被调用方使用同样的密钥和算法验证签名，就可以保证请求参数的真实性和完整性。</p><p>另外，由于在计算哈希值时添加了密钥，相对于直接使用消息摘要算法，更加不容易被穷举法、彩虹表破解，用户密码经过 HMAC 后保存更加安全。</p><h3 id="JWT-中的-HMAC"><a href="#JWT-中的-HMAC" class="headerlink" title="JWT 中的 HMAC"></a>JWT 中的 HMAC</h3><p>HMAC 的一个典型的应用场景就是 JWT。JWT 全称是 JSON Web Token。</p><p>传统的认证方式一般会将认证用户信息保存在服务端，而 JWT 直接将认证用户信息发放给客户端保存。</p><p>既然 JWT 保存在客户端，那么任何人都可以伪造或篡改，那如何解决这个问题？一种方式就是服务端会对 JWT 的 token 使用 HMAC 进行签名，并将签名也放在 token 末尾。下次客户端带上 JWT 请求时，服务端再验证签名是否正确。只要密钥不泄露，就可以保证 token 是服务端生成的，而不是伪造或篡改的。</p><p>JWT token 分为三个部分：</p><ul><li>Header：头部，指定签名算法</li><li>Payload：包含 token 主要传输的信息，这一部分可以包含用户信息，例如用户名等</li><li>Signature：签名，计算方式如下（secret 即密钥）：  <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">HMACSHA256(</span><br><span class="line">  base64UrlEncode(header) + &quot;.&quot; +</span><br><span class="line">  base64UrlEncode(payload),</span><br><span class="line">  secret)</span><br></pre></td></tr></table></figure></li></ul><p>最终对这三个部分 Base64 编码后组合为 JWT 的 token：<br><img src="/img/cryptography-jwt.png" alt="JWT token 的三个部分"></p><h2 id="加密算法"><a href="#加密算法" class="headerlink" title="加密算法"></a>加密算法</h2><p>加密算法分为对称加密算法和非对称加密算法：</p><ul><li>对称加密算法（symmetric-key cryptography）：加密和解密时使用相同的密钥。最常用的是 AES 算法。</li><li>非对称加密算法（asymmetric-key cryptography）：加密和解密使用不同的密钥，例如公钥加密的内容只能用私钥解密，所以又称为公钥加密算法（public-key cryptography）。使用最广泛的是 RSA 算法。</li></ul><h3 id="对称加密算法"><a href="#对称加密算法" class="headerlink" title="对称加密算法"></a>对称加密算法</h3><p>常见的对称加密算法有 DES、3DES、AES，其中 DES 和 3DES 标准由于安全性问题，已经逐渐被 AES 取代。</p><p>AES 有多种工作模式（mode of operation）和填充方式（padding）：</p><ul><li>工作模式：如 ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM，不同的模式参数和加密流程不同。</li><li>填充方式：由于 AES 是一种区块加密（block cipher）算法，加密时会将原始数据按大小拆分成一个个 128 比特（即 16 字节）区块进行加密，如果需要加密的原始数据不是 16 字节的整数倍时，就需要对原始数据进行填充，使其达到 16 字节的整数倍。常用的填充方式有 PKCS5Padding、ISO10126Padding 等，另外如果能保证待加密的原始数据大小为 16 字节的整数倍，也可以选择不填充，即 NoPadding。</li></ul><p>在实际工作中，需要跨团队跨语言对数据加密解密，经常出现使用一个语言加密后，另一个语言无法解密的情况。这一般都是两边选择的工作模式和填充方式不一致导致的。</p><p>下面的代码以 ECB 模式结合 PKCS5Padding 填充方式为例，对数据进行加密和解密：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encryptECB(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/ECB/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decryptECB(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/ECB/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的明文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encryptECB(data.getBytes(), key.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"ECB 模式加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decryptECB(ciphertext, key.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ECB 模式加密结果（Base64）：bB0gie8pCE2RBQoIAAIxeA==</span><br><span class="line">解密结果：Hello World</span><br></pre></td></tr></table></figure></p><p>上面的 ECB 模式虽然简单易用，但是安全性不高。由于该模式对每个 block 进行独立加密，会导致同样的明文块被加密成相同的密文块。下图就是一个很好的例子：<br><img src="/img/java-aes-ecb.png" alt="ECB 模式缺点"></p><p>在 CBC 模式中，引入了初始向量（IV，Initialization Vector）的概念，用于解决 ECB 模式的问题。</p><p>下面是 CBC 模式结合 PKCS5Padding 填充方式的代码示例，加密解密时相比 ECB 模式多了一个初始向量 iv 参数：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encryptCBC(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/CBC/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> IvParameterSpec(iv));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decryptCBC(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/CBC/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> IvParameterSpec(iv));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的原文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line">    String iv = <span class="string">"iviviviviviviviv"</span>; <span class="comment">// CBC 模式需要用到初始向量参数</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"CBC 模式加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CBC 模式加密结果（Base64）：K7bSB51+KxfqaMjJOsPAQg==</span><br><span class="line">解密结果：Hello World</span><br></pre></td></tr></table></figure></p><p>AES 使用非常广泛，可以说只要上网，无论是使用手机 APP 还是 Web 应用，几乎都离不开 AES 加密算法。目前大部分网站，包括手机 APP 后端接口，都已经使用 HTTPS 协议，而 HTTPS 在数据传输阶段大多都是使用 AES 对称加密算法。</p><p>但是，以 AES 为代表的的对称加密算法面临一个问题，就是如何安全的传输密钥。网络中发生数据交换的双方，需要用同一个密钥进行加密和解密，密钥一旦暴露，传输的内容就不再安全。密钥本身如果需要传输，如何保证安全？对于这个问题，就需要用到非对称加密算法。</p><h3 id="非对称加密算法"><a href="#非对称加密算法" class="headerlink" title="非对称加密算法"></a>非对称加密算法</h3><p>1977 年，Rivest、Shamir、Adleman 设计了 RSA 非对称加密算法，并以此获得了 2002 年的图灵奖（计算机领域的国际最高奖项，被誉为”计算机界的诺贝尔奖”）。至今，RSA 算法一直是最广为使用的非对称加密算法。</p><p>RSA 有两个密钥：公钥（public key）和私钥（private key）。</p><p>公钥可以完全公开，任何人都可以获取到。私钥是私有的，要保证不能被泄露出去。</p><p>公钥加密的内容，只有私钥可以解密。私钥加密的内容，也只有公钥可以解密。</p><p>基于以上规则，RSA 有两种不同的用法：</p><ul><li>公钥加密，私钥解密：服务端把公钥公开出去，客户端拿到公钥，把想要传输给服务端的数据通过公钥加密后传输，那么这个数据只有服务端能够解密，因为只有服务端拥有私钥，其他任何中间人即使在传输过程中拿到数据，既不能解密，也无法篡改。</li><li><p>私钥签名，公钥验证签名：内容发布者将发布的内容用消息摘要算法（如 SHA-256）计算哈希值，再用私钥加密哈希值，得到一个签名，并将签名加在发布内容中一起发布，其他人得到这个内容后，可以用公开的公钥解密签名得到哈希值，再对比这个哈希值和内容生成的哈希值是否一致，来保证这份内容没有被篡改过。</p><p>  由于只是验证数据的真实性完整性，所以无需对整个内容进行加密，仅需对内容的哈希值加密即可验证，所以通常会结合消息摘要算法。例如 SHA256 with RSA 签名，就是先用 SHA-256 计算出哈希值，再用 RSA 私钥加密。</p></li></ul><p>上面说到的私钥加密、公钥解密只是理论上成立，实际上不会直接这样用，而是只用于签名。因为一段私钥加密的数据，解密的公钥是公开的，意味着谁都可以解密，这样加密就没有任何意义了。</p><p>接下来通过 Java 代码来体验一下 RSA 算法。</p><p>首先，需要生成一对公钥和私钥。下面通过 openssl 命令来生成一对公钥和私钥：<br><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><span class="line"><span class="comment"># 创建一个 PKCS#8 格式 2048 位的私钥</span></span><br><span class="line">openssl genpkey -out private_key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048</span><br><span class="line"><span class="comment"># 通过私钥生成公钥</span></span><br><span class="line">openssl pkey -<span class="keyword">in</span> private_key.pem -pubout -out public_key.pem</span><br></pre></td></tr></table></figure></p><p>生成的公钥和私钥是 Base64 编码的文本文件，可以直接用文本编辑器打开。拷贝到下面的代码中，可以验证公钥加密、私钥解密，以及私钥签名、公钥验证签名：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    String publicKeyBase64 = <span class="string">"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0XYlulDsTzDWUb6X66Ia\n"</span> +</span><br><span class="line">            <span class="string">"giSn1dKriHvLHYth9hCcaGomdeIQahGnxzE1o76slEyS2HZ164QHqx8Za+LuT6IV\n"</span> +</span><br><span class="line">            <span class="string">"yLhU/ZNLWAZABe/sdNEkhti6vSSOdJE43KS4UVADeSgtN+7uXDuVgm35EPWZjkfV\n"</span> +</span><br><span class="line">            <span class="string">"5hiRX4nT5ALr1niyi1Ax4BWWyG4qX00n1HzY8MvoyiLdNob71qB+amjUNy9bDhcz\n"</span> +</span><br><span class="line">            <span class="string">"CDWtgA/ywOYU5Ec6vMgYfbAXPKGWwo318rS3UH8QtsO8iGcQbZ76q05LNEL8G3fo\n"</span> +</span><br><span class="line">            <span class="string">"0Kssj4fjrVGwSsyGztRRMLfGkW/hOPCDj82+D6dGQlGB3gyB7P1xVbkD67FujQA/\n"</span> +</span><br><span class="line">            <span class="string">"jwIDAQAB"</span>;</span><br><span class="line">    String privateKeyBase64 = <span class="string">"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRdiW6UOxPMNZR\n"</span> +</span><br><span class="line">            <span class="string">"vpfrohqCJKfV0quIe8sdi2H2EJxoaiZ14hBqEafHMTWjvqyUTJLYdnXrhAerHxlr\n"</span> +</span><br><span class="line">            <span class="string">"4u5PohXIuFT9k0tYBkAF7+x00SSG2Lq9JI50kTjcpLhRUAN5KC037u5cO5WCbfkQ\n"</span> +</span><br><span class="line">            <span class="string">"9ZmOR9XmGJFfidPkAuvWeLKLUDHgFZbIbipfTSfUfNjwy+jKIt02hvvWoH5qaNQ3\n"</span> +</span><br><span class="line">            <span class="string">"L1sOFzMINa2AD/LA5hTkRzq8yBh9sBc8oZbCjfXytLdQfxC2w7yIZxBtnvqrTks0\n"</span> +</span><br><span class="line">            <span class="string">"Qvwbd+jQqyyPh+OtUbBKzIbO1FEwt8aRb+E48IOPzb4Pp0ZCUYHeDIHs/XFVuQPr\n"</span> +</span><br><span class="line">            <span class="string">"sW6NAD+PAgMBAAECggEABT96joJ8iTdmB0JJOCQlmeElO1w94/uGCUV2vN2JrawL\n"</span> +</span><br><span class="line">            <span class="string">"LqTtWFr84ya+e03JsSWCAF5ncfEq6AStdGCJLAGZnh/QMVJBbwEpFXz/ZaXfzmkb\n"</span> +</span><br><span class="line">            <span class="string">"tKV31D/XNuABpjfk/mIdT+tymWj8w/nRZbVhlYkDOPKgoc4oOuw/0G3Ru1/VABI+\n"</span> +</span><br><span class="line">            <span class="string">"yulNx93A/JNFGk3Bkm4E7jRWyl0BkAqAX2BZkFbXG/u3Jc0eYXrG74JfMH+MEihG\n"</span> +</span><br><span class="line">            <span class="string">"GDMSpBKNyX5zWkUT6XxpG82t2erHPWYEoNSoFzAUu+7rZ4ECEXxazAQclEHTkR3r\n"</span> +</span><br><span class="line">            <span class="string">"duUZ/XF0GL1WB0GC7+qvV/Z0gxjXuwG9oToFO/0MQQKBgQDu4DuTPWcYwSWY0R1f\n"</span> +</span><br><span class="line">            <span class="string">"qZUOuYRwD+5OQnJMIlKAD32QmvYT/jnvigjss5Qf1IUwf1UMynj2FnVF4D7L+kvq\n"</span> +</span><br><span class="line">            <span class="string">"O7LzYvHAeDQwZGGt2xWBlqjfhumlfBqfklkkqUiH2A5DvfvtbX/kkiY3n9C+oYZp\n"</span> +</span><br><span class="line">            <span class="string">"2ejiOtSC+NqQeB74TluxroEkvwKBgQDgehynybpFl4KkmDhgj++BH5RR+xzXIChb\n"</span> +</span><br><span class="line">            <span class="string">"gtIbbspdE1EyXy7Z9iNAJ8PVjHkSwh8iEfAO4EuJFnonF8UNIsWLr3gsKbQytRxR\n"</span> +</span><br><span class="line">            <span class="string">"cewqaBhTL54Vgl5dmODNrYjkZva5HHDsCLioYGgljdrj5e/gPSAWBrgT6kI+HypQ\n"</span> +</span><br><span class="line">            <span class="string">"/5xyp+KJMQKBgQCMxut1P8eliBa/M+YqvYdR8TVC0bCwwGoZwlR6kiZ+9UQ2zimY\n"</span> +</span><br><span class="line">            <span class="string">"qPHPhZmzFI0V4sTdz+lvphahAqIfljftKBezZklxE6Y2KsKCMk4/W+nUKe9Cjpwm\n"</span> +</span><br><span class="line">            <span class="string">"FJqih31uSX9Gnw18hH7N1u/c8juUTR8o/LpJsUASm9Q7Nf+SeKODWINVgwKBgDEx\n"</span> +</span><br><span class="line">            <span class="string">"UXpLsPBzRYQAf8pZgKkRXJWirC1QtMdpIdY1L0+6Xf7l8QR+9janADmaMSY1OFFl\n"</span> +</span><br><span class="line">            <span class="string">"EPCRorwGGvraMKqyRgxYhcNX2E+MdQo8Jv8cFMiWFNSt3zQvvoQUVX2IOuVSIET5\n"</span> +</span><br><span class="line">            <span class="string">"nE354pjoP2HWD/1aJ9/r1Qc4PRAUEFfzzDssI27hAoGAOsYKtvW6iRn/WVduIRcy\n"</span> +</span><br><span class="line">            <span class="string">"UtBRHHX0U16zGv+I7nOOBIYK5Uan6AjgzG2MfPOBj3cUhMMBDPfVg1cTbonw5Y8F\n"</span> +</span><br><span class="line">            <span class="string">"nSO4VLOtqKy0BRxCIUFqltJXUmj1zAJs84IweCBQ3un/OLVUMgE7qGtaIQy2PBsy\n"</span> +</span><br><span class="line">            <span class="string">"M8mwuUjo3Fu7l11E2Vgz/qY="</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Base64 解码</span></span><br><span class="line">    <span class="keyword">byte</span>[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64.replace(<span class="string">"\n"</span>, <span class="string">""</span>));</span><br><span class="line">    <span class="keyword">byte</span>[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64.replace(<span class="string">"\n"</span>, <span class="string">""</span>));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 生成 PublicKey（公钥） 和 PrivateKey（私钥） 对象</span></span><br><span class="line">    PublicKey publicKey = KeyFactory.getInstance(<span class="string">"RSA"</span>).generatePublic(<span class="keyword">new</span> X509EncodedKeySpec(publicKeyBytes));</span><br><span class="line">    PrivateKey privateKey = KeyFactory.getInstance(<span class="string">"RSA"</span>).generatePrivate(<span class="keyword">new</span> PKCS8EncodedKeySpec(privateKeyBytes));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 待加密、签名的原文</span></span><br><span class="line">    String data = <span class="string">"Hello World"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 公钥加密，私钥解密</span></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encrypt(data.getBytes(), publicKey);</span><br><span class="line">    System.out.println(<span class="string">"RSA 公钥加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decrypt(ciphertext, privateKey);</span><br><span class="line">    System.out.println(<span class="string">"RSA 私钥解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 私钥签名，公钥验证签名</span></span><br><span class="line">    <span class="keyword">byte</span>[] signature = sign(data.getBytes(), privateKey);</span><br><span class="line">    System.out.println(<span class="string">"RSA 私钥签名结果（Base64）："</span> + Base64.getEncoder().encodeToString(signature));</span><br><span class="line">    <span class="keyword">boolean</span> verifySuccess = verify(data.getBytes(), signature, publicKey);</span><br><span class="line">    System.out.println(<span class="string">"RSA 公钥验证签名结果："</span> + verifySuccess);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 公钥加密</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encrypt(<span class="keyword">byte</span>[] data, PublicKey publicKey) <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"RSA"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, publicKey);</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 私钥解密</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decrypt(<span class="keyword">byte</span>[] data, PrivateKey privateKey) <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"RSA"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, privateKey);</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 私钥签名，使用 SHA256 with RSA 签名</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] sign(<span class="keyword">byte</span>[] data, PrivateKey privateKey) <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    Signature signature = Signature.getInstance(<span class="string">"SHA256withRSA"</span>);</span><br><span class="line">    signature.initSign(privateKey);</span><br><span class="line">    signature.update(data);</span><br><span class="line">    <span class="keyword">byte</span>[] result = signature.sign();</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 公钥验证签名</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">verify</span><span class="params">(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] sign, PublicKey publicKey)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    Signature signature = Signature.getInstance(<span class="string">"SHA256withRSA"</span>);</span><br><span class="line">    signature.initVerify(publicKey);</span><br><span class="line">    signature.update(data);</span><br><span class="line">    <span class="keyword">return</span> signature.verify(sign);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RSA 公钥加密结果（Base64）：zoY6KM/RdCjAs7upJ9SIwqfXsSn3hAPu/z/ZPHbKgWN6+X0PpyVJVYT8jacEkzB7S2sJe/wLkO2TqXB2gqvL1AuDRgepVlxV2f6Uwx4DxM2/5RE0fAdTiICV5JEEIw81oLix0GGQ7nLjOhJxN9LaTJ2cXtwgR8gUtLtJ0tdWrxSMuN8FHLA45Nv8Ea1EAUQCvfanYZ2L39l++3/zBdg2wYQwCE6XGFnWnayUsGKYjC7JIufnq5f9VDL/kguLKceLmeTHqq31ccRTOQyhuoZjHCsbfXPlW2AT9ejgAcXy7LkXhYCfma50DBM+KUCfC4YrKBg6wKRqdZee90ZPcUKTkw==</span><br><span class="line">RSA 私钥解密结果：Hello World</span><br><span class="line">RSA 私钥签名结果（Base64）：AbP5zSV/qvkF8fCseVkEaZMscvznQBUDtO3g0U/FIXVmzeR6WXFwPsMd3cC3oCHtnnqsL/aRQrpW6pHU6EzSJ5w6FgY6kD4kWREq9f8LOnyQm7CoS6CK0tUiAjIgG16rtmS+oPbG+mYaZkLzo1Cpkpz2MzuMMbWNivvXRMbj3wLiXyIMqUefawipvm+GPwrWRxesRot2sGtuZcxtMMZs3NHpJ0CXV/mQlYJWEzIiHUY4mqfqpMDL/djPf9td74ABpjk38O6r1Jt75TLnMvkwRdh7pHBQLZ0Tn/6Vx2cVD2D+sE9BuhinO66B6I0QOGVcl3a5C2whp+85zEovvdGlSg==</span><br><span class="line">RSA 公钥验证签名结果：true</span><br></pre></td></tr></table></figure></p><p>目前随处可见的 HTTPS 协议，是基于 SSL/TLS 协议的。在 SSL/TLS 协议中，建立加密的传输通道前，首先有一个握手过程。在握手过程中，客户端会生成一个随机值，并使用公钥加密后传给服务端。这个随机值用于生成对称加密算法的密钥，仅有服务端的私钥可以解密，任何第三方都无法解密，这就解决了前面所说到的对称加密算法密钥传输过程中的安全问题。而握手成功后的通信阶段，则使用对称加密算法进行通信。因为非对称加密算法更加复杂，相对于对称加密算法来说效率不高，不适合用来做大量数据的加密解密。</p><p>另外，SSL/TLS 中用到的数字证书（digital certificate），为了防止伪造，也会由 CA 机构进行数字签名。目前大多数 HTTPS 网站使用的数字证书都是使用 SHA256 with RSA 签名。</p><p>例如，在浏览器上打开 <a href="https://xxgblog.com/">https://xxgblog.com/</a> ，点击地址栏左侧的小锁按钮，查看网站使用的证书，其数字签名算法就是 SHA256 with RSA ：</p><p><img src="/img/cryptography-certificate-sign.png" alt="证书签名算法"></p><h2 id="密钥生成"><a href="#密钥生成" class="headerlink" title="密钥生成"></a>密钥生成</h2><p>如果想要针对 HMAC 或对称加密算法生成更加安全的随机密钥，在 Java 中可以使用 <code>KeyGenerator</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 指定加密算法（如 HmacSHA256、AES）</span></span><br><span class="line">KeyGenerator keyGen = KeyGenerator.getInstance(<span class="string">"HmacSHA256"</span>);</span><br><span class="line"><span class="comment">// 设置密钥长度</span></span><br><span class="line">keyGen.init(<span class="number">256</span>);</span><br><span class="line"><span class="comment">// 生成密钥</span></span><br><span class="line">SecretKey secretKey = keyGen.generateKey();</span><br><span class="line"><span class="comment">// 获取密钥的字节数组</span></span><br><span class="line"><span class="keyword">byte</span>[] keyBytes = secretKey.getEncoded();</span><br></pre></td></tr></table></figure></p><p>对于非对称加密算法，除了可以用上文使用过的 <code>openssl</code> 命令，也可以用 Java 中的 <code>KeyPairGenerator</code> 来生成公钥和私钥：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(<span class="string">"RSA"</span>);</span><br><span class="line">keyPairGenerator.initialize(<span class="number">2048</span>);</span><br><span class="line">KeyPair keyPair = keyPairGenerator.generateKeyPair();</span><br><span class="line">PublicKey publicKey = keyPair.getPublic();</span><br><span class="line">PrivateKey privateKey = keyPair.getPrivate();</span><br></pre></td></tr></table></figure></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;字节码转文本的编码方式&quot;&gt;&lt;a href=&quot;#字节码转文本的编码方式&quot; class=&quot;headerlink&quot; title=&quot;字节码转文本的编码方式&quot;&gt;&lt;/a&gt;字节码转文本的编码方式&lt;/h2&gt;&lt;p&gt;在计算机中，无论是内存、磁盘、网络传输，涉及到的数据都是以二进制格式
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="MD5" scheme="https://xxgblog.com/tags/MD5/"/>
    
      <category term="SHA" scheme="https://xxgblog.com/tags/SHA/"/>
    
      <category term="AES" scheme="https://xxgblog.com/tags/AES/"/>
    
      <category term="RSA" scheme="https://xxgblog.com/tags/RSA/"/>
    
      <category term="Base64" scheme="https://xxgblog.com/tags/Base64/"/>
    
  </entry>
  
  <entry>
    <title>设计模式中的俄罗斯套娃：装饰者（Decorator）模式</title>
    <link href="https://xxgblog.com/2021/10/19/decorator-pattern/"/>
    <id>https://xxgblog.com/2021/10/19/decorator-pattern/</id>
    <published>2021-10-19T03:14:16.000Z</published>
    <updated>2025-06-02T16:03:22.879Z</updated>
    
    <content type="html"><![CDATA[<p>俄罗斯套娃想必大家都不陌生，就是同一种玩具娃娃大的套小的，然后一层一层嵌套下去。</p><p><img src="/img/decorator-pattern-matryoshka.gif" alt="俄罗斯套娃"></p><p>在设计模式中，有一种常用的套娃模式，叫做装饰者（Decorator）模式，又称为包装（Wrapper）模式。</p><h2 id="HttpServletRequest-套娃"><a href="#HttpServletRequest-套娃" class="headerlink" title="HttpServletRequest 套娃"></a>HttpServletRequest 套娃</h2><p>在 Spring 框架开发的 Web 应用中，如果使用了 Spring Security 或 Spring Session，用 Debug 模式观察一下某个请求对应的 HttpServletRequest 对象，会发现这就是一个俄罗斯套娃：</p><p><img src="/img/decorator-pattern-request.png" alt="HttpServletRequest 对象"></p><p>图中可以看到我们拿到的 <code>HttpServletRequest</code> 对象，内部成员中包含了一个 <code>HttpServletRequest</code> 对象，而这个内部的 <code>HttpServletRequest</code> 对象内部又包含了一个 <code>HttpServletRequest</code> 对象，层层包含，层层套娃。这就是一个典型的装饰者模式。</p><p>我们知道，<code>HttpServletRequest</code> 是 Servlet 规范中提供的一个 interface 接口。Servlet 规范本身没有实现 <code>HttpServletRequest</code> 接口，<code>HttpServletRequest</code> 接口一般是由 Servlet 容器来实现，例如 Tomcat、Jetty。如果 Spring Security、Spring Session 等框架想要增强 <code>HttpServletRequest</code> 对象的功能，但是不改变原有对象的接口，最好的办法就是使用装饰者模式。例如：</p><ul><li>Spring Security 增强了 <code>HttpServletRequest.getRemoteUser()</code> 方法，可返回当前通过 Spring Security 框架登录用户的用户名；</li><li>Spring Session 增强了 <code>HttpServletRequest.getSession()</code> 方法，增强后的 Session 取代了 Servlet 容器的默认实现，其读写可以使用一个集中式的存储，例如 Redis，这样可以方便集群中的多个实例共享 Session。</li></ul><h3 id="HttpServletRequestWrapper-ServletRequestWrapper"><a href="#HttpServletRequestWrapper-ServletRequestWrapper" class="headerlink" title="HttpServletRequestWrapper / ServletRequestWrapper"></a>HttpServletRequestWrapper / ServletRequestWrapper</h3><p>在 <code>javax.servlet.http</code> 包下有个 <code>HttpServletRequestWrapper</code> 类[<a href="https://github.com/javaee/servlet-spec/blob/3.1.0/src/main/java/javax/servlet/http/HttpServletRequestWrapper.java" target="_blank" rel="noopener">源码</a>]，继承自 <code>ServletRequestWrapper</code> 类[<a href="https://github.com/javaee/servlet-spec/blob/3.1.0/src/main/java/javax/servlet/ServletRequestWrapper.java" target="_blank" rel="noopener">源码</a>]。可以看到这两个类上的注释：</p><blockquote><p>This class implements the Wrapper or Decorator pattern. Methods default to calling through to the wrapped request object.</p></blockquote><p>翻译：这个类实现了装饰者模式/包装模式，其内部方法默认会直接调用内部包装的 request 对象的相应的方法。</p><p><code>ServletRequestWrapper</code> 本身实现了 <code>ServletRequest</code> 接口，它的构造方法要求传入另一个 <code>ServletRequest</code> 对象，并将这个对象赋值给内部 request 对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ServletRequestWrapper</span> <span class="keyword">implements</span> <span class="title">ServletRequest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ServletRequest request;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ServletRequestWrapper</span><span class="params">(ServletRequest request)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (request == <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Request cannot be null"</span>);   </span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">this</span>.request = request;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>ServletRequestWrapper</code> 对 <code>ServletRequest</code> 接口方法的实现，则是直接调用内部 request 对象对应的方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getContentType</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.request.getContentType();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> ServletInputStream <span class="title">getInputStream</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.request.getInputStream();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getParameter</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.request.getParameter(name);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure></p><p>以上就是一个最基本的装饰器。我们可以直接拿来套娃：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">HttpServletRequest request = ...; <span class="comment">// 已有的 request 对象</span></span><br><span class="line">HttpServletRequest requestWrapper = <span class="keyword">new</span> HttpServletRequestWrapper(request); <span class="comment">// 包装后的对象</span></span><br></pre></td></tr></table></figure></p><p>当然，上面代码没有任何意义，因为 <code>requestWrapper</code> 没有做任何扩展，使用 <code>requestWrapper</code> 对象和直接用 <code>request</code> 对象没有任何区别。真正的装饰者类会继承 <code>ServletRequestWrapper</code> 并在此基础上做增强。</p><p>下面，我们再看下 Spring Security 和 Spring Session 如何对 <code>HttpServletRequest</code> 对象进行装饰。</p><h3 id="Spring-Security-Spring-Session-中的装饰者实现"><a href="#Spring-Security-Spring-Session-中的装饰者实现" class="headerlink" title="Spring Security / Spring Session 中的装饰者实现"></a>Spring Security / Spring Session 中的装饰者实现</h3><p>在 Spring Security 文档 <a href="https://docs.spring.io/spring-security/site/docs/5.5.1/reference/html5/#servletapi" target="_blank" rel="noopener">Servlet API integration</a> 中，可以看到 Spring Security 框架对 <code>HttpServletRequest</code> 对象的 <code>getRemoteUser()</code>、<code>getUserPrincipal()</code>、<code>isUserInRole(String)</code> 等方法进行了增强，例如 <code>getRemoteUser()</code> 方法可以直接返回当前登录用户的用户名。接下来看一下 Spring Security 如何增强这些方法。</p><p>首先，Spring Security 提供了一个过滤器 <code>SecurityContextHolderAwareRequestFilter</code>，对相关请求进行过滤处理。在 <code>SecurityContextHolderAwareRequestFilter</code> <a href="https://github.com/spring-projects/spring-security/blob/e41360b4a364744dbbf1afe25840cee26d466b37/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java#L149" target="_blank" rel="noopener">第 149 行</a> 结合 <code>HttpServlet3RequestFactory</code> <a href="https://github.com/spring-projects/spring-security/blob/e41360b4a364744dbbf1afe25840cee26d466b37/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java#L163" target="_blank" rel="noopener">第 163 行</a> 可以看到，这个 Filter 中创建了一个新的 <code>Servlet3SecurityContextHolderAwareRequestWrapper</code> 对象，这个类继承自 <code>HttpServletRequestWrapper</code> 类，并增强了相关方法。其父类 <code>SecurityContextHolderAwareRequestWrapper</code> 类[<a href="https://github.com/spring-projects/spring-security/blob/5.5.1/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestWrapper.java#L102" target="_blank" rel="noopener">源码</a>]中可以看到对 <code>getRemoteUser()</code> 方法的增强：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SecurityContextHolderAwareRequestWrapper</span> <span class="keyword">extends</span> <span class="title">HttpServletRequestWrapper</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getRemoteUser</span><span class="params">()</span> </span>&#123;</span><br><span class="line">Authentication auth = getAuthentication();</span><br><span class="line"><span class="keyword">if</span> ((auth == <span class="keyword">null</span>) || (auth.getPrincipal() == <span class="keyword">null</span>)) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (auth.getPrincipal() <span class="keyword">instanceof</span> UserDetails) &#123;</span><br><span class="line"><span class="keyword">return</span> ((UserDetails) auth.getPrincipal()).getUsername();</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (auth <span class="keyword">instanceof</span> AbstractAuthenticationToken) &#123;</span><br><span class="line"><span class="keyword">return</span> auth.getName();</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> auth.getPrincipal().toString();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>简单来讲，就是 Spring Security 通过一个 Filter 过滤相关请求，拿到原始的 <code>HttpServletRequest</code> 对象，通过一个继承自 <code>HttpServletRequestWrapper</code> 类的装饰者，增强了 <code>getRemoteUser()</code> 等相关方法，再将增强后的对象传给后续的业务处理，那么后续我们在 Controller 层拿到的 <code>HttpServletRequest</code> 对象就可以直接使用 <code>getRemoteUser()</code> 等方法。</p><p>Spring Session 实现和 Spring Security 类似，这里就不再重复介绍，有兴趣可以看 <code>SessionRepositoryFilter</code> <a href="https://github.com/spring-projects/spring-session/blob/2.5.1/spring-session-core/src/main/java/org/springframework/session/web/http/SessionRepositoryFilter.java" target="_blank" rel="noopener">源码</a>。</p><h2 id="Collections-中的装饰者"><a href="#Collections-中的装饰者" class="headerlink" title="Collections 中的装饰者"></a>Collections 中的装饰者</h2><p>装饰者模式不但可以增强被装饰者的功能，还可以禁用某些功能。当然，禁用实际上也是一种“增强”。</p><p>例如，假设有一个 List，当我们需要将这个 List 传给第三方的某个方法去读，但是由于这个第三方方法不可信，为了防止这个方法对 List 篡改，可以通过装饰器模式禁用 List 的修改方法，装饰成一个只读的 List。</p><p><code>java.util.Collections</code> 中提供了一个静态方法 <code>unmodifiableList(List)</code>，用于将一个 List 封装为只读的 List：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;String&gt; list = ...;</span><br><span class="line">List&lt;String&gt; unmodifiableList = Collections.unmodifiableList(list);</span><br></pre></td></tr></table></figure></p><p>通过这个方法的源码可以看到，<code>Collections.unmodifiableList(List)</code> 方法实际上返回了一个 <code>UnmodifiableList</code>。<code>UnmodifiableList</code> 是一个典型的装饰者，其内部对 List 的读相关方法直接调用被装饰对象的对应方法，而对写相关方法做了限制，抛出 <code>UnsupportedOperationException</code>。下面是 <code>UnmodifiableList</code> 的部分源码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">UnmodifiableList</span>&lt;<span class="title">E</span>&gt; <span class="keyword">extends</span> <span class="title">UnmodifiableCollection</span>&lt;<span class="title">E</span>&gt;</span></span><br><span class="line"><span class="class">                                  <span class="keyword">implements</span> <span class="title">List</span>&lt;<span class="title">E</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">final</span> List&lt;? extends E&gt; list;</span><br><span class="line"></span><br><span class="line">    UnmodifiableList(List&lt;? extends E&gt; list) &#123;</span><br><span class="line">        <span class="keyword">super</span>(list);</span><br><span class="line">        <span class="keyword">this</span>.list = list;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> E <span class="title">get</span><span class="params">(<span class="keyword">int</span> index)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> list.get(index);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> E <span class="title">set</span><span class="params">(<span class="keyword">int</span> index, E element)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(<span class="keyword">int</span> index, E element)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> E <span class="title">remove</span><span class="params">(<span class="keyword">int</span> index)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">indexOf</span><span class="params">(Object o)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> list.indexOf(o);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">lastIndexOf</span><span class="params">(Object o)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> list.lastIndexOf(o);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">addAll</span><span class="params">(<span class="keyword">int</span> index, Collection&lt;? extends E&gt; c)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>java.util.Collections</code> 中还提供了其他一系列装饰者：</p><ul><li><code>unmodifiableSet(Set)</code>、<code>unmodifiableMap(Map)</code> 等方法和 <code>unmodifiableList(List)</code> 类似，用于不同类型的集合的装饰</li><li><code>synchronizedList(List)</code>、<code>synchronizedSet(Set)</code>、<code>synchronizedMap(Map)</code> 等方法使用 <code>synchronized</code> 装饰 List、Set、Map 中的相关方法，返回一个线程安全的集合</li><li><code>checkedList(List, Class)</code>、<code>checkedSet(Set, Class)</code>、<code>checkedMap(List, Class, Class)</code> 等方法返回类型安全的集合，如果插入集合的元素类型不符合要求则会抛出异常</li></ul><h2 id="InputStream-装饰者"><a href="#InputStream-装饰者" class="headerlink" title="InputStream 装饰者"></a>InputStream 装饰者</h2><p>装饰者不但可以增强被装饰者原有的方法，还可以增加新的方法扩展功能。</p><p>在 <code>java.io</code> 包中，针对 <code>InputStream</code> 有一个基础的抽象装饰者 <code>FilterInputStream</code>，其源码如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FilterInputStream</span> <span class="keyword">extends</span> <span class="title">InputStream</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> InputStream in;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="title">FilterInputStream</span><span class="params">(InputStream in)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.in = in;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">read</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> in.read();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>类似于上面讲到的 <code>HttpServletRequestWrapper</code> 类，<code>FilterInputStream</code> 是一个基础的装饰者，它的子类才是具体的装饰者的实现。<code>DataInputStream</code> 就是其中一个典型的装饰者实现。</p><p><code>DataInputStream</code> 用于从被装饰的 <code>InputStream</code> 对象中读取基本数据类型，它继承自 <code>FilterInputStream</code>，并新增了新的方法，如 <code>readByte()</code>、<code>readInt()</code>、<code>readFloat()</code> 等，这些方法是 <code>InputStream</code> 接口中没有的。</p><p>除了 <code>DataInputStream</code> 之外，<code>FilterInputStream</code> 常见的子类装饰者还有：</p><ul><li><code>BufferedInputStream</code> 为被装饰的 <code>InputStream</code> 提供缓冲功能以及支持 <code>mark</code> 和 <code>reset</code> 方法</li><li><code>CipherInputStream</code> 使用加密算法（例如 AES、DES）对 <code>InputStream</code> 中的数据加密或解密</li><li><code>DigestInputStream</code> 使用消息摘要算法（例如 MD5、SHA-256）对 <code>InputStream</code> 中的数据进行消息摘要</li><li><code>DeflaterInputStream</code>、<code>InflaterInputStream</code> 使用 deflate 压缩算法对 <code>InputStream</code> 中的数据压缩或解压</li></ul><h2 id="装饰者模式结构"><a href="#装饰者模式结构" class="headerlink" title="装饰者模式结构"></a>装饰者模式结构</h2><p><img src="/img/decorator-pattern-structure.png" alt="装饰者模式结构"></p><p>图片来源： <a href="https://refactoringguru.cn/design-patterns/decorator" target="_blank" rel="noopener">https://refactoringguru.cn/design-patterns/decorator</a></p><p>下面总结一下在前面的例子中，各个类和上图中的对应关系：</p><ul><li>部件（Component）对应有 <code>HttpServletRequest</code>、<code>List</code>、<code>InputStream</code></li><li>基础装饰（Base Decorator）对应有 <code>HttpServletRequestWrapper</code>、<code>FilterInputStream</code>；</li><li>具体装饰类（Concrete Decorators）对应有 <code>Servlet3SecurityContextHolderAwareRequestWrapper</code>、<code>UnmodifiableList</code>、<code>DataInputStream</code></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;俄罗斯套娃想必大家都不陌生，就是同一种玩具娃娃大的套小的，然后一层一层嵌套下去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/decorator-pattern-matryoshka.gif&quot; alt=&quot;俄罗斯套娃&quot;&gt;&lt;/p&gt;
&lt;p&gt;在设计模式中，有一种常用的套娃模式，叫做
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="设计模式" scheme="https://xxgblog.com/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>Java 8 CompletableFuture 对比 ES6 Promise</title>
    <link href="https://xxgblog.com/2021/08/28/promise-vs-completablefuture/"/>
    <id>https://xxgblog.com/2021/08/28/promise-vs-completablefuture/</id>
    <published>2021-08-28T03:14:16.000Z</published>
    <updated>2024-03-16T14:30:34.474Z</updated>
    
    <content type="html"><![CDATA[<p>JavaScript 语言的执行环境是单线程的，异步编程对于 JavaScript 来说必不可少。JavaScript 传统异步解决方案主要是通过回调函数，而回调函数最大的问题就是 Callback Hell。所以 ES6 标准提供的 Promise 对象，专门用于解决异步编程的问题。</p><p>而 Java 语言是一个支持多线程的语言，语法以同步为主，在实际开发中很少需要用到大量的异步编程。但是要想追求更高的性能，异步通常是更好的选择。例如 Servlet 3 的异步支持、Spring 5 提供的 Spring WebFlux 等，都是为了追求更高的性能。和 JavaScript 一样，传统的 Callback 方式处理 Java 异步也会有 Callback Hell 问题，所以在 Java 8 中新增了和 ES6 的 Promise 类似的对象： <code>java.util.concurrent.CompletableFuture</code> 。</p><h2 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h2><h3 id="ES6-Promise"><a href="#ES6-Promise" class="headerlink" title="ES6 Promise"></a>ES6 Promise</h3><p>在 JavaScript 中创建 Promise 对象：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span>(<span class="params">resolve, reject</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (success)&#123;</span><br><span class="line">        <span class="comment">// 成功</span></span><br><span class="line">        resolve(value);  </span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 失败</span></span><br><span class="line">        reject(error);   </span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><p>例如，jQuery 传统的使用回调函数的 ajax 写法是这样的：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">$.ajax(&#123;</span><br><span class="line">    url: <span class="string">"/url"</span>,</span><br><span class="line">    success: <span class="function"><span class="keyword">function</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 成功</span></span><br><span class="line">    &#125;,</span><br><span class="line">    error: <span class="function"><span class="keyword">function</span>(<span class="params">jqXHR, textStatus, error</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 失败</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><p>如果要把它封装一下，返回一个 Promise 对象，可以这样来写：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">promisifyAjax</span>(<span class="params">url</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="keyword">function</span>(<span class="params">resolve, reject</span>) </span>&#123;</span><br><span class="line">        $.ajax(&#123;</span><br><span class="line">            url: url,</span><br><span class="line">            success: <span class="function"><span class="keyword">function</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">                <span class="comment">// 将 Promise 更新为成功状态</span></span><br><span class="line">                resolve(data);   </span><br><span class="line">            &#125;,</span><br><span class="line">            error: <span class="function"><span class="keyword">function</span>(<span class="params">jqXHR, textStatus, error</span>) </span>&#123;</span><br><span class="line">                <span class="comment">// 将 Promise 更新为失败状态</span></span><br><span class="line">                reject(error);   </span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> promise;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>调用这个封装好的 <code>promisifyAjax</code> 方法：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">promisifyAjax(<span class="string">"/url"</span>).then(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 成功</span></span><br><span class="line">&#125;).catch(<span class="function"><span class="keyword">function</span>(<span class="params">error</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 失败</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><h3 id="Java-8-CompletableFuture"><a href="#Java-8-CompletableFuture" class="headerlink" title="Java 8 CompletableFuture"></a>Java 8 CompletableFuture</h3><p>在 Java 中创建 CompletableFuture 对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">CompletableFuture&lt;String&gt; completableFuture = <span class="keyword">new</span> CompletableFuture&lt;&gt;();</span><br><span class="line"><span class="keyword">if</span> (success) &#123;</span><br><span class="line">    completableFuture.complete(<span class="string">"任务成功"</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    completableFuture.completeExceptionally(<span class="keyword">new</span> RuntimeException(<span class="string">"任务失败"</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>CompletableFuture 可以使用泛型，用于指定这个任务成功后返回的结果的数据类型。</p><p>这里可以用 OkHttp 的异步请求来实战使用一下 CompletableFuture。<a href="https://square.github.io/okhttp/recipes/#asynchronous-get-kt-java" target="_blank" rel="noopener">OkHttp 异步请求的官方示例</a>中使用的异步实现方式是基于 Callback 回调的方式：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> OkHttpClient client = <span class="keyword">new</span> OkHttpClient();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    Request request = <span class="keyword">new</span> Request.Builder()</span><br><span class="line">        .url(<span class="string">"http://publicobject.com/helloworld.txt"</span>)</span><br><span class="line">        .build();</span><br><span class="line">    </span><br><span class="line">    client.newCall(request).enqueue(<span class="keyword">new</span> Callback() &#123;</span><br><span class="line">        <span class="meta">@Override</span> </span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onFailure</span><span class="params">(Call call, IOException e)</span> </span>&#123;</span><br><span class="line">            <span class="comment">// HTTP 请求异常</span></span><br><span class="line">            e.printStackTrace();  </span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="meta">@Override</span> </span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onResponse</span><span class="params">(Call call, Response response)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">            <span class="keyword">try</span> (ResponseBody responseBody = response.body()) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!response.isSuccessful()) &#123; </span><br><span class="line">                    <span class="comment">// 响应状态码异常</span></span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> IOException(<span class="string">"Unexpected code "</span> + response);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// 成功</span></span><br><span class="line">                    System.out.println(responseBody.string());  </span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>下面将这个异步请求封装为一个返回 CompletableFuture 的方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> CompletableFuture&lt;String&gt; <span class="title">asyncRequest</span><span class="params">(String url)</span> </span>&#123;</span><br><span class="line">    CompletableFuture&lt;String&gt; completableFuture = <span class="keyword">new</span> CompletableFuture&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    Request request = <span class="keyword">new</span> Request.Builder()</span><br><span class="line">        .url(url)</span><br><span class="line">        .build();</span><br><span class="line">    </span><br><span class="line">    client.newCall(request).enqueue(<span class="keyword">new</span> Callback() &#123;</span><br><span class="line">        <span class="meta">@Override</span> </span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onFailure</span><span class="params">(Call call, IOException e)</span> </span>&#123;</span><br><span class="line">            <span class="comment">// HTTP 请求异常</span></span><br><span class="line">            completableFuture.completeExceptionally(e);  </span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="meta">@Override</span> </span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onResponse</span><span class="params">(Call call, Response response)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">            <span class="keyword">try</span> (ResponseBody responseBody = response.body()) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!response.isSuccessful()) &#123; </span><br><span class="line">                    <span class="comment">// 响应状态码异常</span></span><br><span class="line">                    completableFuture.completeExceptionally(<span class="keyword">new</span> IOException(<span class="string">"Unexpected code "</span> + response));</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;   </span><br><span class="line">                    <span class="comment">// 成功</span></span><br><span class="line">                    completableFuture.complete(responseBody.string());</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> completableFuture;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>使用封装好的 <code>asyncRequest()</code> 方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">asyncRequest(<span class="string">"/url"</span>).thenAccept(responseText -&gt; &#123;</span><br><span class="line">    <span class="comment">// 成功</span></span><br><span class="line">&#125;).exceptionally(e -&gt; &#123;</span><br><span class="line">    <span class="comment">// 失败</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><p>可以看到这个写法几乎和 ES6 Promise 写法一样。</p><p>Java 中还可以使用 Runnable、Supplier 快速创建 CompletableFuture 对象，两者区别在于 Supplier 有执行结果，而 Runnable 没有：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务没有返回值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">CompletableFuture&lt;Void&gt; f1 = CompletableFuture.runAsync(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        doSomething();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务有返回值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">CompletableFuture&lt;String&gt; f2 = CompletableFuture.supplyAsync(<span class="keyword">new</span> Supplier&lt;String&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">get</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        String res = doSomething();</span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><h2 id="使用案例"><a href="#使用案例" class="headerlink" title="使用案例"></a>使用案例</h2><p>上面基于 <code>jQuery.ajax()</code> 函数封装并返回 Promise 对象，是为了学习 Promise 对象是如何创建的。实际上，无论是 ES6 中的 Promise 还是 Java 8 中的 CompletableFuture，都是被广泛认可的标准 API，已经被大量应用。</p><p>例如开源项目 <a href="https://github.com/axios/axios" target="_blank" rel="noopener">Axios</a>，它是一个基于 Promise 的 HTTP 客户端，既支持浏览器 Ajax，又支持 Node.js：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">axios.get(<span class="string">'/user?ID=12345'</span>)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">response</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// handle success</span></span><br><span class="line">        <span class="built_in">console</span>.log(response);</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch(<span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// handle error</span></span><br><span class="line">        <span class="built_in">console</span>.log(error);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure></p><p>同样，在 Java 11 版本中新增了 <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html" target="_blank" rel="noopener"><code>java.net.http.HttpClient</code></a>，纯天然支持 <code>CompletableFuture</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">    HttpClient client = HttpClient.newBuilder().build();</span><br><span class="line">    HttpRequest request = HttpRequest.newBuilder()</span><br><span class="line">            .uri(URI.create(<span class="string">"http://xxx.com/"</span>))</span><br><span class="line">            .build();</span><br><span class="line">    CompletableFuture&lt;HttpResponse&lt;String&gt;&gt; responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());</span><br><span class="line">    </span><br><span class="line">    responseFuture.thenAccept(response -&gt; &#123;</span><br><span class="line">        System.out.println(response.body());</span><br><span class="line">    &#125;).exceptionally(e -&gt; &#123;</span><br><span class="line">        System.out.println(e);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 防止主线程结束后程序停止</span></span><br><span class="line">    Thread.sleep(Integer.MAX_VALUE);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="then-的链式调用"><a href="#then-的链式调用" class="headerlink" title="then 的链式调用"></a>then 的链式调用</h2><p>ES6 Promise 的 then 方法的返回值同样是一个 Promise，所以可以链式调用：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">axios.get(<span class="string">'/request1'</span>)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">response</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 把第一个请求的结果作为第二个请求的参数，并且返回一个新的 Promise 作为下一个 then 的结果</span></span><br><span class="line">        <span class="keyword">const</span> newPromise = axios.get(<span class="string">'/request2?param='</span> + response);</span><br><span class="line">        <span class="keyword">return</span> newPromise;</span><br><span class="line">    &#125;)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">response</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 输出第二个请求的结果</span></span><br><span class="line">        <span class="built_in">console</span>.log(response);</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch(<span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(error);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure></p><p>Java CompletableFuture 可通过 thenCompose 方法来实现多个 CompletableFuture 链式调用：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">    HttpClient client = HttpClient.newBuilder().build();</span><br><span class="line">    HttpRequest request = HttpRequest.newBuilder()</span><br><span class="line">            .uri(URI.create(<span class="string">"http://foo.com/"</span>))</span><br><span class="line">            .build();</span><br><span class="line">    CompletableFuture&lt;HttpResponse&lt;String&gt;&gt; responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());</span><br><span class="line"></span><br><span class="line">    responseFuture.thenCompose(response -&gt; &#123;</span><br><span class="line">        <span class="comment">// 把第一个请求的结果作为第二个请求的参数</span></span><br><span class="line">        HttpRequest request2 = HttpRequest.newBuilder()</span><br><span class="line">                .uri(URI.create(<span class="string">"http://foo.com/?param="</span> + response.body()))</span><br><span class="line">                .build();</span><br><span class="line">        <span class="comment">// 这里必须返回一个新的 CompletableFuture 作为下一个 then 的结果</span></span><br><span class="line">        CompletableFuture&lt;HttpResponse&lt;String&gt;&gt; responseFuture2 = client.sendAsync(request2, HttpResponse.BodyHandlers.ofString());</span><br><span class="line">        <span class="keyword">return</span> responseFuture2;</span><br><span class="line">    &#125;).thenAccept(response -&gt; &#123;</span><br><span class="line">        <span class="comment">// 输出第二个请求的结果</span></span><br><span class="line">        System.out.println(response);</span><br><span class="line">    &#125;).exceptionally(e -&gt; &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 防止主线程结束后程序停止</span></span><br><span class="line">    Thread.sleep(Integer.MAX_VALUE); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="工具方法"><a href="#工具方法" class="headerlink" title="工具方法"></a>工具方法</h2><p>Promise 中的工具方法：</p><ul><li><p><code>Promise.all()</code> 用于将多个 Promise 包装成一个新的 Promise，相当于让所有任务同时进行，当所有任务都成功后，新的 Promise 才会变为成功状态，只要有一个任务失败，新的 Promise 就会变为失败状态</p>  <figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 同时执行 3 个异步任务</span></span><br><span class="line"><span class="keyword">const</span> allPromise = <span class="built_in">Promise</span>.all([promise1, promise2, promise3]);</span><br><span class="line">allPromise.then(<span class="function">(<span class="params">[result1, result2, result3]</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 3 个异步任务全部成功后，这里可以拿到所有任务的结果</span></span><br><span class="line">&#125;).catch(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 只要有一个任务失败，最终结果就是失败</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></li><li><p><code>Promise.race()</code> 用于让多个任务同时进行，只要有一个任务执行完成后（无论成功还是失败），返回的新的 Promise 就会跟随着变更状态</p>  <figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 发起 HTTP 请求，5 秒内没有响应则超时失败</span></span><br><span class="line"><span class="built_in">Promise</span>.race([</span><br><span class="line">    httpGet(<span class="string">'http://example.com/file.txt'</span>),</span><br><span class="line">    delay(<span class="number">5000</span>).then(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'timeout'</span>);</span><br><span class="line">    &#125;)</span><br><span class="line">])</span><br></pre></td></tr></table></figure></li></ul><p>CompletableFuture 中也提供了上述类似的静态方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> CompletableFuture&lt;Void&gt; <span class="title">allOf</span><span class="params">(CompletableFuture&lt;?&gt;... cfs)</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> CompletableFuture&lt;Object&gt; <span class="title">anyOf</span><span class="params">(CompletableFuture&lt;?&gt;... cfs)</span></span></span><br></pre></td></tr></table></figure></p><p>其中，CompletableFuture 的 <code>allOf()</code> 方法类似于 <code>Promise.all()</code>， <code>anyOf()</code> 方法类似于 <code>Promise.race()</code>。</p><p>其中有一个区别是 CompletableFuture 的 <code>allOf()</code> 方法返回的是一个 <code>CompletableFuture&lt;Void&gt;</code>，也就是拿不到异步任务的执行结果。如果想要像 <code>Promise.all()</code> 一样拿到每一个任务的执行结果，可以对这个方法再进行一下封装：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; CompletableFuture&lt;List&lt;T&gt;&gt; all(CompletableFuture&lt;T&gt; ... cfs) &#123;</span><br><span class="line">    <span class="keyword">return</span> CompletableFuture.allOf(cfs)</span><br><span class="line">            .thenApply(v -&gt; Stream.of(cfs)</span><br><span class="line">                    .map(future -&gt; future.join())</span><br><span class="line">                    .collect(Collectors.toList()));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html" target="_blank" rel="noopener">java.util.concurrent.CompletableFuture</a></li><li><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html" target="_blank" rel="noopener">java.net.http.HttpClient</a></li><li><a href="https://es6.ruanyifeng.com/#docs/promise" target="_blank" rel="noopener">Promise</a></li><li><a href="https://github.com/axios/axios" target="_blank" rel="noopener">Axios</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;JavaScript 语言的执行环境是单线程的，异步编程对于 JavaScript 来说必不可少。JavaScript 传统异步解决方案主要是通过回调函数，而回调函数最大的问题就是 Callback Hell。所以 ES6 标准提供的 Promise 对象，专门用于解决异步
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="Promise" scheme="https://xxgblog.com/tags/Promise/"/>
    
      <category term="CompletableFuture" scheme="https://xxgblog.com/tags/CompletableFuture/"/>
    
      <category term="ES6" scheme="https://xxgblog.com/tags/ES6/"/>
    
  </entry>
  
  <entry>
    <title>Java 中的加密算法： AES</title>
    <link href="https://xxgblog.com/2021/08/03/java-aes/"/>
    <id>https://xxgblog.com/2021/08/03/java-aes/</id>
    <published>2021-08-03T03:14:16.000Z</published>
    <updated>2021-12-01T03:19:31.937Z</updated>
    
    <content type="html"><![CDATA[<h2 id="加密算法"><a href="#加密算法" class="headerlink" title="加密算法"></a>加密算法</h2><p>加密算法通常分为对称加密算法和非对称加密算法：</p><ul><li>对称加密算法（symmetric-key cryptography）：加密和解密时使用相同的密钥。常用的对称加密算法有 DES、AES。</li><li>非对称加密算法（asymmetric-key cryptography）：加密和解密使用不同的密钥，例如公钥加密的内容只能用私钥解密，所以又称为公钥加密算法（public-key cryptography）。使用最广泛的非对称加密算法是 RSA 算法。</li></ul><p>两者有不同的使用场景，而且经常会一起搭配起来使用，例如 SSL/TLS 协议就结合了对称加密算法和非对称加密算法。</p><p>本文主要介绍最常用的对称加密算法：AES。</p><h2 id="AES"><a href="#AES" class="headerlink" title="AES"></a>AES</h2><p>AES 全称 Advanced Encryption Standard，是一种对称加密算法。AES 的出现主要是用来取代 DES 加密算法，因为 AES 的安全性相对更高。</p><p>AES 使用非常广泛，可以说只要上网，无论是使用手机 APP 还是 Web 应用，几乎都离不开 AES 加密算法。因为目前大部分网站，包括手机 APP 后端接口，都已经使用 HTTPS 协议，而 HTTPS 在数据传输阶段大部分都是使用 AES 对称加密算法。</p><p>在学习 AES 之前，首先要知道以下规则：</p><ul><li>AES 是一种区块加密算法，加密时会将原始数据按大小拆分成一个个区块进行加密，区块大小固定为 128 比特（即 16 字节）</li><li>AES 密钥长度可以是 128、192 或 256 比特（即 16、25 或 32 字节），密钥长度越长，安全性越高，而性能也就越低</li></ul><h3 id="AES-工作模式"><a href="#AES-工作模式" class="headerlink" title="AES 工作模式"></a>AES 工作模式</h3><p>AES加密算法有多种工作模式（mode of operation），如：ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM。不同的模式参数和加密流程不同，但是核心仍然是 AES 算法。</p><p>本文主要介绍 ECB、CBC、GCM 三种模式。</p><h3 id="AES-填充方式"><a href="#AES-填充方式" class="headerlink" title="AES 填充方式"></a>AES 填充方式</h3><p>由于 AES 是一种区块加密算法，加密时会将原始数据按大小拆分成一个个 128 比特（即 16 字节）区块进行加密，如果需要加密的原始数据不是 16 字节的整数倍时，就需要对原始数据进行填充，使其达到 16 字节的整数倍。</p><p>常用的填充方式有 PKCS5Padding、ISO10126Padding 等，另外如果能保证待加密的原始数据大小为 16 字节的整数倍，也可以选择不填充，即 NoPadding。</p><h2 id="Java-中的-AES"><a href="#Java-中的-AES" class="headerlink" title="Java 中的 AES"></a>Java 中的 AES</h2><p>Java 中的 <code>javax.crypto.Cipher</code> 类提供加密和解密的功能。</p><p>创建一个 <code>Cipher</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cipher cipher = Cipher.getInstance(<span class="string">"AES/CBC/PKCS5Padding"</span>);</span><br></pre></td></tr></table></figure></p><p><code>Cipher</code> 类 <code>getInstance</code> 方法需传递一个加密算法的名称作为参数，用来创建对应的 <code>Cipher</code>，其格式为 <code>algorithm/mode/padding</code>，即 <code>算法名称/工作模式/填充方式</code>，例如 <code>AES/CBC/PKCS5Padding</code>。具体有哪些可选的加密方式，可以参考文档： </p><p><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher" target="_blank" rel="noopener">https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher</a></p><h3 id="ECB"><a href="#ECB" class="headerlink" title="ECB"></a>ECB</h3><p>ECB 全称为电子密码本（Electronic codebook），将待加密的数据拆分成块，并对每个块进行独立加密。</p><p><img src="/img/java-aes-ecb-encryption.svg" alt="ECB 加密"><br><img src="/img/java-aes-ecb-decryption.svg" alt="ECB 解密"></p><p>代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encryptECB(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/ECB/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decryptECB(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/ECB/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的明文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encryptECB(data.getBytes(), key.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"ECB 模式加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decryptECB(ciphertext, key.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>由于加密后的密文是二进制格式而非字符串，所以这里使用了 Base64 编码方式将其转换成字符串方便输出查看。输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ECB 模式加密结果（Base64）：bB0gie8pCE2RBQoIAAIxeA==</span><br><span class="line">解密结果：Hello World</span><br></pre></td></tr></table></figure></p><p>要想知道上面代码的加密结果是否正确，需要找个“裁判”来判断一下，这里可以用 openssl 命令来作为这个“裁判”：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">echo</span> -n <span class="string">'Hello World'</span> | openssl enc -base64 -aes-128-ecb -K $(<span class="built_in">echo</span> -n <span class="string">'12345678abcdefgh'</span> | od -A n -t x1 | sed <span class="string">'s/ *//g'</span>)</span><br><span class="line">bB0gie8pCE2RBQoIAAIxeA==</span><br></pre></td></tr></table></figure></p><p>加密结果和 Java 代码加密结果一致。</p><p>需要注意，AES 密钥长度只能是 16、25 或 32 字节，如果不符合要求则会异常：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java.security.InvalidKeyException: Invalid AES key length</span><br></pre></td></tr></table></figure></p><p>ECB 模式有一个致命的缺点，由于该模式对每个块进行独立加密，会导致同样的明文块被加密成相同的密文块，相对来说并不是非常安全。下图就是一个很好的例子：</p><p><img src="/img/java-aes-ecb.png" alt="ECB 模式"></p><h3 id="CBC"><a href="#CBC" class="headerlink" title="CBC"></a>CBC</h3><p>CBC 全称为密码分组链接（Cipher-block chaining），它的出现解决 ECB 同样的明文块会被加密成相同的密文块的问题。</p><p>CBC 引入了初始向量的概念（IV，Initialization Vector），第一个明文块先与 IV 进行异或后再加密，后续每个明文块先与前一个密文块进行异或后再加密。</p><p><img src="/img/java-aes-cbc-encryption.svg" alt="CBC 加密"><br><img src="/img/java-aes-cbc-decryption.svg" alt="CBC 解密"></p><p>代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encryptCBC(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/CBC/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> IvParameterSpec(iv));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decryptCBC(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/CBC/PKCS5Padding"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> IvParameterSpec(iv));</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的原文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line">    String iv = <span class="string">"iviviviviviviviv"</span>; <span class="comment">// CBC 模式需要用到初始向量参数</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"CBC 模式加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());</span><br><span class="line">    System.out.println(<span class="string">"解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CBC 模式加密结果（Base64）：K7bSB51+KxfqaMjJOsPAQg==</span><br><span class="line">解密结果：Hello World</span><br></pre></td></tr></table></figure></p><p>用 openssl 命令检查一下上面代码加密结果是否正确：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">echo</span> -n <span class="string">'Hello World'</span> | openssl enc -base64 -aes-128-cbc -K $(<span class="built_in">echo</span> -n <span class="string">'12345678abcdefgh'</span> | od -A n -t x1 | sed <span class="string">'s/ *//g'</span>) -iv $(<span class="built_in">echo</span> -n <span class="string">'iviviviviviviviv'</span> | od -A n -t x1 | sed <span class="string">'s/ *//g'</span>)</span><br><span class="line">K7bSB51+KxfqaMjJOsPAQg==</span><br></pre></td></tr></table></figure></p><h3 id="GCM"><a href="#GCM" class="headerlink" title="GCM"></a>GCM</h3><p>GCM 的全称是 Galois/Counter Mode，它是一种认证加密（authenticated encryption）算法。它不但提供了加密解密，还提供了数据完整性校验，防止篡改。</p><p>AES-GCM 模式是目前使用最广泛的模式，可以尝试抓包看一下目前主流的 https 网站，其中大部分都是基于 GCM 模式。下图是使用抓包工具 Charles 查看浏览器访问 https 网站所使用的加密算法：</p><p><img src="/img/java-aes-https.png" alt="抓包"></p><p>可以看到浏览器一般支持 AES-GCM 和 AES-CBC 模式，最终服务器选择使用 AES-GCM。</p><p>AES-GCM 认证加密需要用到以下参数：</p><ul><li>待加密的明文</li><li>密钥</li><li>初始向量 IV</li><li>additional authenticated data (AAD)，可以为空</li><li>authentication tag 比特位数，必须是 128、120、112、104、96 之一</li></ul><p>代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] encryptGCM(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv, <span class="keyword">byte</span>[] aad, <span class="keyword">int</span> tagLength) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/GCM/NoPadding"</span>);</span><br><span class="line">    cipher.init(Cipher.ENCRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> GCMParameterSpec(tagLength, iv));</span><br><span class="line">    cipher.updateAAD(aad);</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] decryptGCM(<span class="keyword">byte</span>[] data, <span class="keyword">byte</span>[] key, <span class="keyword">byte</span>[] iv, <span class="keyword">byte</span>[] aad, <span class="keyword">int</span> tagLength) <span class="keyword">throws</span> NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException &#123;</span><br><span class="line">    Cipher cipher = Cipher.getInstance(<span class="string">"AES/GCM/NoPadding"</span>);</span><br><span class="line">    cipher.init(Cipher.DECRYPT_MODE, <span class="keyword">new</span> SecretKeySpec(key, <span class="string">"AES"</span>), <span class="keyword">new</span> GCMParameterSpec(tagLength, iv));</span><br><span class="line">    cipher.updateAAD(aad);</span><br><span class="line">    <span class="keyword">byte</span>[] result = cipher.doFinal(data);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的原文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line">    String iv = <span class="string">"iviviviviviviviv"</span>;</span><br><span class="line">    String aad = <span class="string">"aad"</span>; <span class="comment">// AAD 长度无限制，可为空</span></span><br><span class="line">    <span class="keyword">int</span> tagLength = <span class="number">128</span>; <span class="comment">// tag 长度必须是 128、120、112、104、96 之一</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);</span><br><span class="line">    System.out.println(<span class="string">"GCM 模式加密结果（Base64）："</span> + Base64.getEncoder().encodeToString(ciphertext));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] plaintext = decryptGCM(ciphertext, key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);</span><br><span class="line">    System.out.println(<span class="string">"解密结果："</span> + <span class="keyword">new</span> String(plaintext));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">GCM 模式加密结果（Base64）：1UxXmFpdUwMnpI7rh0XfmFqtdZSHTbNC/08g</span><br><span class="line">解密结果：Hello World</span><br></pre></td></tr></table></figure></p><p>AES-GCM 是流加密（Stream cipher）算法，所以对应的填充模式为 NoPadding，即无需填充。</p><p>由于 openssl 命令不支持 GCM 模式，所以这里不能用 openssl 命令来验证加密结果。</p><h4 id="密文和-authentication-tag"><a href="#密文和-authentication-tag" class="headerlink" title="密文和 authentication tag"></a>密文和 authentication tag</h4><p>如果看过 <a href="https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf" target="_blank" rel="noopener">NIST 的 GCM 文档</a> ， 可以看到其 GCM 模式的认证加密实际上会有两个返回值：密文和 authentication tag。</p><blockquote><p>The following two bit strings comprise the output data of the authenticated encryption function: </p><ul><li>A ciphertext, denoted C, whose bit length is the same as that of the plaintext. </li><li>An authentication tag, or tag, for short, denoted T. </li></ul></blockquote><p>其中，authentication tag 是用于校验数据完整性和防篡改的：</p><blockquote><p>A cryptographic checksum on data that is designed to reveal both accidental errors and the intentional modification of the data. </p></blockquote><p>上面的 Java 代码中，加密方法的返回值只有一个字节数组，实际上这个返回值已经包含了密文和 authentication tag，authentication tag 是追加在密文的尾部。所以如果要想分别拿到密文和 authentication tag，需要对返回的字节数组进行拆分：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException </span>&#123;</span><br><span class="line">    String data = <span class="string">"Hello World"</span>; <span class="comment">// 待加密的原文</span></span><br><span class="line">    String key = <span class="string">"12345678abcdefgh"</span>; <span class="comment">// key 长度只能是 16、25 或 32 字节</span></span><br><span class="line">    String iv = <span class="string">"iviviviviviviviv"</span>;</span><br><span class="line">    String aad = <span class="string">"aad"</span>; <span class="comment">// AAD 长度无限制，可为空</span></span><br><span class="line">    <span class="keyword">int</span> tagLength = <span class="number">128</span>; <span class="comment">// tag 长度必须是 128、120、112、104、96 之一</span></span><br><span class="line">    <span class="keyword">int</span> tagLengthBytes = tagLength / <span class="number">8</span>; <span class="comment">// 比特数转字节数</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] result = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">byte</span>[] ciphertext = Arrays.copyOfRange(result, <span class="number">0</span>, result.length - tagLengthBytes); <span class="comment">// 密文</span></span><br><span class="line">    <span class="keyword">byte</span>[] tag = Arrays.copyOfRange(result, result.length - tagLengthBytes, result.length); <span class="comment">// authentication tag</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h2><p><a href="https://github.com/wucao/XToolkit/blob/master/src/main/java/com/xxg/xtoolkit/AesUtil.java" target="_blank" rel="noopener">https://github.com/wucao/XToolkit/blob/master/src/main/java/com/xxg/xtoolkit/AesUtil.java</a></p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86" target="_blank" rel="noopener">维基百科-AES</a></li><li><a href="https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F" target="_blank" rel="noopener">维基百科-工作模式</a></li><li><a href="https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html" target="_blank" rel="noopener">javax.crypto.Cipher</a></li><li><a href="https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf" target="_blank" rel="noopener">GCM</a></li><li><a href="https://wiki.openssl.org/index.php/Enc" target="_blank" rel="noopener">OpenSSL Enc</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;加密算法&quot;&gt;&lt;a href=&quot;#加密算法&quot; class=&quot;headerlink&quot; title=&quot;加密算法&quot;&gt;&lt;/a&gt;加密算法&lt;/h2&gt;&lt;p&gt;加密算法通常分为对称加密算法和非对称加密算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对称加密算法（symmetric-key cryp
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="AES" scheme="https://xxgblog.com/tags/AES/"/>
    
      <category term="加密" scheme="https://xxgblog.com/tags/%E5%8A%A0%E5%AF%86/"/>
    
  </entry>
  
  <entry>
    <title>Redis 分布式锁原理及 Redisson 实现</title>
    <link href="https://xxgblog.com/2021/04/25/redisson-distributed-lock/"/>
    <id>https://xxgblog.com/2021/04/25/redisson-distributed-lock/</id>
    <published>2021-04-25T03:14:16.000Z</published>
    <updated>2021-04-25T15:07:38.907Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Redis-分布式锁原理"><a href="#Redis-分布式锁原理" class="headerlink" title="Redis 分布式锁原理"></a>Redis 分布式锁原理</h2><p>Redis 分布式锁原理，可以直接看官方文档：<br><a href="https://redis.io/commands/set#patterns" target="_blank" rel="noopener">https://redis.io/commands/set#patterns</a></p><blockquote><p>The command <code>SET resource-name anystring NX EX max-lock-time</code> is a simple way to implement a locking system with Redis.</p></blockquote><p><code>SET resource-name anystring NX EX max-lock-time</code> 命令可以基于 Redis 实现分布式锁。</p><blockquote><ul><li><code>NX</code> Only set the key if it does not already exist</li><li><code>EX seconds</code> Set the specified expire time, in seconds</li></ul></blockquote><ul><li><code>NX</code> 仅当 key 不存在时设置成功</li><li><code>EX seconds</code> 失效时间（秒）</li></ul><blockquote><p>A client can acquire the lock if the above command returns <code>OK</code> (or retry after some time if the command returns <code>Nil</code>), and remove the lock just using <code>DEL</code>.</p></blockquote><ul><li>当命令返回 <code>OK</code> 时，该客户端获得锁</li><li>当命令返回 <code>Nil</code> 时，客户端未获得锁，需要过一段时间再重试命令尝试获取锁</li><li>使用 <code>DEL</code> 删除命令可用来释放锁</li></ul><blockquote><p>The lock will be auto-released after the expire time is reached.</p></blockquote><p>当达到失效时间时，锁自动释放。</p><blockquote><p>It is possible to make this system more robust modifying the unlock schema as follows:</p><ul><li>Instead of setting a fixed string, set a non-guessable large random string, called token.</li><li>Instead of releasing the lock with DEL, send a script that only removes the key if the value matches.</li></ul><p>This avoids that a client will try to release the lock after the expire time deleting the key created by another client that acquired the lock later.</p></blockquote><p>更加健壮的释放锁的方式：</p><ul><li>设置的 value 是一个随机生成的无法预测的值，叫做 token</li><li>不再使用 DEL 直接删除 key 来释放锁，而是使用一个 script，仅当 value 匹配 token 时才会删除 key</li></ul><p>这样可以防止某个客户端在超过失效时间后尝试释放锁，直接使用 DEL 可能会删除掉别的客户端添加的锁。</p><p>下面是释放锁脚本的例子：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1]</span><br><span class="line">then</span><br><span class="line">    return redis.call(&quot;del&quot;,KEYS[1])</span><br><span class="line">else</span><br><span class="line">    return 0</span><br><span class="line">end</span><br></pre></td></tr></table></figure></p><blockquote><p>The script should be called with <code>EVAL ...script... 1 resource-name token-value</code></p></blockquote><p>执行 <code>EVAL ...script... 1 resource-name token-value</code> 命令释放锁。</p><p>以上是官方文档中的内容，阅读到这里可以发现一个问题：</p><ul><li>官方的方案中，分布式锁是有个失效时间的，达到失效时间锁会被自动释放，如果此时需要加锁执行的任务还未完成，同时锁又被其他客户端获取到，那么就可能会出现严重的问题；</li><li>如果锁不加上失效时间，万一获得锁的客户端突然 crash 了，没有来得及释放锁，那么这个锁就永远不会被释放。</li></ul><p>针对这个问题，可以看下 Redisson 是如何解决的。</p><h2 id="Redisson-分布式锁"><a href="#Redisson-分布式锁" class="headerlink" title="Redisson 分布式锁"></a>Redisson 分布式锁</h2><p>官方文档：<br><a href="https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers" target="_blank" rel="noopener">https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers</a></p><p>通过以下方式，可以获得一个 key 为 <code>myLock</code> 的 <code>RLock</code> 对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Config config = <span class="keyword">new</span> Config();</span><br><span class="line">config.useSingleServer().setAddress(<span class="string">"redis://localhost:6379"</span>);</span><br><span class="line">RedissonClient redisson = Redisson.create(config);</span><br><span class="line">RLock lock = redisson.getLock(<span class="string">"myLock"</span>);</span><br></pre></td></tr></table></figure></p><p>获取锁和释放锁：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">lock.lock(); <span class="comment">// 获取锁</span></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  ...</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">  lock.unlock(); <span class="comment">// 在 finally 中释放锁</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>RLock</code> 提供了以下多种获取锁的方法：</p><ul><li><code>void lock()</code></li><li><code>void lock(long leaseTime, TimeUnit unit)</code></li><li><code>void lockInterruptibly()</code></li><li><code>void lockInterruptibly(long leaseTime, TimeUnit unit)</code></li><li><code>boolean tryLock()</code></li><li><code>boolean tryLock(long time, TimeUnit unit)</code></li><li><code>boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)</code></li></ul><p><code>RLock</code> 实现了 <code>java.util.concurrent.locks.Lock</code> 接口，所以 <code>RLock</code> 是符合 Java 中的 <code>Lock</code> 接口规范的。以上的方法中，这四个方法是来源于 Java 中的 <code>Lock</code> 接口：</p><ul><li><code>void lock()</code> 获取锁，如果锁不可用，则当前线程一直等待，直到获得到锁</li><li><code>void lockInterruptibly()</code> 和 <code>lock()</code> 方法类似，区别是 <code>lockInterruptibly()</code> 方法在等待的过程中可以被 interrupt 打断</li><li><code>boolean tryLock()</code> 获取锁，不等待，立即返回一个 boolean 类型的值表示是否获取成功</li><li><code>boolean tryLock(long time, TimeUnit unit)</code> 获取锁，如果锁不可用，则等待一段时间，等待的最长时间由 <code>long time</code> 和 <code>TimeUnit unit</code> 两个参数指定，如果超过时间未获得锁则返回 false，获取成功返回 true</li></ul><p>除了以上四个方法外，还有三个方法不是来源于 Java 中的 <code>Lock</code> 接口，而是 <code>RLock</code> 中的方法。这三个方法和上面四个方法有一个最大的区别就是多了一个 <code>long leaseTime</code> 参数。<code>leaseTime</code> 指的就是 Redis 中的 key 的失效时间。通过这三个方法获取到的锁，如果达到 <code>leaseTime</code> 锁还未释放，那么这个锁会自动失效。</p><p>回到上面的问题：如果设置了失效时间，当任务未完成且达到失效时间时，锁会被自动释放；如果不设置失效时间，突然 crash 了，锁又会永远得不到释放。Redisson 是怎么解决这个问题的呢？</p><blockquote><p>If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be changed through <code>Config.lockWatchdogTimeout</code> setting.</p></blockquote><p>为了防止 Redisson 实例 crash 导致锁永远不会被释放，针对未指定 <code>leaseTime</code> 的四个方法，Redisson 为锁维护了看门狗（watchdog）。看门狗每隔一段时间去延长一下锁的失效时间。锁的默认失效时间是 30 秒，可通过 <code>Config.lockWatchdogTimeout</code> 修改。延长失效时间的任务的执行频率也是由该配置项决定，是锁的失效时间的 1/3，即默认每隔 10 秒执行一次。</p><p>如果 Redisson 实例 crash 了，看门狗也会跟着 crash，那么达到失效时间这个 key 会被 Redis 自动清除，锁也就被释放了，不会出现锁永久被占用的情况。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Redis-分布式锁原理&quot;&gt;&lt;a href=&quot;#Redis-分布式锁原理&quot; class=&quot;headerlink&quot; title=&quot;Redis 分布式锁原理&quot;&gt;&lt;/a&gt;Redis 分布式锁原理&lt;/h2&gt;&lt;p&gt;Redis 分布式锁原理，可以直接看官方文档：&lt;br&gt;&lt;a 
      
    
    </summary>
    
      <category term="Redis" scheme="https://xxgblog.com/categories/Redis/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="Redis" scheme="https://xxgblog.com/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>分布式链路追踪系统 Zipkin 埋点库 Brave 使用入门</title>
    <link href="https://xxgblog.com/2021/04/19/zipkin-brave/"/>
    <id>https://xxgblog.com/2021/04/19/zipkin-brave/</id>
    <published>2021-04-19T03:14:16.000Z</published>
    <updated>2021-11-30T06:13:22.934Z</updated>
    
    <content type="html"><![CDATA[<p>微服务架构下，服务之间的关系错综复杂。从调用一个 HTTP API 到最终返回结果，中间可能发生了多个服务间的调用。而这些被调用的服务，可能部署在不同的服务器上，由不同的团队开发，甚至可能使用了不同的编程语言。在这样的环境中，排查性能问题或者定位故障就很麻烦。</p><h2 id="Zipkin-是什么"><a href="#Zipkin-是什么" class="headerlink" title="Zipkin 是什么"></a>Zipkin 是什么</h2><p>Zipkin 是一个分布式链路追踪系统（distributed tracing system）。它可以收集并展示一个 HTTP 请求从开始到最终返回结果之间完整的调用链。</p><p><img src="/img/824e3d2a-413c-4cfb-9a66-e45658772abe.png" alt="Zipkin UI"></p><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><ul><li><code>Trace</code> 代表一个完整的调用链。一个 trace 对应一个随机生成的唯一的 traceId。例如一个 HTTP 请求到响应是一个 trace。一个 trace 内部包含多个 span。</li><li><code>Span</code> Trace 中的一个基本单元。一个 span 同样对应一个随机生成的唯一的 spanId。例如一个 HTTP 请求到响应过程中，内部可能会访问型数据库执行一条 SQL，这是一个新的 span，或者内部调用另外一个服务的 HTTP API 也是一个新的 span。一个 trace 中的所有 span 是一个树形结构，树的根节点叫做 root span。除 root span 外，其他 span 都会包含一个 parentId，表示父级 span 的 spanId。</li><li><p><code>Annotation</code> 每个 span 中包含多个 annotation，用来记录关键事件的时间点。例如一个对外的 HTTP 请求从开始到结束，依次有以下几个 annotation：</p><ul><li><code>cs</code> Client Send，客户端发起请求的，这是一个 span 的开始</li><li><code>sr</code> Server Receive，服务端收到请求开始处理</li><li><code>ss</code> Server Send，服务端处理请求完成并响应</li><li><code>cr</code> Client Receive，客户端收到响应，这个 span 到此结束</li></ul><p>记录了以上的时间点，就可以很容易分析出一个 span 每个阶段的耗时：</p><ul><li><code>cr - cs</code> 是整个流程的耗时</li><li><code>sr - cs</code> 以及 <code>cr - ss</code> 是网络耗时</li><li><code>ss - sr</code> 是被调用服务处理业务逻辑的耗时</li></ul><p>然而，<code>sr</code> 和 <code>ss</code> 两个 annotation 依赖被调用方，如果被调用方没有相应的记录，例如下游服务没有对接 instrumentation 库，或者像执行一条 SQL 这样的场景，被调用方是一个数据库服务，不会记录 <code>sr</code> 和 <code>ss</code>，那么这个 span 就只有 <code>cs</code> 和 <code>cr</code>。</p></li></ul><p>相关文档：</p><ul><li><a href="https://zipkin.io/pages/instrumenting.html" target="_blank" rel="noopener">https://zipkin.io/pages/instrumenting.html</a></li><li>数据模型： <a href="https://zipkin.io/pages/data_model.html" target="_blank" rel="noopener">https://zipkin.io/pages/data_model.html</a></li></ul><h2 id="B3-Propagation"><a href="#B3-Propagation" class="headerlink" title="B3 Propagation"></a>B3 Propagation</h2><p>当上游服务通过 HTTP 调用下游服务，如何将两个服务中的所有 span 串联起来，形成一个 trace，这就需要上游服务将 traceId 等信息传递给下游服务，而不能让下游重新生成一个 traceId。</p><p>Zipkin 通过 B3 传播规范（B3 Propagation），将相关信息（如 traceId、spanId 等）通过 HTTP 请求 Header 传递给下游服务：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">   Client Tracer                                                  Server Tracer     </span><br><span class="line">┌───────────────────────┐                                       ┌───────────────────────┐</span><br><span class="line">│                       │                                       │                       │</span><br><span class="line">│   TraceContext        │          Http Request Headers         │   TraceContext        │</span><br><span class="line">│ ┌───────────────────┐ │         ┌───────────────────┐         │ ┌───────────────────┐ │</span><br><span class="line">│ │ TraceId           │ │         │ X-B3-TraceId      │         │ │ TraceId           │ │</span><br><span class="line">│ │                   │ │         │                   │         │ │                   │ │</span><br><span class="line">│ │ ParentSpanId      │ │ Inject  │ X-B3-ParentSpanId │ Extract │ │ ParentSpanId      │ │</span><br><span class="line">│ │                   ├─┼────────&gt;│                   ├─────────┼&gt;│                   │ │</span><br><span class="line">│ │ SpanId            │ │         │ X-B3-SpanId       │         │ │ SpanId            │ │</span><br><span class="line">│ │                   │ │         │                   │         │ │                   │ │</span><br><span class="line">│ │ Sampling decision │ │         │ X-B3-Sampled      │         │ │ Sampling decision │ │</span><br><span class="line">│ └───────────────────┘ │         └───────────────────┘         │ └───────────────────┘ │</span><br><span class="line">│                       │                                       │                       │</span><br><span class="line">└───────────────────────┘                                       └───────────────────────┘</span><br></pre></td></tr></table></figure><p>相关文档：</p><ul><li><a href="https://github.com/openzipkin/b3-propagation" target="_blank" rel="noopener">https://github.com/openzipkin/b3-propagation</a></li></ul><h2 id="Brave-是什么"><a href="#Brave-是什么" class="headerlink" title="Brave 是什么"></a>Brave 是什么</h2><p>GitHub 仓库： <a href="https://github.com/openzipkin/brave" target="_blank" rel="noopener">https://github.com/openzipkin/brave</a></p><blockquote><p>Brave is a distributed tracing instrumentation library.</p></blockquote><p>翻译： Brave 是分布式链路追踪的埋点库。</p><p>instrumentation 这个单词本意是”仪器、仪表、器乐谱写”，为了更加便于理解，这里我翻译为”埋点”。埋点的意思就是在程序的关键位置（即上面介绍的各个 annotation）做一些记录。</p><p>在 GitHub 仓库的 <a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation" target="_blank" rel="noopener">instrumentation 目录</a>中，可以看到官方已经提供了非常多的 instrumentation。</p><p>另外在 <a href="https://zipkin.io/pages/tracers_instrumentation" target="_blank" rel="noopener">https://zipkin.io/pages/tracers_instrumentation</a> 文档中，还有其他非 Java 语言的 instrumentation 以及非官方提供的 instrumentation，可以根据需要来选择。其他 instrumentation 本文不做介绍，本文重点是 Zipkin 官方提供的 Java 语言 instrumentation ： Brave 。</p><h2 id="Spring-MVC-项目配置-Brave"><a href="#Spring-MVC-项目配置-Brave" class="headerlink" title="Spring MVC 项目配置 Brave"></a>Spring MVC 项目配置 Brave</h2><p>本文以 Web 服务为例，不涉及像 Dubbo 这样的 RPC 服务。</p><p>假设现有一个 Spring MVC 项目想要对接 Zipkin，需要使用 Brave 埋点，并将相关数据提交到 Zipkin 服务上。</p><h3 id="Maven-依赖管理"><a href="#Maven-依赖管理" class="headerlink" title="Maven 依赖管理"></a>Maven 依赖管理</h3><p>首先加入一个 dependencyManagement，这样就不需要在各个依赖包中添加版本号了：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-bom<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.11.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>最新版本号可以在这里查看：<br><a href="https://mvnrepository.com/artifact/io.zipkin.brave/brave-bom" target="_blank" rel="noopener">https://mvnrepository.com/artifact/io.zipkin.brave/brave-bom</a></p><p>需要注意的是，不同版本配置方法会略有差异，具体可以参考官方文档。本文使用的 Brave 版本号为 <a href="https://github.com/openzipkin/brave/tree/5.11.2" target="_blank" rel="noopener">5.11.2</a>。</p><h3 id="创建-Tracing-对象"><a href="#创建-Tracing-对象" class="headerlink" title="创建 Tracing 对象"></a>创建 Tracing 对象</h3><p>添加依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-context-slf4j<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.reporter2<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>zipkin-sender-okhttp3<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>下面提供了两种配置方式（<a href="#Java-配置方式">Java 配置方式</a> 和 <a href="#XML-配置方式">XML 配置方式</a>）创建 Tracing 对象，需要根据项目的实际情况选择其中一种。</p><h4 id="Java-配置方式"><a href="#Java-配置方式" class="headerlink" title="Java 配置方式"></a>Java 配置方式</h4><p>如果现有的项目是 Spring Boot 项目或者非 XML 配置的 Spring 项目，可以采用这种方式。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TracingConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Tracing <span class="title">tracing</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Sender sender = OkHttpSender.create(<span class="string">"http://127.0.0.1:9411/api/v2/spans"</span>);</span><br><span class="line">        Reporter&lt;Span&gt; spanReporter = AsyncReporter.create(sender);</span><br><span class="line"></span><br><span class="line">        Tracing tracing = Tracing.newBuilder()</span><br><span class="line">                .localServiceName(<span class="string">"my-service"</span>)</span><br><span class="line">                .spanReporter(spanReporter)</span><br><span class="line">                .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder()</span><br><span class="line">                        .addScopeDecorator(MDCScopeDecorator.get()).build())</span><br><span class="line">                .build();</span><br><span class="line">        <span class="keyword">return</span> tracing;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h4 id="XML-配置方式"><a href="#XML-配置方式" class="headerlink" title="XML 配置方式"></a>XML 配置方式</h4><p>如果现有项目是采用 XML 配置的 Spring 项目，可以采用这种方式。</p><p>相对于 Java 配置方式，需要多添加一个 <a href="https://github.com/openzipkin/brave/tree/5.11.2/spring-beans" target="_blank" rel="noopener">brave-spring-beans</a> 依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-spring-beans<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>该模块提供了一系列 Spring <code>FactoryBean</code>，用于通过 XML 来创建对象：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"sender"</span> <span class="attr">class</span>=<span class="string">"zipkin2.reporter.beans.OkHttpSenderFactoryBean"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"endpoint"</span> <span class="attr">value</span>=<span class="string">"http://localhost:9411/api/v2/spans"</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"correlationScopeDecorator"</span> <span class="attr">class</span>=<span class="string">"brave.spring.beans.CorrelationScopeDecoratorFactoryBean"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"builder"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"brave.context.slf4j.MDCScopeDecorator"</span> <span class="attr">factory-method</span>=<span class="string">"newBuilder"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"tracing"</span> <span class="attr">class</span>=<span class="string">"brave.spring.beans.TracingFactoryBean"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"localServiceName"</span> <span class="attr">value</span>=<span class="string">"my-service"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"spanReporter"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"zipkin2.reporter.beans.AsyncReporterFactoryBean"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"sender"</span> <span class="attr">ref</span>=<span class="string">"sender"</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"currentTraceContext"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"brave.spring.beans.CurrentTraceContextFactoryBean"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"scopeDecorators"</span> <span class="attr">ref</span>=<span class="string">"correlationScopeDecorator"</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h4 id="代码分析"><a href="#代码分析" class="headerlink" title="代码分析"></a>代码分析</h4><p>上面两种方式本质上是一样的，都是创建了一个 <code>Tracing</code> 对象。</p><p>该对象是单实例的，如果想要在其他地方获取到这个对象，可以通过静态方法 <code>Tracing tracing = Tracing.current()</code> 来获取。</p><p><code>Tracing</code> 对象提供了一系列 instrumentation 所需要的工具，例如 <code>tracing.tracer()</code> 可以获取到 <code>Tracer</code> 对象，<code>Tracer</code> 对象的作用后面会有详细介绍。</p><p>创建 <code>Tracing</code> 对象一些相关属性：</p><ul><li><code>localServiceName</code> 服务的名称</li><li><code>spanReporter</code> 指定一个 <code>Reporter&lt;zipkin2.Span&gt;</code> 对象作为埋点数据的提交方式，这里通常会使用静态方法 <code>AsyncReporter.create(Sender sender)</code> 来创建一个 <code>AsyncReporter</code> 对象，当然如果有特殊需求也可以自己实现 <code>Reporter</code> 接口来自定义提交方式。创建 <code>AsyncReporter</code> 对象需要提供一个 <code>Sender</code>，下面列出了一些官方提供的 <code>Sender</code> 可供选择：<ul><li><code>zipkin-sender-okhttp3</code> 使用 OkHttp3 提交，使用方法：<code>sender = OkHttpSender.create(&quot;http://localhost:9411/api/v2/spans&quot;)</code>，本文中的示例使用的就是这种方式</li><li><code>zipkin-sender-urlconnection</code> 使用 Java 自带的 <code>java.net.HttpURLConnection</code> 提交，使用方法：<code>sender = URLConnectionSender.create(&quot;http://localhost:9411/api/v2/spans&quot;)</code></li><li><code>zipkin-sender-activemq-client</code> 使用 <code>ActiveMQ</code> 消息队列提交，使用方法：<code>sender = ActiveMQSender.create(&quot;failover:tcp://localhost:61616&quot;)</code></li><li><code>zipkin-sender-kafka</code> 使用 <code>Kafka</code> 消息队列提交，使用方法：<code>sender = KafkaSender.create(&quot;localhost:9092&quot;)</code></li><li><code>zipkin-sender-amqp-client</code> 使用 <code>RabbitMQ</code> 消息队列提交，使用方法：<code>sender = RabbitMQSender.create(&quot;localhost:5672&quot;)</code></li></ul></li><li><p><code>currentTraceContext</code>  指定一个 <code>CurrentTraceContext</code> 对象来设置 <code>TraceContext</code> 对象的作用范围，通常会使用 <code>ThreadLocalCurrentTraceContext</code>，也就是用 <code>ThreadLocal</code> 来存放 <code>TraceContext</code>。<code>TraceContext</code> 包含了一个 trace 的相关信息，例如 traceId。</p><p>  由于在 Spring MVC 应用中，一个请求的业务逻辑通常在同一个线程中（暂不考虑异步 Servlet）。一个请求内部的所有业务逻辑应该共用一个 traceId，自然是把 <code>TraceContext</code> 放在 <code>ThreadLocal</code> 中比较合理。这也意味着，默认情况下 traceId 只在当前线程有效，跨线程会失效。当然，跨线程也有对应的方案，本文后续会有详细介绍。</p></li><li><p>在 <code>CurrentTraceContext</code> 中可以添加 <code>ScopeDecorator</code> ，通过 MDC （Mapped Diagnostic Contexts） 机制关联一些日志框架：</p><ul><li><a href="https://github.com/openzipkin/brave/tree/5.11.2/context/slf4j" target="_blank" rel="noopener">brave-context-slf4j</a> SLF4J </li><li><a href="https://github.com/openzipkin/brave/tree/5.11.2/context/log4j2" target="_blank" rel="noopener">brave-context-log4j2</a> Log4J 2</li><li><a href="https://github.com/openzipkin/brave/tree/5.11.2/context/log4j12" target="_blank" rel="noopener">brave-context-log4j12</a> Log4J v1.2  </li></ul><p>以 Logback 为例（本文中案例使用的方式），可以配置下面的 pattern 在日志中输出 traceId 和 spanId：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">pattern</span>&gt;</span>%d [%X&#123;traceId&#125;/%X&#123;spanId&#125;] [%thread] %-5level %logger&#123;36&#125; - %msg%n<span class="tag">&lt;/<span class="name">pattern</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ul><h3 id="Spring-MVC-埋点"><a href="#Spring-MVC-埋点" class="headerlink" title="Spring MVC 埋点"></a>Spring MVC 埋点</h3><p>添加依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-instrumentation-spring-webmvc<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h4 id="创建-HttpTracin-对象"><a href="#创建-HttpTracin-对象" class="headerlink" title="创建 HttpTracin 对象"></a>创建 HttpTracin 对象</h4><p>首先创建 <code>HttpTracing</code> 对象，用于 HTTP 协议链路追踪。</p><p>Java 配置方式：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> HttpTracing <span class="title">httpTracing</span><span class="params">(Tracing tracing)</span></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> HttpTracing.create(tracing);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>XML 配置方式：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"httpTracing"</span> <span class="attr">class</span>=<span class="string">"brave.spring.beans.HttpTracingFactoryBean"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"tracing"</span> <span class="attr">ref</span>=<span class="string">"tracing"</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h4 id="添加-DelegatingTracingFilter"><a href="#添加-DelegatingTracingFilter" class="headerlink" title="添加 DelegatingTracingFilter"></a>添加 DelegatingTracingFilter</h4><p><code>DelegatingTracingFilter</code> 用于处理外部调用的 HTTP 请求，记录 <code>sr</code>（Server Receive） 和 <code>ss</code>（Server Send） 两个 annotation。</p><p>非 Spring Boot 项目可以在 web.xml 中添加 <code>DelegatingTracingFilter</code>：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">filter</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">filter-name</span>&gt;</span>tracingFilter<span class="tag">&lt;/<span class="name">filter-name</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">filter-class</span>&gt;</span>brave.spring.webmvc.DelegatingTracingFilter<span class="tag">&lt;/<span class="name">filter-class</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">filter</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">filter-mapping</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">filter-name</span>&gt;</span>tracingFilter<span class="tag">&lt;/<span class="name">filter-name</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">url-pattern</span>&gt;</span>/*<span class="tag">&lt;/<span class="name">url-pattern</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">filter-mapping</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>如果是 Spring Boot 项目可以用 <code>FilterRegistrationBean</code> 来添加 <code>DelegatingTracingFilter</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> FilterRegistrationBean <span class="title">delegatingTracingFilterRegistrationBean</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    FilterRegistrationBean registration = <span class="keyword">new</span> FilterRegistrationBean();</span><br><span class="line">    registration.setFilter(<span class="keyword">new</span> DelegatingTracingFilter());</span><br><span class="line">    registration.setName(<span class="string">"tracingFilter"</span>);</span><br><span class="line">    <span class="keyword">return</span> registration;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>到此，Spring MVC 项目已经完成了最基本的 Brave 埋点和提交 Zipkin 的配置。如果有现有的 Zipkin 服务，将创建 <code>OkHttpSender</code> 提供的接口地址换成实际地址，启动服务后通过 HTTP 请求一下服务，就会在 Zipkin 上找到一个对应的 trace。</p><h2 id="其他-instrumentation-介绍"><a href="#其他-instrumentation-介绍" class="headerlink" title="其他 instrumentation 介绍"></a>其他 instrumentation 介绍</h2><p>由于每个服务内部还会调用其他服务，例如通过 HTTP 调用外部服务的 Api、连接远程数据库执行 SQL，此时还需要用到其他 instrumentation。</p><p>由于篇幅有限，下面仅介绍几个常用的 instrumentation。</p><h3 id="brave-instrumentation-mysql"><a href="#brave-instrumentation-mysql" class="headerlink" title="brave-instrumentation-mysql"></a>brave-instrumentation-mysql</h3><p><a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation/mysql" target="_blank" rel="noopener">brave-instrumentation-mysql</a> 可以为 MySQL 上执行的每条 SQL 语句生成一个 span，用于分析 SQL 的执行时间。</p><p>添加依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-instrumentation-mysql<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>使用方法：在 JDBC 连接地址末尾加上参数 <code>?statementInterceptors=brave.mysql.TracingStatementInterceptor</code> 即可。</p><p>该模块用于 mysql-connector-java 5.x 版本，另外还有 <a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation/mysql6" target="_blank" rel="noopener">brave-instrumentation-mysql6</a> 和 <a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation/mysql8" target="_blank" rel="noopener">brave-instrumentation-mysql8</a> 可分别用于 mysql-connector-java 6+ 和 mysql-connector-java 8+ 版本。</p><h3 id="brave-instrumentation-okhttp3"><a href="#brave-instrumentation-okhttp3" class="headerlink" title="brave-instrumentation-okhttp3"></a>brave-instrumentation-okhttp3</h3><p><a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation/okhttp3" target="_blank" rel="noopener">brave-instrumentation-okhttp3</a> 用于 OkHttp 3.x，在通过 <code>OkHttpClient</code> 请求外部 API 时，生成 span，并且通过 B3 传播规范将链路信息传递给被调用方。</p><p>添加依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.zipkin.brave<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>brave-instrumentation-okhttp3<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>使用方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">OkHttpClient okHttpClient = <span class="keyword">new</span> OkHttpClient.Builder()</span><br><span class="line">                .dispatcher(<span class="keyword">new</span> Dispatcher(</span><br><span class="line">                        httpTracing.tracing().currentTraceContext()</span><br><span class="line">                                .executorService(<span class="keyword">new</span> Dispatcher().executorService())</span><br><span class="line">                ))</span><br><span class="line">                .addNetworkInterceptor(TracingInterceptor.create(httpTracing))</span><br><span class="line">                .build();</span><br></pre></td></tr></table></figure></p><p>如果你使用的 HTTP 客户端库不是 OkHttp 而是 Apache HttpClient 的话，可以使用 <a href="https://github.com/openzipkin/brave/tree/5.11.2/instrumentation/httpclient" target="_blank" rel="noopener">brave-instrumentation-httpclient</a>。</p><h2 id="更多玩法"><a href="#更多玩法" class="headerlink" title="更多玩法"></a>更多玩法</h2><h3 id="获取当前-traceId-和-spanId"><a href="#获取当前-traceId-和-spanId" class="headerlink" title="获取当前 traceId 和 spanId"></a>获取当前 traceId 和 spanId</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Span currentSpan = Tracing.currentTracer().currentSpan(); <span class="comment">// 获取当前 span</span></span><br><span class="line"><span class="keyword">if</span> (currentSpan != <span class="keyword">null</span>) &#123;</span><br><span class="line">    String traceId = currentSpan.context().traceIdString();</span><br><span class="line">    String spanId = currentSpan.context().spanIdString();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="自定义-tag"><a href="#自定义-tag" class="headerlink" title="自定义 tag"></a>自定义 tag</h3><p>可将业务相关的信息写入 tag 中，方便在查看调用链信息时关联查看业务相关信息。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Span currentSpan = Tracing.currentTracer().currentSpan(); <span class="comment">// 获取当前 span</span></span><br><span class="line"><span class="keyword">if</span> (currentSpan != <span class="keyword">null</span>) &#123;</span><br><span class="line">    currentSpan.tag(<span class="string">"biz.k1"</span>, <span class="string">"v1"</span>).tag(<span class="string">"biz.k2"</span>, <span class="string">"v2"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="创建新-span"><a href="#创建新-span" class="headerlink" title="创建新 span"></a>创建新 span</h3><p>如果使用了某些组件访问外部服务，找不到官方或开源的 instrumentation，或者有一个本地的耗时任务，也想通过创建一个 span 来记录任务的运行时间和结果，可以自己创建一个新的 span。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">ScopedSpan span = Tracing.currentTracer().startScopedSpan(<span class="string">"span name"</span>); <span class="comment">// 创建一个 Span 并记录 Span 开始时间，将 TraceContext 通过 ThreadLocal 绑定到当前线程</span></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 访问外部服务 或 本地耗时任务</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    span.error(e); <span class="comment">// 任务出错</span></span><br><span class="line">    <span class="keyword">throw</span> e;</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    span.finish(); <span class="comment">// 记录 Span 的结束时间，移除当前 TraceContext 和 ThreadLocal 绑定</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>需要注意的是，<code>ScopedSpan</code> 是非线程安全的，如果需要跨线程，可以用下面这种方式：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Tracer tracer = Tracing.currentTracer();</span><br><span class="line">Span span = tracer.nextSpan().name(<span class="string">"span name"</span>).start(); <span class="comment">// 创建一个 Span 并记录 Span 开始时间</span></span><br><span class="line"><span class="keyword">try</span> (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) &#123; <span class="comment">// 将 TraceContext 通过 ThreadLocal 绑定到当前线程，任务结束后会通过 try-with-resources 关闭来解除绑定</span></span><br><span class="line">    <span class="comment">// 访问外部服务 或 本地耗时任务</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    span.error(e); <span class="comment">// 任务出错</span></span><br><span class="line">    <span class="keyword">throw</span> e;</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    span.finish(); <span class="comment">// 记录 Span 的结束时间</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="跨线程追踪"><a href="#跨线程追踪" class="headerlink" title="跨线程追踪"></a>跨线程追踪</h3><h4 id="使用包装过的-Runnable-和-Callable-对象"><a href="#使用包装过的-Runnable-和-Callable-对象" class="headerlink" title="使用包装过的 Runnable 和 Callable 对象"></a>使用包装过的 Runnable 和 Callable 对象</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Runnable runnable = ...; <span class="comment">// 原始的 Runnable 对象</span></span><br><span class="line">Runnable tracingRunnable = Tracing.current().currentTraceContext().wrap(runnable); <span class="comment">// 包装过的 Runnable 对象</span></span><br></pre></td></tr></table></figure><p>同样的方式也可以使用于 <code>Callable</code> 对象。</p><h4 id="使用包装过的线程池"><a href="#使用包装过的线程池" class="headerlink" title="使用包装过的线程池"></a>使用包装过的线程池</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ExecutorService service = ....;</span><br><span class="line">ExecutorService proxiedService = tracing.currentTraceContext().executorService(service);</span><br></pre></td></tr></table></figure><h2 id="对接除-Zipkin-外的其他分布式追踪系统"><a href="#对接除-Zipkin-外的其他分布式追踪系统" class="headerlink" title="对接除 Zipkin 外的其他分布式追踪系统"></a>对接除 Zipkin 外的其他分布式追踪系统</h2><p>除 Zipkin 之外，还有很多优秀的开源或商业的分布式链路追踪系统。其中一部分对 Zipkin 协议做了兼容，如果不想使用 Zipkin 也是可以尝试一下其他的分布式链路追踪系统。</p><ul><li><a href="https://www.jaegertracing.io/" target="_blank" rel="noopener">Jaeger</a> 是 Uber 开源的一套分布式追踪系统，可以通过它的 9411 端口对接： <a href="https://www.jaegertracing.io/docs/1.22/getting-started/#migrating-from-zipkin" target="_blank" rel="noopener">https://www.jaegertracing.io/docs/1.22/getting-started/#migrating-from-zipkin</a><br>  <img src="/img/483b6f62-e19c-4333-b4ef-e2de7d4f4acf.png" alt="Jaeger UI"></li><li><a href="https://skywalking.apache.org/" target="_blank" rel="noopener">SkyWalking</a> 提供了 Zipkin receiver 来接收 Zipkin 格式的数据： <a href="https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/backend-receivers.md#zipkin-receiver" target="_blank" rel="noopener">https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/backend-receivers.md#zipkin-receiver</a><br>  <img src="/img/7c2fd9fd-530a-4f24-ba05-219e284c203d.png" alt="SkyWalking UI"></li><li><a href="https://www.aliyun.com/product/xtrace" target="_blank" rel="noopener">阿里云链路追踪</a> 是阿里云提供的商业链路追踪系统，对接文档： <a href="https://help.aliyun.com/document_detail/95862.html" target="_blank" rel="noopener">https://help.aliyun.com/document_detail/95862.html</a><br>  <img src="/img/09e9701c-bd25-405e-8f69-c0816e9b0bc0.png" alt="阿里云链路追踪 UI"></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;微服务架构下，服务之间的关系错综复杂。从调用一个 HTTP API 到最终返回结果，中间可能发生了多个服务间的调用。而这些被调用的服务，可能部署在不同的服务器上，由不同的团队开发，甚至可能使用了不同的编程语言。在这样的环境中，排查性能问题或者定位故障就很麻烦。&lt;/p&gt;
&lt;h
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Zipkin" scheme="https://xxgblog.com/tags/Zipkin/"/>
    
      <category term="Brave" scheme="https://xxgblog.com/tags/Brave/"/>
    
  </entry>
  
  <entry>
    <title>基于 Netty 实现 WebSocket 服务器</title>
    <link href="https://xxgblog.com/2021/04/14/netty-websocket/"/>
    <id>https://xxgblog.com/2021/04/14/netty-websocket/</id>
    <published>2021-04-14T12:19:16.000Z</published>
    <updated>2021-10-18T10:25:33.938Z</updated>
    
    <content type="html"><![CDATA[<h2 id="WebSocket-协议介绍"><a href="#WebSocket-协议介绍" class="headerlink" title="WebSocket 协议介绍"></a>WebSocket 协议介绍</h2><p>WebSocket 协议是一种在单个 TCP 连接上进行全双工通信的协议，在建立连接完成握手阶段后，服务端也可以主动推送数据给客户端，使得 Web 浏览器和服务器之间的交互性更强大。</p><p>目前 WebSocket 协议应用非常广泛，大部分浏览器均已支持 WebSocket，不仅仅在 Web 应用中，其他很多类型应用（例如游戏）也经常用到 WebSocket 协议。</p><h2 id="WebSocket-建立连接的过程"><a href="#WebSocket-建立连接的过程" class="headerlink" title="WebSocket 建立连接的过程"></a>WebSocket 建立连接的过程</h2><p>WebSocket 分为握手阶段（ handshake ）和数据传输阶段（ data transfer ）。</p><h3 id="握手阶段（-handshake-）"><a href="#握手阶段（-handshake-）" class="headerlink" title="握手阶段（ handshake ）"></a>握手阶段（ handshake ）</h3><p>在客户端和服务器建立 WebSocket 连接之前，客户端首先要发送一个 HTTP 协议的握手请求：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">GET /chat HTTP/1.1</span><br><span class="line">Host: server.example.com</span><br><span class="line">Upgrade: websocket</span><br><span class="line">Connection: Upgrade</span><br><span class="line">Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==</span><br><span class="line">Origin: http://example.com</span><br><span class="line">Sec-WebSocket-Protocol: chat, superchat</span><br><span class="line">Sec-WebSocket-Version: 13</span><br></pre></td></tr></table></figure></p><p>其中请求头 <code>Connection: Upgrade</code> 和 <code>Upgrade: websocket</code> 表示客户端想要升级协议为 WebSocket。服务器进行如下响应完成握手：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">HTTP/1.1 101 Switching Protocols</span><br><span class="line">Upgrade: websocket</span><br><span class="line">Connection: Upgrade</span><br><span class="line">Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=</span><br><span class="line">Sec-WebSocket-Protocol: chat</span><br></pre></td></tr></table></figure></p><p>完成握手后，接下来就是双向的数据传输的过程。</p><h3 id="数据传输阶段（-data-transfer-）"><a href="#数据传输阶段（-data-transfer-）" class="headerlink" title="数据传输阶段（ data transfer ）"></a>数据传输阶段（ data transfer ）</h3><p>数据传输阶段传输的内容以帧（ frame ）为单位，其中分为控制帧（Control Frame）和数据帧（Data Frame）：</p><ul><li>控制帧（Control Frame）：包括 <code>Close</code>、<code>Ping</code>、<code>Pong</code> 帧，<code>Close</code> 用于关闭 WebSocket 连接，<code>Ping</code> 和 <code>Pong</code> 用于心跳检测</li><li>数据帧（Data Frame）：包括 <code>Text</code> 和 <code>Binary</code> 帧，分别用于传输文本和二进制数据</li></ul><h2 id="Netty-实现-WebSocket-服务器"><a href="#Netty-实现-WebSocket-服务器" class="headerlink" title="Netty 实现 WebSocket 服务器"></a>Netty 实现 WebSocket 服务器</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">WebSocketServer</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        EventLoopGroup bossGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line">        EventLoopGroup workerGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            ServerBootstrap b = <span class="keyword">new</span> ServerBootstrap();</span><br><span class="line">            b.group(bossGroup, workerGroup)</span><br><span class="line">                    .channel(NioServerSocketChannel.class)</span><br><span class="line">                    .childHandler(<span class="keyword">new</span> ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">                        <span class="meta">@Override</span></span><br><span class="line">                        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">initChannel</span><span class="params">(SocketChannel ch)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">                            ChannelPipeline pipeline = ch.pipeline();</span><br><span class="line">                            pipeline.addLast(<span class="keyword">new</span> HttpServerCodec()); <span class="comment">// HTTP 协议解析，用于握手阶段</span></span><br><span class="line">                            pipeline.addLast(<span class="keyword">new</span> HttpObjectAggregator(<span class="number">65536</span>)); <span class="comment">// HTTP 协议解析，用于握手阶段</span></span><br><span class="line">                            pipeline.addLast(<span class="keyword">new</span> WebSocketServerCompressionHandler()); <span class="comment">// WebSocket 数据压缩扩展</span></span><br><span class="line">                            pipeline.addLast(<span class="keyword">new</span> WebSocketServerProtocolHandler(<span class="string">"/"</span>, <span class="keyword">null</span>, <span class="keyword">true</span>)); <span class="comment">// WebSocket 握手、控制帧处理</span></span><br><span class="line">                            pipeline.addLast(<span class="keyword">new</span> MyWebSocketServerHandler());</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;);</span><br><span class="line">            ChannelFuture f = b.bind(<span class="number">8080</span>).sync();</span><br><span class="line">            f.channel().closeFuture().sync();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            workerGroup.shutdownGracefully();</span><br><span class="line">            bossGroup.shutdownGracefully();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyWebSocketServerHandler</span> <span class="keyword">extends</span> <span class="title">SimpleChannelInboundHandler</span>&lt;<span class="title">WebSocketFrame</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">channelRead0</span><span class="params">(ChannelHandlerContext ctx, WebSocketFrame frame)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (frame <span class="keyword">instanceof</span> TextWebSocketFrame) &#123; <span class="comment">// 此处仅处理 Text Frame</span></span><br><span class="line">            String request = ((TextWebSocketFrame) frame).text();</span><br><span class="line">            ctx.channel().writeAndFlush(<span class="keyword">new</span> TextWebSocketFrame(<span class="string">"收到: "</span> + request));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>WebSocketServerProtocolHandler</code> 会帮我们处理握手、<code>Close</code>、<code>Ping</code>、<code>Pong</code> 帧等 WebSocket 协议底层，并且将 <code>Text</code> 和 <code>Binary</code> 数据帧传递给 pipeline 中下一个 handler 中，也就是 <code>MyWebSocketServerHandler</code>，我们只需要实现业务逻辑而无需关注 WebSocket 协议本身的细节。</p><p>以上是 Netty 实现的一个简单的 WebSocket 的服务器。启动成功后，可以网上搜索 WebSocket 在线测试工具连接 ws://localhost:8080/ 进行测试。</p><h2 id="握手完成事件监听"><a href="#握手完成事件监听" class="headerlink" title="握手完成事件监听"></a>握手完成事件监听</h2><p>如果想要获取客户端连接 WebSocket 服务器使用的请求 URL，包括 URL 中的参数，以及请求的 Header，例如想要通过请求 Header 中的 token 对用户进行认证，可以在握手成功后处理这些。</p><p>在 Netty 源码 <a href="https://github.com/netty/netty/blob/netty-4.1.42.Final/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java#L216" target="_blank" rel="noopener"><code>WebSocketServerProtocolHandler.java</code> 第 216 行</a> 代码中可以看到，<code>WebSocketServerProtocolHandler</code> 会动态在 pipeline 中添加一个 <code>WebSocketServerProtocolHandshakeHandler</code>，用于处理握手阶段。<a href="https://github.com/netty/netty/blob/netty-4.1.42.Final/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java#L112" target="_blank" rel="noopener"><code>WebSocketServerProtocolHandshakeHandler</code> 第 112 行</a> 处理握手成功后，会触发一个 <code>WebSocketServerProtocolHandler.HandshakeComplete</code> 事件。</p><p>下面在 <code>MyWebSocketServerHandler</code> 中添加重写 <code>userEventTriggered</code> 方法，监听握手成功后的事件：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyWebSocketServerHandler</span> <span class="keyword">extends</span> <span class="title">SimpleChannelInboundHandler</span>&lt;<span class="title">WebSocketFrame</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">channelRead0</span><span class="params">(ChannelHandlerContext ctx, WebSocketFrame frame)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// ......</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">userEventTriggered</span><span class="params">(ChannelHandlerContext ctx, Object evt)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (evt <span class="keyword">instanceof</span> WebSocketServerProtocolHandler.HandshakeComplete) &#123;</span><br><span class="line">            WebSocketServerProtocolHandler.HandshakeComplete handshakeCompletedEvent = (WebSocketServerProtocolHandler.HandshakeComplete) evt;</span><br><span class="line">            String uri = handshakeCompletedEvent.requestUri(); <span class="comment">// 握手请求 URI</span></span><br><span class="line">            HttpHeaders headers = handshakeCompletedEvent.requestHeaders(); <span class="comment">// 握手请求头</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://tools.ietf.org/html/rfc6455" target="_blank" rel="noopener">https://tools.ietf.org/html/rfc6455</a></li><li><a href="https://github.com/netty/netty/tree/netty-4.1.42.Final/example/src/main/java/io/netty/example/http/websocketx/server" target="_blank" rel="noopener">https://github.com/netty/netty/tree/netty-4.1.42.Final/example/src/main/java/io/netty/example/http/websocketx/server</a></li></ul><h2 id="本文源码"><a href="#本文源码" class="headerlink" title="本文源码"></a>本文源码</h2><ul><li><a href="https://github.com/wucao/mina-netty-twisted/tree/master/src/main/java/com/xxg/network/netty/websocket" target="_blank" rel="noopener">https://github.com/wucao/mina-netty-twisted/tree/master/src/main/java/com/xxg/network/netty/websocket</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;WebSocket-协议介绍&quot;&gt;&lt;a href=&quot;#WebSocket-协议介绍&quot; class=&quot;headerlink&quot; title=&quot;WebSocket 协议介绍&quot;&gt;&lt;/a&gt;WebSocket 协议介绍&lt;/h2&gt;&lt;p&gt;WebSocket 协议是一种在单个 TCP
      
    
    </summary>
    
      <category term="异步网络编程" scheme="https://xxgblog.com/categories/%E5%BC%82%E6%AD%A5%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="WebSocket" scheme="https://xxgblog.com/tags/WebSocket/"/>
    
      <category term="Netty" scheme="https://xxgblog.com/tags/Netty/"/>
    
  </entry>
  
  <entry>
    <title>Java try-with-resources 中的细节</title>
    <link href="https://xxgblog.com/2020/07/22/try-with-resources/"/>
    <id>https://xxgblog.com/2020/07/22/try-with-resources/</id>
    <published>2020-07-22T03:14:16.000Z</published>
    <updated>2020-07-23T03:37:27.043Z</updated>
    
    <content type="html"><![CDATA[<p>在 Java 7 之前，程序中如果有需要关闭的资源，例如 <code>java.io.InputStream</code>、<code>java.sql.Connection</code> 等，通常会在 finally 中关闭，例如：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">InputStream inputStream = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    inputStream = <span class="keyword">new</span> FileInputStream(<span class="string">"/my/file"</span>);</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (inputStream != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            inputStream.close();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>在 Java 7 以及后续版本中，支持 try-with-resources，任何实现 <code>java.lang.AutoCloseable</code> 接口的类，包括 <code>java.io.Closeable</code> 的实现类，都可以通过 try-with-resources 来关闭。</p><p>上面代码通过 try-with-resources 可以简化为：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> (InputStream inputStream = <span class="keyword">new</span> FileInputStream(<span class="string">"/my/file"</span>)) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="支持定义多个-resources"><a href="#支持定义多个-resources" class="headerlink" title="支持定义多个 resources"></a>支持定义多个 resources</h3><p>通过 JDBC 查询数据库时，会依次创建 <code>Connection</code>、<code>Statment</code>、<code>ResultSet</code>，并且这三个资源都需要关闭，那么可以这样写：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> (Connection connection = DriverManager.getConnection(url, user, password);</span><br><span class="line">     Statement statement = connection.createStatement();</span><br><span class="line">     ResultSet resultSet = statement.executeQuery(<span class="string">"SELECT ..."</span>)) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="多个-resources-的关闭顺序"><a href="#多个-resources-的关闭顺序" class="headerlink" title="多个 resources 的关闭顺序"></a>多个 resources 的关闭顺序</h3><p>如果在 try 中定义了多个 resources，那么它们关闭的顺序和创建的顺序是相反的。上面的例子中，依次创建了 <code>Connection</code>、<code>Statment</code>、<code>ResultSet</code> 对象，最终关闭时会依次关闭 <code>ResultSet</code>、<code>Statment</code>、<code>Connection</code>，所以不用担心 <code>Connection</code> 会先 close。</p><p>官方文档：</p><blockquote><p>Note that the close methods of resources are called in the opposite order of their creation.</p></blockquote><h3 id="catch、finally-和-close-的先后顺序"><a href="#catch、finally-和-close-的先后顺序" class="headerlink" title="catch、finally 和 close 的先后顺序"></a>catch、finally 和 close 的先后顺序</h3><p>官方文档：</p><blockquote><p>In a try-with-resources statement, any catch or finally block is run after the resources declared have been closed.</p></blockquote><p>在 try-with-resources 中，catch 和 finally 中的代码在资源关闭之后运行。</p><p>以下是一种错误写法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Connection connection = DriverManager.getConnection(url, user, password);</span><br><span class="line"><span class="keyword">try</span> (Connection connection2 = connection) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (SQLException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        Statement statement = connection.createStatement(); <span class="comment">// 异常，此时 Connection 已关闭</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125; <span class="keyword">catch</span> (SQLException ex) &#123;</span><br><span class="line">        ex.printStackTrace();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="创建和关闭-resources-时的异常处理"><a href="#创建和关闭-resources-时的异常处理" class="headerlink" title="创建和关闭 resources 时的异常处理"></a>创建和关闭 resources 时的异常处理</h3><p>在 try-with-resources 中，如果创建资源发生异常，即 <code>try (...)</code> 中小括号里的代码出现异常，以及 close 时发生异常，都是会在 catch 中捕捉到的。例如：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> (InputStream inputStream = <span class="keyword">new</span> FileInputStream(<span class="string">"/not/exist/file"</span>)) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace(); <span class="comment">// 如果文件不存在，这里会捕获异常</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这段代码中，如果 <code>new FileInputStream(&quot;/not/exist/file&quot;)</code> 对应的文件不存在，就会抛出异常，这个异常会在下面的 catch 中捕获到。</p><h3 id="Suppressed-Exceptions"><a href="#Suppressed-Exceptions" class="headerlink" title="Suppressed Exceptions"></a>Suppressed Exceptions</h3><p>在 try-with-resources 中，如果 try block（即 try 后面大括号中的代码）抛出异常，会触发资源的 close，如果此时 close 也发生了异常，那么 catch 中会捕获到哪一个呢？</p><p>由于 close 抛出异常不是很常见，所以自己实现一个 <code>AutoCloseable</code> 实现类：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyResource</span> <span class="keyword">implements</span> <span class="title">AutoCloseable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> Exception(<span class="string">"doSomething exception"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">close</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> Exception(<span class="string">"close exception"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>测试代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> (MyResource myResource = <span class="keyword">new</span> MyResource()) &#123;</span><br><span class="line">            myResource.doSomething();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>运行结果：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">java.lang.Exception: doSomething exception</span><br><span class="line">at com.xxg.MyResource.doSomething(MyResource.java:<span class="number">6</span>)</span><br><span class="line">at com.xxg.Test.main(Test.java:<span class="number">12</span>)</span><br><span class="line">Suppressed: java.lang.Exception: close exception</span><br><span class="line">at com.xxg.MyResource.close(MyResource.java:<span class="number">11</span>)</span><br><span class="line">at com.xxg.Test.main(Test.java:<span class="number">13</span>)</span><br></pre></td></tr></table></figure></p><p>可以看到 catch 中捕获的是 <code>doSomething()</code> 方法抛出的异常，同时这个异常会包含一个 Suppressed Exception，即 <code>close()</code> 方法抛出的异常。</p><p>如果想直接拿到 <code>close()</code> 方法抛出的异常，可以通过 <code>Throwable.getSuppressed()</code> 方法获取：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> (MyResource myResource = <span class="keyword">new</span> MyResource()) &#123;</span><br><span class="line">    myResource.doSomething();</span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    <span class="comment">// e 是 doSomething 抛出的异常，想要拿到 close 方法抛出的异常需要通过 e.getSuppressed 方法获取</span></span><br><span class="line">    Throwable[] suppressedExceptions = e.getSuppressed();</span><br><span class="line">    Throwable closeException = suppressedExceptions[<span class="number">0</span>];</span><br><span class="line">    closeException.printStackTrace();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="Closeable-和-AutoCloseable"><a href="#Closeable-和-AutoCloseable" class="headerlink" title="Closeable 和 AutoCloseable"></a>Closeable 和 AutoCloseable</h3><p><code>AutoCloseable</code> 是 Java 7 新增的接口，<code>Closeable</code> 早就有了。二者的关系是 <code>Closeable extends AutoCloseable</code>。二者都仅包含一个 <code>close()</code> 方法。那么为什么 Java 7 还要新增 <code>AutoCloseable</code> 接口呢？</p><p><code>Closeable</code> 在 <code>java.io</code> 包下，主要用于 IO 相关的资源的关闭，其 <code>close()</code> 方法定义了抛出 <code>IOException</code> 异常。其实现类实现 <code>close()</code> 方法时，不允许抛出除 <code>IOException</code>、 <code>RuntimeException</code> 外其他类型的异常。</p><p>而 <code>AutoCloseable</code> 位于 <code>java.lang</code> 包下，使用更广泛。其 <code>close()</code> 方法定义是 <code>void close() throws Exception</code>，也就是它的实现类的 <code>close()</code> 方法对异常抛出是没有限制的。</p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在 Java 7 之前，程序中如果有需要关闭的资源，例如 &lt;code&gt;java.io.InputStream&lt;/code&gt;、&lt;code&gt;java.sql.Connection&lt;/code&gt; 等，通常会在 finally 中关闭，例如：&lt;br&gt;&lt;figure class=&quot;h
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
  </entry>
  
  <entry>
    <title>Java 8 函数式编程：Lambda 表达式和方法引用</title>
    <link href="https://xxgblog.com/2020/04/22/java-8-lambda/"/>
    <id>https://xxgblog.com/2020/04/22/java-8-lambda/</id>
    <published>2020-04-22T03:14:16.000Z</published>
    <updated>2020-04-21T17:32:23.719Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在很多其他语言中，函数是一等公民。例如 JavaScript 中，函数（Function）和字符串（String）、数字（Number）、对象（Object）等一样是一种数据类型。可以这样定义函数：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> myFunction = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    doSomething();</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><p>也可以将函数作为参数：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">    doSomething(); </span><br><span class="line">&#125;, <span class="number">1000</span>);</span><br></pre></td></tr></table></figure></p><p>在 Java 中，函数不是一等公民。如果想要像其他语言一样定义一个函数，只能通过定义一个接口来实现，例如 <code>Runnable</code>。</p><p>在 Java 8 之前，可以通过匿名类的方式来创建 <code>Runnable</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Thread thread = <span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        doSomethong();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line">thread.start();</span><br></pre></td></tr></table></figure></p><p>Java 8 中可以通过 lambda 表达式来创建：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Thread thread = <span class="keyword">new</span> Thread(() -&gt; doSomethong());</span><br><span class="line">thread.start();</span><br></pre></td></tr></table></figure></p><p>也就是：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Runnable runnable = <span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        doSomethong();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><p>简化成了：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Runnable runnable = () -&gt; doSomethong();</span><br></pre></td></tr></table></figure></p><p>是不是看起来像 JavaScript 的函数定义：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> myFunction = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    doSomething();</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><h2 id="FunctionalInterface"><a href="#FunctionalInterface" class="headerlink" title="@FunctionalInterface"></a>@FunctionalInterface</h2><blockquote><p>An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method.</p></blockquote><p><code>@FunctionalInterface</code> 注解用于表明一个接口是函数式接口（functional interface）。函数式接口必须有且只有一个抽象方法。</p><p>例如 <code>java.lang.Runnable</code> 就是一个函数式接口，有且仅有一个抽象方法 <code>run</code> 。<code>Runnable</code> 源码中就有加上注解 <code>@FunctionalInterface</code> ：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@FunctionalInterface</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><blockquote><p>Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.</p></blockquote><p>函数式接口实例可以通过 lambda 表达式、方法引用（method reference）、构造方法引用（constructor reference）的方式来创建。</p><blockquote><p>However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration.</p></blockquote><p>编译器会把满足函数式接口定义（有且只有一个抽象方法）的任何接口视为函数式接口 ，无论有没有 <code>@FunctionalInterface</code> 注解。</p><p>以上两条总结一下：当一个接口符合函数式接口定义（有且只有一个抽象方法），那么就可以通过 lambda 表达式、方法引用的方式来创建，无论该接口有没有加上 <code>@FunctionalInterface</code> 注解。</p><p>下面列出一些 Java 中的函数式接口：</p><ul><li><code>java.lang.Runnable</code></li><li><code>java.util.concurrent.Callable</code></li><li><code>java.util.Comparator</code></li><li><code>java.util.function</code> 包下的函数式接口，例如 <code>Predicate</code>、<code>Consumer</code>、<code>Function</code>、<code>Supplier</code> 等</li></ul><h2 id="java-util-function"><a href="#java-util-function" class="headerlink" title="java.util.function"></a>java.util.function</h2><p>在 <code>java.util.function</code> 包下，定义了大量的函数式接口，每个接口都有且只有一个抽象方法，这些接口的区别在于其中的抽象方法的参数和返回值不同。</p><table><thead><tr><th>类型</th><th>参数个数</th><th>参数类型</th><th>返回值类型</th></tr></thead><tbody><tr><td><code>Function&lt;T,R&gt;</code></td><td>1</td><td>T</td><td>R</td></tr><tr><td><code>IntFunction&lt;R&gt;</code></td><td>1</td><td>int</td><td>R</td></tr><tr><td><code>LongFunction&lt;R&gt;</code></td><td>1</td><td>long</td><td>R</td></tr><tr><td><code>DoubleFunction&lt;R&gt;</code></td><td>1</td><td>double</td><td>R</td></tr><tr><td><code>ToIntFunction&lt;T&gt;</code></td><td>1</td><td>T</td><td>int</td></tr><tr><td><code>ToLongFunction&lt;T&gt;</code></td><td>1</td><td>T</td><td>long</td></tr><tr><td><code>ToDoubleFunction&lt;T&gt;</code></td><td>1</td><td>T</td><td>double</td></tr><tr><td><code>IntToLongFunction</code></td><td>1</td><td>int</td><td>long</td></tr><tr><td><code>IntToDoubleFunction</code></td><td>1</td><td>int</td><td>double</td></tr><tr><td><code>LongToIntFunction</code></td><td>1</td><td>long</td><td>int</td></tr><tr><td><code>LongToDoubleFunction</code></td><td>1</td><td>long</td><td>double</td></tr><tr><td><code>DoubleToIntFunction</code></td><td>1</td><td>double</td><td>int</td></tr><tr><td><code>DoubleToLongFunction</code></td><td>1</td><td>double</td><td>long</td></tr><tr><td><code>BiFunction&lt;T,U,R&gt;</code></td><td>2</td><td>T,U</td><td>R</td></tr><tr><td><code>ToIntBiFunction&lt;T,U&gt;</code></td><td>2</td><td>T,U</td><td>int</td></tr><tr><td><code>ToLongBiFunction&lt;T,U&gt;</code></td><td>2</td><td>T,U</td><td>long</td></tr><tr><td><code>ToDoubleBiFunction&lt;T,U&gt;</code></td><td>2</td><td>T,U</td><td>double</td></tr><tr><td><code>UnaryOperator&lt;T&gt;</code></td><td>1</td><td>T</td><td>T</td></tr><tr><td><code>IntUnaryOperator</code></td><td>1</td><td>int</td><td>int</td></tr><tr><td><code>LongUnaryOperator</code></td><td>1</td><td>long</td><td>long</td></tr><tr><td><code>DoubleUnaryOperator</code></td><td>1</td><td>double</td><td>double</td></tr><tr><td><code>BinaryOperator&lt;T&gt;</code></td><td>2</td><td>T,T</td><td>T</td></tr><tr><td><code>IntBinaryOperator</code></td><td>2</td><td>int,int</td><td>int</td></tr><tr><td><code>LongBinaryOperator</code></td><td>2</td><td>long,long</td><td>long</td></tr><tr><td><code>DoubleBinaryOperator</code></td><td>2</td><td>double,double</td><td>double</td></tr><tr><td><code>Consumer&lt;T&gt;</code></td><td>1</td><td>T</td><td>void</td></tr><tr><td><code>IntConsumer</code></td><td>1</td><td>int</td><td>void</td></tr><tr><td><code>LongConsumer</code></td><td>1</td><td>long</td><td>void</td></tr><tr><td><code>DoubleConsumer</code></td><td>1</td><td>double</td><td>void</td></tr><tr><td><code>BiConsumer&lt;T,U&gt;</code></td><td>2</td><td>T,U</td><td>void</td></tr><tr><td><code>ObjIntConsumer&lt;T&gt;</code></td><td>2</td><td>T,int</td><td>void</td></tr><tr><td><code>ObjLongConsumer&lt;T&gt;</code></td><td>2</td><td>T,long</td><td>void</td></tr><tr><td><code>ObjDoubleConsumer&lt;T&gt;</code></td><td>2</td><td>T,double</td><td>void</td></tr><tr><td><code>Supplier&lt;T&gt;</code></td><td>0</td><td>-</td><td>T</td></tr><tr><td><code>BooleanSupplier</code></td><td>0</td><td>-</td><td>boolean</td></tr><tr><td><code>IntSupplier</code></td><td>0</td><td>-</td><td>int</td></tr><tr><td><code>LongSupplier</code></td><td>0</td><td>-</td><td>long</td></tr><tr><td><code>DoubleSupplier</code></td><td>0</td><td>-</td><td>double</td></tr><tr><td><code>Predicate&lt;T&gt;</code></td><td>1</td><td>T</td><td>boolean</td></tr><tr><td><code>IntPredicate</code></td><td>1</td><td>int</td><td>boolean</td></tr><tr><td><code>LongPredicate</code></td><td>1</td><td>long</td><td>boolean</td></tr><tr><td><code>DoublePredicate</code></td><td>1</td><td>double</td><td>boolean</td></tr><tr><td><code>BiPredicate&lt;T,U&gt;</code></td><td>2</td><td>T,U</td><td>boolean</td></tr></tbody></table><h2 id="Lambda-表达式"><a href="#Lambda-表达式" class="headerlink" title="Lambda 表达式"></a>Lambda 表达式</h2><blockquote><p>One issue with anonymous classes is that if the implementation of your anonymous class is very simple, such as an interface that contains only one method, then the syntax of anonymous classes may seem unwieldy and unclear. In these cases, you’re usually trying to pass functionality as an argument to another method, such as what action should be taken when someone clicks a button. Lambda expressions enable you to do this, to treat functionality as method argument, or code as data.</p></blockquote><p>当一个接口中只有一个方法时（即满足函数式接口定义），此时通过匿名类的语法来编写代码显得比较笨重。使用 lambda 表达式可以将功能作为参数，将代码作为数据。</p><p>一个 Lambda 表达式分为以下三个部分：</p><ul><li>Argument List    ： 参数列表</li><li>Arrow Token ： 箭头符号，即 <code>-&gt;</code></li><li>Body ： 包含一个表达式或者一整块代码</li></ul><p>下面举几个例子：</p><ol><li><p>定义一个函数式接口对象，用于求两个 int 之和，包含两个 int 类型参数 <code>x</code> 和 <code>y</code>，返回 <code>x + y</code> 的值：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IntBinaryOperator sum = (x, y) -&gt; x + y;</span><br></pre></td></tr></table></figure></li><li><p>定义一个函数式接口对象，无参数，返回42：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IntSupplier intSupplier = () -&gt; <span class="number">42</span>;</span><br></pre></td></tr></table></figure></li><li><p>定义一个函数式接口对象，用于输出字符串，包含一个 String 类型的参数 <code>s</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></pre></td><td class="code"><pre><span class="line">Consumer&lt;String&gt; stringConsumer = s -&gt; &#123;</span><br><span class="line">    System.out.println(s);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li></ol><h2 id="方法引用（Method-Reference）"><a href="#方法引用（Method-Reference）" class="headerlink" title="方法引用（Method Reference）"></a>方法引用（Method Reference）</h2><blockquote><p>You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it’s often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.</p></blockquote><p>如果 lambda 表达式只是调用一个已有的方法，那么可以直接使用方法引用。</p><p>例如输出 <code>List</code> 中的元素，用 lambda 表达式：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;String&gt; list = Arrays.asList(<span class="string">"1"</span>, <span class="string">"22"</span>, <span class="string">"333"</span>);</span><br><span class="line">list.forEach(s -&gt; System.out.println(s));</span><br></pre></td></tr></table></figure></p><p>改用方法引用更加简洁：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;String&gt; list = Arrays.asList(<span class="string">"1"</span>, <span class="string">"22"</span>, <span class="string">"333"</span>);</span><br><span class="line">list.forEach(System.out::println);</span><br></pre></td></tr></table></figure></p><p>也就是：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Consumer&lt;String&gt; stringConsumer = s -&gt; System.out.println(s);</span><br></pre></td></tr></table></figure></p><p>简化成了：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Consumer&lt;String&gt; stringConsumer = System.out::println; <span class="comment">// 将一个已有的方法赋值给一个函数式接口对象</span></span><br></pre></td></tr></table></figure></p><p>方法引用有以下几种类型：</p><ol><li><p><code>类名::静态方法名</code> ： 静态方法引用</p><p> 例如定义一个 <code>max</code> 函数式接口对象，用于求两个 int 中的最大值：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IntBinaryOperator max = Math::max;</span><br></pre></td></tr></table></figure><p> <code>IntBinaryOperator</code> 表示有两个 int 参数且返回值为 int 的函数，<code>Math.max()</code> 静态方法符合要求。</p></li><li><p><code>对象名::非静态方法名</code> ： 对象的方法引用</p><p> 例如定义一个 <code>println</code> 函数式接口对象，用于输出字符串：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Consumer&lt;String&gt; println = System.out::println;</span><br></pre></td></tr></table></figure><p> <code>Consumer&lt;String&gt;</code> 表示有一个 String 类型参数且无返回值的函数，<code>System.out.println()</code> 方法符合要求。</p></li><li><p><code>类名::new</code> ： 构造方法引用</p><p> 例如定义一个 <code>createHashMap</code> 函数式接口对象，用于创建一个 <code>HashMap</code>：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Supplier&lt;HashMap&gt; createHashMap = HashMap::<span class="keyword">new</span>;</span><br></pre></td></tr></table></figure><p> <code>Supplier&lt;HashMap&gt;</code> 表示有一个无参数且返回值为 HashMap 的函数，<code>HashMap</code> 的构造函数符合要求。</p></li><li><p><code>类名::非静态方法名</code> ： 文档中解释为：Reference to an instance method of an arbitrary object of a particular type 。如果不理解的话，下面举个例子来说明一下。</p><p> 定义一个 <code>concat</code> 函数式接口对象，用于拼接两个字符串：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">BinaryOperator&lt;String&gt; concat = String::concat;</span><br></pre></td></tr></table></figure><p> <code>BinaryOperator&lt;String&gt;</code> 表示有两个 String 类型参数且返回值为 String 的函数。注意 String 类的 <code>concat</code> 不是静态方法，且 <code>String.concat(String str)</code> 只有一个参数，看似不符合要求。实际上它相当于：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">BinaryOperator&lt;String&gt; concat = (s1, s2) -&gt; s1.concat(s2);</span><br></pre></td></tr></table></figure><p> 即调用第一个参数 <code>s1</code> 的 <code>concat</code> 方法，传入参数 <code>s2</code>。</p></li></ol><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><p><a href="/2020/04/17/java-8-stream/">Java 8 Stream 总结</a></p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html</a></li><li><a href="https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html</a></li><li><a href="https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html" target="_blank" rel="noopener">https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html</a></li><li><a href="http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html" target="_blank" rel="noopener">http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html</a></li><li><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html</a></li><li><a href="https://docs.oracle.com/javase/9/docs/api/java/util/function/package-summary.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;在很多其他语言中，函数是一等公民。例如 JavaScript 中，函数（Function）和字符串（String）、数字（Number）、对
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="Lambda" scheme="https://xxgblog.com/tags/Lambda/"/>
    
  </entry>
  
  <entry>
    <title>Java 8 Stream 总结</title>
    <link href="https://xxgblog.com/2020/04/17/java-8-stream/"/>
    <id>https://xxgblog.com/2020/04/17/java-8-stream/</id>
    <published>2020-04-17T03:14:16.000Z</published>
    <updated>2026-07-14T13:40:27.411Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Stream-简介"><a href="#Stream-简介" class="headerlink" title="Stream 简介"></a>Stream 简介</h2><h3 id="Stream-是什么"><a href="#Stream-是什么" class="headerlink" title="Stream 是什么"></a>Stream 是什么</h3><blockquote><p>Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections.</p></blockquote><p>Stream 是 Java 8 新特性，可对 Stream 中元素进行函数式编程操作，例如 map-reduce。</p><p>先来看一段代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> sum = widgets.stream()</span><br><span class="line">                 .filter(b -&gt; b.getColor() == RED)</span><br><span class="line">                 .mapToInt(b -&gt; b.getWeight())</span><br><span class="line">                 .sum();</span><br></pre></td></tr></table></figure></p><p>这段 Java 代码看起来是不是像通过 SQL 来操作集合：<br><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="keyword">sum</span>(weight) <span class="keyword">from</span> widgets <span class="keyword">where</span> color=<span class="string">'RED'</span>;</span><br></pre></td></tr></table></figure></p><h3 id="Stream-类型"><a href="#Stream-类型" class="headerlink" title="Stream 类型"></a>Stream 类型</h3><p><code>java.util.stream</code> 包下提供了以下四种类型的 Stream：</p><ul><li><code>Stream</code>  ： 对象类型对应的 Stream</li><li><code>IntStream</code>  ： 基本类型 int 对应的 Stream</li><li><code>LongStream</code>  ： 基本类型 long 对应的 Stream</li><li><code>DoubleStream</code>  ： 基本类型 double 对应的 Stream</li></ul><h2 id="如何获得-Stream"><a href="#如何获得-Stream" class="headerlink" title="如何获得 Stream"></a>如何获得 Stream</h2><h3 id="Collection-to-Stream"><a href="#Collection-to-Stream" class="headerlink" title="Collection to Stream"></a>Collection to Stream</h3><p><code>List</code>、<code>Set</code> 等 <code>Collection</code> 接口的实现类，可以通过 <code>Collection.stream()</code> 或 <code>Collection.parallelStream()</code> 方法返回 Stream 对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;String&gt; stringList = ...;</span><br><span class="line">Stream&lt;String&gt; stream = stringList.stream();</span><br></pre></td></tr></table></figure></p><h3 id="Array-to-Stream"><a href="#Array-to-Stream" class="headerlink" title="Array to Stream"></a>Array to Stream</h3><p>可以通过静态方法 <code>Arrays.stream(T[] array)</code> 或 <code>Stream.of(T... values)</code> 将数组转为 Stream：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">String[] stringArray = ...;</span><br><span class="line">Stream&lt;String&gt; stringStream1 = Arrays.stream(stringArray); <span class="comment">//  方法一</span></span><br><span class="line">Stream&lt;String&gt; stringStream2 = Stream.of(stringArray); <span class="comment">// 方法二</span></span><br></pre></td></tr></table></figure></p><p>基本类型数组可以通过类似的方法转为 <code>IntStream</code>、<code>LongStream</code>、<code>DoubleStream</code> ：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span>[] intArray = &#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;;</span><br><span class="line">IntStream intStream1 = Arrays.stream(intArray);</span><br><span class="line">IntStream intStream2 = IntStream.of(intArray);</span><br></pre></td></tr></table></figure></p><p>另外， <code>Stream.of(T... values)</code>、<code>IntStream.of(int... values)</code> 等静态方法支持 varargs（可变长度参数），可直接创建 Stream：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br></pre></td></tr></table></figure></p><h3 id="Map-to-Stream"><a href="#Map-to-Stream" class="headerlink" title="Map to Stream"></a>Map to Stream</h3><p>Map 本身不是 Collection 的实现类，没有 <code>stream()</code> 或 <code>parallelStream()</code> 方法，可以通过 <code>Map.entrySet()</code>、<code>Map.keySet()</code>、<code>Map.values()</code> 返回一个 <code>Collection</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;Integer, String&gt; map = ...;</span><br><span class="line">Stream&lt;Map.Entry&lt;Integer, String&gt;&gt; stream = map.entrySet().stream();</span><br></pre></td></tr></table></figure></p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p><code>String</code> 按字符拆分成 <code>IntStream</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">String s = <span class="string">"Hello World"</span>;</span><br><span class="line">IntStream stringStream = s.chars(); <span class="comment">// 返回将字符串每个 char 转为 int 创建 Stream</span></span><br></pre></td></tr></table></figure></p><p><code>BufferedReader</code> 生成按行分隔的 <code>Stream&lt;String&gt;</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">BufferedReader bufferedReader = ...;</span><br><span class="line">Stream&lt;String&gt; lineStream = bufferedReader.lines();</span><br></pre></td></tr></table></figure></p><p><code>Stream.iterate()</code>、<code>Stream.generate()</code> 生成 Stream：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;Date&gt; dateStream = Stream.iterate(<span class="keyword">new</span> Date(), d -&gt; <span class="keyword">new</span> Date(d.getTime() + <span class="number">86400000</span>)).limit(<span class="number">5</span>); <span class="comment">// 从当前日期开始生成 5 天连续日期</span></span><br><span class="line">Stream&lt;Double&gt; randomStream = Stream.generate(() -&gt; Math.random()).limit(<span class="number">10</span>); <span class="comment">// 生成 10 哥随机数</span></span><br></pre></td></tr></table></figure></p><p><code>IntStream</code>、<code>LongStream</code> 提供了静态方法 <code>range</code> 生成对应的 Stream：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.range(<span class="number">1</span>, <span class="number">5</span>); <span class="comment">// 1,2,3,4 (不包含5)</span></span><br></pre></td></tr></table></figure></p><h2 id="Stream-的方法"><a href="#Stream-的方法" class="headerlink" title="Stream 的方法"></a>Stream 的方法</h2><h3 id="intermediate-operation-和-terminal-operation"><a href="#intermediate-operation-和-terminal-operation" class="headerlink" title="intermediate operation 和 terminal operation"></a>intermediate operation 和 terminal operation</h3><blockquote><p>Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a <code>Collection</code>, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as <code>Stream.filter</code> or <code>Stream.map</code>; and a terminal operation such as <code>Stream.forEach</code> or <code>Stream.reduce</code>.</p></blockquote><p>Stream 操作分为 中间操作（intermediate operation）和 最终操作（terminal operation），这些操作结合起来形成 stream pipeline。stream pipeline 包含一个 Stream 源，后面跟着零到多个 intermediate operations（例如 <code>Stream.filter</code>、<code>Stream.map</code>），再跟上一个 terminal operation（例如 <code>Stream.forEach</code>、<code>Stream.reduce</code>）。</p><p>intermediate operation 用于对 Stream 中元素处理和转换，terminal operation 用于得到最终结果。</p><p>例如在本文开头的例子中，包含以下 intermediate operation 和 terminal operation：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> sum = widgets.stream()</span><br><span class="line">                 .filter(b -&gt; b.getColor() == RED) <span class="comment">// intermediate operation</span></span><br><span class="line">                 .mapToInt(b -&gt; b.getWeight()) <span class="comment">// intermediate operation</span></span><br><span class="line">                 .sum(); <span class="comment">// terminal operation</span></span><br></pre></td></tr></table></figure></p><h3 id="intermediate-operation"><a href="#intermediate-operation" class="headerlink" title="intermediate operation"></a>intermediate operation</h3><blockquote><p>Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.</p></blockquote><p>intermediate operation 会再次返回一个新的 Stream，所以可以支持链式调用。</p><p>intermediate operation 还有一个重要特性，延迟（lazy）性：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">IntStream.of(<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>).filter(i -&gt; &#123;</span><br><span class="line">    System.out.println(i);</span><br><span class="line">    <span class="keyword">return</span> i &gt; <span class="number">1</span>;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><p>以上这段代码并不会输出：<code>1 2 3 4</code>，实际上这段代码运行后没有任何输出，也就是 <code>filter</code> 并未执行。因为 <code>filter</code> 是一个 intermediate operation，如果想要 <code>filter</code> 执行，必须加上一个 terminal operation：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">IntStream.of(<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>).filter(i -&gt; &#123;</span><br><span class="line">    System.out.println(i);</span><br><span class="line">    <span class="keyword">return</span> i &gt; <span class="number">1</span>;</span><br><span class="line">&#125;).sum();</span><br></pre></td></tr></table></figure></p><h4 id="intermediate-operation-常用方法"><a href="#intermediate-operation-常用方法" class="headerlink" title="intermediate operation 常用方法"></a>intermediate operation 常用方法</h4><ul><li><code>filter</code>  ：  按条件过滤，类似于 SQL 中的 <code>where</code> 语句</li><li><code>limit(long n)</code>  ：  截取 Stream 的前 n 条数据，生成新的 Stream，类似于 MySQL 中的 <code>limit n</code> 语句</li><li><code>skip(long n)</code>  ： 跳过前 n 条数据，结合 <code>limit</code> 使用 <code>stream.skip(offset).limit(count)</code>，效果相当于 MySQL 中的 <code>LIMIT offset,count</code> 语句</li><li><code>sorted</code>  ：   排序，类似于 SQL 中的 <code>order by</code> 语句</li><li><code>distinct</code>  ：  排除 Stream 中重复的元素，通过 <code>equals</code> 方法来判断重复，这个和 SQL 中的 <code>distinct</code> 类似</li><li><code>boxed</code>  ：  将 <code>IntStream</code>、<code>LongStream</code>、<code>DoubleStream</code> 转换为 <code>Stream&lt;Integer&gt;</code>、<code>Stream&lt;Long&gt;</code>、<code>Stream&lt;Double&gt;</code></li><li><code>peek</code>  ：  类似于 <code>forEach</code>，二者区别是 <code>forEach</code> 是 terminal operation，<code>peek</code> 是 intermediate operation</li><li><code>map</code>、<code>mapToInt</code>、<code>mapToLong</code>、<code>mapToDouble</code>、<code>mapToObj</code>  ：  这些方法会传入一个函数作为参数，将 Stream 中的每个元素通过这个函数转换，转换后组成一个新的 Stream。<code>mapToXxx</code> 中的 Xxx 表示转换后的元素类型，也就是传入的函数返回值，例如 mapToInt 就是将原 Stream 中的每个元素转为 int 类型，最终返回一个 IntStream</li><li><code>flatMap</code>、<code>flatMapToInt</code>、<code>flatMapToLong</code>、<code>flatMapToDouble</code>  ：  类似 <code>map</code>、<code>mapToXxx</code>，不同的是 <code>flatMap</code> 会将一个元素转为一个 Stream，其中可包含0到多个元素，最终将每个 Stream 中的所有元素组成一个新的 Stream 返回</li></ul><h4 id="map、flatMap-区别"><a href="#map、flatMap-区别" class="headerlink" title="map、flatMap 区别"></a>map、flatMap 区别</h4><p><code>map</code> 和 <code>flatMap</code> 的区别就是 <code>map</code> 是一对一，<code>flatMap</code> 是一对零到多，可以用下图简单说明：</p><p><img src="/img/62cbb4d9-ca15-4f57-bacf-b1a90e1e30e0.jpg" alt="map、flatMap 区别"></p><ol><li><p><code>map</code> 示例</p><p> 通过 <code>mapToInt</code> 获取一个字符串集合中每个字符串长度：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stringStream = Stream.of(<span class="string">"test1"</span>, <span class="string">"test23"</span>, <span class="string">"test4"</span>);</span><br><span class="line">IntStream intStream = stringStream.mapToInt(String::length);</span><br></pre></td></tr></table></figure><p> 通过 <code>String.length</code> 函数可以将每个 String 转为一个 int，最终组成一个 IntStream。以上代码中的 <code>stringStream</code> 和 <code>intStream</code> 中的元素是一一对应的，每个字符串对应一个长度，两个 Stream 的元素数量是一致的。</p></li><li><p><code>flatMap</code> 示例</p><p> 通过 <code>flatMapToInt</code> 将一个字符串集合中每个字符串按字符拆分，组成一个新的 Stream：</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stringStream = Stream.of(<span class="string">"test1"</span>, <span class="string">"test23"</span>, <span class="string">"test4"</span>);</span><br><span class="line">IntStream intStream = stringStream.flatMapToInt(String::chars);</span><br></pre></td></tr></table></figure><p> 每个字符串按字符拆分后可能会得到 0 到多个字符，最终得到的 <code>intStream</code> 元素数量和 <code>stringStream</code> 的元素数量可能不一致。</p></li></ol><p>以下表格列出了所有map相关的方法以及转换规则：</p><table><thead><tr><th>Stream</th><th>方法</th><th>函数类型</th><th>函数参数</th><th>函数返回值</th><th>转换后</th></tr></thead><tbody><tr><td><code>Stream&lt;T&gt;</code></td><td><code>map</code></td><td><code>Function</code></td><td><code>T</code></td><td><code>R</code></td><td><code>Stream&lt;R&gt;</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>mapToInt</code></td><td><code>ToIntFunction</code></td><td><code>T</code></td><td><code>int</code></td><td><code>IntStream</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>mapToLong</code></td><td><code>ToLongFunction</code></td><td><code>T</code></td><td><code>long</code></td><td><code>LongStream</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>mapToDouble</code></td><td><code>ToDoubleFunction</code></td><td><code>T</code></td><td><code>double</code></td><td><code>DoubleStream</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>flatMap</code></td><td><code>Function</code></td><td><code>T</code></td><td><code>Stream&lt;R&gt;</code></td><td><code>Stream&lt;R&gt;</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>flatMapToInt</code></td><td><code>Function</code></td><td><code>T</code></td><td><code>IntStream</code></td><td><code>IntStream</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>flatMapToLong</code></td><td><code>Function</code></td><td><code>T</code></td><td><code>LongStream</code></td><td><code>LongStream</code></td></tr><tr><td><code>Stream&lt;T&gt;</code></td><td><code>flatMapToDouble</code></td><td><code>Function</code></td><td><code>T</code></td><td><code>DoubleStream</code></td><td><code>DoubleStream</code></td></tr><tr><td><code>IntStream</code></td><td><code>map</code></td><td><code>IntUnaryOperator</code></td><td><code>int</code></td><td><code>int</code></td><td><code>IntStream</code></td></tr><tr><td><code>IntStream</code></td><td><code>mapToLong</code></td><td><code>IntToLongFunction</code></td><td><code>int</code></td><td><code>long</code></td><td><code>LongStream</code></td></tr><tr><td><code>IntStream</code></td><td><code>mapToDouble</code></td><td><code>IntToDoubleFunction</code></td><td><code>int</code></td><td><code>double</code></td><td><code>DoubleStream</code></td></tr><tr><td><code>IntStream</code></td><td><code>mapToObj</code></td><td><code>IntFunction</code></td><td><code>int</code></td><td><code>R</code></td><td><code>Stream&lt;R&gt;</code></td></tr><tr><td><code>IntStream</code></td><td><code>flatMap</code></td><td><code>IntFunction</code></td><td><code>int</code></td><td><code>IntStream</code></td><td><code>IntStream</code></td></tr><tr><td><code>LongStream</code></td><td><code>map</code></td><td><code>LongUnaryOperator</code></td><td><code>long</code></td><td><code>long</code></td><td><code>LongStream</code></td></tr><tr><td><code>LongStream</code></td><td><code>mapToInt</code></td><td><code>LongToIntFunction</code></td><td><code>long</code></td><td><code>int</code></td><td><code>IntStream</code></td></tr><tr><td><code>LongStream</code></td><td><code>mapToDouble</code></td><td><code>LongToDoubleFunction</code></td><td><code>long</code></td><td><code>double</code></td><td><code>DoubleStream</code></td></tr><tr><td><code>LongStream</code></td><td><code>mapToObj</code></td><td><code>LongFunction</code></td><td><code>long</code></td><td><code>R</code></td><td><code>Stream&lt;R&gt;</code></td></tr><tr><td><code>LongStream</code></td><td><code>flatMap</code></td><td><code>LongFunction</code></td><td><code>long</code></td><td><code>LongStream</code></td><td><code>LongStream</code></td></tr><tr><td><code>DoubleStream</code></td><td><code>map</code></td><td><code>DoubleUnaryOperator</code></td><td><code>double</code></td><td><code>double</code></td><td><code>DoubleStream</code></td></tr><tr><td><code>DoubleStream</code></td><td><code>mapToInt</code></td><td><code>DoubleToIntFunction</code></td><td><code>double</code></td><td><code>int</code></td><td><code>IntStream</code></td></tr><tr><td><code>DoubleStream</code></td><td><code>mapToLong</code></td><td><code>DoubleToLongFunction</code></td><td><code>double</code></td><td><code>long</code></td><td><code>LongStream</code></td></tr><tr><td><code>DoubleStream</code></td><td><code>mapToObj</code></td><td><code>DoubleFunction</code></td><td><code>double</code></td><td><code>R</code></td><td><code>Stream&lt;R&gt;</code></td></tr><tr><td><code>DoubleStream</code></td><td><code>flatMap</code></td><td><code>DoubleFunction</code></td><td><code>double</code></td><td><code>DoubleStream</code></td><td><code>DoubleStream</code></td></tr></tbody></table><p>例如对一个 <code>Stream&lt;Stirng&gt;</code> 执行 <code>stream.mapToInt(String::length)</code>，可以理解为将一个参数为 <code>String</code> 返回值为 <code>int</code> 的函数 <code>String::length</code> 传入 <code>mapToInt</code> 方法作为参数，最终返回一个 <code>IntStream</code>。</p><h3 id="terminal-operation"><a href="#terminal-operation" class="headerlink" title="terminal operation"></a>terminal operation</h3><blockquote><p>Terminal operations, such as Stream.forEach or IntStream.sum, may traverse the stream to produce a result or a side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used; if you need to traverse the same data source again, you must return to the data source to get a new stream.</p></blockquote><p>当 terminal operation 执行过后，Stream 就不能再使用了，如果想要再使用就必须重新创建一个新的 Stream：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br><span class="line">intStream.forEach(System.out::println); <span class="comment">// 第一次执行 terminal operation forEach 正常</span></span><br><span class="line">intStream.forEach(System.out::println); <span class="comment">// 第二次执行会抛出异常 IllegalStateException: stream has already been operated upon or closed</span></span><br></pre></td></tr></table></figure></p><h4 id="terminal-operation-常用方法"><a href="#terminal-operation-常用方法" class="headerlink" title="terminal operation 常用方法"></a>terminal operation 常用方法</h4><ul><li><code>forEach</code> ： 迭代Stream</li><li><code>toArray</code>  ： 转为数组</li><li><code>max</code> ：  取最大值</li><li><code>min</code>  ： 取最小值</li><li><code>sum</code>  ： 求和</li><li><code>count</code>  ： Stream 中元素数量</li><li><code>average</code>  ： 求平均数</li><li><code>findFirst</code>  ： 返回第一个元素</li><li><code>findAny</code>  ： 返回流中的某一个元素</li><li><code>allMatch</code> ：  是否所有元素都满足条件</li><li><code>anyMatch</code>  ： 是否存在元素满足条件</li><li><code>noneMatch</code> ： 是否没有元素满足条件</li><li><code>reduce</code> ： 执行聚合操作，上面的 <code>sum</code>、<code>min</code>、<code>max</code> 方法一般是基于 <code>reduce</code> 来实现的</li><li><code>collect</code> ： 执行相对 <code>reduce</code> 更加复杂的聚合操作，上面的 <code>average</code> 方法一般是基于 <code>collect</code> 来实现的</li></ul><h4 id="reduce"><a href="#reduce" class="headerlink" title="reduce"></a>reduce</h4><p>先看一段使用 <code>reduce</code> 来实现 <code>sum</code> 求和的代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">8</span>);</span><br><span class="line"><span class="keyword">int</span> sum = intStream.reduce(<span class="number">0</span>, Integer::sum);</span><br></pre></td></tr></table></figure></p><p>或者<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">8</span>);</span><br><span class="line"><span class="keyword">int</span> sum = intStream.reduce(<span class="number">0</span>, (a, b) -&gt; a + b);</span><br></pre></td></tr></table></figure></p><p>上面例子中的 <code>reduce</code> 方法有两个参数：</p><ul><li><code>identity</code>  ： 初始值，当 Stream 中没有元素时也会作为默认值返回</li><li><code>accumulator</code>  ： 一个带有两个参数和一个返回值的函数，例如上面代码中的 <code>Integer::sum</code> 或者 <code>(a, b) -&gt; a + b</code> 求和函数</li></ul><p>以上代码等同于：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> result = identity;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> element : intArray)</span><br><span class="line">    result = Integer.sum(result, element); <span class="comment">// 或者 result = result + element;</span></span><br><span class="line"><span class="keyword">return</span> result;</span><br></pre></td></tr></table></figure></p><h4 id="collect"><a href="#collect" class="headerlink" title="collect"></a>collect</h4><p>先看一段代码，将一个 <code>Stream&lt;String&gt;</code> 中的元素拼接成一个字符串，如果用 <code>reduce</code> 可以这样实现：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stream = Stream.of(<span class="string">"Hello"</span>, <span class="string">"World"</span>);</span><br><span class="line">String result = stream.reduce(<span class="string">""</span>, String::concat); <span class="comment">// 或者 String result = stream.reduce("", (a, b) -&gt; a + b);</span></span><br></pre></td></tr></table></figure></p><p>当 Stream 中有大量元素时，用字符串拼接方式性能会大打折扣，应该使用性能更高的 <code>StringBuilder</code>，可以通过 <code>collect</code> 方法来实现：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stream = Stream.of(<span class="string">"Hello"</span>, <span class="string">"World"</span>);</span><br><span class="line">StringBuilder result = stream.collect(StringBuilder::<span class="keyword">new</span>, StringBuilder::append, StringBuilder::append);</span><br></pre></td></tr></table></figure></p><p>上面例子中的 <code>collect</code> 方法有三个参数：</p><ul><li><code>supplier</code>  ： 传入一个函数，用于创建一个存放聚合计算结果的容器（result container），例如上面的例子中第一个传入参数 <code>StringBuilder::new</code> ，该函数用于创建一个新的 <code>StringBuilder</code> 来存放拼接字符串的结果</li><li><code>accumulator</code>  ： 传入一个函数，用于将 Stream 中的一个元素合并到 result container 中，例如上面的例子中第二个传入参数 <code>StringBuilder::append</code> ，该函数用于将 Stream 中的字符串 append 到 StringBuilder 中</li><li><code>combiner</code>  ： 传入一个函数，用于将两个 result container 合并，这个函数一般会在并行流中用到，例如上面的例子中第三个传入参数 <code>StringBuilder::append</code> ，该函数用于将两个 <code>StringBuilder</code> 合并</li></ul><p>下面再用 <code>collect</code> 实现求平均数：</p><p>计算平均数需要有两个关键的数据：数量、总和，首先需要创建一个 result container 存放这两个值，并定义相关方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Averager</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> total = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">double</span> <span class="title">average</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> count &gt; <span class="number">0</span> ? ((<span class="keyword">double</span>) total) / count : <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">accumulate</span><span class="params">(<span class="keyword">int</span> i)</span> </span>&#123;</span><br><span class="line">        total += i;</span><br><span class="line">        count++;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">combine</span><span class="params">(Averager other)</span> </span>&#123;</span><br><span class="line">        total += other.total;</span><br><span class="line">        count += other.count;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>通过计算平均值：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">IntStream intStream = IntStream.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>);</span><br><span class="line">Averager averager = intStream.collect(Averager::<span class="keyword">new</span>, Averager::accumulate, Averager::combine);</span><br><span class="line">System.out.println(averager.average()); <span class="comment">// 2.5</span></span><br></pre></td></tr></table></figure></p><h4 id="Collector"><a href="#Collector" class="headerlink" title="Collector"></a>Collector</h4><p><code>Stream</code> 接口中还有一个 <code>collect</code> 的重载方法，仅有一个参数：<code>collect(Collector collector)</code>。</p><p><code>Collector</code> 是什么：</p><blockquote><p>This class encapsulates the functions used as arguments in the collect operation that requires three arguments (supplier, accumulator, and combiner functions).</p></blockquote><p><code>Collector</code> 实际上就是一个包含 <code>supplier</code>、<code>accumulator</code>、<code>combiner</code> 函数的类，可以实现对常用聚合算法的抽象和复用。</p><p>例如将 <code>Stream&lt;String&gt;</code> 中的元素拼接成一个字符串，用 <code>Collector</code> 实现：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">JoinCollector</span> <span class="keyword">implements</span> <span class="title">Collector</span>&lt;<span class="title">String</span>, <span class="title">StringBuilder</span>, <span class="title">String</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Supplier&lt;StringBuilder&gt; <span class="title">supplier</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> StringBuilder::<span class="keyword">new</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> BiConsumer&lt;StringBuilder, String&gt; <span class="title">accumulator</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> StringBuilder::append;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> BinaryOperator&lt;StringBuilder&gt; <span class="title">combiner</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> StringBuilder::append;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Function&lt;StringBuilder, String&gt; <span class="title">finisher</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> StringBuilder::toString;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Set&lt;Characteristics&gt; <span class="title">characteristics</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Collections.emptySet();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>或者直接用 <code>Collector.of()</code> 静态方法直接创建一个 <code>Collector</code> 对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Collector&lt;String, StringBuilder, String&gt; joinCollector = Collector.of(StringBuilder::<span class="keyword">new</span>,</span><br><span class="line">                StringBuilder::append,</span><br><span class="line">                StringBuilder::append,</span><br><span class="line">                StringBuilder::toString);</span><br><span class="line">Stream&lt;String&gt; stream = Stream.of(<span class="string">"Hello"</span>, <span class="string">"World"</span>);</span><br><span class="line">String result = stream.collect(joinCollector);</span><br></pre></td></tr></table></figure></p><p>另外还有一个更简单的方式，使用 <code>Collectors.joining()</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stream = Stream.of(<span class="string">"Hello"</span>, <span class="string">"World"</span>);</span><br><span class="line">String result = stream.collect(Collectors.joining());</span><br></pre></td></tr></table></figure></p><h4 id="Collectors"><a href="#Collectors" class="headerlink" title="Collectors"></a>Collectors</h4><p>在 <code>java.util.stream.Collectors</code>  ： 中提供了大量常用的 <code>Collector</code>：</p><ul><li><code>Collectors.toList()</code>  ： 将 Stream 转为 List</li><li><code>Collectors.toSet()</code>  ： 将 Stream 转为 Set</li><li><code>Collectors.joining()</code>  ： 将 Stream 中的字符串拼接</li><li><code>Collectors.groupingBy()</code>  ： 将 Stream 中的元素分组，类似于 SQL 中的 <code>group by</code> 语句</li><li><code>Collectors.counting()</code>  ： 用于计算 Stream 中元素数量，<code>stream.collect(Collectors.counting())</code> 等同于 <code>stream.count()</code></li><li><code>Collectors.averagingDouble()</code>、<code>Collectors.averagingInt()</code>、<code>Collectors.averagingLong()</code>  ： 计算平均数</li></ul><p>上面只列出了 <code>Collectors</code> 中的一部分方法，还有其他常用的方法可以参考文档。</p><p>下面列出一些 <code>Collectors</code> 的实用示例：</p><ul><li><p>将 Stream 转为 List：</p>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;String&gt; stream = Stream.of(<span class="string">"Hello"</span>, <span class="string">"World"</span>);</span><br><span class="line">List&lt;String&gt; list = stream.collect(Collectors.toList());</span><br></pre></td></tr></table></figure></li><li><p>将学生（Student）按年龄分组，返回每个年龄对应的学生列表：</p>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;Student&gt; stream = ...;</span><br><span class="line">Map&lt;Integer, List&lt;Student&gt;&gt; data = stream.collect(Collectors.groupingBy(Student::getAge));</span><br></pre></td></tr></table></figure></li><li><p>将学生（Student）按年龄分组，返回每个年龄对应的学生数量，实现和 SQL 一样的结果： <code>select age,count(*) from student group by age</code>：</p>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;Student&gt; stream = ...;</span><br><span class="line">Map&lt;Integer, Long&gt; data = stream.collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));</span><br></pre></td></tr></table></figure></li><li><p>计算学生（Student）年龄平均数：</p>  <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Stream&lt;Student&gt; stream = ...;</span><br><span class="line">Double data = stream.collect(Collectors.averagingInt(Student::getAge)); </span><br><span class="line"><span class="comment">// 或者可以 double average = stream.mapToInt(Student::getAge).average().getAsDouble();</span></span><br></pre></td></tr></table></figure></li></ul><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><p><a href="/2020/04/22/java-8-lambda/">Java 8 函数式编程：Lambda 表达式和方法引用</a></p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html</a></li><li><a href="https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html</a></li><li><a href="https://docs.oracle.com/javase/tutorial/collections/streams/examples/ReductionExamples.java" target="_blank" rel="noopener">https://docs.oracle.com/javase/tutorial/collections/streams/examples/ReductionExamples.java</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Stream-简介&quot;&gt;&lt;a href=&quot;#Stream-简介&quot; class=&quot;headerlink&quot; title=&quot;Stream 简介&quot;&gt;&lt;/a&gt;Stream 简介&lt;/h2&gt;&lt;h3 id=&quot;Stream-是什么&quot;&gt;&lt;a href=&quot;#Stream-是什么&quot; cla
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="Stream" scheme="https://xxgblog.com/tags/Stream/"/>
    
  </entry>
  
  <entry>
    <title>Spring Cloud Eureka 服务实现不停机（Zero-downtime）部署</title>
    <link href="https://xxgblog.com/2020/03/24/spring-cloud-eureka-zero-downtime/"/>
    <id>https://xxgblog.com/2020/03/24/spring-cloud-eureka-zero-downtime/</id>
    <published>2020-03-24T03:14:16.000Z</published>
    <updated>2021-04-08T06:46:41.321Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>互联网产品高速迭代，通常伴随着高频次的版本发布。部署新版上线需要重启服务，直接 kill 服务进程可能会造成服务短暂不可用，从而影响到正在使用的用户。</p><p>Spring Cloud 项目中一般会用到 Ribbon 作为负载均衡，那么是不是只要保证每个服务部署多台服务器，发布时采用 Rolling Update 分批次部署，保证一部分服务器正常提供服务的同时发布另一部分服务器，Ribbon 就能自动切换，保证服务的不间断？然而并不是。</p><h2 id="产生原因"><a href="#产生原因" class="headerlink" title="产生原因"></a>产生原因</h2><p>所有服务的状态保存在注册中心，即 Eureka Server。一个服务要想获取其他服务的实例列表和状态，需要通过 Eureka Client 定时从 Eureka Server 中获取并缓存下来，默认时间间隔是30秒。Eureka Client 和 Eureka Server 是通过 HTTP 协议通信，请求由 Eureka Client 发起，而不是基于长连接或者 Eureka Server 主动推送，所以无法立即知道其他服务状态变更。</p><p>即使同一个服务部署多台机器，每台机器依次发布，当其中一个服务实例重启时，服务调用方是无法第一时间知道的，所以还是会调用到这台暂时无法提供服务的实例上。这样会造成短暂的访问失败，这段时间也会对正在使用产品的用户造成一定的影响。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>基于以上的原因，在部署应用时应该按照以下步骤进行（为了简单起见，假设一个应用部署两个实例）：</p><ol><li>将服务的一个实例在注册中心的状态设置为 DOWN</li><li>等待一段时间，直到其他服务缓存刷新，不再调用到这台服务器上</li><li>停止服务，更新代码，重新启动，等待，直到启动成功</li></ol><p>完成后，再重复以上步骤部署另一个实例。</p><h3 id="缩短缓存刷新时间"><a href="#缩短缓存刷新时间" class="headerlink" title="缩短缓存刷新时间"></a>缩短缓存刷新时间</h3><p>上面第 2 步中，需要等待一段时间，直到不再有新的请求调用到某个实例上。具体要等多久，其他调用者的请求才会不再访问到这台状态为 DOWN 的实例呢？这里涉及到三个配置项：</p><ul><li><code>eureka.client.registryFetchIntervalSeconds</code> Eureka 客户端每隔多久去 Eureka 服务器拉取最新的注册信息，默认值 30（秒）。</li><li><code>ribbon.ServerListRefreshInterval</code> Ribbon 的缓存刷新间隔时间，默认 30000（毫秒）。Eureka 客户端拉取到最新注册信息后，Ribbon、Feign 等组件不会立即生效，是因为 Ribbon 还有一层缓存。</li><li><code>eureka.server.responseCacheUpdateIntervalMs</code> Eureka Server 返回最新的注册信息的接口缓存刷新时间间隔，默认 30000（毫秒）。有时候会看到 Eureka 页面和 <code>/eureka/apps</code> 接口的服务状态不一致，就是因为 <code>/eureka/apps</code> 接口默认会有 30 秒缓存。</li></ul><p>在默认情况下，当一个服务状态改为 DOWN，最长可能需要 30+30+30 秒，所有的缓存才会刷新，其他调用者才不会调用到这个状态为 DOWN 的实例。这就意味着修改服务实例状态为 DOWN 后需要等待 90 秒，才能进行下一步操作。</p><p>为了让部署时间缩短，可以将以上三个配置项都修改为 5 秒：</p><p>Eureka Server：<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">eureka:</span></span><br><span class="line"><span class="attr">  server:</span></span><br><span class="line"><span class="attr">    responseCacheUpdateIntervalMs:</span> <span class="number">5000</span></span><br></pre></td></tr></table></figure></p><p>Eureka Client（即各个服务）：<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ribbon:</span></span><br><span class="line"><span class="attr">  ServerListRefreshInterval:</span> <span class="number">5000</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line"><span class="attr">  client:</span></span><br><span class="line"><span class="attr">    registryFetchIntervalSeconds:</span> <span class="number">5</span></span><br></pre></td></tr></table></figure></p><p>此时部署时将实例状态设为 DOWN 后，只需要等待 15 秒即可停止进程。</p><p>完成以上配置后，下面给出了两种方案实现服务 Zero-downtime 部署：</p><h3 id="方案一：基于部署脚本实现"><a href="#方案一：基于部署脚本实现" class="headerlink" title="方案一：基于部署脚本实现"></a>方案一：基于部署脚本实现</h3><h4 id="第一步：修改实例状态为-DOWN"><a href="#第一步：修改实例状态为-DOWN" class="headerlink" title="第一步：修改实例状态为 DOWN"></a>第一步：修改实例状态为 DOWN</h4><p>有两种方案可以修改实例的状态，选择其一即可：</p><ol><li>直接调用 Eureka Server API 修改：PUT /eureka/apps/{appID}/{instanceID}/status?value=DOWN</li><li>调用服务实例对应的 actuator endpoint：<code>/service-registry</code></li></ol><p>我更偏向使用方法二，对应的命令：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -H <span class="string">"Content-Type:application/json"</span> -X POST http://&#123;host:port&#125;/actuator/service-registry?status=DOWN</span><br></pre></td></tr></table></figure></p><p>如果 actuator endpoint 加了 Spring Security Basic 认证，则还需要加上用户名和密码：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -H <span class="string">"Content-Type:application/json"</span> -X POST -u &#123;username&#125;:&#123;password&#125; http://&#123;host:port&#125;/actuator/service-registry?status=DOWN</span><br></pre></td></tr></table></figure></p><h4 id="第二步：等待其他调用者的缓存刷新"><a href="#第二步：等待其他调用者的缓存刷新" class="headerlink" title="第二步：等待其他调用者的缓存刷新"></a>第二步：等待其他调用者的缓存刷新</h4><p>这里直接通过 sleep 来等待 15 秒：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sleep 15s</span><br></pre></td></tr></table></figure></p><h4 id="第三步：实例部署"><a href="#第三步：实例部署" class="headerlink" title="第三步：实例部署"></a>第三步：实例部署</h4><p>这一步主要需要注意：</p><ul><li>尽量不要使用 <code>kill -9 pid</code> 强制杀掉进程，而应该使用 <code>kill pid</code> 或者 <code>kill -15 pid</code> 关闭进程。使用 <code>kill pid</code> 或者 <code>kill -15 pid</code> 关闭进程之前，Eureka Client 会给 Eureka Server 请求删除自己，后续服务再次启动后会重新注册为 UP 状态。如果使用 <code>kill -9 pid</code> 强制杀掉进程，Eureka Client 没有办法注销自己，Eureka Server 就不知道该实例已下线，直到长时间收不到心跳才会删除该实例。如果在 Eureka Server 删除实例之前实例启动了，那么它的状态还是会保持 DOWN 状态。如果确实需要用到 <code>kill -9 pid</code> 强制杀掉进程，那么服务重启后需要再通过第一步的方式将实例状态设为 UP。</li><li>服务启动后，需要等待并确认启动成功后，才可以开始部署下一台服务器。这里我们可以定时去请求 Spring Boot 提供的 actuator endpoint <code>/health</code> 接口，例如每隔 1 秒请求一次，直到接口可以正常访问，即可认为服务启动成功。</li></ul><h3 id="解决方案二：基于服务自身代码实现"><a href="#解决方案二：基于服务自身代码实现" class="headerlink" title="解决方案二：基于服务自身代码实现"></a>解决方案二：基于服务自身代码实现</h3><p>上面一种基于部署脚本实现的方案，把设置服务在 Eureka 注册中心的状态为 DOWN 以及等待 15 秒的步骤放到了部署脚本中。这里再提供另一种方案，将这两个步骤放在服务的代码中。</p><p>具体的方法是添加一个 <code>EventListener</code>，用于监听 <code>ContextClosedEvent</code>，即 Spring 的 <code>ApplicationContext</code> 关闭的事件，例如使用 <code>kill pid</code> 或者 <code>kill -15 pid</code> 关闭进程就会触发该事件。在监听到事件之后，代码中通过 <code>Thread.sleep(15000)</code> 来实现等待 15 秒。具体代码如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EurekaGracefulShutdownListener</span> <span class="keyword">implements</span> <span class="title">ApplicationListener</span>&lt;<span class="title">ContextClosedEvent</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ApplicationContext applicationContext;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ContextClosedEvent event)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// Spring Cloud 中会有两个 ApplicationContext，这里排除掉 Spring Cloud 创建的 “bootstrap” context，否则此处会触发两次</span></span><br><span class="line">        <span class="keyword">if</span> (applicationContext == event.getApplicationContext()) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">15000</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>大家可能会发现以上代码中并没有将该服务在 Eureka 注册中心的状态设置为 DOWN，这是因为 spring-cloud-netflix-eureka-client 组件中已经有一个 <code>EventListener</code> 监听了 <code>ContextClosedEvent</code> 事件，并且在我们的 <code>EventListener</code> 触发之前将状态置为 DOWN，所以我们在上面的代码中就没必要多此一举了。相关源码： <a href="https://github.com/spring-cloud/spring-cloud-netflix/blob/v2.1.3.RELEASE/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaAutoServiceRegistration.java#L154" target="_blank" rel="noopener">EurekaAutoServiceRegistration</a></p><p>添加以上代码后，如果使用 <code>kill pid</code> 或者 <code>kill -15 pid</code> 关闭进程，进程实际上不会立即关闭，而是继续提供服务 15 秒后，进程才会真正被杀死。所以同样不能使用 <code>kill -9 pid</code> 强制杀掉进程。</p><p><em>本文基于 Spring Boot 2.1.x 及 Spring Cloud Greenwich 版本</em></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot; title=&quot;问题&quot;&gt;&lt;/a&gt;问题&lt;/h2&gt;&lt;p&gt;互联网产品高速迭代，通常伴随着高频次的版本发布。部署新版上线需要重启服务，直接 kill 服务进程可能会造成服务短暂不可用，从而影响到正在使
      
    
    </summary>
    
      <category term="Spring Cloud" scheme="https://xxgblog.com/categories/Spring-Cloud/"/>
    
    
      <category term="Spring Cloud" scheme="https://xxgblog.com/tags/Spring-Cloud/"/>
    
      <category term="Eureka" scheme="https://xxgblog.com/tags/Eureka/"/>
    
      <category term="微服务" scheme="https://xxgblog.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>Spring Data Redis 如何将 RedisTemplate 注入为 ListOperations 类型</title>
    <link href="https://xxgblog.com/2020/03/12/spring-redistemplate-listoperations/"/>
    <id>https://xxgblog.com/2020/03/12/spring-redistemplate-listoperations/</id>
    <published>2020-03-12T03:14:16.000Z</published>
    <updated>2020-03-12T10:08:22.721Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在 Spring Data Redis 官方文档中，可以看到这样一个常规用法：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:p</span>=<span class="string">"http://www.springframework.org/schema/p"</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"jedisConnectionFactory"</span> <span class="attr">class</span>=<span class="string">"org.springframework.data.redis.connection.jedis.JedisConnectionFactory"</span> <span class="attr">p:use-pool</span>=<span class="string">"true"</span>/&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- redis template definition --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"redisTemplate"</span> <span class="attr">class</span>=<span class="string">"org.springframework.data.redis.core.RedisTemplate"</span> <span class="attr">p:connection-factory-ref</span>=<span class="string">"jedisConnectionFactory"</span>/&gt;</span></span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Example</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// inject the actual template</span></span><br><span class="line">  <span class="meta">@Autowired</span></span><br><span class="line">  <span class="keyword">private</span> RedisTemplate&lt;String, String&gt; template;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// inject the template as ListOperations</span></span><br><span class="line">  <span class="meta">@Resource</span>(name=<span class="string">"redisTemplate"</span>)</span><br><span class="line">  <span class="keyword">private</span> ListOperations&lt;String, String&gt; listOps;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addLink</span><span class="params">(String userId, URL url)</span> </span>&#123;</span><br><span class="line">    listOps.leftPush(userId, url.toExternalForm());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码摘自：<a href="https://docs.spring.io/spring-data/redis/docs/2.2.5.RELEASE/reference/html/#redis:template" target="_blank" rel="noopener">https://docs.spring.io/spring-data/redis/docs/2.2.5.RELEASE/reference/html/#redis:template</a></p><p><code>RedisTemplate</code> 和 <code>ListOperations</code> 并没有继承关系，这里是怎么将 <code>RedisTemplate</code> 注入到 <code>ListOperations</code> 类型上去的呢？而且不但可以将 <code>RedisTemplate</code> 注入到 <code>ListOperations</code> ，也可以注入到 <code>ValueOperations</code>、<code>SetOperations</code>、<code>ZSetOperations</code>、<code>HashOperations</code> 等类型上。</p><h2 id="PropertyEditor"><a href="#PropertyEditor" class="headerlink" title="PropertyEditor"></a>PropertyEditor</h2><p>Spring 框架可以通过 <code>java.beans.PropertyEditor</code> 接口的实现类来实现类型转换。</p><p>Spring Data Redis 提供了 <code>ListOperationsEditor</code> 可以将 <code>RedisTemplate</code> 转为 <code>ListOperations</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ListOperationsEditor</span> <span class="keyword">extends</span> <span class="title">PropertyEditorSupport</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setValue</span><span class="params">(Object value)</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (value <span class="keyword">instanceof</span> RedisOperations) &#123;</span><br><span class="line"><span class="keyword">super</span>.setValue(((RedisOperations) value).opsForList());</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Editor supports only conversion of type "</span> + RedisOperations.class);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码摘自：<a href="https://github.com/spring-projects/spring-data-redis/blob/2.2.5.RELEASE/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java" target="_blank" rel="noopener">https://github.com/spring-projects/spring-data-redis/blob/2.2.5.RELEASE/src/main/java/org/springframework/data/redis/core/ListOperationsEditor.java</a></p><p>以上代码中，<code>RedisOperations</code> 是 <code>RedisTemplate</code> 的父级接口，<code>((RedisOperations) value).opsForList()</code> 实际上就是调用 <code>RedisTemplate.opsForList()</code> 获取 <code>ListOperations</code>。</p><h2 id="Spring-如何注册-PropertyEditor"><a href="#Spring-如何注册-PropertyEditor" class="headerlink" title="Spring 如何注册 PropertyEditor"></a>Spring 如何注册 PropertyEditor</h2><blockquote><p>Note also that the standard JavaBeans infrastructure automatically discovers PropertyEditor classes (without you having to register them explicitly) if they are in the same package as the class they handle and have the same name as that class, with Editor appended. For example, one could have the following class and package structure, which would be sufficient for the SomethingEditor class to be recognized and used as the PropertyEditor for Something-typed properties.</p></blockquote><p>摘自：<a href="https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/core.html#beans-beans-conversion" target="_blank" rel="noopener">https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/core.html#beans-beans-conversion</a></p><p>文档中提到，如果 <code>PropertyEditor</code> 类与它们处理的类在同一个包中，并且类名再加上 <code>Editor</code> 后缀，则无需显式注册，该 <code>PropertyEditor</code> 可以被自动发现。</p><p>在 Spring Data Redis 源码中可以看到，<code>ListOperations</code> 类和 <code>ListOperationsEditor</code> 都在 <code>org.springframework.data.redis.core</code> 包下，且 <code>ListOperationsEditor</code> 符合命名规则，即在 <code>ListOperations</code> 类名上加上 <code>Editor</code> 后缀，所以可以自动发现并生效。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot; title=&quot;问题&quot;&gt;&lt;/a&gt;问题&lt;/h2&gt;&lt;p&gt;在 Spring Data Redis 官方文档中，可以看到这样一个常规用法：&lt;/p&gt;
&lt;figure class=&quot;highlight x
      
    
    </summary>
    
      <category term="Spring Framework" scheme="https://xxgblog.com/categories/Spring-Framework/"/>
    
    
      <category term="Redis" scheme="https://xxgblog.com/tags/Redis/"/>
    
      <category term="Spring Data" scheme="https://xxgblog.com/tags/Spring-Data/"/>
    
      <category term="PropertyEditor" scheme="https://xxgblog.com/tags/PropertyEditor/"/>
    
      <category term="RedisTemplate" scheme="https://xxgblog.com/tags/RedisTemplate/"/>
    
      <category term="Operations" scheme="https://xxgblog.com/tags/Operations/"/>
    
  </entry>
  
  <entry>
    <title>Nginx失败重试中的HTTP协议幂等问题: non_idempotent</title>
    <link href="https://xxgblog.com/2019/04/19/nginx-non-idempotent/"/>
    <id>https://xxgblog.com/2019/04/19/nginx-non-idempotent/</id>
    <published>2019-04-19T14:18:36.000Z</published>
    <updated>2019-04-20T16:47:38.938Z</updated>
    
    <content type="html"><![CDATA[<p>Nginx通过反向代理做负载均衡时，如果被代理的其中一个服务发生错误或者超时的时候，通常希望Nginx自动重试其他的服务，从而实现服务的高可用性。实际上Nginx本身默认会有错误重试机制，并且可以通过<code>proxy_next_upstream</code>来自定义配置。</p><p>如果不了解HTTP协议以及Nginx的机制，就可能在使用过程中遇到各种各样的坑。例如服务出现了错误或超时却未重试，或者一些例如创建订单或发送短信这类的HTTP接口，客户端只发送一次请求，后台却由于Nginx重试导致创建了多个订单，或者收到多条短信，导致一些业务上的问题。</p><h2 id="proxy-next-upstream"><a href="#proxy-next-upstream" class="headerlink" title="proxy_next_upstream"></a>proxy_next_upstream</h2><p>在Nginx配置文件中，<code>proxy_next_upstream</code>用于指定在什么情况下Nginx会将请求转移到其他服务器上。其默认值是<code>proxy_next_upstream error timeout</code>，即发生网络错误以及超时，才会重试其他服务器。默认情况下服务返回500状态码是不会重试的，如果想在响应500状态码时也进行重试，可以配置：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proxy_next_upstream error timeout http_500;</span><br></pre></td></tr></table></figure></p><p>当然还有<code>http_502</code>、<code>http_503</code>、<code>http_404</code>等可以指定在出现哪些状态码的情况下需要重试。具体配置项可以参考官方文档: <a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream" target="_blank" rel="noopener">http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream</a> 。</p><p>用一个最简单的例子来测试一下该特性，例如下面是Spring Boot写了一个简单的HTTP接口，返回500状态码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">NginxRetryApplication</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">SpringApplication.run(NginxRetryApplication.class, args);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TestController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">test</span><span class="params">()</span> </span>&#123;</span><br><span class="line">System.out.println(<span class="string">"收到一个请求"</span>); <span class="comment">// 打印日志</span></span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(); <span class="comment">// 抛出异常, 返回500状态码</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>分别使用9030和9031两个端口号启动该Spring Boot服务，然后Nginx配置好负载均衡：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">upstream nginxretry &#123;</span><br><span class="line">    server 127.0.0.1:9030 max_fails=0;</span><br><span class="line">server 127.0.0.1:9031 max_fails=0;</span><br><span class="line">&#125;</span><br><span class="line">server &#123;</span><br><span class="line">    listen 9039;</span><br><span class="line">    location / &#123;</span><br><span class="line">        proxy_pass http://nginxretry;</span><br><span class="line">        proxy_next_upstream error timeout http_500;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>注意：以上配置中<code>max_fails=0</code>是为了更方便的测试Nginx错误重试机制。<code>max_fails</code>默认值是1，用于指定一个server在一段时间内（默认10s）发生错误次数达到多少次，Nginx就会自动将该服务器下线。这里设置为0是禁用这个特性，防止在测试过程中服务器被踢下线不好测试。线上环境下一般不会设置<code>max_fails=0</code>。</p><p>配置完成后重启Nginx，使用GET方式请求 <a href="http://localhost:9039/" target="_blank" rel="noopener">http://localhost:9039/</a> ，再分别查看9030和9031两个端口号对应的服务日志，可以发现两个服务都收到请求，也就是Nginx在访问其中一个服务收到500错误状态码后，又尝试去访问另一个服务。</p><p>再次使用POST方式请求 <a href="http://localhost:9039/" target="_blank" rel="noopener">http://localhost:9039/</a> ，再分别查看9030和9031两个端口号对应的服务日志，可以发现只有一个服务收到请求。也就是<strong>当请求类型是POST时，Nginx默认不会失败重试</strong>。如果想让POST请求也会失败重试，可以继续向下阅读。</p><h2 id="non-idempotent"><a href="#non-idempotent" class="headerlink" title="non_idempotent"></a>non_idempotent</h2><p>在<a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream" target="_blank" rel="noopener">Nginx文档</a>中可以看到<code>proxy_next_upstream</code>有一个选项<code>non_idempotent</code>:</p><blockquote><p>normally, requests with a non-idempotent method (POST, LOCK, PATCH) are not passed to the next server if a request has been sent to an upstream server (1.9.13); enabling this option explicitly allows retrying such requests;</p></blockquote><p>通常情况下，如果请求使用非等幂方法（POST、LOCK、PATCH），请求失败后不会再到其他服务器进行重试。加上<code>non_idempotent</code>选项后，即使是非幂等请求类型（例如POST请求），发生错误后也会重试。</p><p>如果想让POST请求也会失败重试，需要配置<code>non_idempotent</code>：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">upstream nginxretry &#123;</span><br><span class="line">    server 127.0.0.1:9030 max_fails=0;</span><br><span class="line">server 127.0.0.1:9031 max_fails=0;</span><br><span class="line">&#125;</span><br><span class="line">server &#123;</span><br><span class="line">    listen 9039;</span><br><span class="line">    location / &#123;</span><br><span class="line">        proxy_pass http://nginxretry;</span><br><span class="line">        proxy_next_upstream error timeout http_500 non_idempotent;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>重启Nginx后再次使用POST请求访问 <a href="http://localhost:9039/" target="_blank" rel="noopener">http://localhost:9039/</a> ，再分别查看9030和9031两个端口号对应的服务日志，可以看到两个服务都收到请求，也就是POST请求也会重试了。不过实际上在生产环境中，不建议加上<code>non_idempotent</code>选项，具体原因可以继续往下阅读。</p><h2 id="什么是幂等方法"><a href="#什么是幂等方法" class="headerlink" title="什么是幂等方法"></a>什么是幂等方法</h2><p>在<a href="https://tools.ietf.org/html/rfc7231#section-4.2.2" target="_blank" rel="noopener">HTTP协议规范</a>中，对幂等方法（Idempotent Method）做了以下定义：</p><blockquote><p> A request method is considered “idempotent” if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.</p></blockquote><p>如果使用该方法的多个相同请求对服务器的预期效果与单个请求的效果相同，则认为请求方法是幂等的。常见的HTTP请求方法中，GET是幂等的，而POST是非幂等的。如果在回答面试题”GET和POST区别”时能答出这一点，才能说明对HTTP协议有一定的理解。</p><p>在做业务开发是如何理解幂等性，举个最简单的例子：GET方法一般用于获取数据，如果获取的是数据库数据，对应的是SELECT语句。同样的SELECT语句执行一次还是多次，都不会影响数据。而POST一般对应INSERT，如果执行多次后，可能会造成数据重复插入的问题。所以不要使用GET方法做一些INSERT操作，在业务开发时要遵循HTTP协议规范。</p><p>生产环境中为什么不建议加上<code>non_idempotent</code>选项？因为无论是发生500错误还是timeout，服务器上的业务可能都已经执行过了，而重试会导致非幂等方法重复执行，从而导致业务问题，例如一个请求会创建了多个订单，或者收到多条短信的问题。</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream" target="_blank" rel="noopener">http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream</a></li><li><a href="https://tools.ietf.org/html/rfc7231" target="_blank" rel="noopener">https://tools.ietf.org/html/rfc7231</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Nginx通过反向代理做负载均衡时，如果被代理的其中一个服务发生错误或者超时的时候，通常希望Nginx自动重试其他的服务，从而实现服务的高可用性。实际上Nginx本身默认会有错误重试机制，并且可以通过&lt;code&gt;proxy_next_upstream&lt;/code&gt;来自定义配
      
    
    </summary>
    
      <category term="Nginx" scheme="https://xxgblog.com/categories/Nginx/"/>
    
    
      <category term="Nginx" scheme="https://xxgblog.com/tags/Nginx/"/>
    
      <category term="HTTP" scheme="https://xxgblog.com/tags/HTTP/"/>
    
      <category term="反向代理" scheme="https://xxgblog.com/tags/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/"/>
    
      <category term="负载均衡" scheme="https://xxgblog.com/tags/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/"/>
    
  </entry>
  
  <entry>
    <title>Java IPv6相关属性preferIPv4Stack、preferIPv6Addresses介绍</title>
    <link href="https://xxgblog.com/2019/02/17/java-ipv6-system-properties/"/>
    <id>https://xxgblog.com/2019/02/17/java-ipv6-system-properties/</id>
    <published>2019-02-17T03:14:16.000Z</published>
    <updated>2019-02-17T17:18:46.787Z</updated>
    
    <content type="html"><![CDATA[<h2 id="IPv6背景介绍"><a href="#IPv6背景介绍" class="headerlink" title="IPv6背景介绍"></a>IPv6背景介绍</h2><p>目前被广泛使用的IPv4，它的最大问题是网络地址资源有限。IPv4仅有32二进制位，满打满算也仅有不到43亿个IP地址，已经完全不能满足目前需求。IPv6有128二进制位，地址数量非常庞大。目前主流操作系统早已支持IPv6，Google、Facebook和Yahoo等网站也早已支持IPv6。2017年底中共中央办公厅、国务院办公厅印发<a href="http://www.gov.cn/zhengce/2017-11/26/content_5242389.htm" target="_blank" rel="noopener">《推进互联网协议第六版（IPv6）规模部署行动计划》</a>，推动国内IPv6的支持。目前已有大量国内网站和APP已经支持IPv6。未来数年将是IPv4到IPv6的过渡时期，IPv6和IPv4会并存，IPv6使用率也会逐渐提高。</p><h2 id="Java对IPv6的支持"><a href="#Java对IPv6的支持" class="headerlink" title="Java对IPv6的支持"></a>Java对IPv6的支持</h2><p>在<a href="https://docs.oracle.com/javase/8/docs/technotes/guides/net/ipv6_guide/index.html" target="_blank" rel="noopener">Networking IPv6 User Guide</a>文档中，介绍了Java对IPv6的支持情况：</p><blockquote><p>IPv6 in Java is transparent and automatic. Porting is not necessary; there is no need to recompile source files.</p></blockquote><p>Java中对IPv6的支持是透明的且自动化的，无需移植，也无需重新编译源码。相对于其他语言而言（例如C++），Java很好封装了IPv4和IPv6两种版本的不同，Java一般不需要在编写代码时关注IPv6如何支持，一个原本运行在IPv4环境的代码放到支持IPv6环境中一般也可以直接使用。</p><h2 id="Java-IPv6相关系统属性"><a href="#Java-IPv6相关系统属性" class="headerlink" title="Java IPv6相关系统属性"></a>Java IPv6相关系统属性</h2><p>由于Java封装了IPv4和IPv6的差异，在代码层面上不需要关注同时也无法设置使用IPv4还是IPv6，Java提供了<code>java.net.preferIPv4Stack</code>、<code>java.net.preferIPv6Addresses</code>两个系统属性，用于设置协议栈和地址族的选择。</p><p>这两个系统参数既可以在Java代码中设置：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">System.setProperty(<span class="string">"java.net.preferIPv4Stack"</span>, <span class="string">"true"</span>);</span><br></pre></td></tr></table></figure></p><p>也可以在启动时通过JVM参数设置：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-Djava.net.preferIPv4Stack=<span class="literal">true</span></span><br></pre></td></tr></table></figure></p><h3 id="java-net-preferIPv4Stack（默认值false）"><a href="#java-net-preferIPv4Stack（默认值false）" class="headerlink" title="java.net.preferIPv4Stack（默认值false）"></a>java.net.preferIPv4Stack（默认值false）</h3><p>官方文档解释：</p><blockquote><p>If IPv6 is available on the operating system, the underlying native socket will be an IPv6 socket. This allows Java applications to connect to, and accept connections from, both IPv4 and IPv6 hosts.</p></blockquote><p>当<code>java.net.preferIPv4Stack</code>为默认值false时，在支持IPv6的双栈系统上，使用Java的Socket会默认通过底层native方法创建一个IPv6 Socket，这个IPv6 Socket可以同时支持和IPv4或IPv6主机通信。如果设置为true，Java程序将无法使用IPv6进行网络通信，也就是仅支持IPv4。</p><p>例如，当TCP客户端<code>java.net.preferIPv4Stack</code>设置为true时，如果想创建一个host为IPv6的Socket，会抛出异常<code>java.net.SocketException: Protocol family unavailable</code>，设置为false时则程序可以正常运行：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">System.setProperty(<span class="string">"java.net.preferIPv4Stack"</span>, <span class="string">"true"</span>);</span><br><span class="line">Socket socket = <span class="keyword">new</span> Socket(<span class="string">"::1"</span>, <span class="number">80</span>); <span class="comment">// 访问IPv6地址::1（相当于IPv4的127.0.0.1）的80端口会抛出异常</span></span><br></pre></td></tr></table></figure></p><p>当TCP服务器<code>java.net.preferIPv4Stack</code>设置为true时，通过IPv6访问就会访问不通。</p><p>在正常情况下，Tomcat或者Jetty等Java服务器启动后，可以使用浏览器通过IPv4地址<a href="http://127.0.0.1:8080" target="_blank" rel="noopener">http://127.0.0.1:8080</a>或IPv6地址<a href="http://[::1]:8080" target="_blank" rel="noopener">http://[::1]:8080</a>两种方式访问，此时通过<code>lsof -i:8080</code>命令可以看到对应进程的Type为IPv6：<br><img src="/img/c3c91a63-6a81-410d-8ede-6904aa0d7d6c.jpg" alt="lsof命令"></p><p>如果加上JVM参数<code>-Djava.net.preferIPv4Stack=true</code>启动，此时通过<code>lsof -i:8080</code>命令可以看到对应进程的Type为IPv4：<br><img src="/img/f17a9d70-80a4-11e6-ae22-56b6b6499688.jpg" alt="lsof命令"><br>此时通过IPv6地址<a href="http://[::1]:8080" target="_blank" rel="noopener">http://[::1]:8080</a>将无法访问，仅可通过IPv4地址<a href="http://127.0.0.1:8080" target="_blank" rel="noopener">http://127.0.0.1:8080</a>访问。</p><h3 id="java-net-preferIPv6Addresses（默认值false）"><a href="#java-net-preferIPv6Addresses（默认值false）" class="headerlink" title="java.net.preferIPv6Addresses（默认值false）"></a>java.net.preferIPv6Addresses（默认值false）</h3><p>官方文档解释：</p><blockquote><p>By default, IPv4 addresses are preferred over IPv6 addresses, for example, when querying the name service (for instance, DNS service), IPv4 addresses would be returned ahead of IPv6 addresses. </p></blockquote><p>当<code>java.net.preferIPv6Addresses</code>为默认值false时，IPv4地址会优先使用，例如在DNS通过域名查询IP地址时，会优先使用IPv4地址，反之设为true，则会优先使用IPv6地址。</p><p>Google目前支持IPv4和IPv6，可以<code>host</code>命令查看域名<code>www.google.com</code>DNS解析的IPv4和IPv6地址：<br><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><span class="line">host www.google.com</span><br><span class="line">&gt; www.google.com has address 66.220.146.94</span><br><span class="line">&gt; www.google.com has IPv6 address 2404:6800:4008:803::2004</span><br></pre></td></tr></table></figure></p><p>默认情况下会优先使用IPv4地址：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">System.out.println(InetAddress.getByName(<span class="string">"www.google.com"</span>));</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">www.google.com/66.220.146.94</span><br></pre></td></tr></table></figure></p><p>当<code>java.net.preferIPv6Addresses</code>设置为true时会优先使用IPv6地址：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">System.setProperty(<span class="string">"java.net.preferIPv6Addresses"</span>, <span class="string">"true"</span>);</span><br><span class="line">System.out.println(InetAddress.getByName(<span class="string">"www.google.com"</span>));</span><br></pre></td></tr></table></figure></p><p>输出：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">www.google.com/2404:6800:4008:803:0:0:0:2004</span><br></pre></td></tr></table></figure></p><p>另外，通过<code>InetAddress.getLocalHost()</code>方法返回本机IP地址，最终返回的是IPv4还是IPv6地址，也会由<code>java.net.preferIPv6Addresses</code>的值来决定。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/net/ipv6_guide/index.html" target="_blank" rel="noopener">https://docs.oracle.com/javase/8/docs/technotes/guides/net/ipv6_guide/index.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;IPv6背景介绍&quot;&gt;&lt;a href=&quot;#IPv6背景介绍&quot; class=&quot;headerlink&quot; title=&quot;IPv6背景介绍&quot;&gt;&lt;/a&gt;IPv6背景介绍&lt;/h2&gt;&lt;p&gt;目前被广泛使用的IPv4，它的最大问题是网络地址资源有限。IPv4仅有32二进制位，满打满算
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="IPv6" scheme="https://xxgblog.com/tags/IPv6/"/>
    
  </entry>
  
  <entry>
    <title>利用X-Forwarded-For伪造客户端IP漏洞成因及防范</title>
    <link href="https://xxgblog.com/2018/10/12/x-forwarded-for-header-trick/"/>
    <id>https://xxgblog.com/2018/10/12/x-forwarded-for-header-trick/</id>
    <published>2018-10-12T03:14:16.000Z</published>
    <updated>2019-05-14T16:43:04.063Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h2><p>在Web应用开发中，经常会需要获取客户端IP地址。一个典型的例子就是投票系统，为了防止刷票，需要限制每个IP地址只能投票一次。</p><h2 id="如何获取客户端IP"><a href="#如何获取客户端IP" class="headerlink" title="如何获取客户端IP"></a>如何获取客户端IP</h2><p>在Java中，获取客户端IP最直接的方式就是使用<code>request.getRemoteAddr()</code>。这种方式能获取到连接服务器的客户端IP，在中间没有代理的情况下，的确是最简单有效的方式。但是目前互联网Web应用很少会将应用服务器直接对外提供服务，一般都会有一层Nginx做反向代理和负载均衡，有的甚至可能有多层代理。在有反向代理的情况下，直接使用<code>request.getRemoteAddr()</code>获取到的IP地址是Nginx所在服务器的IP地址，而不是客户端的IP。</p><p>HTTP协议是基于TCP协议的，由于<code>request.getRemoteAddr()</code>默认获取到的是TCP层直接连接的客户端的IP，对于Web应用服务器来说直接连接它的客户端实际上是Nginx，也就是TCP层是拿不到真实客户端的IP。</p><p>为了解决上面的问题，很多HTTP代理会在HTTP协议头中添加<code>X-Forwarded-For</code>头，用来追踪请求的来源。<code>X-Forwarded-For</code>的格式如下：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">X-Forwarded-For: client1, proxy1, proxy2</span><br></pre></td></tr></table></figure></p><p><code>X-Forwarded-For</code>包含多个IP地址，每个值通过逗号+空格分开，最左边（client1）是最原始客户端的IP地址，中间如果有多层代理，每一层代理会将连接它的客户端IP追加在<code>X-Forwarded-For</code>右边。</p><p>下面就是一种常用的获取客户端真实IP的方法，首先从HTTP头中获取<code>X-Forwarded-For</code>，如果<code>X-Forwarded-For</code>头存在就按逗号分隔取最左边第一个IP地址，不存在直接通过<code>request.getRemoteAddr()</code>获取IP地址：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getClientIp</span><span class="params">(HttpServletRequest request)</span> </span>&#123;</span><br><span class="line">    String xff = request.getHeader(<span class="string">"X-Forwarded-For"</span>);</span><br><span class="line">    <span class="keyword">if</span> (xff == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> request.getRemoteAddr();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> xff.contains(<span class="string">","</span>) ? xff.split(<span class="string">","</span>)[<span class="number">0</span>] : xff;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>另外，要让Nginx支持<code>X-Forwarded-For</code>头，需要配置：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></pre></td></tr></table></figure></p><p><code>$proxy_add_x_forwarded_for</code>会将和Nginx直接连接的客户端IP追加在请求原有<code>X-Forwarded-For</code>值的右边。</p><h2 id="伪造X-Forwarded-For"><a href="#伪造X-Forwarded-For" class="headerlink" title="伪造X-Forwarded-For"></a>伪造X-Forwarded-For</h2><p>一般的客户端（例如浏览器）发送HTTP请求是没有<code>X-Forwarded-For</code>头的，当请求到达第一个代理服务器时，代理服务器会加上<code>X-Forwarded-For</code>请求头，并将值设为客户端的IP地址（也就是最左边第一个值），后面如果还有多个代理，会依次将IP追加到<code>X-Forwarded-For</code>头最右边，最终请求到达Web应用服务器，应用通过获取<code>X-Forwarded-For</code>头取左边第一个IP即为客户端真实IP。</p><p>但是如果客户端在发起请求时，请求头上带上一个伪造的<code>X-Forwarded-For</code>，由于后续每层代理只会追加而不会覆盖，那么最终到达应用服务器时，获取的左边第一个IP地址将会是客户端伪造的IP。也就是上面的Java代码中<code>getClientIp()</code>方法获取的IP地址很有可能是伪造的IP地址，如果一个投票系统用这种方式做的IP限制，那么很容易会被刷票。</p><p>伪造<code>X-Forwarded-For</code>头的方法很简单，例如Postman就可以轻松做到：<br><img src="/img/38165b59-6e62-430f-8b35-4b17540be135.png" alt="Postman伪造X-Forwarded-For"></p><p>当然你也可以写一段刷票程序或者脚本，每次请求时添加<code>X-Forwarded-For</code>头并随机生成一个IP来实现刷票的目的。</p><h2 id="如何防范"><a href="#如何防范" class="headerlink" title="如何防范"></a>如何防范</h2><h3 id="方法一"><a href="#方法一" class="headerlink" title="方法一"></a>方法一</h3><p>在直接对外的Nginx反向代理服务器上配置：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proxy_set_header X-Forwarded-For $remote_addr;</span><br></pre></td></tr></table></figure></p><p>如果有多层Nginx代理，内层的Nginx配置：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></pre></td></tr></table></figure></p><p>在最外层Nginx（即直接对外提供服务的Nginx）使用<code>$remote_addr</code>代替上面的<code>$proxy_add_x_forwarded_for</code>，可以防止伪造<code>X-Forwarded-For</code>。<code>$proxy_add_x_forwarded_for</code>会在原有<code>X-Forwarded-For</code>上追加IP，这就相当于给了伪造<code>X-Forwarded-For</code>的机会。而<code>$remote_addr</code>是获取的是直接TCP连接的客户端IP，这个是无法伪造的，即使客户端伪造也会被覆盖掉，而不是追加。</p><p>需要注意的是，如果有多层代理，只在直接对外访问的Nginx上配置<code>X-Forwarded-For</code>为<code>$remote_addr</code>，内层的Nginx还是要配置为<code>$proxy_add_x_forwarded_for</code>，不然内层的Nginx又会覆盖掉客户端的真实IP。</p><p>完成以上配置后，业务代码中再通过上面的<code>getClientIp()</code>方法，获取<code>X-Forwarded-For</code>最左边的IP地址即为真实的客户端地址，且客户端也无法伪造。</p><h3 id="方法二"><a href="#方法二" class="headerlink" title="方法二"></a>方法二</h3><p>Tomcat服务器解决方案：<a href="https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html" target="_blank" rel="noopener">org.apache.catalina.valves.RemoteIpValve</a></p><p><code>RemoteIpValve</code>可以替换Servlet API中<code>request.getRemoteAddr()</code>方法的实现，让<code>request.getRemoteAddr()</code>方法从<code>X-Forwarded-For</code>头中获取IP地址。也就是在业务代码中不需要再自己实现类似于上面的<code>getClientIp()</code>方法来从<code>X-Forwarded-For</code>中获取IP，而是直接使用<code>request.getRemoteAddr()</code>方法。想要使用<code>RemoteIpValve</code>，仅需要在Tomcat配置文件server.xml中Host元素内末尾加上：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">Valve</span> <span class="attr">className</span>=<span class="string">"org.apache.catalina.valves.RemoteIpValve"</span> <span class="attr">...</span> /&gt;</span></span><br></pre></td></tr></table></figure></p><p><code>RemoteIpValve</code>有一套防止伪造<code>X-Forwarded-For</code>的机制，实现思路：遍历<code>X-Forwarded-For</code>头中的IP地址，和方法一不同的是，不是直接取左边第一个IP，而是从右向左遍历。遍历时可以根据正则表达式剔除掉内网IP和已知的代理服务器本身的IP（例如192.168开头的IP），那么拿到的第一个非剔除IP就会是一个可信任的客户端IP。这种方法的巧妙之处在于，即使伪造<code>X-Forwarded-For</code>，那么请求到达应用服务器时，伪造的IP也会在<code>X-Forwarded-For</code>值的左边，真实的IP为放到右边的某个位置，从右向左遍历就可以避免取到这些伪造的IP地址。</p><h3 id="方法三"><a href="#方法三" class="headerlink" title="方法三"></a>方法三</h3><p>Node.js 框架 Egg.js 的解决方案：<a href="https://eggjs.org/zh-cn/tutorials/proxy.html" target="_blank" rel="noopener">https://eggjs.org/zh-cn/tutorials/proxy.html</a></p><p>Egg.js 可通过设置<code>maxProxyCount</code>指定代理层数，然后取<code>X-Forwarded-For</code>头中从右往左数第<code>maxProxyCount</code>个IP即为真实 IP 地址，如果有伪造 IP 地址了必然在最左边，就会被忽略掉。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;问题背景&quot;&gt;&lt;a href=&quot;#问题背景&quot; class=&quot;headerlink&quot; title=&quot;问题背景&quot;&gt;&lt;/a&gt;问题背景&lt;/h2&gt;&lt;p&gt;在Web应用开发中，经常会需要获取客户端IP地址。一个典型的例子就是投票系统，为了防止刷票，需要限制每个IP地址只能投票一次
      
    
    </summary>
    
      <category term="Nginx" scheme="https://xxgblog.com/categories/Nginx/"/>
    
    
      <category term="Nginx" scheme="https://xxgblog.com/tags/Nginx/"/>
    
      <category term="Http" scheme="https://xxgblog.com/tags/Http/"/>
    
  </entry>
  
  <entry>
    <title>Apache Commons DbUtils整合Spring框架实现简单的CRUD</title>
    <link href="https://xxgblog.com/2018/08/30/dbutils-spring/"/>
    <id>https://xxgblog.com/2018/08/30/dbutils-spring/</id>
    <published>2018-08-30T03:14:16.000Z</published>
    <updated>2019-04-09T12:08:02.576Z</updated>
    
    <content type="html"><![CDATA[<p>Commons DbUtils是Apache提供的一个对JDBC进行简单封装的开源工具类库，能够简化JDBC相关的开发。Commons DbUtils可以非常方便的整合Spring Framework，比较轻量级，执行SQL语句非常方便（特别是查询语句），可以代替Spring JdbcTemplate、MyBatis等数据库访问层技术。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>DbUtils通过<code>QueryRunner</code>类来执行SQL，使用起来非常类似于Spring框架中的<code>JdbcTemplate</code>。<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"dataSource"</span> <span class="attr">class</span>=<span class="string">"com.zaxxer.hikari.HikariDataSource"</span> <span class="attr">destroy-method</span>=<span class="string">"close"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"driverClassName"</span> <span class="attr">value</span>=<span class="string">"com.mysql.jdbc.Driver"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"jdbcUrl"</span> <span class="attr">value</span>=<span class="string">"jdbc:mysql://localhost:3306/test_db"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"username"</span> <span class="attr">value</span>=<span class="string">"root"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"password"</span> <span class="attr">value</span>=<span class="string">"xxx"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"queryRunner"</span> <span class="attr">class</span>=<span class="string">"org.apache.commons.dbutils.QueryRunner"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">ref</span>=<span class="string">"dataSource"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>我这里使用的连接池是HikariCP，这里可以根据需要换成其他的连接池，例如DBCP、Druid等。</p><p>由于DbUtils本身不支持Spring事务，如果想要支持事务，例如<code>@Transactional</code>注解，还需要给DataSource加一层代理：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"dataSource"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"targetDataSource"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"com.zaxxer.hikari.HikariDataSource"</span> <span class="attr">destroy-method</span>=<span class="string">"close"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"driverClassName"</span> <span class="attr">value</span>=<span class="string">"com.mysql.jdbc.Driver"</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"jdbcUrl"</span> <span class="attr">value</span>=<span class="string">"jdbc:mysql://localhost:3306/test_db"</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"username"</span> <span class="attr">value</span>=<span class="string">"root"</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"password"</span> <span class="attr">value</span>=<span class="string">"xxx"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"queryRunner"</span> <span class="attr">class</span>=<span class="string">"org.apache.commons.dbutils.QueryRunner"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">ref</span>=<span class="string">"dataSource"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>Java代码中可以通过Autowired引入QueryRunner：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserDao</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> QueryRunner queryRunner;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="增删改"><a href="#增删改" class="headerlink" title="增删改"></a>增删改</h2><p>增删改操作，也就是INSERT、DELETE、UPDATE语句，都可以通过QueryRunner的execute方法来直接执行：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">queryRunner.execute(<span class="string">"delete from user_info"</span>);</span><br><span class="line">queryRunner.execute(<span class="string">"update user_info set user_name=?,user_age=? where user_id=?"</span>, <span class="string">"xxg"</span>, <span class="number">28</span>, <span class="number">6</span>);</span><br></pre></td></tr></table></figure></p><p>由于DbUtils本身也是基于JDBC中的PreparedStatement来实现的，所以也是支持SQL中带有参数的。</p><h2 id="查询"><a href="#查询" class="headerlink" title="查询"></a>查询</h2><h3 id="ResultSetHandler"><a href="#ResultSetHandler" class="headerlink" title="ResultSetHandler"></a>ResultSetHandler</h3><p><code>ResultSetHandler</code>是DbUtils中的一个接口，该接口的实现类可用于将JDBC查询语句返回的结果（也就是<code>ResultSet</code>），转成你想要的数据类型。这个和Spring JdbcTemplate查询时用到的<code>RowMapper</code>接口非常类似。</p><p>下面写了一个<code>ResultSetHandler</code>实现类，将一条SQL的查询结果转为一个List<user>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; list = queryRunner.query(<span class="string">"select * from user_info limit 100"</span>, <span class="keyword">new</span> ResultSetHandler&lt;List&lt;User&gt;&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> List&lt;User&gt; <span class="title">handle</span><span class="params">(ResultSet rs)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">        List&lt;User&gt; l = <span class="keyword">new</span> ArrayList&lt;User&gt;();</span><br><span class="line">        <span class="keyword">while</span> (rs.next()) &#123;</span><br><span class="line">            User user = <span class="keyword">new</span> User();</span><br><span class="line">            user.setUserId(rs.getInt(<span class="string">"user_id"</span>));</span><br><span class="line">            user.setUserName(rs.getString(<span class="string">"user_name"</span>));</span><br><span class="line">            user.setCreateTime(rs.getTimestamp(<span class="string">"create_time"</span>));</span><br><span class="line">            l.add(user);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> l;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></user></p><p>由于<code>ResultSetHandler</code>接口中只有一个抽象方法，所以如果是Java 8版本的话也可以使用Lambda表达式来简化代码：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; list = queryRunner.query(<span class="string">"select * from user_info limit 100"</span>, rs -&gt; &#123;</span><br><span class="line">    List&lt;User&gt; l = <span class="keyword">new</span> ArrayList&lt;User&gt;();</span><br><span class="line">    <span class="keyword">while</span> (rs.next()) &#123;</span><br><span class="line">        User user = <span class="keyword">new</span> User();</span><br><span class="line">        user.setUserId(rs.getInt(<span class="string">"user_id"</span>));</span><br><span class="line">        user.setUserName(rs.getString(<span class="string">"user_name"</span>));</span><br><span class="line">        user.setCreateTime(rs.getTimestamp(<span class="string">"create_time"</span>));</span><br><span class="line">        l.add(user);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> l;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><h3 id="常用ResultSetHandler实现类"><a href="#常用ResultSetHandler实现类" class="headerlink" title="常用ResultSetHandler实现类"></a>常用ResultSetHandler实现类</h3><p>DbUtils提供了一些常用的<code>ResultSetHandler</code>实现类，可以简化查询，一般情况下不需要像上面那样自己来实现<code>ResultSetHandler</code>接口。</p><h4 id="ScalarHandler"><a href="#ScalarHandler" class="headerlink" title="ScalarHandler"></a>ScalarHandler</h4><p>用于返回查询结果第一行第一列数据：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">long</span> count = queryRunner.query(<span class="string">"select count(*) from user_info"</span>, <span class="keyword">new</span> ScalarHandler&lt;Long&gt;()); <span class="comment">// 查询count</span></span><br><span class="line">String userName = queryRunner.query(<span class="string">"select user_name from user_info where user_id=?"</span>, <span class="keyword">new</span> ScalarHandler&lt;String&gt;(), <span class="number">1</span>); <span class="comment">// 查询user_id=1的用户的用户名</span></span><br></pre></td></tr></table></figure></p><h4 id="ColumnListHandler"><a href="#ColumnListHandler" class="headerlink" title="ColumnListHandler"></a>ColumnListHandler</h4><p>返回查询结果（所有行）第一列的数据List：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">List&lt;String&gt; userNameList = queryRunner.query(<span class="string">"select user_name from user_info"</span>, <span class="keyword">new</span> ColumnListHandler&lt;String&gt;()); <span class="comment">// 查询所有用户的user_name</span></span><br></pre></td></tr></table></figure></p><h4 id="MapHandler"><a href="#MapHandler" class="headerlink" title="MapHandler"></a>MapHandler</h4><p>返回查询结果第一行数据（所有列）并组装成Map，Map的key为列名，value为值：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;String, Object&gt; userInfo = queryRunner.query(<span class="string">"select user_id,user_name from user_info where user_id=1"</span>, <span class="keyword">new</span> MapHandler());</span><br><span class="line"><span class="keyword">long</span> userId = (Long) userInfo.get(<span class="string">"user_id"</span>);</span><br><span class="line">String userName = (String) userInfo.get(<span class="string">"user_name"</span>);</span><br></pre></td></tr></table></figure></p><h4 id="MapListHandler"><a href="#MapListHandler" class="headerlink" title="MapListHandler"></a>MapListHandler</h4><p>和<code>MapHandler</code>机制类似，<code>MapListHandler</code>会将ResultSet转成一个<code>List&lt;Map&lt;String, Object&gt;&gt;</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">List&lt;Map&lt;String, Object&gt;&gt; dataList = queryRunner.query(<span class="string">"select user_id,user_name from user_info"</span>, <span class="keyword">new</span> MapListHandler());</span><br></pre></td></tr></table></figure></p><h4 id="ArrayHandler"><a href="#ArrayHandler" class="headerlink" title="ArrayHandler"></a>ArrayHandler</h4><p>返回查询结果第一行数据，将所有列值按顺序组成一个数据：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Object[] data = queryRunner.query(<span class="string">"select user_id,user_name from user_info where user_id=1"</span>, <span class="keyword">new</span> ArrayHandler());</span><br><span class="line"><span class="keyword">long</span> userId = (Long) data[<span class="number">0</span>];</span><br><span class="line">String userName = (String) data[<span class="number">1</span>];</span><br></pre></td></tr></table></figure></p><h4 id="ArrayListHandler"><a href="#ArrayListHandler" class="headerlink" title="ArrayListHandler"></a>ArrayListHandler</h4><p>和<code>ArrayHandler</code>机制类似，<code>ArrayListHandler</code>会将ResultSet转成一个<code>List&lt;Object[]&gt;</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">List&lt;Object[]&gt; list = queryRunner.query(<span class="string">"select user_id,user_name from user_info"</span>, <span class="keyword">new</span> ArrayListHandler());</span><br></pre></td></tr></table></figure></p><h4 id="KeyedHandler"><a href="#KeyedHandler" class="headerlink" title="KeyedHandler"></a>KeyedHandler</h4><p>将ResultSet转为<code>Map&lt;?, Map&lt;String, Object&gt;&gt;</code>，外层Map每个元素对应查询结果的一条数据，key为数据的主键或者唯一索引，value也是一个Map，内容是一行数据的列名和值，和<code>MapHandler</code>机制类似：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;Long, Map&lt;String, Object&gt;&gt; dataMap = queryRunner.query(<span class="string">"select user_id,user_name from user_info"</span>, <span class="keyword">new</span> KeyedHandler&lt;Long&gt;(<span class="string">"user_id"</span>)); <span class="comment">// Key指定为user_id列</span></span><br><span class="line">Map&lt;String, Object&gt; data = dataMap.get(<span class="number">1L</span>); <span class="comment">// 获取user_id=1的一条记录</span></span><br><span class="line"><span class="keyword">long</span> userId = (Long) data.get(<span class="string">"user_id"</span>);</span><br><span class="line">String userName = (String) data.get(<span class="string">"user_name"</span>);</span><br></pre></td></tr></table></figure></p><h4 id="BeanHandler"><a href="#BeanHandler" class="headerlink" title="BeanHandler"></a>BeanHandler</h4><p><code>BeanHandler</code>是比较实用的一个类，可以通过反射机制将查询结果第一行数据根据数据库列名映射到Java对象上，先定义一个Java对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">User</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> userId;</span><br><span class="line">    <span class="keyword">private</span> String userName;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> userAge;</span><br><span class="line">    <span class="keyword">private</span> Date createTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 省略getter setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>执行查询：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 查询user_id=1的用户数据并返回User对象</span></span><br><span class="line">User user = queryRunner.query(<span class="string">"select user_id as userId,user_name as userName,user_age as userAge,create_time as createTime from user_info where user_id=1"</span>, <span class="keyword">new</span> BeanHandler&lt;User&gt;(User.class));</span><br></pre></td></tr></table></figure></p><p>数据库列名很多人会使用下划线作为单词间分隔符，而Java命名规范要求变量名是驼峰命名，这样会导致无法直接映射，所以上面代码中在SQL语句上通过<code>AS</code>将列名下划线分隔符转成驼峰命名。但是如果字段比较多，或者想使用<code>select *</code>查询，上面的这种方法就不好使了。下面提供一种方案，可以将带有下划线分隔符的列名映射到驼峰命名的Java对象上：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">User user = queryRunner.query(<span class="string">"select user_id,user_name,user_age,create_time from user_info where user_id=1"</span>,</span><br><span class="line">        <span class="keyword">new</span> BeanHandler&lt;User&gt;(User.class, <span class="keyword">new</span> BasicRowProcessor(<span class="keyword">new</span> GenerousBeanProcessor())));</span><br></pre></td></tr></table></figure></p><h4 id="BeanListHandler"><a href="#BeanListHandler" class="headerlink" title="BeanListHandler"></a>BeanListHandler</h4><p>和<code>BeanHandler</code>机制类似，<code>BeanListHandler</code>可以将多条查询结果转为Java Bean的List：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; userList = queryRunner.query(<span class="string">"select user_id as userId,user_name as userName,user_age as userAge,create_time as createTime from user_info"</span>,</span><br><span class="line">        <span class="keyword">new</span> BeanListHandler&lt;User&gt;(User.class));</span><br></pre></td></tr></table></figure></p><p>同样也可以将带有下划线分隔符的列名映射到驼峰命名的Java对象上：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; userList = queryRunner.query(<span class="string">"select user_id,user_name,user_age,create_time from user_info"</span>,</span><br><span class="line">        <span class="keyword">new</span> BeanListHandler&lt;User&gt;(User.class, <span class="keyword">new</span> BasicRowProcessor(<span class="keyword">new</span> GenerousBeanProcessor())));</span><br></pre></td></tr></table></figure></p><h4 id="BeanMapHandler"><a href="#BeanMapHandler" class="headerlink" title="BeanMapHandler"></a>BeanMapHandler</h4><p>类似于<code>KeyedHandler</code>机制，将ResultSet转为一个Map，Map中每条数据对应查询结果的一条数据，key为数据的主键或者唯一索引，value是数据通过反射机制转成的Java对象：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;Long, User&gt; users = queryRunner.query(<span class="string">"select user_id as userId,user_name as userName,user_age as userAge,create_time as createTime from user_info"</span>,</span><br><span class="line">        <span class="keyword">new</span> BeanMapHandler&lt;Long, User&gt;(User.class, <span class="string">"userId"</span>)); <span class="comment">// 使用userId列作为Map的key</span></span><br><span class="line">User user1 = users.get(<span class="number">1L</span>); <span class="comment">// 获取user_id=1的用户</span></span><br></pre></td></tr></table></figure></p><p>同样也可以将带有下划线分隔符的列名映射到驼峰命名的Java对象上：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Map的key默认使用查询语句中的第一列（即主键user_id）</span></span><br><span class="line">Map&lt;Long, User&gt; users = queryRunner.query(<span class="string">"select user_id,user_name,user_age,create_time from user_info"</span>,</span><br><span class="line">        <span class="keyword">new</span> BeanMapHandler&lt;Long, User&gt;(User.class, <span class="keyword">new</span> BasicRowProcessor(<span class="keyword">new</span> GenerousBeanProcessor())));</span><br><span class="line">User user1 = users.get(<span class="number">1L</span>); <span class="comment">// 获取user_id=1的用户</span></span><br></pre></td></tr></table></figure></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Commons DbUtils是Apache提供的一个对JDBC进行简单封装的开源工具类库，能够简化JDBC相关的开发。Commons DbUtils可以非常方便的整合Spring Framework，比较轻量级，执行SQL语句非常方便（特别是查询语句），可以代替Sprin
      
    
    </summary>
    
      <category term="Java" scheme="https://xxgblog.com/categories/Java/"/>
    
    
      <category term="Java" scheme="https://xxgblog.com/tags/Java/"/>
    
      <category term="Spring Framework" scheme="https://xxgblog.com/tags/Spring-Framework/"/>
    
  </entry>
  
  <entry>
    <title>去除Spring Security认证：Pre-Authentication配置</title>
    <link href="https://xxgblog.com/2018/05/22/spring-security-pre-authentication/"/>
    <id>https://xxgblog.com/2018/05/22/spring-security-pre-authentication/</id>
    <published>2018-05-22T03:14:16.000Z</published>
    <updated>2018-08-30T07:26:50.673Z</updated>
    
    <content type="html"><![CDATA[<p>Spring Security官方文档对Pre-Authentication是这样解释的：</p><blockquote><p>There are situations where you want to use Spring Security for authorization, but the user has already been reliably authenticated by some external system prior to accessing the application. We refer to these situations as “pre-authenticated” scenarios.</p></blockquote><p>这里面涉及到Spring Security中两个概念，认证(Authentication)和授权(Authorization)。有关这两个概念的介绍，网上可以搜索到其他相关资料，这里仅通俗易懂的解释一下：</p><ul><li>认证(Authentication)：认证就是判断用户身份是否合法，例如用户名密码登录就是认证，如果一个用户拥有正确的密码，即可通过认证；</li><li>授权(Authorization)：用户认证通过了，但是每个用户的权限不同，判断用户有哪些权限以及是否有权限访问某些资源，就是授权。</li></ul><p>Spring Security框架提供了认证和授权的功能，但是有可能只希望使用Spring Security的授权功能，而不使用它提供的认证功能，比如使用一些其他认证方式，那么就可以使用Pre-Authentication。</p><h2 id="Pre-Authentication配置"><a href="#Pre-Authentication配置" class="headerlink" title="Pre-Authentication配置"></a>Pre-Authentication配置</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span></span></span><br><span class="line"><span class="tag">       <span class="attr">xmlns:security</span>=<span class="string">"http://www.springframework.org/schema/security"</span></span></span><br><span class="line"><span class="tag">       <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span></span><br><span class="line"><span class="tag">       <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/beans/spring-beans.xsd</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/security</span></span></span><br><span class="line"><span class="tag"><span class="string">        http://www.springframework.org/schema/security/spring-security.xsd"</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">security:http</span> <span class="attr">entry-point-ref</span>=<span class="string">"http403ForbiddenEntryPoint"</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 省略其他配置 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">security:custom-filter</span> <span class="attr">position</span>=<span class="string">"PRE_AUTH_FILTER"</span> <span class="attr">ref</span>=<span class="string">"preauthFilter"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">security:http</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"http403ForbiddenEntryPoint"</span> <span class="attr">class</span>=<span class="string">"org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"preauthFilter"</span> <span class="attr">class</span>=<span class="string">"com.xxg.test.auth.PreauthFilter"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"authenticationManager"</span> <span class="attr">ref</span>=<span class="string">"authenticationManager"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"preauthAuthProvider"</span> <span class="attr">class</span>=<span class="string">"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"preAuthenticatedUserDetailsService"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"userDetailsServiceWrapper"</span> <span class="attr">class</span>=<span class="string">"org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"userDetailsService"</span> <span class="attr">ref</span>=<span class="string">"userDetailsService"</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"userDetailsService"</span> <span class="attr">class</span>=<span class="string">"com.xxg.test.auth.UserDetailsServiceImpl"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">security:authentication-manager</span> <span class="attr">alias</span>=<span class="string">"authenticationManager"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">security:authentication-provider</span> <span class="attr">ref</span>=<span class="string">"preauthAuthProvider"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">security:authentication-manager</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><p>由于不再使用Spring Security提供的默认的用户名密码登录认证，需要修改<code>entry-point-ref</code>为<code>Http403ForbiddenEntryPoint</code>，否则会出现异常：</p><p>No AuthenticationEntryPoint could be established. Please make sure you have a login mechanism configured through the namespace (such as form-login) or specify a custom AuthenticationEntryPoint with the ‘entry-point-ref’ attribute</p><p>配置Pre-Authentication的最主要的部分是需要添加一个<code>position=&quot;PRE_AUTH_FILTER&quot;</code>的Filter，这个Filter继承抽象类<code>AbstractPreAuthenticatedProcessingFilter</code>：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PreauthFilter</span> <span class="keyword">extends</span> <span class="title">AbstractPreAuthenticatedProcessingFilter</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 重写，返回用户名，这个用户名是经过其他方式认证过</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Object <span class="title">getPreAuthenticatedPrincipal</span><span class="params">(HttpServletRequest request)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (authenticated) &#123;</span><br><span class="line">            <span class="comment">// 可以通过request获取当前认证过的用户名，比如通过参数、HTTP请求头或者Cookie获取token，再通过token调用第三方接口获取用户名</span></span><br><span class="line">            <span class="keyword">return</span> <span class="string">"your_username"</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 如果认证失败，可以返回null，表示anonymous匿名用户</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 这个方法一般情况下不需要重写，直接返回空字符串即可</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Object <span class="title">getPreAuthenticatedCredentials</span><span class="params">(HttpServletRequest request)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>另外还有个重点配置<code>userDetailsService</code>，这个是用于用户认证后的授权。这里需要一个<code>UserDetailsService</code>的实现类，来获取用户的所有权限。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserDetailsServiceImpl</span> <span class="keyword">implements</span> <span class="title">UserDetailsService</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserDetails <span class="title">loadUserByUsername</span><span class="params">(String username)</span> <span class="keyword">throws</span> UsernameNotFoundException </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 这里可以通过用户名获取对应的权限</span></span><br><span class="line">        Collection&lt;GrantedAuthority&gt; auths = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        auths.add(<span class="keyword">new</span> SimpleGrantedAuthority(<span class="string">"ROLE_USER"</span>));</span><br><span class="line">        auths.add(<span class="keyword">new</span> SimpleGrantedAuthority(<span class="string">"ROLE_SUPER_ADMIN"</span>));</span><br><span class="line"></span><br><span class="line">        User user = <span class="keyword">new</span> User(username, <span class="string">""</span>, auths);</span><br><span class="line">        <span class="keyword">return</span> user;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h2><p>如果是普通的浏览器访问的Web，以上完成配置后，用户在浏览器上首次访问会调用<code>AbstractPreAuthenticatedProcessingFilter</code>的<code>getPreAuthenticatedPrincipal</code>以及<code>UserDetailsService</code>的<code>loadUserByUsername</code>方法来获取认证用户和授权，并将相关信息保存到Session中，后续的请求直接通过Session获取用户信息，不再重复调用这些方法。</p><p>而对于API接口来说，一般情况下不会使用Session来做会话控制，例如可能会通过token的方式。API接口相对来说每次接口访问都是无状态的，所以针对每次请求都需要重新认证和授权。这个时候可以设置<code>create-session=&quot;stateless&quot;</code>来禁掉Spring Security使用Session：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">security:http</span> <span class="attr">entry-point-ref</span>=<span class="string">"http403ForbiddenEntryPoint"</span> <span class="attr">create-session</span>=<span class="string">"stateless"</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- 省略其他配置 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">security:custom-filter</span> <span class="attr">position</span>=<span class="string">"PRE_AUTH_FILTER"</span> <span class="attr">ref</span>=<span class="string">"preauthFilter"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">security:http</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://docs.spring.io/spring-security/site/docs/4.2.3.RELEASE/reference/html/preauth.html" target="_blank" rel="noopener">https://docs.spring.io/spring-security/site/docs/4.2.3.RELEASE/reference/html/preauth.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Spring Security官方文档对Pre-Authentication是这样解释的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are situations where you want to use Spring Security for authoriz
      
    
    </summary>
    
      <category term="Spring Framework" scheme="https://xxgblog.com/categories/Spring-Framework/"/>
    
    
      <category term="Spring Framework" scheme="https://xxgblog.com/tags/Spring-Framework/"/>
    
      <category term="Spring Security" scheme="https://xxgblog.com/tags/Spring-Security/"/>
    
  </entry>
  
  <entry>
    <title>ActiveMQ整合Spring JMS</title>
    <link href="https://xxgblog.com/2018/05/12/spring-jms-activemq/"/>
    <id>https://xxgblog.com/2018/05/12/spring-jms-activemq/</id>
    <published>2018-05-12T03:14:16.000Z</published>
    <updated>2018-05-12T14:38:13.906Z</updated>
    
    <content type="html"><![CDATA[<p>JMS即Java消息服务（Java Message Service），是Java平台上的一套关于消息中间件的规范，或者说是一套统一的API。支持JMS的消息中间件有很多，ActiveMQ算是其中最常用的一个。</p><h2 id="JMS两种模型"><a href="#JMS两种模型" class="headerlink" title="JMS两种模型"></a>JMS两种模型</h2><p>JMS支持以下两种模型，本文将会对这两种模型分别介绍如何整合Spring：</p><ul><li>点对点（Point-to-Point），对应的destination是Queue</li><li>发布订阅（Publish/Subscribe），对应的destination是Topic</li></ul><h2 id="建立连接"><a href="#建立连接" class="headerlink" title="建立连接"></a>建立连接</h2><p>无论是点对点还是发布订阅，生产者或者消费者，第一步是要获取连接。ActiveMQ提供了<code>org.apache.activemq.pool.PooledConnectionFactory</code>连接池，类似于常用的数据库连接池，用于管理和复用连接。<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- ActiveMQ连接池 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"connectionFactory"</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.pool.PooledConnectionFactory"</span> <span class="attr">destroy-method</span>=<span class="string">"stop"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.ActiveMQConnectionFactory"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"brokerURL"</span> <span class="attr">value</span>=<span class="string">"tcp://localhost:61616"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h2 id="生产者"><a href="#生产者" class="headerlink" title="生产者"></a>生产者</h2><p>Spring提供了JmsTemplate可以很方便的发送消息。</p><h3 id="点对点模型"><a href="#点对点模型" class="headerlink" title="点对点模型"></a>点对点模型</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"jmsTemplate"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jms.core.JmsTemplate"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span> <span class="attr">ref</span>=<span class="string">"connectionFactory"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"defaultDestination"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.command.ActiveMQQueue"</span>&gt;</span> <span class="comment">&lt;!-- 指定Destination为Queue（点对点模型） --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"testqueue"</span> /&gt;</span> <span class="comment">&lt;!-- Queue name --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="发布订阅模型"><a href="#发布订阅模型" class="headerlink" title="发布订阅模型"></a>发布订阅模型</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"jmsTemplate"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jms.core.JmsTemplate"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span> <span class="attr">ref</span>=<span class="string">"connectionFactory"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"defaultDestination"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.command.ActiveMQTopic"</span>&gt;</span> <span class="comment">&lt;!-- 指定Destination为Topic（发布订阅模型） --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"testtopic"</span> /&gt;</span> <span class="comment">&lt;!-- Topic name --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="发送消息代码"><a href="#发送消息代码" class="headerlink" title="发送消息代码"></a>发送消息代码</h3><p>使用上面定义的JmsTemplate，通过JmsTemplate.send方法可以发送一条文本类型消息。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> JmsTemplate jmsTemplate;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">send</span><span class="params">(String mesasage)</span> </span>&#123;</span><br><span class="line">    jmsTemplate.send(<span class="keyword">new</span> MessageCreator() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> Message <span class="title">createMessage</span><span class="params">(Session session)</span> <span class="keyword">throws</span> JMSException </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> session.createTextMessage(mesasage);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>如果你正在使用Java 8，使用Lambda表达式发送消息会更加方便：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> JmsTemplate jmsTemplate;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">send</span><span class="params">(String mesasage)</span> </span>&#123;</span><br><span class="line">    jmsTemplate.send(session -&gt; session.createTextMessage(mesasage));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="消费者"><a href="#消费者" class="headerlink" title="消费者"></a>消费者</h2><p>JmsTemplate也可以作为消费者使用，但是它是同步的。下面介绍Spring JMS提供的异步的消费者方案。</p><h3 id="点对点模型-1"><a href="#点对点模型-1" class="headerlink" title="点对点模型"></a>点对点模型</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 消息监听器 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"messageListener"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jms.listener.DefaultMessageListenerContainer"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span> <span class="attr">ref</span>=<span class="string">"connectionFactory"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"destination"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.command.ActiveMQQueue"</span>&gt;</span> <span class="comment">&lt;!-- 指定Destination为Queue（点对点模型） --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"testqueue"</span> /&gt;</span> <span class="comment">&lt;!-- Queue name --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"messageListener"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"com.xxg.jms.listener.ConsumerMessageListener"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="发布订阅模型-1"><a href="#发布订阅模型-1" class="headerlink" title="发布订阅模型"></a>发布订阅模型</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 消息监听器 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"messageListener"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jms.listener.DefaultMessageListenerContainer"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span> <span class="attr">ref</span>=<span class="string">"connectionFactory"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"destination"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.command.ActiveMQTopic"</span>&gt;</span> <span class="comment">&lt;!-- 指定Destination为Topic（发布订阅模型） --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"testtopic"</span> /&gt;</span> <span class="comment">&lt;!-- Topic name --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"messageListener"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"com.xxg.jms.listener.ConsumerMessageListener"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="发布订阅模型消息者持久订阅（Durable-Subscriber）"><a href="#发布订阅模型消息者持久订阅（Durable-Subscriber）" class="headerlink" title="发布订阅模型消息者持久订阅（Durable Subscriber）"></a>发布订阅模型消息者持久订阅（Durable Subscriber）</h3><p>上面给出的是非持久订阅的发布订阅模型消费者，这里来单独说一下持久订阅。需要注意的是，持久订阅（Durable Subscriber）并非消息持久化（DeliveryMode.PERSISTENT），这是两个不同的概念。</p><p>非持久订阅的消费者，如果消费者程序挂了，那么挂了的这段时间的消息是收不到的，即使再重启起来也收不到。持久订阅消费者可以让消费者在重启后任然能收到停止的这段时间的消息，避免遗漏。</p><p>要想使用持久订阅，需要给消费者设置一个唯一的client ID和Subscriber Name，这样可以让ActiveMQ记住这个消费者，当消费者断开连接程序停止，ActiveMQ也会给这个消费者保留这段时间内的消息，下次同一个消费者（client ID和Subscriber Name相同）重新连接上还能收到消息。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- ActiveMQ连接池 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"connectionFactory"</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.pool.PooledConnectionFactory"</span> <span class="attr">destroy-method</span>=<span class="string">"stop"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.ActiveMQConnectionFactory"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"brokerURL"</span> <span class="attr">value</span>=<span class="string">"tcp://localhost:61616"</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"clientID"</span> <span class="attr">value</span>=<span class="string">"clientID_123456"</span> /&gt;</span>  <span class="comment">&lt;!-- 指定一个唯一的clientID --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 消息监听器 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"messageListener"</span> <span class="attr">class</span>=<span class="string">"org.springframework.jms.listener.DefaultMessageListenerContainer"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"connectionFactory"</span> <span class="attr">ref</span>=<span class="string">"connectionFactory"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"destination"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"org.apache.activemq.command.ActiveMQTopic"</span>&gt;</span> <span class="comment">&lt;!-- 指定Destination为Topic（发布订阅模型） --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"testtopic"</span> /&gt;</span> <span class="comment">&lt;!-- Topic name --&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"messageListener"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"com.xxg.jms.listener.ConsumerMessageListener"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"durableSubscriptionName"</span> <span class="attr">value</span>=<span class="string">"durableSubscriptionName_123456"</span> /&gt;</span> <span class="comment">&lt;!-- 指定一个唯一的Name --&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="接收消息代码"><a href="#接收消息代码" class="headerlink" title="接收消息代码"></a>接收消息代码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConsumerMessageListener</span> <span class="keyword">implements</span> <span class="title">MessageListener</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(Message message)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (message <span class="keyword">instanceof</span> TextMessage) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(((TextMessage) message).getText());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">catch</span> (JMSException ex) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(ex);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Message must be of type TextMessage"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="http://activemq.apache.org/spring-support.html" target="_blank" rel="noopener">http://activemq.apache.org/spring-support.html</a></li><li><a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#jms" target="_blank" rel="noopener">https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#jms</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;JMS即Java消息服务（Java Message Service），是Java平台上的一套关于消息中间件的规范，或者说是一套统一的API。支持JMS的消息中间件有很多，ActiveMQ算是其中最常用的一个。&lt;/p&gt;
&lt;h2 id=&quot;JMS两种模型&quot;&gt;&lt;a href=&quot;#J
      
    
    </summary>
    
      <category term="Spring Framework" scheme="https://xxgblog.com/categories/Spring-Framework/"/>
    
    
      <category term="Spring Framework" scheme="https://xxgblog.com/tags/Spring-Framework/"/>
    
      <category term="JMS" scheme="https://xxgblog.com/tags/JMS/"/>
    
      <category term="ActiveMQ" scheme="https://xxgblog.com/tags/ActiveMQ/"/>
    
  </entry>
  
</feed>
