<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Allen</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://lazyallen.github.io/"/>
  <updated>2023-05-05T15:58:10.047Z</updated>
  <id>https://lazyallen.github.io/</id>
  
  <author>
    <name>Allen Chang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>记一次unicode编码引发的问题</title>
    <link href="https://lazyallen.github.io/2023/05/05/unicode-normalization/"/>
    <id>https://lazyallen.github.io/2023/05/05/unicode-normalization/</id>
    <published>2023-05-04T16:00:00.000Z</published>
    <updated>2023-05-05T15:58:10.047Z</updated>
    
    <content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>遇到的场景是：客户从PDF粘贴一段文字到搜索框，内容是<code>工时规范</code>四个中文字，发现搜不出结果，但是自己手动敲击文字则是正常。请求如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">https://xxx?name=%E5%B7%A5%E6%97%B6%E8%A7%84%E8%8C%83</span><br><span class="line">https://xxx?name=%E2%BC%AF%E6%97%B6%E8%A7%84%E8%8C%83</span><br></pre></td></tr></table></figure><p>以上链接是URL编码后的结果。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/202305051837254.png" alt=""></p><p>对比得知两者区别在于编码是不一样的，但展示结果看起来是一致的。URL编码是16进制的，区别只有前面几个编码不同，也就是工字。</p><ul><li>E5B7A5</li><li>E2BCAF</li></ul><h3 id="排查思路"><a href="#排查思路" class="headerlink" title="排查思路"></a>排查思路</h3><p>遇到的第一个问题是，为什么不同的编码会出现一样的字？<br>通常URL编码对应的是UTF8编码格式，可从<a href="http://www.mytju.com/classcode/tools/encode_utf8.asp">encode_utf8</a> 网站检索如下。</p><table><thead><tr><th>字符</th><th>编码10进制</th><th>编码16进制</th><th>Unicode编码10进制</th><th>Unicode编码16进制</th></tr></thead><tbody><tr><td>工</td><td>15054757</td><td>E5B7A5</td><td>24037</td><td>5DE5</td></tr><tr><td>⼯</td><td>14859439</td><td>E2BCAF</td><td>12079</td><td>2F2F</td></tr></tbody></table><p>对于编码来讲，我的理解就是一组kv集合，key就是编码，v就是值。无论是UTF8还是unicode都是一样的，只是两者范围和大小上有所不同。  </p><p>通过<a href="https://symbl.cc/en/search/?q=12079">symbl</a>可以通过unicode编码获取对应的字符是什么，也就是get(k)。</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/202305051854104.png" alt=""></p><p>可以看到两者的归属是不一样的：</p><ul><li>Kangxi Radicals: 康熙部首</li><li>CJK Unified Ideographs: <a href="https://www.zhihu.com/question/480184045">中日韩统一表意文字</a></li></ul><p>也就是说，两个工字都是中文，只是一个是部首偏旁，另外一个我们正常使用的汉字。<br>目前有使用ChatGPT辅助我查找问题，当我扔上面两个字符给到ChatGPT时，它告诉我们区别在于是否是全角和半角，于是我开始搜索全角和半角的区别。但显然并不是这个原因，也就是说ChatGPT在回答这种问题时，是会误导用户的。</p><h3 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h3><p>得知问题所在后，就是开始找解决方案了。检索发现unicode是提供 normalization 功能的。我理解解决的就是兼容性的问题，比如两个长得很像字符，不只是中文，其他语言也有类似的问题。在计算机的世界里面，这两者的编码不同，但在人类世界中可能是一致的。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; s1 = &#x27;Spicy Jalape\u00f1o&#x27;</span><br><span class="line">&gt;&gt;&gt; s2 = &#x27;Spicy Jalapen\u0303o&#x27;</span><br><span class="line">&gt;&gt;&gt; s1</span><br><span class="line">&#x27;Spicy Jalapeño&#x27;</span><br><span class="line">&gt;&gt;&gt; s2</span><br><span class="line">&#x27;Spicy Jalapeño&#x27;</span><br><span class="line"></span><br><span class="line">&gt;&gt;&gt; import unicodedata</span><br><span class="line">&gt;&gt;&gt; t1 = unicodedata.normalize(&#x27;NFC&#x27;, s1)</span><br><span class="line">&gt;&gt;&gt; t2 = unicodedata.normalize(&#x27;NFC&#x27;, s2)</span><br><span class="line">&gt;&gt;&gt; t1 == t2</span><br><span class="line">True</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对应Java代码也很简单</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Normalizer.normalize(argStr, Normalizer.Form.NFKC);</span><br></pre></td></tr></table></figure><p>基本解决方案有了，但如何集成到工程当中呢。工程使用的SpringBoot, Web使用的是Spring MVC。问题是如何把这段逻辑放在工厂中，最简单的方案就是哪个字段有问题就转一下就可以，这样做的问题就是改的多了就是多处存在相同的代码片段。类似这种统一处理的问题，在Spring中最明显就是AOP方案。做一个切面，获取到所有的参数都做一遍规范化处理。这样做当然可以，只是过于简单粗暴了，笔者在想有没有更优雅的方案。<br>那么需要先回答一个问题：Spring是如何帮我们自动把URL编码转成文本的？其实这里应该有两个过程：</p><ul><li>URL编码的二进制流转成文本</li><li>文本绑定到方法的参数上</li></ul><p>在Spring MVC中，实际做这件事的是<code>HandlerMethodArgumentResolver</code>，这个接口有大量的默认实现去解析不同场景下的参数。<br>例如RequestMappingHandlerAdapter#getDefaultArgumentResolvers()</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">private List&lt;HandlerMethodArgumentResolver&gt; getDefaultArgumentResolvers() &#123;</span><br><span class="line">List&lt;HandlerMethodArgumentResolver&gt; resolvers = new ArrayList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">// Annotation-based argument resolution</span><br><span class="line">resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));</span><br><span class="line">resolvers.add(new RequestParamMapMethodArgumentResolver());</span><br><span class="line">resolvers.add(new PathVariableMethodArgumentResolver());</span><br><span class="line">resolvers.add(new PathVariableMapMethodArgumentResolver());</span><br><span class="line">resolvers.add(new MatrixVariableMethodArgumentResolver());</span><br><span class="line">resolvers.add(new MatrixVariableMapMethodArgumentResolver());</span><br><span class="line">resolvers.add(new ServletModelAttributeMethodProcessor(false));</span><br><span class="line">resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));</span><br><span class="line">resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));</span><br><span class="line">resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));</span><br><span class="line">resolvers.add(new RequestHeaderMapMethodArgumentResolver());</span><br><span class="line">resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));</span><br><span class="line">resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));</span><br><span class="line">resolvers.add(new SessionAttributeMethodArgumentResolver());</span><br><span class="line">resolvers.add(new RequestAttributeMethodArgumentResolver());</span><br><span class="line"></span><br><span class="line">// Type-based argument resolution</span><br><span class="line">resolvers.add(new ServletRequestMethodArgumentResolver());</span><br><span class="line">resolvers.add(new ServletResponseMethodArgumentResolver());</span><br><span class="line">resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));</span><br><span class="line">resolvers.add(new RedirectAttributesMethodArgumentResolver());</span><br><span class="line">resolvers.add(new ModelMethodProcessor());</span><br><span class="line">resolvers.add(new MapMethodProcessor());</span><br><span class="line">resolvers.add(new ErrorsMethodArgumentResolver());</span><br><span class="line">resolvers.add(new SessionStatusMethodArgumentResolver());</span><br><span class="line">resolvers.add(new UriComponentsBuilderMethodArgumentResolver());</span><br><span class="line"></span><br><span class="line">// Custom arguments</span><br><span class="line">if (getCustomArgumentResolvers() != null) &#123;</span><br><span class="line">resolvers.addAll(getCustomArgumentResolvers());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// Catch-all</span><br><span class="line">resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));</span><br><span class="line">resolvers.add(new ServletModelAttributeMethodProcessor(true));</span><br><span class="line"></span><br><span class="line">return resolvers;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>HandlerMethodArgumentResolver 提供两个方法：</p><ul><li>判断是否支持该解析器，返回值是boolean</li><li>实际用来解析的方法，返回的是Object，也就是解析后的值<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><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></pre></td><td class="code"><pre><span class="line">public interface HandlerMethodArgumentResolver &#123;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Whether the given &#123;@linkplain MethodParameter method parameter&#125; is</span><br><span class="line"> * supported by this resolver.</span><br><span class="line"> * @param parameter the method parameter to check</span><br><span class="line"> * @return &#123;@code true&#125; if this resolver supports the supplied parameter;</span><br><span class="line"> * &#123;@code false&#125; otherwise</span><br><span class="line"> */</span><br><span class="line">boolean supportsParameter(MethodParameter parameter);</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Resolves a method parameter into an argument value from a given request.</span><br><span class="line"> * A &#123;@link ModelAndViewContainer&#125; provides access to the model for the</span><br><span class="line"> * request. A &#123;@link WebDataBinderFactory&#125; provides a way to create</span><br><span class="line"> * a &#123;@link WebDataBinder&#125; instance when needed for data binding and</span><br><span class="line"> * type conversion purposes.</span><br><span class="line"> * @param parameter the method parameter to resolve. This parameter must</span><br><span class="line"> * have previously been passed to &#123;@link #supportsParameter&#125; which must</span><br><span class="line"> * have returned &#123;@code true&#125;.public interface HandlerMethodArgumentResolver &#123;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Whether the given &#123;@linkplain MethodParameter method parameter&#125; is</span><br><span class="line"> * supported by this resolver.</span><br><span class="line"> * @param parameter the method parameter to check</span><br><span class="line"> * @return &#123;@code true&#125; if this resolver supports the supplied parameter;</span><br><span class="line"> * &#123;@code false&#125; otherwise</span><br><span class="line"> */</span><br><span class="line">boolean supportsParameter(MethodParameter parameter);</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Resolves a method parameter into an argument value from a given request.</span><br><span class="line"> * A &#123;@link ModelAndViewContainer&#125; provides access to the model for the</span><br><span class="line"> * request. A &#123;@link WebDataBinderFactory&#125; provides a way to create</span><br><span class="line"> * a &#123;@link WebDataBinder&#125; instance when needed for data binding and</span><br><span class="line"> * type conversion purposes.</span><br><span class="line"> * @param parameter the method parameter to resolve. This parameter must</span><br><span class="line"> * have previously been passed to &#123;@link #supportsParameter&#125; which must</span><br><span class="line"> * have returned &#123;@code true&#125;.</span><br><span class="line"> * @param mavContainer the ModelAndViewContainer for the current request</span><br><span class="line"> * @param webRequest the current request</span><br><span class="line"> * @param binderFactory a factory for creating &#123;@link WebDataBinder&#125; instances</span><br><span class="line"> * @return the resolved argument value, or &#123;@code null&#125; if not resolvable</span><br><span class="line"> * @throws Exception in case of errors with the preparation of argument values</span><br><span class="line"> */</span><br><span class="line">@Nullable</span><br><span class="line">Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,</span><br><span class="line">NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;</span><br><span class="line"></span><br><span class="line">&#125;public interface HandlerMethodArgumentResolver &#123;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Whether the given &#123;@linkplain MethodParameter method parameter&#125; is</span><br><span class="line"> * supported by this resolver.</span><br><span class="line"> * @param parameter the method parameter to check</span><br><span class="line"> * @return &#123;@code true&#125; if this resolver supports the supplied parameter;</span><br><span class="line"> * &#123;@code false&#125; otherwise</span><br><span class="line"> */</span><br><span class="line">boolean supportsParameter(MethodParameter parameter);</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Resolves a method parameter into an argument value from a given request.</span><br><span class="line"> * A &#123;@link ModelAndViewContainer&#125; provides access to the model for the</span><br><span class="line"> * request. A &#123;@link WebDataBinderFactory&#125; provides a way to create</span><br><span class="line"> * a &#123;@link WebDataBinder&#125; instance when needed for data binding and</span><br><span class="line"> * type conversion purposes.</span><br><span class="line"> * @param parameter the method parameter to resolve. This parameter must</span><br><span class="line"> * have previously been passed to &#123;@link #supportsParameter&#125; which must</span><br><span class="line"> * have returned &#123;@code true&#125;.</span><br><span class="line"> * @param mavContainer the ModelAndViewContainer for the current request</span><br><span class="line"> * @param webRequest the current request</span><br><span class="line"> * @param binderFactory a factory for creating &#123;@link WebDataBinder&#125; instances</span><br><span class="line"> * @return the resolved argument value, or &#123;@code null&#125; if not resolvable</span><br><span class="line"> * @throws Exception in case of errors with the preparation of argument values</span><br><span class="line"> */</span><br><span class="line">@Nullable</span><br><span class="line">Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,</span><br><span class="line">NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"> * @param mavContainer the ModelAndViewContainer for the current request</span><br><span class="line"> * @param webRequest the current request</span><br><span class="line"> * @param binderFactory a factory for creating &#123;@link WebDataBinder&#125; instances</span><br><span class="line"> * @return the resolved argument value, or &#123;@code null&#125; if not resolvable</span><br><span class="line"> * @throws Exception in case of errors with the preparation of argument values</span><br><span class="line"> */</span><br><span class="line">@Nullable</span><br><span class="line">Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,</span><br><span class="line">NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure></li></ul><p>很明显，<code>@RequestParam</code> 注解对应的类就是<code>RequestParamMethodArgumentResolver</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception &#123;</span><br><span class="line">HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);</span><br><span class="line"></span><br><span class="line">if (servletRequest != null) &#123;</span><br><span class="line">Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);</span><br><span class="line">if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) &#123;</span><br><span class="line">return mpArg;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Object arg = null;</span><br><span class="line">MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);</span><br><span class="line">if (multipartRequest != null) &#123;</span><br><span class="line">List&lt;MultipartFile&gt; files = multipartRequest.getFiles(name);</span><br><span class="line">if (!files.isEmpty()) &#123;</span><br><span class="line">arg = (files.size() == 1 ? files.get(0) : files);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">if (arg == null) &#123;</span><br><span class="line">String[] paramValues = request.getParameterValues(name);</span><br><span class="line">if (paramValues != null) &#123;</span><br><span class="line">arg = (paramValues.length == 1 ? paramValues[0] : paramValues);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">return arg;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>简单看其中逻辑可见，最后是通过<code>request.getParameterValues(name);</code>获取到实际的值，这里跟下去的话就到了tomcat那层的代码。<br>笔者在思考是否有什么类似于编码的参数，可以用于配置编码这个流程，实际找到有<code>HttpProperties</code>这个配置类，但其中只是对于编码类型的配置，不涉及normalization的部分。<br>现在目标就是如何增强<code>RequestParamMethodArgumentResolver</code>，使其有规范化的能力。思路只有在<code>RequestParamMethodArgumentResolver</code>本身动手。    </p><h4 id="是否提供扩展点进行定制化"><a href="#是否提供扩展点进行定制化" class="headerlink" title="是否提供扩展点进行定制化"></a>是否提供扩展点进行定制化</h4><p>简单阅读代码发现，该类还继承<code>AbstractNamedValueMethodArgumentResolver</code>，该类有一个方法<code>handleResolvedValue</code>,签名如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Invoked after a value is resolved.</span><br><span class="line"> * @param arg the resolved argument value</span><br><span class="line"> * @param name the argument name</span><br><span class="line"> * @param parameter the argument parameter type</span><br><span class="line"> * @param mavContainer the &#123;@link ModelAndViewContainer&#125; (may be &#123;@code null&#125;)</span><br><span class="line"> * @param webRequest the current request</span><br><span class="line"> */</span><br><span class="line">protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,</span><br><span class="line">@Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest) &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这个可以看作一个扩展点，但很无奈的是，这个只是一个void方法，Java是值传递，而且String时不可变的，所以就算重写这个方法也没办法对返回新的arg。</p><h4 id="重新定义一个RequestParamMethodArgumentResolver"><a href="#重新定义一个RequestParamMethodArgumentResolver" class="headerlink" title="重新定义一个RequestParamMethodArgumentResolver"></a>重新定义一个RequestParamMethodArgumentResolver</h4><p>要解决两个问题</p><ul><li>重新定义RequestParamMethodArgumentResolver，并且重写<code>resolveName</code>方法。</li><li>如何覆盖默认的RequestParamMethodArgumentResolver</li></ul><p>简单实现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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">public class RequestParamMethodNormalizedArgumentResolver extends RequestParamMethodArgumentResolver &#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">public RequestParamMethodNormalizedArgumentResolver(boolean useDefaultResolution) &#123;</span><br><span class="line">super(useDefaultResolution);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public RequestParamMethodNormalizedArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) &#123;</span><br><span class="line">super(beanFactory, useDefaultResolution);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@Override</span><br><span class="line">protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception &#123;</span><br><span class="line">Object arg = super.resolveName(name, parameter, request);</span><br><span class="line">// 对参数进行NFKC规范化</span><br><span class="line">if (Objects.nonNull(arg) &amp;&amp; arg instanceof String ) &#123;</span><br><span class="line">String argStr = String.valueOf(arg);</span><br><span class="line">if(StringUtils.isNotEmpty(argStr))&#123;</span><br><span class="line">arg = Normalizer.normalize(argStr, Normalizer.Form.NFKC);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">return arg;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于第二个问题，根据上面<code>getDefaultArgumentResolvers</code>方法可知，其顺序是hardcode的。只要在合适时间去set这个list就可以。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">@Configuration</span><br><span class="line">public class WebMvcConfig extends WebMvcConfigurationSupport implements BeanFactoryAware, InitializingBean &#123;</span><br><span class="line">    @Nullable</span><br><span class="line">    private ConfigurableBeanFactory beanFactory;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;</span><br><span class="line">    @Override</span><br><span class="line">    public void setBeanFactory(BeanFactory beanFactory) throws BeansException &#123;</span><br><span class="line">        if (beanFactory instanceof ConfigurableBeanFactory) &#123;</span><br><span class="line">            this.beanFactory = (ConfigurableBeanFactory) beanFactory;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public void afterPropertiesSet() &#123;</span><br><span class="line">        List&lt;HandlerMethodArgumentResolver&gt; argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();</span><br><span class="line">        List&lt;HandlerMethodArgumentResolver&gt; newArgumentResolvers = new LinkedList&lt;&gt;();</span><br><span class="line">        newArgumentResolvers.add(new RequestParamMethodNormalizedArgumentResolver(this.beanFactory,false));</span><br><span class="line">        newArgumentResolvers.addAll(argumentResolvers);</span><br><span class="line">        requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(newArgumentResolvers));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于第2个问题，我同时也让ChatGPT提供了一段代码，发现其只是用来增加自定义的Resolver，顺序依旧在默认实现后面。其实ChatGPT的话不能全信，只能信一部份，作为提供思路的辅助，其提供的代码并不一定就能直接使用。<br>到现在，对于URL的Query参数的normalization完成了。<br>当然这只是解决Query参数的场景，对于Body里面的参数，甚至Header，Path里面的参数，要考虑的话都需要去考虑。<br>实际上，笔者认为如果前端提交请求之前，做了normalization处理的话，就能从源头上避免大部份的问题。</p><h3 id="感想"><a href="#感想" class="headerlink" title="感想"></a>感想</h3><p>事后有两个感想：</p><ul><li>也许这个只是一个伪问题：因为用户的确提交的字符本身就是「错」的，只是这两个字符长得很像而已。是否有必要帮助用户去规范化字符，也许体验上会好很多，毕竟在实际使用中，很少人真的会搜寻部首。笔者的疑问是，对于一个错误的输入，是否应该返回一个对的输出？其实这个抉择在方案设计时经常会碰到，所谓的优化会不会去掩盖掉「错误」？笔者个人觉得，其实不应该返回对的输出。</li><li>ChatGPT的作用没有想象中那么大，其给出的回答可以作为参考方向，因为有可能回答部份都是错的。对于开发而言，依旧需要依托自己的经验，去不断提问题，探究问题的本质，整理思路，不断摸索，千万不能完全依托ChatGPT的回答。</li></ul><h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><ul><li><a href="https://symbl.cc/en/unicode/blocks/cjk-unified-ideographs/">symbl.cc</a></li><li><a href="http://www.mytju.com/classcode/tools/encode_utf8.asp">mytju.com</a></li><li><a href="https://xobo.org/unicode-normalization-nfd-nfc-nfkd-nfkc/">从⽅不是方到Unicode正规化NFD, NFC, NFKD, NFKC</a></li><li><a href="https://www.zhihu.com/question/480184045">中文和日文里长得一样的字在计算机中是不是同一个字？</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h3&gt;&lt;p&gt;遇到的场景是：客户从PDF粘贴一段文字到搜索框，内容是&lt;code&gt;工时规范&lt;/code&gt;四个中文字，发现搜不出结果，但是自己手动敲击文字则是
      
    
    </summary>
    
    
      <category term="Cloud Native" scheme="https://lazyallen.github.io/categories/Cloud-Native/"/>
    
    
      <category term="Helm" scheme="https://lazyallen.github.io/tags/Helm/"/>
    
  </entry>
  
  <entry>
    <title>Helm工程实践：如何维护一个Helm工程</title>
    <link href="https://lazyallen.github.io/2023/02/18/helm-practice/"/>
    <id>https://lazyallen.github.io/2023/02/18/helm-practice/</id>
    <published>2023-02-17T16:00:00.000Z</published>
    <updated>2023-02-18T15:41:58.439Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Helm简介"><a href="#Helm简介" class="headerlink" title="Helm简介"></a>Helm简介</h2><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/202302182306051.png" alt=""></p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/202302182307563.png" alt=""></p><ul><li>包管理</li><li>模板引擎:Go template</li></ul><h3 id="helm-3-渲染资源的顺序"><a href="#helm-3-渲染资源的顺序" class="headerlink" title="helm 3 渲染资源的顺序"></a>helm 3 渲染资源的顺序</h3><p>假设有个chart “A” 创建了下面的Kubernetes对象:</p><ul><li>namespace “A-Namespace” </li><li>statefulset “A-StatefulSet” </li><li>service “A-Service”</li></ul><p>另外，A是依赖于chart B创建的对象:</p><ul><li>namespace “B-Namespace” </li><li>replicaset “B-ReplicaSet” </li><li>service “B-Service”</li></ul><p>安装/升级chart A后，会创建/修改一个单独的Helm版本。这个版本会按顺序创建/升级以下所有的Kubernetes对象:</p><ul><li>A-Namespace </li><li>B-Namespace </li><li>A-Service </li><li>B-Service </li><li>B-ReplicaSet </li><li>A-StatefulSet</li></ul><p>这是因为当Helm安卓/升级chart时，chart中所有的Kubernetes对象以及依赖会</p><ul><li>聚合成一个单一的集合;然后 </li><li>按照类型和名称排序;</li><li>然后按这个顺序创建/升级。<br>至此会为chart及其依赖创建一个包含所有对象的release版本。 Kubernetes类型的安装顺序会按照kind_sorter.go(查看Helm源文件)中给出的枚举顺序进行。</li></ul><h3 id="CRD的限制"><a href="#CRD的限制" class="headerlink" title="CRD的限制"></a>CRD的限制</h3><p>不像大部分的Kubernetes对象，CRD是全局安装的。因此Helm管理CRD时会采取非常谨慎的方式。 CRD受到以下限制:</p><ul><li>CRD从不重新安装。 如果Helm确定crds/目录中的CRD已经存在(忽略版本)，Helm不会安装或升级。 </li><li>CRD从不会在升级或回滚时安装。Helm只会在安装时创建CRD。 </li><li>CRD从不会被删除。自动删除CRD会删除集群中所有命名空间中的所有CRD内容。因此Helm不会删除CRD。<br>希望升级或删除CRD的操作员应该谨慎地手动执行此操作。</li></ul><h2 id="chart-示例"><a href="#chart-示例" class="headerlink" title="chart 示例"></a>chart 示例</h2><h3 id="开发流程"><a href="#开发流程" class="headerlink" title="开发流程"></a>开发流程</h3><ul><li>kubeconfig : 用于chart 的部署测试。非必选，如没有k8s环境，后面只能使用template 输出渲染文件;</li><li>VS code 及插件:<ul><li><a href="https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools">https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools</a> </li><li><a href="https://marketplace.visualstudio.com/items?itemName=Tim-Koehler.helm-intellisense">https://marketplace.visualstudio.com/items?itemName=Tim-Koehler.helm-intellisense</a> </li><li><a href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml">https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml</a></li></ul></li><li>helm 及插件<ul><li><a href="https://helm.sh/docs/intro/install/">https://helm.sh/docs/intro/install/</a> </li><li><a href="https://github.com/norwoodj/helm-docs">https://github.com/norwoodj/helm-docs</a> </li><li><a href="https://github.com/karuppiah7890/helm-schema-gen">https://github.com/karuppiah7890/helm-schema-gen</a> </li><li><a href="https://github.com/chartmuseum/helm-push">https://github.com/chartmuseum/helm-push</a></li></ul></li></ul><h3 id="获取开发环境"><a href="#获取开发环境" class="headerlink" title="获取开发环境"></a>获取开发环境</h3><h4 id="使用Chart-Starter-包"><a href="#使用Chart-Starter-包" class="headerlink" title="使用Chart Starter 包"></a>使用Chart Starter 包</h4><p>对于相似度很高的项目，可以使用chart starter ，可以节约修改 chart name的时间;clone 仓库到本地后，加入到HELM_DATA_HOME目录下的starters目录中。之后再根据每个业务组件自身需要进行修改</p><h4 id="使用helm-create"><a href="#使用helm-create" class="headerlink" title="使用helm create"></a>使用helm create</h4><p>适合从零开始开发的chart</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">helm create mychart</span><br><span class="line">rm -rf mychart/templates/*</span><br></pre></td></tr></table></figure><p>在容器里开发</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># Kubernetesk8s config copy~/.kube/config</span><br><span class="line">$ docker image pull registry.xxx.com/xxx/helm-playground:xxxx</span><br><span class="line">$ docker run -it --rm -v ~/.kube:/root/.kube xxxx /bin/bash</span><br></pre></td></tr></table></figure><h3 id="修改templates-和-values"><a href="#修改templates-和-values" class="headerlink" title="修改templates 和 values"></a>修改templates 和 values</h3><p>修改需要部署的资源文件和values，保证一个应用的最小可部署资源集合;</p><p>开发建议   </p><ul><li>命名规范:template文件用小写字母;values中的变量使用驼峰;<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">$ tree ./ ./</span><br><span class="line"> Chart.yaml</span><br><span class="line"> README.md</span><br><span class="line"> README.md.gotmpl</span><br><span class="line"> templates</span><br><span class="line">    NOTES.txt</span><br><span class="line">    _helpers.tpl</span><br><span class="line">    bootstrap-configmap.yaml</span><br><span class="line">    clusterrole.yaml</span><br><span class="line">    clusterrolebinding.yaml</span><br><span class="line">    deployment.yaml</span><br><span class="line">    init-job.yaml</span><br><span class="line">    persistentvolume.yaml</span><br><span class="line">    persistentvolumeclaim.yaml</span><br><span class="line">    service-monitor.yaml</span><br><span class="line">    service.yaml</span><br><span class="line">    serviceaccount.yaml</span><br><span class="line">    storageclass.yaml</span><br><span class="line"> values.schema.json</span><br><span class="line"> values.yaml</span><br><span class="line">1 directory, 18 files</span><br></pre></td></tr></table></figure>不要hardcode namespace，而是通过 – namespace flag 动态传入，后者灵活性更强。</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># suggest</span><br><span class="line">helm install business-defender ./ -n whale-component --debug --atomic</span><br><span class="line"># not suggest</span><br><span class="line">metadata:</span><br><span class="line">  namespace: &#123;&#123; .Release.Namespace &#125;&#125;</span><br></pre></td></tr></table></figure><p><a href="https://github.com/helm/helm/issues/5465">https://github.com/helm/helm/issues/5465</a></p><ul><li>_helpers.tpl 可类比成 Utils 类:比较通用，复用比较多 的逻辑 比较复杂的逻辑 </li></ul><p>在_helpers.tpl 定义Named Templates 时，统一以chart.name 开头。相当于把chart.name 当作name templates 的命名空间，避免引用冲突;</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># good</span><br><span class="line">&#123;&#123;/* Create a default fully qualified app name. */&#125;&#125;</span><br><span class="line">&#123;&#123;- define &quot;business-defender.fullname&quot; -&#125;&#125;</span><br><span class="line">...</span><br><span class="line">&#123;&#123;- end &#125;&#125;</span><br><span class="line"># bad</span><br><span class="line">&#123;&#123;/* Create a default fully qualified app name. */&#125;&#125;</span><br><span class="line">&#123;&#123;- define &quot;fullname&quot; -&#125;&#125;</span><br><span class="line">...</span><br><span class="line">&#123;&#123;- end &#125;&#125;</span><br></pre></td></tr></table></figure><ul><li>给values写注释，并用helm-docs生成文档，用helm schema-gen 生成schema.json文件<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">global:</span><br><span class="line">  # -- machine architecture</span><br><span class="line">  hostArch: &quot;amd64&quot;</span><br><span class="line">  # -- docker registry</span><br><span class="line">  dockerRegistry: &quot;registry.xxxx.com&quot;</span><br><span class="line">  # -- prometheus namespace</span><br><span class="line">  monitorNamespace: &quot;monitoring&quot;</span><br></pre></td></tr></table></figure></li></ul><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>语法验证</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ helm lint</span><br><span class="line">==&gt; Linting .</span><br><span class="line">[INFO] Chart.yaml: icon is recommended</span><br><span class="line">1 chart(s) linted, 0 chart(s) failed</span><br></pre></td></tr></table></figure><p>渲染验证</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">helm template ./ --debug -n whale-component</span><br><span class="line">helm install business-defender ./ -n whale-component --debug --dry-run</span><br></pre></td></tr></table></figure><p>template 和 install –dry-run 的区别在于是否会和 Kubernetes Api Server 通行。使用 install –dry-run ,相当于helm 渲染资源后使用kubectl 的–dry-run 验证一样;因为可以和k8s通信，可以获取一些k8s的元数据，此时，.Capabilities 内置对象才有意义;</p><p>部署验证<br>验证时，需要在 defender 环境下验证，因为chart中可能包含一些预定的资源依赖，比如 password-secret;</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># atomic flag</span><br><span class="line">helm install business-defender ./ -n whale-component --debug --atomic</span><br><span class="line"></span><br><span class="line"># wait delete</span><br><span class="line">helm uninstall business-defender -n whale-component --wait --debug</span><br><span class="line"></span><br><span class="line">$ helm list -n whale-component</span><br><span class="line"></span><br><span class="line"># Release</span><br><span class="line">$ helm get all business-defender -n whale-component</span><br></pre></td></tr></table></figure><h3 id="pre-commit-生成readme-md-和-values-schema-json"><a href="#pre-commit-生成readme-md-和-values-schema-json" class="headerlink" title="pre-commit:生成readme.md 和 values.schema.json"></a>pre-commit:生成readme.md 和 values.schema.json</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># helm plugin schema-genvaluesschema.jsonhelm installvalues.yaml.</span><br><span class="line">helm schema-gen values.yaml &gt; values.schema.json</span><br><span class="line"># valuesreadme.md</span><br><span class="line">helm-docs</span><br></pre></td></tr></table></figure><h3 id="发布"><a href="#发布" class="headerlink" title="发布"></a>发布</h3><p>手动发布<br>需要手动安装 cm-push :<a href="https://github.com/chartmuseum/helm-push">https://github.com/chartmuseum/helm-push</a></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ helm plugin install https://github.com/chartmuseum/helm-push</span><br><span class="line">$ helm repo add --username &lt;username&gt; --password &lt;password&gt;  business https://registry.xxx.com/chartrepo</span><br><span class="line">/business</span><br><span class="line">$ helm cm-push --username=&lt;username&gt;  --password=&lt;password&gt;  . business --version 2.2.1l</span><br></pre></td></tr></table></figure><p>另外也可以通过页面上传</p><p>CI发布  </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">image:</span><br><span class="line">  name: registry.xxx.com/viper-ce/library/helm-tool:3.6.3</span><br><span class="line">  entrypoint: [&quot;/bin/sh&quot;, &quot;-c&quot;]</span><br><span class="line">variables:</span><br><span class="line">  HELM_CI_REPO_ADDR: https://registry.xxx.com/chartrepo/business</span><br><span class="line">  HELM_CI_REPO_NAME: business</span><br><span class="line">stages:</span><br><span class="line">  - lint</span><br><span class="line">  - render</span><br><span class="line">  - release</span><br><span class="line">lint-chart:</span><br><span class="line">  stage: lint</span><br><span class="line">  script:</span><br><span class="line">- helm lint .</span><br><span class="line">render-chart:</span><br><span class="line">  stage: render</span><br><span class="line">  script:</span><br><span class="line">    - helm template .</span><br><span class="line">release-chart:</span><br><span class="line">  stage: release</span><br><span class="line">  script:</span><br><span class="line">    - export CHART_VERSION=$(awk &#x27;/^version/&#123;print $NF&#125;&#x27; Chart.yaml)</span><br><span class="line">    - helm repo add --username $&#123;HELM_REPO_USERNAME&#125; --password $&#123;HELM_REPO_PASSWD&#125; $&#123;HELM_CI_REPO_NAME&#125;</span><br><span class="line">$&#123;HELM_CI_REPO_ADDR&#125;</span><br><span class="line">    - helm push . $&#123;HELM_CI_REPO_NAME&#125; --version &quot;$&#123;CHART_VERSION&#125;-$&#123;CI_COMMIT_SHA:0:8&#125;&quot;</span><br><span class="line">    - helm push . $&#123;HELM_CI_REPO_NAME&#125; --version &quot;$&#123;CHART_VERSION&#125;&quot; --force</span><br><span class="line">  only:</span><br><span class="line">- master</span><br></pre></td></tr></table></figure><p>合入master时才发布一个版本;<br>每次release 会push两次:</p><ul><li>带 commit id 的版本</li><li>不带 commid id 的stable版本，强制覆盖仓库中的stable版本</li></ul><h2 id="repo管理"><a href="#repo管理" class="headerlink" title="repo管理"></a>repo管理</h2><p>仓库选择:</p><ul><li>harbor chart repo :<a href="https://registry.xxx.com/chartrepo/business">https://registry.xxx.com/chartrepo/business</a></li><li>infra团队自建并维护的chartmuseum:<a href="http://sz.xxx.xxx.net/chartrepo/sensefoundry-stable/">http://sz.xxx.xxx.net/chartrepo/sensefoundry-stable/</a></li></ul><p>是否需要单独创建一个project存储chartrepo，业务chart 和 中间件chart 是否需要区分?</p><h2 id="版本管理"><a href="#版本管理" class="headerlink" title="版本管理"></a>版本管理</h2><p>版本约定<br>helm chart 的版本管理主要是通过 Chart.yaml里面的appVersion和version去管理  </p><ul><li>appVersion:主要对应 应用 本身的版本<br>比如 business-defender 的版本是1.0.2-SNAPSHOT</li><li>version:主要对应 helm chart 版本，helm package 通过读取 Chart.yaml 里的 version进行 chart 打包，然后推送到仓库</li></ul><p>如 当前业务系统的版本为 2.2.0 ，helm chart的commit_id 6336fbe，此时version:2.2.0-6336fbe</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">version: 2.2.0</span><br><span class="line">appVersion: &quot;1.0.2-SNAPSHOT&quot;</span><br></pre></td></tr></table></figure><p>仓库中存在:</p><ul><li>以version/chart version/ 业务系统版本 的stable版本</li><li>带有commit id 的开发版本</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Helm简介&quot;&gt;&lt;a href=&quot;#Helm简介&quot; class=&quot;headerlink&quot; title=&quot;Helm简介&quot;&gt;&lt;/a&gt;Helm简介&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://lazyallen-blog-1251933423.cos.ap-gua
      
    
    </summary>
    
    
      <category term="Cloud Native" scheme="https://lazyallen.github.io/categories/Cloud-Native/"/>
    
    
      <category term="Helm" scheme="https://lazyallen.github.io/tags/Helm/"/>
    
  </entry>
  
  <entry>
    <title>浅析Dubbo的事件驱动机制</title>
    <link href="https://lazyallen.github.io/2020/11/07/dubbo-event-driven-introduction/"/>
    <id>https://lazyallen.github.io/2020/11/07/dubbo-event-driven-introduction/</id>
    <published>2020-11-06T16:00:00.000Z</published>
    <updated>2022-12-07T07:03:32.627Z</updated>
    
    <content type="html"><![CDATA[<p>本文介绍的是Dubbo 2.7.5中的事件驱动机制，简要分析了其原理，分析了引入事件驱动的原因。</p><span id="more"></span><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><blockquote><p>事件驱动程序设计（英语：Event-driven programming）是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作（如鼠标的按键，键盘的按键动作）或者是由其他程序的消息来决定的。事件驱动程序设计这种设计模型是在交互程序（Interactive program）的情况下孕育而生的。</p></blockquote><p>从这段维基百科的引用可知，事件驱动一开始是由于交互程序中的行为而产生的。这种设计也符合现实中的行为，生活当中经常需要应对一件事情从而行为做出改变。比如听到了闹钟声就要起床，闹钟声就是一个事件，而起床则是人们对于这件事做出的动作。<br>我们可以将闹钟起床抽象拆分为两个部分：</p><ul><li>事件：具体的事情；</li><li>事件触发时做出的反应；</li></ul><h2 id="Java对Event的支持"><a href="#Java对Event的支持" class="headerlink" title="Java对Event的支持"></a>Java对Event的支持</h2><p>在JDK1.1版本中的<code>java.util</code> 包中有两个类：EventObject 和 EventListener，所对应的概念就是上面提到的两个部分。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> java.util;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * &lt;p&gt;</span></span><br><span class="line"><span class="comment"> * The root class from which all event state objects shall be derived.</span></span><br><span class="line"><span class="comment"> * &lt;p&gt;</span></span><br><span class="line"><span class="comment"> * All Events are constructed with a reference to the object, the &quot;source&quot;,</span></span><br><span class="line"><span class="comment"> * that is logically deemed to be the object upon which the Event in question</span></span><br><span class="line"><span class="comment"> * initially occurred upon.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> JDK1.1</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EventObject</span> <span class="keyword">implements</span> <span class="title class_">java</span>.io.Serializable &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> <span class="number">5516075349620653480L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * The object on which the Event initially occurred.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">transient</span> Object  source;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Constructs a prototypical Event.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span>    source    The object on which the Event initially occurred.</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@exception</span>  IllegalArgumentException  if source is null.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">EventObject</span><span class="params">(Object source)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (source == <span class="literal">null</span>)</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;null source&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.source = source;</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">     * The object on which the Event initially occurred.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span>   The object on which the Event initially occurred.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">getSource</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> source;</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">     * Returns a String representation of this EventObject.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span>  A a String representation of this EventObject.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">toString</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> getClass().getName() + <span class="string">&quot;[source=&quot;</span> + source + <span class="string">&quot;]&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>EventObject中持有一个source的Object对象，同时可以通过getSource() 方法获取到source，所以source就是真实的事件。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> java.util;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * A tagging interface that all event listener interfaces must extend.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> JDK1.1</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">EventListener</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而EventListener 是一个没有方法的Interface，具体的动作行为我们可以通过继承的方式去定义。<br>如前言中说的，事件驱动是由交互应用所产生，以上两个类在Java GUI编程中随处可见，在<code>java.awt.event</code>中封装了一系列的Event和EventLister，用于描述诸如鼠标，输入框、窗口等GUI事件。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/07/28/15958402192846.jpg" alt="-w350"></p><h2 id="Dubbo中-事件发布的实现-Java-Event："><a href="#Dubbo中-事件发布的实现-Java-Event：" class="headerlink" title="Dubbo中 事件发布的实现 Java Event："></a>Dubbo中 事件发布的实现 Java Event：</h2><p>在Dubbo 2.7.5的<a href="https://github.com/apache/dubbo/issues/4094">Release版本</a>中，添加了事件驱动机制。在org.apache.dubbo.event中，主要看3个类：Event、EventListener、EventDispatcher，其中EventDispatcher可译作事件分发器，其用于分发和执行相应的Event。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.apache.dubbo.event;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.dubbo.common.extension.ExtensionLoader;</span><br><span class="line"><span class="keyword">import</span> org.apache.dubbo.common.extension.SPI;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.Executor;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * &#123;<span class="doctag">@link</span> Event Dubbo Event&#125; Dispatcher</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> Event</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> EventListener</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> DirectEventDispatcher</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2.7.5</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@SPI(&quot;direct&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">EventDispatcher</span> <span class="keyword">extends</span> <span class="title class_">Listenable</span>&lt;EventListener&lt;?&gt;&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Direct &#123;<span class="doctag">@link</span> Executor&#125; uses sequential execution model</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="type">Executor</span> <span class="variable">DIRECT_EXECUTOR</span> <span class="operator">=</span> Runnable::run;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Dispatch a Dubbo event to the registered &#123;<span class="doctag">@link</span> EventListener Dubbo event listeners&#125;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> event a &#123;<span class="doctag">@link</span> Event Dubbo event&#125;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">dispatch</span><span class="params">(Event event)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * The &#123;<span class="doctag">@link</span> Executor&#125; to dispatch a &#123;<span class="doctag">@link</span> Event Dubbo event&#125;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> default implementation directly invoke &#123;<span class="doctag">@link</span> Runnable#run()&#125; method, rather than multiple-threaded</span></span><br><span class="line"><span class="comment">     * &#123;<span class="doctag">@link</span> Executor&#125;. If the return value is &lt;code&gt;null&lt;/code&gt;, the behavior is same as default.</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@see</span> #DIRECT_EXECUTOR</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">default</span> Executor <span class="title function_">getExecutor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> DIRECT_EXECUTOR;</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">     * The default extension of &#123;<span class="doctag">@link</span> EventDispatcher&#125; is loaded by &#123;<span class="doctag">@link</span> ExtensionLoader&#125;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> the default extension of &#123;<span class="doctag">@link</span> EventDispatcher&#125;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">static</span> EventDispatcher <span class="title function_">getDefaultExtension</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ExtensionLoader.getExtensionLoader(EventDispatcher.class).getDefaultExtension();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>从接口上的注解@SPI(“direct”)可知，EventDispatcher是一个SPI接口，且默认实现对应为DirectEventDispatcher。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.apache.dubbo.event;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Direct &#123;<span class="doctag">@link</span> EventDispatcher&#125; implementation uses current thread execution model</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> EventDispatcher</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2.7.5</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">DirectEventDispatcher</span> <span class="keyword">extends</span> <span class="title class_">AbstractEventDispatcher</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">DirectEventDispatcher</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(DIRECT_EXECUTOR);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>大部分逻辑在AbstractEventDispatcher中实现，我们具体看AbstractEventDispatcher的内容。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractEventDispatcher</span> <span class="keyword">implements</span> <span class="title class_">EventDispatcher</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">Object</span> <span class="variable">mutex</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ConcurrentMap&lt;Class&lt;? <span class="keyword">extends</span> <span class="title class_">Event</span>&gt;, List&lt;EventListener&gt;&gt; listenersCache = <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Executor executor;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addEventListener</span><span class="params">(EventListener&lt;?&gt; listener)</span> <span class="keyword">throws</span> NullPointerException, IllegalArgumentException &#123;</span><br><span class="line">        Listenable.assertListener(listener);</span><br><span class="line">        doInListener(listener, listeners -&gt; &#123;</span><br><span class="line">            addIfAbsent(listeners, listener);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">dispatch</span><span class="params">(Event event)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="type">Executor</span> <span class="variable">executor</span> <span class="operator">=</span> getExecutor();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// execute in sequential or parallel execution model</span></span><br><span class="line">        executor.execute(() -&gt; &#123;</span><br><span class="line">            sortedListeners(entry -&gt; entry.getKey().isAssignableFrom(event.getClass()))</span><br><span class="line">                    .forEach(listener -&gt; &#123;</span><br><span class="line">                        <span class="keyword">if</span> (listener <span class="keyword">instanceof</span> ConditionalEventListener) &#123;</span><br><span class="line">                            <span class="type">ConditionalEventListener</span> <span class="variable">predicateEventListener</span> <span class="operator">=</span> (ConditionalEventListener) listener;</span><br><span class="line">                            <span class="keyword">if</span> (!predicateEventListener.accept(event)) &#123; <span class="comment">// No accept</span></span><br><span class="line">                                <span class="keyword">return</span>;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                        <span class="comment">// Handle the event</span></span><br><span class="line">                        listener.onEvent(event);</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="comment">/**</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> the non-null &#123;<span class="doctag">@link</span> Executor&#125;</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="keyword">public</span> <span class="keyword">final</span> Executor <span class="title function_">getExecutor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> executor;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从关键代码中可以看出，AbstractEventDispatcher中包含3个成员变量，其中listenersCache维护的是listeners列表，executor表示的是用户执行listener逻辑的线程执行器，默认使用当前线程。dispatch()中通过Event的类型从listenersCache找到对应listeners列表后，逐一执行listener.onEvent(event),onEvent()所定义的则是listener中的实际业务逻辑。</p><p>补充一下，Dubbo的Event和EventListener继承的是JDK的EventObject和EventListener，也就是说Dubbo的事件驱动基于JDK的事件驱动机制。</p><h2 id="为什么Dubbo发布到2-7-5版本才添加了事件驱动机制"><a href="#为什么Dubbo发布到2-7-5版本才添加了事件驱动机制" class="headerlink" title="为什么Dubbo发布到2.7.5版本才添加了事件驱动机制"></a>为什么Dubbo发布到2.7.5版本才添加了事件驱动机制</h2><p>在2.7.5版本前，Dubbo支持<a href="http://dubbo.apache.org/zh-cn/docs/user/demos/events-notify.html">事件通知</a>特性。对于特定服务，在调用之前、调用之后、出现异常时，会触发 oninvoke、onreturn、onthrow 三个事件，可以配置当事件发生时，通知哪个类的哪个方法.与其叫做事件通知，更像是一种AOP机制，在服务调用生命周期中切入业务逻辑。</p><p>之所以Dubbo在2.7.5才添加了事件驱动机制，是因为在这个版本，Dubbo同时加入服务自省架构。关于服务自省架构可以阅读<a href="https://developer.aliyun.com/article/763877">Dubbo 迈出云原生重要一步 - 应用级服务发现解析</a>解释的非常详细。在这里简单解释，通俗来讲，在Dubbo里面我们所讲的服务指的RPC Service，粒度是接口级别的。倘若后面再接触Spring Cloud体系的微服务，则发现其中服务指的是一个应用，粒度是应用级别的（之前我就很好奇为什么Spring Cloud为什么不维护接口的信息呢）。</p><ul><li>对于Consumer而言，其更应该关注的是Provider实例的IP+PORT列表，而Provider所发布的接口信息并非是必选项。</li><li>粒度为接口级别的Dubbo服务会带来更多的数据冗余</li></ul><p>为此，Dubbo决定把自己的微服务发现模型也向Spring Cloud去拉齐，也就是说之前在注册中心维护的数据结构从RPC Service -&gt; Instance 变为 Application -&gt; Instance。<br>举个例子：</p><p>RPC Service -&gt; Instance：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dubbo://10.74.139.59:20881/com.dubbo.spi.demo.api.IHelloService?anyhost=true&amp;application=demo-provider&amp;dubbo=2.6.0&amp;generic=false&amp;interface=com.dubbo.spi.demo.api.IHelloService&amp;methods=hello&amp;pid=40849&amp;side=provider&amp;timestamp=1594650881680</span><br></pre></td></tr></table></figure><p>Application -&gt; Instance</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;name&quot;:&quot;spring-cloud-alibaba-dubbo-provider&quot;,&quot;id&quot;:&quot;9c5c1bce-13de-4023-b2d8-fde037ae6041&quot;,&quot;address&quot;:&quot;10.74.139.119&quot;,&quot;port&quot;:9090,&quot;sslPort&quot;:null,&quot;payload&quot;:&#123;&quot;@class&quot;:&quot;org.springframework.cloud.zookeeper.discovery.ZookeeperInstance&quot;,&quot;id&quot;:&quot;spring-cloud-alibaba-dubbo-provider-1&quot;,&quot;name&quot;:&quot;spring-cloud-alibaba-dubbo-provider&quot;,&quot;metadata&quot;:&#123;&quot;dubbo.metadata-service.urls&quot;:&quot;[ \&quot;dubbo://127.0.0.1:20880/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&amp;application=spring-cloud-alibaba-dubbo-provider&amp;bind.ip=127.0.0.1&amp;bind.port=20880&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=spring-cloud-alibaba-dubbo-provider&amp;interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&amp;methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&amp;pid=15432&amp;qos.enable=false&amp;release=2.7.6&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1594977679715&amp;version=1.0.0\&quot; ]&quot;,&quot;dubbo.protocols.dubbo.port&quot;:&quot;20880&quot;,&quot;dubbo.protocols.rest.port&quot;:&quot;9090&quot;&#125;&#125;,&quot;registrationTimeUTC&quot;:1594977677964,&quot;serviceType&quot;:&quot;DYNAMIC&quot;,&quot;uriSpec&quot;:&#123;&quot;parts&quot;:[&#123;&quot;value&quot;:&quot;scheme&quot;,&quot;variable&quot;:true&#125;,&#123;&quot;value&quot;:&quot;://&quot;,&quot;variable&quot;:false&#125;,&#123;&quot;value&quot;:&quot;address&quot;,&quot;variable&quot;:true&#125;,&#123;&quot;value&quot;:&quot;:&quot;,&quot;variable&quot;:false&#125;,&#123;&quot;value&quot;:&quot;port&quot;,&quot;variable&quot;:true&#125;]&#125;&#125;</span><br></pre></td></tr></table></figure><p>看起来数据结构更大了，但由于服务粒度不同，总体来说需要维护的数据变少了。<br>怎么兼容旧版本的设计？新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移，即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理，做到对用户侧完全无感知。<br>为此，Dubbo新增了一个MetaService的服务，服务端实例会暴露一个预定义的 MetadataService RPC 服务，消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/07/28/15958654702230.jpg" alt="-w739"><br>服务的提供者需要维护一个元数据服务（MetadataService）.<br>服务自省架构流程更加复杂，执行动作之间的关联非常紧密。</p><blockquote><p>相较于传统的 Dubbo 架构，服务自省架构的执行流程更为复杂，执行动作之间的关联非常紧密，如 Dubbo Service 服务实例注册前需要完成 Dubbo 服务 revision 的计算，并将其添加至服务实例的 metadata 中。又如当 Dubbo Service 服务实例出现变化时，Consumer 元数据需要重新计算。这些动作被 “事件”（Event）驱动，驱动者被定义为“事件分发器”（ EventDispatcher ），而动作的处理则由“事件监听器”（EventListener）执行，三者均为 “Dubbo 事件“的核心组件</p></blockquote><h2 id="Dubbo-中的事件分类"><a href="#Dubbo-中的事件分类" class="headerlink" title="Dubbo 中的事件分类"></a>Dubbo 中的事件分类</h2><p>Dubbo 提供的事件类型如下：</p><ul><li>Dubbo进程级别：提供一些进程的勾子事件，如：DubboShutdownHookRegisteredEvent、DubboShutdownHookUnregisteredEvent、DubboServiceDestroyedEvent；</li><li>和服务发现组件生命周期有关：ServiceDiscoveryInitializingEvent、ServiceDiscoveryInitializedEvent、ServiceDiscoveryExceptionEvent、ServiceDiscoveryDestroyingEvent、ServiceDiscoveryDestroyedEvent；</li><li>和服务暴露和引用的生命周期有关：ServiceConfigExportedEvent、ServiceConfigUnexportedEvent、ReferenceConfigInitializedEvent、ReferenceConfigDestroyedEvent</li><li>和服务实例注册和注销等有关：ServiceInstancePreRegisteredEvent、ServiceInstanceRegisteredEvent、ServiceInstancePreUnregisteredEvent、ServiceInstanceUnregisteredEvent、ServiceInstancesChangedEvent；</li></ul><p>在Dubbo源码中有一个LoggingEventListenerTest测试类可以很好的演示。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testOnEvent</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"></span><br><span class="line">        <span class="type">URL</span> <span class="variable">connectionURL</span> <span class="operator">=</span> URL.valueOf(<span class="string">&quot;file:///Users/Home&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="type">ServiceDiscovery</span> <span class="variable">serviceDiscovery</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileSystemServiceDiscovery</span>();</span><br><span class="line"></span><br><span class="line">        serviceDiscovery.initialize(connectionURL);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceDiscoveryStartingEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceDiscoveryInitializingEvent</span>(serviceDiscovery, serviceDiscovery));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceDiscoveryStartedEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceDiscoveryInitializedEvent</span>(serviceDiscovery, serviceDiscovery));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceInstancePreRegisteredEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceInstancePreRegisteredEvent</span>(serviceDiscovery, createInstance()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceInstanceRegisteredEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceInstanceRegisteredEvent</span>(serviceDiscovery, createInstance()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceInstancesChangedEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceInstancesChangedEvent</span>(<span class="string">&quot;test&quot;</span>, singleton(createInstance())));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceInstancePreUnregisteredEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceInstancePreUnregisteredEvent</span>(serviceDiscovery, createInstance()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceInstanceUnregisteredEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceInstanceUnregisteredEvent</span>(serviceDiscovery, createInstance()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceDiscoveryStoppingEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceDiscoveryDestroyingEvent</span>(serviceDiscovery, serviceDiscovery));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ServiceDiscoveryStoppedEvent</span></span><br><span class="line">        listener.onEvent(<span class="keyword">new</span> <span class="title class_">ServiceDiscoveryDestroyedEvent</span>(serviceDiscovery, serviceDiscovery));</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>LoggingEventListenerTest模拟了服务发现组件从初始化到服务注册、注销、到组件注销的整个生命周期，测试结果如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[28/07/20 00:13:08:661 CST]  INFO logger.LoggerFactory: using logger: org.apache.dubbo.common.logger.log4j.Log4jLoggerAdapter</span><br><span class="line">[28/07/20 00:13:08:773 CST]  INFO listener.LoggingEventListener:  [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is initializing..., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:773 CST]  INFO listener.LoggingEventListener:  [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is initialized., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:776 CST]  INFO listener.LoggingEventListener:  [DUBBO] DefaultServiceInstance&#123;id=&#x27;45903280824015&#x27;, serviceName=&#x27;A&#x27;, host=&#x27;127.0.0.1&#x27;, port=8080, enabled=true, healthy=true, metadata=&#123;dubbo.metadata-service.url-params=&#123;&quot;dubbo&quot;:&#123;&quot;application&quot;:&quot;dubbo-provider-demo&quot;,&quot;deprecated&quot;:&quot;false&quot;,&quot;group&quot;:&quot;dubbo-provider-demo&quot;,&quot;version&quot;:&quot;1.0.0&quot;,&quot;timestamp&quot;:&quot;1564845042651&quot;,&quot;dubbo&quot;:&quot;2.0.2&quot;,&quot;provider.host&quot;:&quot;192.168.0.102&quot;,&quot;provider.port&quot;:&quot;20880&quot;&#125;&#125;, dubbo.metadata-service.urls=[ &quot;dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&amp;application=spring-cloud-alibaba-dubbo-provider&amp;bind.ip=192.168.0.102&amp;bind.port=20881&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=spring-cloud-alibaba-dubbo-provider&amp;interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&amp;methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&amp;pid=17134&amp;qos.enable=false&amp;register=true&amp;release=2.7.3&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1564826098503&amp;version=1.0.0&quot; ]&#125;&#125; is registering into org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18..., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:777 CST]  INFO listener.LoggingEventListener:  [DUBBO] DefaultServiceInstance&#123;id=&#x27;45903281361282&#x27;, serviceName=&#x27;A&#x27;, host=&#x27;127.0.0.1&#x27;, port=8080, enabled=true, healthy=true, metadata=&#123;dubbo.metadata-service.url-params=&#123;&quot;dubbo&quot;:&#123;&quot;application&quot;:&quot;dubbo-provider-demo&quot;,&quot;deprecated&quot;:&quot;false&quot;,&quot;group&quot;:&quot;dubbo-provider-demo&quot;,&quot;version&quot;:&quot;1.0.0&quot;,&quot;timestamp&quot;:&quot;1564845042651&quot;,&quot;dubbo&quot;:&quot;2.0.2&quot;,&quot;provider.host&quot;:&quot;192.168.0.102&quot;,&quot;provider.port&quot;:&quot;20880&quot;&#125;&#125;, dubbo.metadata-service.urls=[ &quot;dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&amp;application=spring-cloud-alibaba-dubbo-provider&amp;bind.ip=192.168.0.102&amp;bind.port=20881&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=spring-cloud-alibaba-dubbo-provider&amp;interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&amp;methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&amp;pid=17134&amp;qos.enable=false&amp;register=true&amp;release=2.7.3&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1564826098503&amp;version=1.0.0&quot; ]&#125;&#125; has been registered into org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:777 CST]  INFO listener.LoggingEventListener:  [DUBBO] The services&#x27;[name : test] instances[size : 1] has been changed., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:778 CST]  INFO listener.LoggingEventListener:  [DUBBO] DefaultServiceInstance&#123;id=&#x27;45903282570448&#x27;, serviceName=&#x27;A&#x27;, host=&#x27;127.0.0.1&#x27;, port=8080, enabled=true, healthy=true, metadata=&#123;dubbo.metadata-service.url-params=&#123;&quot;dubbo&quot;:&#123;&quot;application&quot;:&quot;dubbo-provider-demo&quot;,&quot;deprecated&quot;:&quot;false&quot;,&quot;group&quot;:&quot;dubbo-provider-demo&quot;,&quot;version&quot;:&quot;1.0.0&quot;,&quot;timestamp&quot;:&quot;1564845042651&quot;,&quot;dubbo&quot;:&quot;2.0.2&quot;,&quot;provider.host&quot;:&quot;192.168.0.102&quot;,&quot;provider.port&quot;:&quot;20880&quot;&#125;&#125;, dubbo.metadata-service.urls=[ &quot;dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&amp;application=spring-cloud-alibaba-dubbo-provider&amp;bind.ip=192.168.0.102&amp;bind.port=20881&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=spring-cloud-alibaba-dubbo-provider&amp;interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&amp;methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&amp;pid=17134&amp;qos.enable=false&amp;register=true&amp;release=2.7.3&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1564826098503&amp;version=1.0.0&quot; ]&#125;&#125; is registering from org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18..., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:778 CST]  INFO listener.LoggingEventListener:  [DUBBO] DefaultServiceInstance&#123;id=&#x27;45903282953719&#x27;, serviceName=&#x27;A&#x27;, host=&#x27;127.0.0.1&#x27;, port=8080, enabled=true, healthy=true, metadata=&#123;dubbo.metadata-service.url-params=&#123;&quot;dubbo&quot;:&#123;&quot;application&quot;:&quot;dubbo-provider-demo&quot;,&quot;deprecated&quot;:&quot;false&quot;,&quot;group&quot;:&quot;dubbo-provider-demo&quot;,&quot;version&quot;:&quot;1.0.0&quot;,&quot;timestamp&quot;:&quot;1564845042651&quot;,&quot;dubbo&quot;:&quot;2.0.2&quot;,&quot;provider.host&quot;:&quot;192.168.0.102&quot;,&quot;provider.port&quot;:&quot;20880&quot;&#125;&#125;, dubbo.metadata-service.urls=[ &quot;dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&amp;application=spring-cloud-alibaba-dubbo-provider&amp;bind.ip=192.168.0.102&amp;bind.port=20881&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=spring-cloud-alibaba-dubbo-provider&amp;interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&amp;methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&amp;pid=17134&amp;qos.enable=false&amp;register=true&amp;release=2.7.3&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1564826098503&amp;version=1.0.0&quot; ]&#125;&#125; has been unregistered from org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:778 CST]  INFO listener.LoggingEventListener:  [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is stopping..., dubbo version: , current host: 192.168.1.110</span><br><span class="line">[28/07/20 00:13:08:778 CST]  INFO listener.LoggingEventListener:  [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is stopped., dubbo version: , current host: 192.168.1.110</span><br></pre></td></tr></table></figure><h2 id="Dubbo-Spring-Cloud-的事件驱动机制"><a href="#Dubbo-Spring-Cloud-的事件驱动机制" class="headerlink" title="Dubbo Spring Cloud 的事件驱动机制"></a>Dubbo Spring Cloud 的事件驱动机制</h2><p>在Spring Cloud Alibaba中也有类似的事件驱动设计 区别于Dubbo的事件驱动基于语言级别的Event抽象，Spring Cloud Alibaba则是基于Spring ApplicationContext 框架级别的事件发布机制。<br>具体实现可自行阅读源码，在com.alibaba.cloud.dubbo.registry.event下有四个类：</p><ul><li>ServiceInstancePreRegisteredEvent：服务预注册事件；</li><li>ServiceInstanceRegisteredEvent：服务注册完毕事件；</li><li>ServiceInstancesChangedEvent：服务实例变更事件；</li><li>SubscribedServicesChangedEvent：所订阅服务变更事件；</li></ul><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><ul><li><a href="https://zh.wikipedia.org/wiki/%E4%BA%8B%E4%BB%B6%E9%A9%85%E5%8B%95%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88">事件驱动程序设计</a></li><li><a href="https://developer.aliyun.com/article/763877">Dubbo 迈出云原生重要一步 - 应用级服务发现解析</a></li><li><a href="https://mercyblitz.github.io/2020/05/11/Apache-Dubbo-%E6%9C%8D%E5%8A%A1%E8%87%AA%E7%9C%81%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/">Apache-Dubbo-服务自省架构设计</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文介绍的是Dubbo 2.7.5中的事件驱动机制，简要分析了其原理，分析了引入事件驱动的原因。&lt;/p&gt;
    
    </summary>
    
    
      <category term="RPC" scheme="https://lazyallen.github.io/categories/RPC/"/>
    
    
      <category term="Dubbo" scheme="https://lazyallen.github.io/tags/Dubbo/"/>
    
  </entry>
  
  <entry>
    <title>解惑：对于SPI的一些理解</title>
    <link href="https://lazyallen.github.io/2020/10/02/java-spi-think/"/>
    <id>https://lazyallen.github.io/2020/10/02/java-spi-think/</id>
    <published>2020-10-01T16:00:00.000Z</published>
    <updated>2022-12-07T07:04:01.086Z</updated>
    
    <content type="html"><![CDATA[<p>很早就了解过<code>SPI</code>的概念，刚开始知道是看JDBC驱动实现的时候发现有用到，后面陆陆续续发现在<code>Dubbo</code>中也有<code>Dubbo SPI</code>的概念，希望可以把自己的理解和困惑记录下来。</p><span id="more"></span><h2 id="SPI解决了什么问题？"><a href="#SPI解决了什么问题？" class="headerlink" title="SPI解决了什么问题？"></a>SPI解决了什么问题？</h2><p>一种技术的产生必然有其产生的原因，如果现有的技术可以满足解决，就不会出现一种新的解决方案。简单来说，<code>Java SPI</code>实际上是<code>“基于接口的编程＋策略模式＋配置文件”</code>组合实现的动态加载机制。简单理解，既然策略模式可以解决，为什么还要<code>SPI</code>呢？这里举一个例子。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">Pay</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AliPay</span> <span class="keyword">implements</span> <span class="title class_">pay</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">WeChatPay</span> <span class="keyword">implements</span> <span class="title class_">Pay</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Bussiness</span>&#123;</span><br><span class="line">    <span class="comment">//使用支付宝</span></span><br><span class="line">    <span class="type">Pay</span> <span class="variable">pay</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AliPay</span>();</span><br><span class="line">    <span class="comment">//老板跟阿里吵架了，该用微信支付了</span></span><br><span class="line">    <span class="type">Pay</span> <span class="variable">pay</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">WeChat</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于支付方式来说，目前市场上有2种主流的支付方式。公司目前使用的是支付宝，突然有一天，老板接受了一篇专访，结果马爸爸骂老板是“三姓家奴”，老板一怒之下把支付宝给下了，改用微信支付。这时，对于开发者来说的改动看起来简单只是new 一个新的实现而已。<br>直接改动原来的代码不是说不可以，只是违背了<code>开闭原则</code>的设计原则，对扩展开发，对修改关闭。换一个角度，你永远无法直接需求会怎么变动，万一哪天老板要求改回来怎么办。</p><h2 id="SPI的实现原理"><a href="#SPI的实现原理" class="headerlink" title="SPI的实现原理"></a>SPI的实现原理</h2><p>对于上一节的例子，可以采用SPI的方式，具体Java SPI的使用这里不做过多解释。可以简单看下JDBC中的例子。</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">ServiceLoader&lt;Driver&gt; loadedDrivers = ServiceLoader.load(Driver.class);</span><br><span class="line">Iterator&lt;Driver&gt; driversIterator = loadedDrivers.iterator();</span><br></pre></td></tr></table></figure><p>在进入代码之前，我们可以猜测一下实现方式如何。在功能表象而言，<code>SPI</code>提供的能力其实就是在应用的运行期，加载并实例化 <code>META-INF/services</code>配置中定义的类。<br>进入<code>load</code>方法发现也差不多，这里给出关键的代码片段。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">hasNextService</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (nextName != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (configs == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">String</span> <span class="variable">fullName</span> <span class="operator">=</span> PREFIX + service.getName();</span><br><span class="line">                    <span class="keyword">if</span> (loader == <span class="literal">null</span>)</span><br><span class="line">                        configs = ClassLoader.getSystemResources(fullName);</span><br><span class="line">                    <span class="keyword">else</span></span><br><span class="line">                        <span class="comment">//从配置文件找到类的全限定名</span></span><br><span class="line">                        configs = loader.getResources(fullName);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException x) &#123;</span><br><span class="line">                    fail(service, <span class="string">&quot;Error locating configuration files&quot;</span>, x);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">while</span> ((pending == <span class="literal">null</span>) || !pending.hasNext()) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!configs.hasMoreElements()) &#123;</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                pending = parse(service, configs.nextElement());</span><br><span class="line">            &#125;</span><br><span class="line">            nextName = pending.next();</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> S <span class="title function_">nextService</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (!hasNextService())</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">NoSuchElementException</span>();</span><br><span class="line">            <span class="type">String</span> <span class="variable">cn</span> <span class="operator">=</span> nextName;</span><br><span class="line">            nextName = <span class="literal">null</span>;</span><br><span class="line">            Class&lt;?&gt; c = <span class="literal">null</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">                c = Class.forName(cn, <span class="literal">false</span>, loader);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (ClassNotFoundException x) &#123;</span><br><span class="line">                fail(service,</span><br><span class="line">                     <span class="string">&quot;Provider &quot;</span> + cn + <span class="string">&quot; not found&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (!service.isAssignableFrom(c)) &#123;</span><br><span class="line">                fail(service,</span><br><span class="line">                     <span class="string">&quot;Provider &quot;</span> + cn  + <span class="string">&quot; not a subtype&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="type">S</span> <span class="variable">p</span> <span class="operator">=</span> service.cast(c.newInstance());</span><br><span class="line">                providers.put(cn, p);</span><br><span class="line">                <span class="keyword">return</span> p;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Throwable x) &#123;</span><br><span class="line">                fail(service,</span><br><span class="line">                     <span class="string">&quot;Provider &quot;</span> + cn + <span class="string">&quot; could not be instantiated&quot;</span>,</span><br><span class="line">                     x);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>();          <span class="comment">// This cannot happen</span></span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><h2 id="JDBC中的SPI机制"><a href="#JDBC中的SPI机制" class="headerlink" title="JDBC中的SPI机制"></a>JDBC中的SPI机制</h2><p><code>JDK</code>制定了<code>JDBC</code>规范，落实到代码层面可简单认为<code>JDK</code>提供了<code>JDBC interface</code>接口，各个数据库厂商根据这一份规范做各自的实现。当我们需要使用<code>MySql</code>时，就可以引入相应的依赖，同时找到<code>MySql</code>的实现去使用，正好符合<code>SPI</code> 的应用场景。<br>简单分析一下，<code>JDBC</code>的<code>SPI</code>的使用逻辑。入口在<code>conn = DriverManager.getConnection(url, username, password);</code>,当执行到这一句代码时，会触发<code>DriverManager</code>的加载和初始化，进入<code>DriverManager</code>可以看到下面的<code>static</code>的静态代码块，我们知道<code>JVM</code>在加载类时会触发<code>static</code>代码块的执行。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line">     * Load the initial JDBC drivers by checking the System property</span><br><span class="line">     * jdbc.properties and then use the &#123;@code ServiceLoader&#125; mechanism</span><br><span class="line">     */</span><br><span class="line">    static &#123;</span><br><span class="line">        loadInitialDrivers();</span><br><span class="line">        println(&quot;JDBC DriverManager initialized&quot;);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>从代码块的注释可以看出，<code>loadInitialDrivers()</code> 做的事情仅仅是加载初始化的<code>JDBC</code>驱动，这里会首先去检查系统配置中的<code>jdbc.properties</code>配置，之后再会使用<code>ServiceLoader</code>去加载。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/10/25/20201025-at-1236-am.png" alt="2020-10-25 at 12.36 A"></p><p>可以看到，具体的实现类为<code>com.mysql.cj.jdbc.Driver</code>，也就是说此时会触发<code>com.mysql.cj.jdbc.Driver</code>的加载初始化，我们在看一下<code>com.mysql.cj.jdbc.Driver</code>的代码。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">public class Driver extends NonRegisteringDriver implements java.sql.Driver &#123;</span><br><span class="line">    public Driver() throws SQLException &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    static &#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            DriverManager.registerDriver(new Driver());</span><br><span class="line">        &#125; catch (SQLException var1) &#123;</span><br><span class="line">            throw new RuntimeException(&quot;Can&#x27;t register driver!&quot;);</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>com.mysql.cj.jdbc.Driver</code> 的做的事情非常简单，就是把自己注册到<code>DriverManager</code>中，注意，此时<code>com.mysql.cj.jdbc.Driver</code> 可以看作已经完成了加载初始化，并<code>new</code>出了一个实例注册到了<code>DriverManager</code>中。依旧从<code>conn = DriverManager.getConnection(url, username, password);</code>的方法可以看到<code>DriverManager</code>是如何使用<code>registeredDrivers</code>的,从注释中可以看到会从已注册的<code>registeredDrivers</code>逐个尝试进行一次连接，拿到正确的<code>Connection</code>就返回。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Walk through the loaded registeredDrivers attempting to make a connection.</span></span><br><span class="line">        <span class="comment">// Remember the first exception that gets raised so we can reraise it.</span></span><br><span class="line">        <span class="type">SQLException</span> <span class="variable">reason</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span>(DriverInfo aDriver : registeredDrivers) &#123;</span><br><span class="line">            <span class="comment">// If the caller does not have permission to load the driver then</span></span><br><span class="line">            <span class="comment">// skip it.</span></span><br><span class="line">            <span class="keyword">if</span>(isDriverAllowed(aDriver.driver, callerCL)) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    println(<span class="string">&quot;    trying &quot;</span> + aDriver.driver.getClass().getName());</span><br><span class="line">                    <span class="type">Connection</span> <span class="variable">con</span> <span class="operator">=</span> aDriver.driver.connect(url, info);</span><br><span class="line">                    <span class="keyword">if</span> (con != <span class="literal">null</span>) &#123;</span><br><span class="line">                        <span class="comment">// Success!</span></span><br><span class="line">                        println(<span class="string">&quot;getConnection returning &quot;</span> + aDriver.driver.getClass().getName());</span><br><span class="line">                        <span class="keyword">return</span> (con);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (SQLException ex) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (reason == <span class="literal">null</span>) &#123;</span><br><span class="line">                        reason = ex;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                println(<span class="string">&quot;    skipping: &quot;</span> + aDriver.getClass().getName());</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>另外多提一点，<code>Java SPI</code>在这里还打破了类的双亲委托机制。我们知道，<code>JVM</code>在加载类时，会遵循双亲委托机制，同时对于一个<code>Class</code>而言，其中的依赖类也会使用加载该<code>Class</code>的类加载器去加载。比如说，<code>Class A</code> 中有一个 <code>Class B</code>的依赖，在<code>JVM</code> 加载 <code>A</code>时，假如用的是启动类加载器，此时也只能用启动类加载器去加载<code>B</code> （前提是 <code>B</code> 还没被加载到<code>JVM</code>中，我们知道<code>JVM</code>在加载<code>Class</code>时首先会检查该<code>Class</code>是否已经加载到<code>JVM</code>中，如果没有被加载，则使用双亲委托机制去加载）。为什么这样做呢？反面去想的话，如果<code>A</code>是启动类加载器加载的，<code>A</code>必定属于<code>JDK</code>的核心类，倘若<code>A</code>中的依赖不由启动类加载器去加载，而使用应用类加载器去加载，此时如果此时在应用目录伪造一个 核心的类 ，比如说<code>Object</code>类，让应用类加载器去加载<code>Object</code>类到<code>JVM</code>，此时必定带来安全的风险。<br>那么类比于<code>JDBC</code>，<code>DriverManager</code> 属于<code>JDK</code>的核心包，可知加载<code>DriverManager</code>必定是启动类加载器，那么<code>DriverManager</code>中的依赖应该也是由启动类加载去加载，但我们知道<code>Driver</code>接口的实现类是第三方厂商自定义的，这些实现类必然不会被启动类加载器去加载。怎么解决这个问题呢？只要启动类加载器加载<code>DriverManager</code>时，提前把第三方厂商实现的<code>Driver</code>实现类加载到JVM中就行。所以在<code>DriverManager</code> 的<code>static</code>静态代码块中的<code>loadInitialDrivers()</code>的做事情就是提前加载。</p><h2 id="RPC框架、可扩展性"><a href="#RPC框架、可扩展性" class="headerlink" title="RPC框架、可扩展性"></a>RPC框架、可扩展性</h2><p>在后续看<code>Dubbo</code> 和 <code>soft-rpc</code>等 <code>RPC</code>框架中，发现均存在类似<code>SPI</code>的设计，例如<code>Dubbo</code>自己实现类一套<code>Dubbo SPI</code>的机制，比<code>Java SPI</code>更加优雅，并且可以做到按需加载。<br>这里引用何小锋老师的一段解释：</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/10/25/20201025-at-108-am.png" alt="2020-10-25 at 1.08 A"></p><blockquote><p>在 RPC 框架里面，我们是怎么支持插件化架构的呢？我们可以将每个功能点抽象成一个接口，将这个接口作为插件的契约，然后把这个功能的接口与功能的实现分离，并提供接口的默认实现。加上了插件功能之后，我们的 RPC 框架就包含了两大核心体系——核心功能体系与插件体系</p></blockquote><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/10/25/20201025-at-109-am.png" alt="2020-10-25 at 1.09 A"></p><blockquote><p>这时，整个架构就变成了一个微内核架构，我们将每个功能点抽象成一个接口，将这个接口作为插件的契约，然后把这个功能的接口与功能的实现分离并提供接口的默认实现。这样的架构相比之前的架构，有很多优势。首先它的可扩展性很好，实现了开闭原则，用户可以非常方便地通过插件扩展实现自己的功能，而且不需要修改核心功能的本身；其次就是保持了核心包的精简，依赖外部包少，这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。</p></blockquote><h2 id="一点个人理解"><a href="#一点个人理解" class="headerlink" title="一点个人理解"></a>一点个人理解</h2><p><code>SPI</code>最大的好处就是，对于使用方（调用方）而言，屏蔽了变化性，服务的提供方可以动态去提供各种各样的服务（接口实现类），相比于传统的设计，由程序员自己去手动编写静态的代码逻辑去维护这种变化，一方面不符合开闭原则，一方面维护也是一个成本（如果服务很多的话，例如像<code>Dubbo</code>各种各样的<code>SPI</code>接口实现）。<br>这种可插拔的设计，和<code>Spring IOC</code>设计 很像，变化时只需要改变实现方 使用方可无感知使用，对使用方来说无侵入性。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;很早就了解过&lt;code&gt;SPI&lt;/code&gt;的概念，刚开始知道是看JDBC驱动实现的时候发现有用到，后面陆陆续续发现在&lt;code&gt;Dubbo&lt;/code&gt;中也有&lt;code&gt;Dubbo SPI&lt;/code&gt;的概念，希望可以把自己的理解和困惑记录下来。&lt;/p&gt;
    
    </summary>
    
    
      <category term="SPI" scheme="https://lazyallen.github.io/categories/SPI/"/>
    
    
      <category term="Java" scheme="https://lazyallen.github.io/tags/Java/"/>
    
  </entry>
  
  <entry>
    <title>Groovy Script Console：如何找出Jenkins中所有授信失效的项目</title>
    <link href="https://lazyallen.github.io/2020/07/02/jenkins-groovy-script-console/"/>
    <id>https://lazyallen.github.io/2020/07/02/jenkins-groovy-script-console/</id>
    <published>2020-07-01T16:00:00.000Z</published>
    <updated>2022-12-07T07:06:26.376Z</updated>
    
    <content type="html"><![CDATA[<p>本文来源于在一次Jenkins培训当中，有用户提问：我怎么知道自己的授信失效了。于是通过Jenkins提供的Groovy Script Console能力，编写脚本找出Jenkins中所有授信失效的项目。</p><span id="more"></span><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>公司自建的<code>CI平台</code>基于<code>Jenkins</code>的，为了方便解答用户使用时碰到的问题，团队直接建立内部沟通群直接对接到用户。时间一长，发现用户经常遇到<code>无法clone代码</code>的问题,日志类似如下。</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/04/10/20200410-at-1228-am.png" alt="2020-04-10 at 12.28 A"></p><p>仔细看会发现是由<code>Authentication failed</code>导致的，往往是由于用户密码修改后，没有同步在<code>CI平台</code>及时更新从而授信失效导致的。</p><h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>由于公司网络安全管控，每个员工均要在<code>90天</code>周期更换一次密码。而更新密码之后，又没有手动同步到CI平台。最直接的现象就是，点进项目编辑页，会明显看到<code>stderr: remote: HTTP Basic: Access denied</code>的<code>error信息</code>。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/04/10/20200410-at-1227-am.png" alt="2020-04-10 at 12.27 A"></p><h2 id="能想到的几种解决方案"><a href="#能想到的几种解决方案" class="headerlink" title="能想到的几种解决方案"></a>能想到的几种解决方案</h2><p>当沟通群一旦有人丢出来拉不到代码的问题，绝大部分都是由这个问题导致的，而我们也群里也不厌其烦帮用户解释了一次又一次，只要更新一下对应的授信密码就行。回过头来想，如何尽可能减少重复的事情发生呢？</p><ul><li><code>90天</code>周期更新密码时，自动同步更新平台授信：可以彻底解决，但目前无法实现</li><li>主动找出平台中哪些授信是已经失效了的，主动通知用户去更新：由被动用户找变成了主动告知用户，当前最切实的方案</li><li>建议用户使用<code>SSH方式</code>的授信，密码过期了也没事：对于新项目可以主动引导用户使用<code>SSH类型</code>授信，由于历史问题，平台上已有项目绝大部分是<code>UserPassword类型</code>授信</li></ul><p>以上看来，如果可以每天定时轮询平台上所有的项目的授信，找出已失效的授信的项目，发送提醒邮件给用户及时更新密码，并建议用户换成<code>SSH授信</code>，是目前最切实际的做法。那么问题第一步就是，如何找到失效授信的任务？<br>个人觉得有两种思路：</p><ul><li>遍历所有上一次运行失败的项目，查看日志中是否包含<code>Access denied</code>关键字</li><li>遍历所有项目，模拟调用<code>check url</code>请求，判断当前项目是否授信失效</li></ul><p>对于第2点，需要补充说明：当在项目编辑页面，所看到的<code>error提示</code>，是<code>Jenkins</code>调用了<code>check url</code> 请求返回的校验结果。<br>Request如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Request URL: http://&#123;host&#125;:8081/job/test-fail-job-cased-by-credential-failed/descriptorByName/hudson.plugins.git.UserRemoteConfig/checkUrl</span><br><span class="line">value: http://git.midea.com/paas/customize-xxl-job.git</span><br><span class="line">credentialsId: c5axxx10-xxxx-4192-xxxx-25b6xx8e00b</span><br></pre></td></tr></table></figure><p>Response如下：</p><figure class="highlight xml"><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="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">error</span>&gt;</span><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#x27;/static/18ef55c1/images/none.gif&#x27;</span> <span class="attr">height</span>=<span class="string">16</span> <span class="attr">width</span>=<span class="string">1</span>&gt;</span>Failed to connect to repository : Command <span class="symbol">&amp;quot;</span>/usr/local/bin/git ls-remote -h http://git.midea.com/paas/customize-xxl-job.git HEAD<span class="symbol">&amp;quot;</span> returned status code 128:<span class="tag">&lt;<span class="name">br</span>&gt;</span>stdout: <span class="tag">&lt;<span class="name">br</span>&gt;</span>stderr: remote: HTTP Basic: Access denied<span class="tag">&lt;<span class="name">br</span>&gt;</span>fatal: Authentication failed for <span class="symbol">&amp;#039;</span>http://git.midea.com/paas/customize-xxl-job.git/<span class="symbol">&amp;#039;</span><span class="tag">&lt;<span class="name">br</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>接下来，我们将用<code>Script Console</code> 去实现以上两种思路。提前说明两点：</p><ul><li>本次实验环境：Jenkins version <code>2.89.3</code></li><li>实验脚本不建议直接在生产环境上使用</li></ul><h2 id="Groovy-Script-Console-介绍"><a href="#Groovy-Script-Console-介绍" class="headerlink" title="Groovy Script Console 介绍"></a>Groovy Script Console 介绍</h2><p><code>Jenkins</code> 提供了一个 <code>Groovy 脚本控制台</code>，允许用户在 <code>Jenkins</code> 主运行时或代理上的运行时中运行任意的 <code>Groovy</code> 脚本。它提供了可以做很多事情的能力：</p><ul><li>创建子进程，并在 <code>Jenkins master</code> 和<code>agents</code>上执行任意命令；</li><li>它甚至可以读取 <code>Jenkins master</code>上拥有访问权限的文件(比如 <code>/etc/passwd</code>)；</li><li>甚至可以解密<code>Jenkins配置凭据</code>；</li><li>普通用户若拥有使用<code>Groovy Script Console</code> 权限等同于拥有管理员权限；</li><li><code>Groovy Script Console</code>之所以如此强大，是因为它最初是为 <code>Jenkins 开发人员</code>设计的一个调试界面，但后来发展成为 <code>Jenkins Admin</code> 用来<code>配置 Jenkins</code> 和<code>调试 Jenkins runtime问题</code>的一个界面；</li><li>由于<code>Groovy Script Console</code>提供了强大的功能，<code>Jenkins</code> 及其<code>Agents</code>不应该以 <code>root</code> 用户身份在 <code>Linux</code> 上运行，在任何操作系统上也不应该以 <code>root</code> 用户身份运行；</li><li>确保您的 <code>Jenkins instance</code>的安全；</li></ul><h3 id="在Agents上运行Groovy-Script"><a href="#在Agents上运行Groovy-Script" class="headerlink" title="在Agents上运行Groovy Script"></a>在Agents上运行Groovy Script</h3><p>可以在节点管理侧边栏菜单，点击<code>脚本控制台</code>即可。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/04/10/20200410-at-114-am.png" alt="2020-04-10 at 1.14 A"><br>除此之外，也可以在<code>Master</code>的<code>Groovy Script Console</code>运行脚本执行到<code>Agents</code>上</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">import</span> hudson.util.RemotingDiagnostics</span><br><span class="line"><span class="keyword">import</span> jenkins.model.Jenkins</span><br><span class="line"></span><br><span class="line">String agent_name = <span class="string">&#x27;your agent name&#x27;</span></span><br><span class="line"><span class="comment">//groovy script you want executed on an agent</span></span><br><span class="line">groovy_script = <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">println System.getenv(&quot;PATH&quot;)</span></span><br><span class="line"><span class="string">println &quot;uname -a&quot;.execute().text</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span>.trim()</span><br><span class="line"></span><br><span class="line">String result</span><br><span class="line">Jenkins.instance.slaves.find &#123; agent -&gt;</span><br><span class="line">    agent.name == agent_name</span><br><span class="line">&#125;.with &#123; agent -&gt;</span><br><span class="line">    result = RemotingDiagnostics.executeGroovy(groovy_script, agent.channel)</span><br><span class="line">&#125;</span><br><span class="line">println result</span><br></pre></td></tr></table></figure><h3 id="支持远程访问"><a href="#支持远程访问" class="headerlink" title="支持远程访问"></a>支持远程访问</h3><p>通过<code>Bash</code>提交<code>Groovy</code>文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl --user &#x27;username:api-token&#x27; --data-urlencode \</span><br><span class="line">  &quot;script=$(&lt; ./somescript.groovy)&quot; https://jenkins/scriptText</span><br></pre></td></tr></table></figure><h3 id="示例案例"><a href="#示例案例" class="headerlink" title="示例案例"></a>示例案例</h3><p>以下仓库中有许多可以作为参考的<code>Groovy Script</code>，基本上涉及大部分用户需要的场景：</p><ul><li><a href="https://github.com/cloudbees/jenkins-scripts">Cloudbees jenkins-脚本库</a></li><li><a href="https://github.com/samrocketman/jenkins-script-console-scripts">Sam Gleske 的 jenkins-script-console-scripts 存储库</a><br>由于 <code>Groovy</code> 脚本直接访问 <code>Jenkins</code> 的接口，通常已有脚本很容易因为 <code>Jenkins 版本</code>升级后过时，从而导致在运行脚本时返回异常，因为 <code>Jenkins core</code>或 <code>Jenkins 插件</code>中的公共方法和接口已经更改。 在试用例子的时候要记住这一点。</li></ul><h3 id="举个栗子：一行代码，禁用所有Jobs"><a href="#举个栗子：一行代码，禁用所有Jobs" class="headerlink" title="举个栗子：一行代码，禁用所有Jobs"></a>举个栗子：一行代码，禁用所有Jobs</h3><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Jenkins.instance.getAllItems(hudson.model.AbstractProject.<span class="keyword">class</span>).each &#123;i -&gt; i.setDisabled(<span class="literal">true</span>); i.save() &#125;</span><br></pre></td></tr></table></figure><p>参照<a href="https://javadoc.jenkins.io/archive/jenkins-2.89/">Jenkins 2.89 接口文档</a>，这行代码做的事情是获取所有的项目，一一设置为<code>Disabled</code>保存。</p><h2 id="遍历所有上一次运行失败的项目，且日志中包含Access-denied关键字"><a href="#遍历所有上一次运行失败的项目，且日志中包含Access-denied关键字" class="headerlink" title="遍历所有上一次运行失败的项目，且日志中包含Access denied关键字"></a>遍历所有上一次运行失败的项目，且日志中包含Access denied关键字</h2><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">jobs = Jenkins.instance.getAllItems()</span><br><span class="line">    jobs.each &#123; job -&gt;</span><br><span class="line">      <span class="keyword">if</span> (job <span class="keyword">instanceof</span> com.cloudbees.hudson.plugins.folder.AbstractFolder) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">        buildNums = job.getBuilds().size()</span><br><span class="line">        <span class="keyword">if</span>(buildNums&gt;<span class="number">0</span>)&#123;</span><br><span class="line">            lastBuild = job.getLastBuild()</span><br><span class="line">            <span class="keyword">if</span>(lastBuild &amp;&amp; lastBuild.result == Result.FAILURE)&#123;</span><br><span class="line">                isAccessDenied = lastBuild.getLog().contains(<span class="string">&#x27;Access denied&#x27;</span>)</span><br><span class="line">                <span class="keyword">if</span>(isAccessDenied)&#123;</span><br><span class="line">                    println <span class="string">&#x27;JOB: &#x27;</span> + job.fullName + <span class="string">&#x27; TimestampString: &#x27;</span> + lastBuild.getTimestampString2()</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></pre></td></tr></table></figure><ul><li>遍历所有的<code>Jobs</code>，这里忽略了<code>folder</code></li><li>统计每个<code>Job</code>的运行次数</li><li>对于运行过的<code>Job</code>获取最后一次<code>Build</code>，并判断对应的log中是否存在<code>Access denied</code>关键字</li><li>打印符合条件的<code>JOB</code>信息</li></ul><h2 id="遍历所有项目，模拟调用check-url请求，判断当前项目是否授信失效"><a href="#遍历所有项目，模拟调用check-url请求，判断当前项目是否授信失效" class="headerlink" title="遍历所有项目，模拟调用check url请求，判断当前项目是否授信失效"></a>遍历所有项目，模拟调用check url请求，判断当前项目是否授信失效</h2><p>对于这个思路，要解决的问题是如何模拟调用<code>check url</code>请求，我一开始的思路是，获取到每个任务对应的仓库信息和授信信息，从授信信息中获取明文信息后，然后直接使用<code>HTTP</code>请求<code>gitlab</code>判断，太麻烦了，还不如直接请求<code>Jenkins</code>的<code>check url</code>请求。仔细观察请求<code>URL``http://&#123;host&#125;:8081/job/test-fail-job-cased-by-credential-failed/descriptorByName/hudson.plugins.git.UserRemoteConfig/checkUrl</code>猜测，<code>Jenkins</code>应该也是直接调用的<code>git</code>插件的<code>checkUrl</code>方法，于是找到<code>git</code>插件源码的<code>UserRemoteConfig</code>类，果然找到对应的<code>doCheckUrl</code>方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequirePOST</span></span><br><span class="line">        <span class="keyword">public</span> FormValidation <span class="title function_">doCheckUrl</span><span class="params">(<span class="meta">@AncestorInPath</span> Item item,</span></span><br><span class="line"><span class="params">                                         <span class="meta">@QueryParameter</span> String credentialsId,</span></span><br><span class="line"><span class="params">                                         <span class="meta">@QueryParameter</span> String value)</span> <span class="keyword">throws</span> IOException, InterruptedException &#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// Normally this permission is hidden and implied by Item.CONFIGURE, so from a view-only form you will not be able to use this check.</span></span><br><span class="line">            <span class="comment">// (TODO under certain circumstances being granted only USE_OWN might suffice, though this presumes a fix of JENKINS-31870.)</span></span><br><span class="line">            <span class="keyword">if</span> (item == <span class="literal">null</span> &amp;&amp; !Jenkins.get().hasPermission(Jenkins.ADMINISTER) ||</span><br><span class="line">                item != <span class="literal">null</span> &amp;&amp; !item.hasPermission(CredentialsProvider.USE_ITEM)) &#123;</span><br><span class="line">                <span class="keyword">return</span> FormValidation.ok();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> Util.fixEmptyAndTrim(value);</span><br><span class="line">            <span class="keyword">if</span> (url == <span class="literal">null</span>)</span><br><span class="line">                <span class="keyword">return</span> FormValidation.error(Messages.UserRemoteConfig_CheckUrl_UrlIsNull());</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (url.indexOf(<span class="string">&#x27;$&#x27;</span>) &gt;= <span class="number">0</span>)</span><br><span class="line">                <span class="comment">// set by variable, can&#x27;t validate</span></span><br><span class="line">                <span class="keyword">return</span> FormValidation.ok();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// get git executable on master</span></span><br><span class="line">            EnvVars environment;</span><br><span class="line">            <span class="type">Jenkins</span> <span class="variable">jenkins</span> <span class="operator">=</span> Jenkins.get();</span><br><span class="line">            <span class="keyword">if</span> (item <span class="keyword">instanceof</span> Job) &#123;</span><br><span class="line">                environment = ((Job) item).getEnvironment(jenkins, TaskListener.NULL);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="type">Computer</span> <span class="variable">computer</span> <span class="operator">=</span> jenkins.toComputer();</span><br><span class="line">                environment = computer == <span class="literal">null</span> ? <span class="keyword">new</span> <span class="title class_">EnvVars</span>() : computer.buildEnvironment(TaskListener.NULL);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">GitClient</span> <span class="variable">git</span> <span class="operator">=</span> Git.with(TaskListener.NULL, environment)</span><br><span class="line">                    .using(GitTool.getDefaultInstallation().getGitExe())</span><br><span class="line">                    .getClient();</span><br><span class="line">            <span class="type">StandardCredentials</span> <span class="variable">credential</span> <span class="operator">=</span> lookupCredentials(item, credentialsId, url);</span><br><span class="line">            git.addDefaultCredentials(credential);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// Should not track credentials use in any checkURL method, rather should track</span></span><br><span class="line">            <span class="comment">// credentials use at the point where the credential is used to perform an</span></span><br><span class="line">            <span class="comment">// action (like poll the repository, clone the repository, publish a change</span></span><br><span class="line">            <span class="comment">// to the repository).</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">// attempt to connect the provided URL</span></span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                git.getHeadRev(url, <span class="string">&quot;HEAD&quot;</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (GitException e) &#123;</span><br><span class="line">                <span class="keyword">return</span> FormValidation.error(Messages.UserRemoteConfig_FailedToConnect(e.getMessage()));</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> FormValidation.ok();</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>所以只要调用<code>UserRemoteConfig.DescriptorImpl</code>内部类的<code>doCheckUrl</code>方法即可，最后实现的脚本如下：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hudson.plugins.git.*</span><br><span class="line">UserRemoteConfig.DescriptorImpl descriptor = Jenkins.instance.getDescriptorByType(hudson.plugins.git.UserRemoteConfig.DescriptorImpl.<span class="keyword">class</span>)</span><br><span class="line"></span><br><span class="line">Jenkins.instance.getAllItems().each&#123;job -&gt; </span><br><span class="line">    <span class="keyword">if</span> (job <span class="keyword">instanceof</span> com.cloudbees.hudson.plugins.folder.AbstractFolder) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">    <span class="keyword">if</span> (job <span class="keyword">instanceof</span> hudson.model.ExternalJob) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">    <span class="keyword">def</span> scmlist=[]</span><br><span class="line">    <span class="keyword">if</span>(job <span class="keyword">instanceof</span> org.jenkinsci.plugins.workflow.job.WorkflowJob)&#123;</span><br><span class="line">        scms = job.getSCMs()</span><br><span class="line">        scmlist.addAll(scms)</span><br><span class="line">    &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">        scmlist.add(job.scm)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span>(scmlist)&#123;</span><br><span class="line">        <span class="keyword">def</span> errorInfo=<span class="string">&#x27;&#x27;</span></span><br><span class="line">        scmlist.each&#123;scm -&gt; </span><br><span class="line">            <span class="keyword">if</span>(scm <span class="keyword">instanceof</span> GitSCM)&#123;</span><br><span class="line">                scm.userRemoteConfigs.each&#123; urc -&gt;</span><br><span class="line">                <span class="comment">//println urc.name +&#x27; &#x27;+ urc.url +&#x27; &#x27;+ urc.credentialsId</span></span><br><span class="line">                formValidation = descriptor.doCheckUrl(job,urc.credentialsId,urc.url)</span><br><span class="line">                <span class="comment">//println &#x27;ERROR&#x27;==formValidation.kind.name()?formValidation:&#x27;ok&#x27;</span></span><br><span class="line">                <span class="keyword">if</span>(<span class="string">&#x27;ERROR&#x27;</span>==formValidation.kind.name())&#123;</span><br><span class="line">                    errorInfo += <span class="string">&quot;url: $urc.url, reason: $formValidation&quot;</span></span><br><span class="line">                    <span class="comment">//println job.getAbsoluteUrl() + &#x27;check git url failed,reason : &#x27;+formValidation</span></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 class="keyword">if</span>(errorInfo?.trim())&#123;</span><br><span class="line">            println job.getAbsoluteUrl() + <span class="string">&#x27;check git url failed,error info : &#x27;</span>+errorInfo</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>需要特别<code>import</code>插件的包<code>hudson.plugins.git.*</code></li><li>通过<code>Jenkins.instance.getDescriptorByType()</code>获取<code>UserRemoteConfig.DescriptorImpl</code>的实例</li><li>遍历所有项目，每个项目获取对应的<code>SCM</code>信息，其中包含了<code>credentialsId</code>和<code>url</code>信息</li><li>调用<code>descriptor.doCheckUrl(job,urc.credentialsId,urc.url)</code>方法验证授信是否失效</li><li>打印所有失效的项目信息</li></ul><h2 id="彩蛋：解密Jenkins授信"><a href="#彩蛋：解密Jenkins授信" class="headerlink" title="彩蛋：解密Jenkins授信"></a>彩蛋：解密Jenkins授信</h2><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.cloudbees.plugins.credentials.Credentials</span><br><span class="line"></span><br><span class="line">Set&lt;Credentials&gt; allCredentials = <span class="keyword">new</span> HashSet&lt;Credentials&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(</span><br><span class="line">      com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.<span class="keyword">class</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">allCredentials.addAll(creds)</span><br><span class="line"></span><br><span class="line">Jenkins.instance.getAllItems(com.cloudbees.hudson.plugins.folder.Folder.<span class="keyword">class</span>).each&#123; f -&gt;</span><br><span class="line"> creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(</span><br><span class="line">      com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.<span class="keyword">class</span>, f)</span><br><span class="line">  allCredentials.addAll(creds)</span><br><span class="line">  </span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (c <span class="keyword">in</span> allCredentials) &#123;</span><br><span class="line">   println( ( c.properties.privateKeySource ? <span class="string">&quot;ID: &quot;</span> + c.id + <span class="string">&quot;, UserName: &quot;</span> + c.username + <span class="string">&quot;, Private Key: &quot;</span> + c.getPrivateKey() : <span class="string">&quot;&quot;</span>))</span><br><span class="line">      println( ( c.properties.password ? <span class="string">&quot;ID: &quot;</span> + c.id + <span class="string">&quot;, UserName: &quot;</span> + c.username + <span class="string">&quot;, Password: &quot;</span> + c.password : <span class="string">&quot;&quot;</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>感想如下：</p><ul><li>脚本调试成本很高：将逻辑拆分为多个步骤，按照步骤一步一步完善代码，上一步跑通后再实现下一步；</li><li>对照<code>Jenkins</code>文档：建议参照对应版本<code>Jenkins</code>的<code>Javadoc</code>,减少出错；</li><li>经常出现“无该方法签名”的错误：通常是脚本中的字段或方法在当前版本的<code>Jenkins</code>中不存在，请检查<code>Javadoc</code>的版本和<code>Jenkins</code>版本是否一致；</li><li>观察<code>Jenkins</code>是如何实现的：别着急实现，可以看看<code>Jenkins</code>自己是如何实现的，不然可能会走弯路；</li><li>之前实现过一个功能，实时获取<code>Jenkins</code>正在执行和正在队列中的<code>Job</code>信息，当时还不知道有<code>Script Console这种</code>东西，实现方案是调用<code>Jenkins</code> 的<code>HTTP API</code>，获取<code>XML</code>的内容，再用特定的解析语法获取到<code>Job</code>信息。能获取到的Job字段内容很少，且还要处理<code>XML</code>解析逻辑。现在看来使用<code>Script Console</code>实现会更加简单快捷。</li></ul><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><ul><li><a href="https://jenkins.io/doc/book/managing/script-console/#script-console">Script Console</a></li><li><a href="https://github.com/jenkinsci/git-plugin">git-plugin</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文来源于在一次Jenkins培训当中，有用户提问：我怎么知道自己的授信失效了。于是通过Jenkins提供的Groovy Script Console能力，编写脚本找出Jenkins中所有授信失效的项目。&lt;/p&gt;
    
    </summary>
    
    
      <category term="工具" scheme="https://lazyallen.github.io/categories/%E5%B7%A5%E5%85%B7/"/>
    
    
      <category term="Jenkins" scheme="https://lazyallen.github.io/tags/Jenkins/"/>
    
      <category term="Groovy" scheme="https://lazyallen.github.io/tags/Groovy/"/>
    
      <category term="CICD" scheme="https://lazyallen.github.io/tags/CICD/"/>
    
  </entry>
  
  <entry>
    <title>深入浅出MyBatis 源码：配置文件解析</title>
    <link href="https://lazyallen.github.io/2020/05/12/read-code-mybatis-configuration/"/>
    <id>https://lazyallen.github.io/2020/05/12/read-code-mybatis-configuration/</id>
    <published>2020-05-11T16:00:00.000Z</published>
    <updated>2022-12-07T07:04:26.940Z</updated>
    
    <content type="html"><![CDATA[<p>本文将会介绍<code>MyBatis</code>配置文件解析部分的代码解读，从创建一个<code>SqlSessionFactory</code>作为入口，引入MyBatis配置文件的说明。简要说明配置文件中常用标签的用法和说明，根据每个标签，详细介绍<code>MyBatis</code>是如何解析这些标签的。</p><span id="more"></span><h2 id="创建一个SqlSessionFactory的几种方式"><a href="#创建一个SqlSessionFactory的几种方式" class="headerlink" title="创建一个SqlSessionFactory的几种方式"></a>创建一个SqlSessionFactory的几种方式</h2><blockquote><p>每个基于 <code>MyBatis</code> 的应用都是以一个 <code>SqlSessionFactory</code> 的实例为核心的。<code>SqlSessionFactory</code> 的实例可以通过 <code>SqlSessionFactoryBuilder</code> 获得。而 <code>SqlSessionFactoryBuilder</code> 则可以从 <code>XML</code> 配置文件或一个预先定制的 <code>Configuration</code> 的实例构建出 <code>SqlSessionFactory</code> 的实例。</p></blockquote><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="type">String</span> <span class="variable">resource</span> <span class="operator">=</span> <span class="string">&quot;org/mybatis/example/mybatis-config.xml&quot;</span>;</span><br><span class="line"><span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> Resources.getResourceAsStream(resource);</span><br><span class="line"><span class="type">SqlSessionFactory</span> <span class="variable">sqlSessionFactory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlSessionFactoryBuilder</span>().build(inputStream);</span><br></pre></td></tr></table></figure><p>从上文的实例代码可以看出，<code>sqlSessionFactory</code> 实例是通过<code>SqlSessionFactoryBuilder</code> 的<code>build</code> 方法构建出来的。我们进入<code>SqlSessionFactoryBuilder</code> 中可以看到。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SqlSessionFactoryBuilder</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader, String environment)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader, Properties properties)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader, String environment, Properties properties)</span>&#123;...&#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(InputStream inputStream)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(InputStream inputStream, String environment)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(InputStream inputStream, Properties properties)</span>&#123;...&#125;;</span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(InputStream inputStream, String environment, Properties properties)</span>&#123;...&#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Configuration config)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">DefaultSqlSessionFactory</span>(config);</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>SqlSessionFactoryBuilder</code>拥有<code>9</code>个<code>build</code> 重载方法，大概可以分为两类：</p><ul><li>以XML配置文件输入流的方式，如<code>Read</code> 字符流或 <code>InputStream</code>字节流</li><li>预先实例化一个<code>Configuration</code> 实例</li></ul><p><code>SqlSessionFactoryBuilder</code> 从命名可以看出就是<code>SqlSessionFactory</code>构建器，功能是去构建出<code>SqlSessionFactory</code>实例，而<code>SqlSessionFactory</code> 再去构建出<code>SqlSession</code> , 这个<code>SqlSession</code> 可以理解为数据库客户端连接服务端的会话。在现实生活中，我们是知道数据库服务器的主机地址,端口,用户名,密码等信息的，对应用而言，也应该有个「地方」去记录这些配置信息，在<code>MyBatis</code>中，这个「地方」就是「配置文件」，一般命名为<code>mybatis-config.xml</code>。XML文件是文件层面的「配置」，而<code>Configuration</code> 类是<code>Java Class</code>层面上的「配置」。</p><h2 id="MyBatis-配置文件的使用"><a href="#MyBatis-配置文件的使用" class="headerlink" title="MyBatis 配置文件的使用"></a>MyBatis 配置文件的使用</h2><p>一般<code>MyBatis</code> 的配置文件是一个XML文件，</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span> ?&gt;</span></span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">configuration</span></span></span><br><span class="line"><span class="meta">  <span class="keyword">PUBLIC</span> <span class="string">&quot;-//mybatis.org//DTD Config 3.0//EN&quot;</span></span></span><br><span class="line"><span class="meta">  <span class="string">&quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">environments</span> <span class="attr">default</span>=<span class="string">&quot;development&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">environment</span> <span class="attr">id</span>=<span class="string">&quot;development&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">transactionManager</span> <span class="attr">type</span>=<span class="string">&quot;JDBC&quot;</span>/&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dataSource</span> <span class="attr">type</span>=<span class="string">&quot;POOLED&quot;</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">&quot;driver&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;driver&#125;&quot;</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">&quot;url&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;url&#125;&quot;</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">&quot;username&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;username&#125;&quot;</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">&quot;password&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;password&#125;&quot;</span>/&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dataSource</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">environment</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">environments</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">mappers</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">mapper</span> <span class="attr">resource</span>=<span class="string">&quot;org/mybatis/example/BlogMapper.xml&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">mappers</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br></pre></td></tr></table></figure><p>大概可以分为两部分：</p><ul><li>XML<a href="http://mybatis.org/dtd/mybatis-3-config.dtd">头部声明</a>DTD文件（<code>Document Type Definition</code>）：用于验证格式的正确性，关于<code>DTD</code>的介绍可以查看<a href="https://www.runoob.com/xml/xml-dtd.html">这里</a>。</li><li>以<code>configuration</code>为头节点的配置节点树：用于设置MyBatis的行为和属性</li></ul><p>这里稍微多说一句，配置文件的头部声明是HTTP协议的，那是不是意味着校验XML合法性时必须请求网络一次？<br>在初始化<code>XMLConfigBuilder</code>时，发现也同时传入了一个<code>XMLMapperEntityResolver</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"><span class="keyword">public</span> <span class="title function_">XMLConfigBuilder</span><span class="params">(InputStream inputStream, String environment, Properties props)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>(<span class="keyword">new</span> <span class="title class_">XPathParser</span>(inputStream, <span class="literal">true</span>, props, <span class="keyword">new</span> <span class="title class_">XMLMapperEntityResolver</span>()), environment, props);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>可以看到<code>XMLMapperEntityResolver</code>#<code>resolveEntity</code>方法中，会直接读取存在本地的dtd文件，这样保证了就算处于离线环境依旧可以成功校验配置文件。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 本地 mybatis-config.dtd 文件</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">MYBATIS_CONFIG_DTD</span> <span class="operator">=</span> <span class="string">&quot;org/apache/ibatis/builder/xml/mybatis-3-config.dtd&quot;</span>;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 本地 mybatis-mapper.dtd 文件</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">MYBATIS_MAPPER_DTD</span> <span class="operator">=</span> <span class="string">&quot;org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd&quot;</span>;</span><br><span class="line">    </span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> InputSource <span class="title function_">resolveEntity</span><span class="params">(String publicId, String systemId)</span> <span class="keyword">throws</span> SAXException &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (systemId != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="type">String</span> <span class="variable">lowerCaseSystemId</span> <span class="operator">=</span> systemId.toLowerCase(Locale.ENGLISH);</span><br><span class="line">                <span class="comment">// 本地 mybatis-config.dtd 文件</span></span><br><span class="line">                <span class="keyword">if</span> (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) &#123;</span><br><span class="line">                    <span class="keyword">return</span> getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);</span><br><span class="line">                <span class="comment">// 本地 mybatis-mapper.dtd 文件</span></span><br><span class="line">                &#125; <span class="keyword">else</span> <span class="keyword">if</span> (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) &#123;</span><br><span class="line">                    <span class="keyword">return</span> getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">SAXException</span>(e.toString());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>从<a href="https://mybatis.org/mybatis-3/zh/configuration.html">文档</a>得知全部配置如下，关于配置的解释和使用这里不作具体说明，文档的解释比较详细。</p><ul><li>configuration（配置）<ul><li>properties（属性）</li><li>settings（设置）</li><li>typeAliases（类型别名）</li><li>typeHandlers（类型处理器）</li><li>objectFactory（对象工厂）</li><li>plugins（插件）</li><li>environments（环境配置）</li><li>environment（环境变量）</li><li>transactionManager（事务管理器）</li><li>dataSource（数据源）</li><li>databaseIdProvider（数据库厂商标识）</li><li>mappers（映射器）</li></ul></li></ul><h2 id="MyBatis-配置文件的使用的parse-过程"><a href="#MyBatis-配置文件的使用的parse-过程" class="headerlink" title="MyBatis 配置文件的使用的parse 过程"></a>MyBatis 配置文件的使用的parse 过程</h2><p><code>MyBatis</code> 配置文件的解析就是将XML配置转换为<code>Configuration</code>实例的过程。可以分为两步：</p><ul><li>第一步：通过<code>XPathParser</code>将XML转换为<code>org.w3c.dom.Document</code>对象</li><li>第二步：通过<code>eval</code> 节点属性，设置<code>Configuration</code>属性</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader, String environment, Properties properties)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 创建 XMLConfigBuilder 对象，执行 XML 解析</span></span><br><span class="line">            <span class="type">XMLConfigBuilder</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">XMLConfigBuilder</span>(reader, environment, properties);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 创建 DefaultSqlSessionFactory 对象</span></span><br><span class="line">            <span class="keyword">return</span> build(parser.parse());</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">&quot;Error building SqlSession.&quot;</span>, e);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            ErrorContext.instance().reset();</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                reader.close();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">                <span class="comment">// Intentionally ignore. Prefer previous error.</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>回过头再看<code>SqlSessionFactoryBuilder</code>#<code>build</code>方法,创建 <code>XMLConfigBuilder</code> 对象时就完成转换为<code>Document</code>对象的过程，<code>parser.parse()</code>做的就是构造<code>Configuration</code>实例。</p><h3 id="创建-org-w3c-dom-Document-对象"><a href="#创建-org-w3c-dom-Document-对象" class="headerlink" title="创建 org.w3c.dom.Document 对象"></a>创建 org.w3c.dom.Document 对象</h3><p><code>XMLConfigBuilder</code>持有<code>XPathParser</code>，<code>XPathParser</code>持有<code>Document</code>，在构造<code>XMLConfigBuilder</code>时，同时也会构造<code>XPathParser</code>，在构造<code>XPathParser</code>时通过调用<code>createDocument</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">XPathParser</span><span class="params">(InputStream inputStream, <span class="type">boolean</span> validation, Properties variables)</span> &#123;</span><br><span class="line">        commonConstructor(validation, variables, <span class="literal">null</span>);</span><br><span class="line">        <span class="built_in">this</span>.document = createDocument(<span class="keyword">new</span> <span class="title class_">InputSource</span>(inputStream));</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h3 id="构造Configuration"><a href="#构造Configuration" class="headerlink" title="构造Configuration"></a>构造Configuration</h3><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2019/12/25/xmlconfigbuilder.png" alt="XMLConfigBuilde"></p><p><code>XMLConfigBuilder</code>的<code>parse</code>方法会返回一个<code>Configuration</code>实例。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 解析 XML 成 Configuration 对象。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> Configuration 对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> Configuration <span class="title function_">parse</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 若已解析，抛出 BuilderException 异常</span></span><br><span class="line">        <span class="keyword">if</span> (parsed) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BuilderException</span>(<span class="string">&quot;Each XMLConfigBuilder can only be used once.&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 标记已解析</span></span><br><span class="line">        parsed = <span class="literal">true</span>;</span><br><span class="line">        <span class="comment">// 解析 XML configuration 节点</span></span><br><span class="line">        parseConfiguration(parser.evalNode(<span class="string">&quot;/configuration&quot;</span>));</span><br><span class="line">        <span class="keyword">return</span> configuration;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>对于配置文件的解析全部体现在<code>parseConfiguration(parser.evalNode(&quot;/configuration&quot;));</code>，进入到该方法中可以看到，这个方法做的事情就是去一一解析Document对象中的标签，然后将解析后的标签值设置到<code>configuration</code>实例中。其中的每一个方法都代表了对一种配置解析。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">parseConfiguration</span><span class="params">(XNode root)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//issue #117 read properties first</span></span><br><span class="line">            <span class="comment">// 解析 &lt;properties /&gt; 标签</span></span><br><span class="line">            propertiesElement(root.evalNode(<span class="string">&quot;properties&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;settings /&gt; 标签</span></span><br><span class="line">            <span class="type">Properties</span> <span class="variable">settings</span> <span class="operator">=</span> settingsAsProperties(root.evalNode(<span class="string">&quot;settings&quot;</span>));</span><br><span class="line">            <span class="comment">// 加载自定义的 VFS 实现类</span></span><br><span class="line">            loadCustomVfs(settings);</span><br><span class="line">            <span class="comment">// 解析 &lt;typeAliases /&gt; 标签</span></span><br><span class="line">            typeAliasesElement(root.evalNode(<span class="string">&quot;typeAliases&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;plugins /&gt; 标签</span></span><br><span class="line">            pluginElement(root.evalNode(<span class="string">&quot;plugins&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;objectFactory /&gt; 标签</span></span><br><span class="line">            objectFactoryElement(root.evalNode(<span class="string">&quot;objectFactory&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;objectWrapperFactory /&gt; 标签</span></span><br><span class="line">            objectWrapperFactoryElement(root.evalNode(<span class="string">&quot;objectWrapperFactory&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;reflectorFactory /&gt; 标签</span></span><br><span class="line">            reflectorFactoryElement(root.evalNode(<span class="string">&quot;reflectorFactory&quot;</span>));</span><br><span class="line">            <span class="comment">// 赋值 &lt;settings /&gt; 到 Configuration 属性</span></span><br><span class="line">            settingsElement(settings);</span><br><span class="line">            <span class="comment">// read it after objectFactory and objectWrapperFactory issue #631</span></span><br><span class="line">            <span class="comment">// 解析 &lt;environments /&gt; 标签</span></span><br><span class="line">            environmentsElement(root.evalNode(<span class="string">&quot;environments&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;databaseIdProvider /&gt; 标签</span></span><br><span class="line">            databaseIdProviderElement(root.evalNode(<span class="string">&quot;databaseIdProvider&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;typeHandlers /&gt; 标签</span></span><br><span class="line">            typeHandlerElement(root.evalNode(<span class="string">&quot;typeHandlers&quot;</span>));</span><br><span class="line">            <span class="comment">// 解析 &lt;mappers /&gt; 标签</span></span><br><span class="line">            mapperElement(root.evalNode(<span class="string">&quot;mappers&quot;</span>));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BuilderException</span>(<span class="string">&quot;Error parsing SQL Mapper Configuration. Cause: &quot;</span> + e, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>由于配置项过多，我们这对其中几个比较常用的配置解析进行说明。</p><h4 id="解析properties标签"><a href="#解析properties标签" class="headerlink" title="解析properties标签"></a>解析properties标签</h4><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">properties</span> <span class="attr">resource</span>=<span class="string">&quot;org/mybatis/example/config.properties&quot;</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">&quot;username&quot;</span> <span class="attr">value</span>=<span class="string">&quot;dev_user&quot;</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">&quot;password&quot;</span> <span class="attr">value</span>=<span class="string">&quot;F2Fa3!33TYyg&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">dataSource</span> <span class="attr">type</span>=<span class="string">&quot;POOLED&quot;</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">&quot;driver&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;driver&#125;&quot;</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">&quot;url&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;url&#125;&quot;</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">&quot;username&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;username&#125;&quot;</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">&quot;password&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;password&#125;&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dataSource</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>properties</code>的作用简单说就是去动态替换属性的值。<code>properties</code>基本和<code>Java</code>语言中的<code>Properties</code>是一个意思，其本质就体现<code>Configuration</code>中的variables字段。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 变量 Properties 对象。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * 参见 &#123;<span class="doctag">@link</span> org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(XNode context)&#125; 方法</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="type">Properties</span> <span class="variable">variables</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Properties</span>();</span><br></pre></td></tr></table></figure><p>细心一点就会发现，其实<code>Properties</code>不仅可以在XML配置中定义，而且还可以通过读取<code>Properties</code>方法，以及通过<code>XMLConfigBuilder（reader, environment, properties）</code>的方式传入到<code>variables</code>中。</p><ul><li>XML配置中的<code>Properties</code></li><li>应用中的<code>Properties</code>文件</li><li>作为<code>XMLConfigBuilder</code>构造方法参数传递的<code>Properties</code><br>当以上3个地方都有相同名字的<code>Properties</code>时，那么<code>MyBatis</code>会用哪一个呢？对于这一点，文档中有特别解释。</li></ul><blockquote><p>如果属性在不只一个地方进行了配置，那么 <code>MyBatis</code> 将按照下面的顺序来加载：<br>在 <code>properties</code> 元素体内指定的属性首先被读取。<br>然后根据 <code>properties</code> 元素中的 <code>resource</code> 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件，并覆盖已读取的同名属性。<br>最后读取作为方法参数传递的属性，并覆盖已读取的同名属性。<br>因此，通过方法参数传递的属性具有最高优先级，<code>resource/url</code> 属性中指定的配置文件次之，最低优先级的是 <code>properties</code> 属性中指定的属性。</p></blockquote><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">void</span> <span class="title function_">propertiesElement</span><span class="params">(XNode context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span> (context != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 读取XML子标签们，为 Properties 对象</span></span><br><span class="line">            <span class="type">Properties</span> <span class="variable">defaults</span> <span class="operator">=</span> context.getChildrenAsProperties();</span><br><span class="line">            <span class="comment">// 读取Properties文件 resource 和 url 属性</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">resource</span> <span class="operator">=</span> context.getStringAttribute(<span class="string">&quot;resource&quot;</span>);</span><br><span class="line">            <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> context.getStringAttribute(<span class="string">&quot;url&quot;</span>);</span><br><span class="line">            <span class="keyword">if</span> (resource != <span class="literal">null</span> &amp;&amp; url != <span class="literal">null</span>) &#123; <span class="comment">// resource 和 url 都存在的情况下，抛出 BuilderException 异常</span></span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BuilderException</span>(<span class="string">&quot;The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 读取本地 Properties 配置文件到 defaults 中。</span></span><br><span class="line">            <span class="keyword">if</span> (resource != <span class="literal">null</span>) &#123;</span><br><span class="line">                defaults.putAll(Resources.getResourceAsProperties(resource));</span><br><span class="line">                <span class="comment">// 读取远程 Properties 配置文件到 defaults 中。</span></span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (url != <span class="literal">null</span>) &#123;</span><br><span class="line">                defaults.putAll(Resources.getUrlAsProperties(url));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 作为XMLConfigBuilder构造方法参数传递的Properties</span></span><br><span class="line">            <span class="comment">// 覆盖 configuration 中的 Properties 对象到 defaults 中。</span></span><br><span class="line">            <span class="type">Properties</span> <span class="variable">vars</span> <span class="operator">=</span> configuration.getVariables();</span><br><span class="line">            <span class="keyword">if</span> (vars != <span class="literal">null</span>) &#123;</span><br><span class="line">                defaults.putAll(vars);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 设置 defaults 到 parser 和 configuration 中。</span></span><br><span class="line">            parser.setVariables(defaults);</span><br><span class="line">            configuration.setVariables(defaults);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>以上做了两件事：</p><ul><li>依次分别读取XML配置的<code>Properties</code>标签、读取本地文件系统或远程网络的<code>Properties</code>配置文件（取决于<code>&lt;properties&gt;</code>节点的 <code>resource</code> 和 <code>url</code> 是否为空）、作为<code>XMLConfigBuilder</code>构造方法的<code>Properties</code>参数，将其全部<code>putAll</code>到<code>defaults</code>中。</li><li>设置 <code>defaults</code> 到 <code>parser</code> 和 <code>configuration</code> 中<br>所以，优先级最高的是作为<code>XMLConfigBuilder</code>构造方法的<code>Properties</code>参数、其次是本地文件系统或远程网络的<code>Properties</code>配置文件、优先级最低的是XML配置的<code>Properties</code>标签。</li></ul><h4 id="解析settings标签"><a href="#解析settings标签" class="headerlink" title="解析settings标签"></a>解析settings标签</h4><p><code>settings</code>是对<code>MyBatis</code> 行为和属性的定义，比如<code>useGeneratedKeys</code>这个<code>setting</code>代表的含义就是允许 <code>JDBC</code> 支持自动生成主键。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> Properties <span class="title function_">settingsAsProperties</span><span class="params">(XNode context)</span> &#123;</span><br><span class="line">        <span class="comment">// 将子标签，解析成 Properties 对象</span></span><br><span class="line">        <span class="keyword">if</span> (context == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Properties</span>();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Properties</span> <span class="variable">props</span> <span class="operator">=</span> context.getChildrenAsProperties();</span><br><span class="line">        <span class="comment">// Check that all settings are known to the configuration class</span></span><br><span class="line">        <span class="comment">// 校验每个属性，在 Configuration 中，有相应的 setting 方法，否则抛出 BuilderException 异常</span></span><br><span class="line">        <span class="type">MetaClass</span> <span class="variable">metaConfig</span> <span class="operator">=</span> MetaClass.forClass(Configuration.class, localReflectorFactory);</span><br><span class="line">        <span class="keyword">for</span> (Object key : props.keySet()) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!metaConfig.hasSetter(String.valueOf(key))) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BuilderException</span>(<span class="string">&quot;The setting &quot;</span> + key + <span class="string">&quot; is not known.  Make sure you spelled it correctly (case sensitive).&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> props;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>以上做了两件事：</p><ul><li>解析<code>setting</code>标签到<code>props</code></li><li>反射<code>Configuration</code>，循环<code>props</code>，调用<code>metaConfig.hasSetter</code>方法检测<code>Configuration</code>中是否持有对应的<code>setter</code>方法，否则抛出异常。</li></ul><p>为什么需要这样做，我的理解是在对<code>settings</code>设置到<code>Configuration</code>前端，需要先进行安全检查，从而保证用于填写的<code>settings</code>标签都是正确的。</p><h4 id="解析typeAliases标签"><a href="#解析typeAliases标签" class="headerlink" title="解析typeAliases标签"></a>解析typeAliases标签</h4><p><code>typeAliases</code>中文直译就是「类型别名」，解决的问题在于简化类的全限名的冗余。</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">typeAliases</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Author&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Author&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Blog&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Blog&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Comment&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Comment&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Post&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Post&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Section&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Section&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">alias</span>=<span class="string">&quot;Tag&quot;</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Tag&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">package</span> <span class="attr">name</span>=<span class="string">&quot;domain.blog&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">typeAliases</span>&gt;</span></span><br></pre></td></tr></table></figure><p>现在当前的XML文件中，如果之后在<code>resultType</code>中要用到<code>domain.blog.Author</code>类型引用，就只要用<code>Author</code>就可以代替。在<code>typeAliases</code>标签中支持两种注册别名的方式：</p><ul><li>准确注册：<code>typeAlias</code>标签，定义<code>alias</code>为别名，<code>type</code>为别名引用，用于为准确的一个类注册别名；</li><li>包注册：<code>package</code>标签，<code>name</code>属性，将整个包中的类都注册为别名，除非类中已经声明<code>@Alias</code>注解，否则别名默认为类的小写。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">typeAliasesElement</span><span class="params">(XNode parent)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (parent != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 遍历子节点</span></span><br><span class="line">            <span class="keyword">for</span> (XNode child : parent.getChildren()) &#123;</span><br><span class="line">                <span class="comment">// 指定为包的情况下，注册包下的每个类</span></span><br><span class="line">                <span class="keyword">if</span> (<span class="string">&quot;package&quot;</span>.equals(child.getName())) &#123;</span><br><span class="line">                    <span class="type">String</span> <span class="variable">typeAliasPackage</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;name&quot;</span>);</span><br><span class="line">                    configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);</span><br><span class="line">                <span class="comment">// 指定为类的情况下，直接注册类和别名</span></span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="type">String</span> <span class="variable">alias</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;alias&quot;</span>);</span><br><span class="line">                    <span class="type">String</span> <span class="variable">type</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;type&quot;</span>);</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        Class&lt;?&gt; clazz = Resources.classForName(type); <span class="comment">// 获得类是否存在</span></span><br><span class="line">                        <span class="comment">// 注册到 typeAliasRegistry 中</span></span><br><span class="line">                        <span class="keyword">if</span> (alias == <span class="literal">null</span>) &#123;</span><br><span class="line">                            typeAliasRegistry.registerAlias(clazz);</span><br><span class="line">                        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                            typeAliasRegistry.registerAlias(alias, clazz);</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123; <span class="comment">// 若类不存在，则抛出 BuilderException 异常</span></span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BuilderException</span>(<span class="string">&quot;Error registering typeAlias for &#x27;&quot;</span> + alias + <span class="string">&quot;&#x27;. Cause: &quot;</span> + e, e);</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>以上做了3件事：</p><ul><li>遍历<code>typeAlias</code>标签</li><li>匹配到<code>package</code>时，将整个包的类都注册为别名</li><li>否则直接注册类和别名，注册之前需要先检查一下类是否存在，不存在就跑出异常</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TypeAliasRegistry</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="keyword">private</span> <span class="keyword">final</span> Map&lt;String, Class&lt;?&gt;&gt; TYPE_ALIASES = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</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="comment">     * 另外，在 &#123;<span class="doctag">@link</span> org.apache.ibatis.session.Configuration&#125; 构造方法中，也有默认的注册</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TypeAliasRegistry</span><span class="params">()</span> &#123;</span><br><span class="line">        registerAlias(<span class="string">&quot;string&quot;</span>, String.class);</span><br><span class="line"></span><br><span class="line">        registerAlias(<span class="string">&quot;byte&quot;</span>, Byte.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;long&quot;</span>, Long.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;short&quot;</span>, Short.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;int&quot;</span>, Integer.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;integer&quot;</span>, Integer.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;double&quot;</span>, Double.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;float&quot;</span>, Float.class);</span><br><span class="line">        registerAlias(<span class="string">&quot;boolean&quot;</span>, Boolean.class);</span><br><span class="line">        ...</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，注册到<code>MyBatis</code>中的别名和类都存在一个叫<code>typeAliasRegistry</code>字段中，打开<code>TypeAliasRegistry</code>这个类，发现其持有一个以<code>String</code>为<code>key</code>，<code>Class&lt;?&gt;</code>为<code>value</code>的<code>hashmap</code>，而在构造方法中也初始化了一系列的<code>JDK</code>原生的对象类型，这也解释了我们不用自己动手再去注册这个基础对象类型。在注释中，说到在<code>Configuration</code>的构造方法中也有类似的注册，其中注册大多都是业务对象的别名。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">Configuration</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 注册到 typeAliasRegistry 中 begin ~~~~</span></span><br><span class="line">        typeAliasRegistry.registerAlias(<span class="string">&quot;JDBC&quot;</span>, JdbcTransactionFactory.class);</span><br><span class="line">        typeAliasRegistry.registerAlias(<span class="string">&quot;MANAGED&quot;</span>, ManagedTransactionFactory.class);</span><br><span class="line">        ...</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>这里还有个问题没解决，<code>MyBatis</code>时怎么处理没有声明<code>alias</code>这种情况的呢？比如下面这种场景</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">typeAliases</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">typeAlias</span> <span class="attr">type</span>=<span class="string">&quot;domain.blog.Author&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">typeAliases</span>&gt;</span></span><br></pre></td></tr></table></figure><p>点进<code>TypeAliasRegistry#registerAlias</code>方法则解释了对该情况的处理</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//alias为null的情况</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">registerAlias</span><span class="params">(Class&lt;?&gt; type)</span> &#123;</span><br><span class="line">        <span class="comment">// 默认为，简单类名,获取全路径类名的简称,比如， 全限定类名 xyz.coolblog.model.Author 的别名为 author。</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">alias</span> <span class="operator">=</span> type.getSimpleName();</span><br><span class="line">        <span class="comment">// 如果有注解，使用注册上的名字</span></span><br><span class="line">        <span class="type">Alias</span> <span class="variable">aliasAnnotation</span> <span class="operator">=</span> type.getAnnotation(Alias.class);</span><br><span class="line">        <span class="keyword">if</span> (aliasAnnotation != <span class="literal">null</span>) &#123;</span><br><span class="line">            alias = aliasAnnotation.value();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 注册类型与别名的注册表</span></span><br><span class="line">        registerAlias(alias, type);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">registerAlias</span><span class="params">(String alias, Class&lt;?&gt; value)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (alias == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeException</span>(<span class="string">&quot;The parameter alias cannot be null&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// issue #748</span></span><br><span class="line">        <span class="comment">// 转换成小写</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> alias.toLowerCase(Locale.ENGLISH);</span><br><span class="line">        <span class="keyword">if</span> (TYPE_ALIASES.containsKey(key) &amp;&amp; TYPE_ALIASES.get(key) != <span class="literal">null</span> &amp;&amp; !TYPE_ALIASES.get(key).equals(value)) &#123; <span class="comment">// 冲突，抛出 TypeException 异常</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeException</span>(<span class="string">&quot;The alias &#x27;&quot;</span> + alias + <span class="string">&quot;&#x27; is already mapped to the value &#x27;&quot;</span> + TYPE_ALIASES.get(key).getName() + <span class="string">&quot;&#x27;.&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        TYPE_ALIASES.put(key, value);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>MyBatis</code>对于<code>alias</code>为<code>null</code>的情况的处理方式是直接获取注册的这个类的<code>SimpleName</code>，之后再检查这个类中是否存在<code>alias</code>注解，有的话直接用声明的注解中的值，注册到<code>typeAliasRegistry</code>之前需要将<code>alias</code>全部小写。</p><h4 id="解析environments标签"><a href="#解析environments标签" class="headerlink" title="解析environments标签"></a>解析environments标签</h4><p>一般而言，我们通常在配置文件中定义<code>environment</code></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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">environments</span> <span class="attr">default</span>=<span class="string">&quot;development&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">environment</span> <span class="attr">id</span>=<span class="string">&quot;development&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">transactionManager</span> <span class="attr">type</span>=<span class="string">&quot;JDBC&quot;</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">&quot;...&quot;</span> <span class="attr">value</span>=<span class="string">&quot;...&quot;</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">transactionManager</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dataSource</span> <span class="attr">type</span>=<span class="string">&quot;POOLED&quot;</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">&quot;driver&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;driver&#125;&quot;</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">&quot;url&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;url&#125;&quot;</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">&quot;username&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;username&#125;&quot;</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">&quot;password&quot;</span> <span class="attr">value</span>=<span class="string">&quot;$&#123;password&#125;&quot;</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dataSource</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">environment</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">environments</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>environments</code>的中文解释是「环境配置」，在<code>MyBatis</code>的文档中特别强调了，<code>SqlSessionFactory</code>和<code>environment</code>是一对一关系，也就是说，对于一个数据库环境比如<code>MySQL</code>环境，需要一个<code>SqlSessionFactory</code>实例，但如果这时需要增加一个数据库环境（比如<code>Oracle</code>环境或者另外一台<code>MySQL</code>主机环境），则需要再添加一个<code>environment</code>，再添加一个<code>SqlSessionFactory</code>实例。所以不会出现说，一个<code>SqlSessionFactory</code>实例同时选择多个<code>environment</code>的情况。</p><blockquote><p>尽管可以配置多个环境，但每个 <code>SqlSessionFactory</code> 实例只能选择一种环境。<br>每个数据库对应一个 <code>SqlSessionFactory</code> 实例</p></blockquote><p>这样就解释了，在创建<code>SqlSessionFactory</code>实例时，可以通过直接将<code>environment</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"><span class="type">SqlSessionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlSessionFactoryBuilder</span>().build(reader, environment);</span><br><span class="line"><span class="type">SqlSessionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlSessionFactoryBuilder</span>().build(reader, environment, properties);</span><br></pre></td></tr></table></figure><p>而<code>environment</code>在<code>MyBatis</code> 中便是<code>org.apache.ibatis.mapping.Environment</code>这个类，<code>environmentsElement</code>方法做的事情就是，先找出<code>default</code>环境，遍历<code>environments</code>节点找出对应的环境配置，然后初始化<code>TransactionFactory</code>、<code>DataSourceFactory</code>、<code>DataSource</code> 这些实例，最后再构造出该环境并设置到<code>configuration</code>中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">environmentsElement</span><span class="params">(XNode context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span> (context != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// environment 属性非空，从 default 属性获得</span></span><br><span class="line">            <span class="keyword">if</span> (environment == <span class="literal">null</span>) &#123;</span><br><span class="line">                environment = context.getStringAttribute(<span class="string">&quot;default&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 遍历 XNode 节点</span></span><br><span class="line">            <span class="keyword">for</span> (XNode child : context.getChildren()) &#123;</span><br><span class="line">                <span class="comment">// 判断 environment 是否匹配</span></span><br><span class="line">                <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;id&quot;</span>);</span><br><span class="line">                <span class="keyword">if</span> (isSpecifiedEnvironment(id)) &#123;</span><br><span class="line">                    <span class="comment">// 解析 `&lt;transactionManager /&gt;` 标签，返回 TransactionFactory 对象</span></span><br><span class="line">                    <span class="type">TransactionFactory</span> <span class="variable">txFactory</span> <span class="operator">=</span> transactionManagerElement(child.evalNode(<span class="string">&quot;transactionManager&quot;</span>));</span><br><span class="line">                    <span class="comment">// 解析 `&lt;dataSource /&gt;` 标签，返回 DataSourceFactory 对象</span></span><br><span class="line">                    <span class="type">DataSourceFactory</span> <span class="variable">dsFactory</span> <span class="operator">=</span> dataSourceElement(child.evalNode(<span class="string">&quot;dataSource&quot;</span>));</span><br><span class="line">                    <span class="type">DataSource</span> <span class="variable">dataSource</span> <span class="operator">=</span> dsFactory.getDataSource();</span><br><span class="line">                    <span class="comment">// 创建 Environment.Builder 对象</span></span><br><span class="line">                    Environment.<span class="type">Builder</span> <span class="variable">environmentBuilder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Environment</span>.Builder(id)</span><br><span class="line">                            .transactionFactory(txFactory)</span><br><span class="line">                            .dataSource(dataSource);</span><br><span class="line">                    <span class="comment">// 构造 Environment 对象，并设置到 configuration 中</span></span><br><span class="line">                    configuration.setEnvironment(environmentBuilder.build());</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><h4 id="解析mappers标签"><a href="#解析mappers标签" class="headerlink" title="解析mappers标签"></a>解析mappers标签</h4><p>在<code>MyBatis</code> 中有两种配置文件：</p><ul><li>全局配置的<code>Configuration</code>文件</li><li>用于记录SQL语句的<code>Mapper</code>文件</li></ul><p>在<code>Configuration</code> 文件中，<code>mappers</code>标签的用处就是去声明具体执行的<code>SQL</code>语句记录在哪里。<code>mappers</code>标签支持4种声明方式：</p><ol><li>使用相对于类路径的资源引用：<code>&lt;mapper resource=&quot;abc.xml&quot;/&gt;</code></li><li>使用完全限定资源定位符（URL）:<code>&lt;mapper url=&quot;file:///abc.xml&quot;/&gt;</code></li><li>使用映射器接口实现类的完全限定类名:<code>&lt;mapperclass=&quot;abcMapper&quot;/&gt;</code></li><li>将包内的映射器接口实现全部注册为映射器:<code>&lt;package name=&quot;org.mybatis.builder&quot;/&gt;</code></li></ol><p>我的理解以上的声明方式可以分为两类：一类是引用XML文件，一类是引用<code>Mapper Interface</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">mapperElement</span><span class="params">(XNode parent)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span> (parent != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 遍历子节点</span></span><br><span class="line">            <span class="keyword">for</span> (XNode child : parent.getChildren()) &#123;</span><br><span class="line">                <span class="comment">// 如果是 package 标签，则扫描该包</span></span><br><span class="line">                <span class="keyword">if</span> (<span class="string">&quot;package&quot;</span>.equals(child.getName())) &#123;</span><br><span class="line">                    <span class="comment">// 获得包名</span></span><br><span class="line">                    <span class="type">String</span> <span class="variable">mapperPackage</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;name&quot;</span>);</span><br><span class="line">                    <span class="comment">// 添加到 configuration 中</span></span><br><span class="line">                    configuration.addMappers(mapperPackage);</span><br><span class="line">                <span class="comment">// 如果是 mapper 标签，</span></span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// 获得 resource、url、class 属性</span></span><br><span class="line">                    <span class="type">String</span> <span class="variable">resource</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;resource&quot;</span>);</span><br><span class="line">                    <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;url&quot;</span>);</span><br><span class="line">                    <span class="type">String</span> <span class="variable">mapperClass</span> <span class="operator">=</span> child.getStringAttribute(<span class="string">&quot;class&quot;</span>);</span><br><span class="line">                    <span class="comment">// 使用相对于类路径的资源引用</span></span><br><span class="line">                    <span class="keyword">if</span> (resource != <span class="literal">null</span> &amp;&amp; url == <span class="literal">null</span> &amp;&amp; mapperClass == <span class="literal">null</span>) &#123;</span><br><span class="line">                        ErrorContext.instance().resource(resource);</span><br><span class="line">                        <span class="comment">// 获得 resource 的 InputStream 对象</span></span><br><span class="line">                        <span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> Resources.getResourceAsStream(resource);</span><br><span class="line">                        <span class="comment">// 创建 XMLMapperBuilder 对象</span></span><br><span class="line">                        <span class="type">XMLMapperBuilder</span> <span class="variable">mapperParser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">XMLMapperBuilder</span>(inputStream, configuration, resource, configuration.getSqlFragments());</span><br><span class="line">                        <span class="comment">// 执行解析</span></span><br><span class="line">                        mapperParser.parse();</span><br><span class="line">                    <span class="comment">// 使用完全限定资源定位符（URL）</span></span><br><span class="line">                    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (resource == <span class="literal">null</span> &amp;&amp; url != <span class="literal">null</span> &amp;&amp; mapperClass == <span class="literal">null</span>) &#123;</span><br><span class="line">                        ErrorContext.instance().resource(url);</span><br><span class="line">                        <span class="comment">// 获得 url 的 InputStream 对象</span></span><br><span class="line">                        <span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> Resources.getUrlAsStream(url);</span><br><span class="line">                        <span class="comment">// 创建 XMLMapperBuilder 对象</span></span><br><span class="line">                        <span class="type">XMLMapperBuilder</span> <span class="variable">mapperParser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">XMLMapperBuilder</span>(inputStream, configuration, url, configuration.getSqlFragments());</span><br><span class="line">                        <span class="comment">// 执行解析</span></span><br><span class="line">                        mapperParser.parse();</span><br><span class="line">                    <span class="comment">// 使用映射器接口实现类的完全限定类名</span></span><br><span class="line">                    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (resource == <span class="literal">null</span> &amp;&amp; url == <span class="literal">null</span> &amp;&amp; mapperClass != <span class="literal">null</span>) &#123;</span><br><span class="line">                        <span class="comment">// 获得 Mapper 接口</span></span><br><span class="line">                        Class&lt;?&gt; mapperInterface = Resources.classForName(mapperClass);</span><br><span class="line">                        <span class="comment">// 添加到 configuration 中</span></span><br><span class="line">                        configuration.addMapper(mapperInterface);</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> <span class="title class_">BuilderException</span>(<span class="string">&quot;A mapper element may only specify a url, resource or class, but not more than one.&quot;</span>);</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><code>MyBatis</code>解析<code>mappers</code>标签时，会便利每一个<code>mapper</code>的节点，首先区分是否为<code>package</code>，若属性不为<code>package</code>则检测其他3种情况，根据每种情况再具体处理。<br>可以跟进<code>package</code>的代码往下看。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="keyword">void</span> <span class="title function_">addMapper</span><span class="params">(Class&lt;T&gt; type)</span> &#123;</span><br><span class="line">        <span class="comment">// 判断，必须是接口。</span></span><br><span class="line">        <span class="keyword">if</span> (type.isInterface()) &#123;</span><br><span class="line">            <span class="comment">// 已经添加过，则抛出 BindingException 异常</span></span><br><span class="line">            <span class="keyword">if</span> (hasMapper(type)) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BindingException</span>(<span class="string">&quot;Type &quot;</span> + type + <span class="string">&quot; is already known to the MapperRegistry.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">boolean</span> <span class="variable">loadCompleted</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// 添加到 knownMappers 中</span></span><br><span class="line">                knownMappers.put(type, <span class="keyword">new</span> <span class="title class_">MapperProxyFactory</span>&lt;&gt;(type));</span><br><span class="line">                <span class="comment">// It&#x27;s important that the type is added before the parser is run</span></span><br><span class="line">                <span class="comment">// otherwise the binding may automatically be attempted by the</span></span><br><span class="line">                <span class="comment">// mapper parser. If the type is already known, it won&#x27;t try.</span></span><br><span class="line">                <span class="comment">// 解析 Mapper 的注解配置</span></span><br><span class="line">                <span class="type">MapperAnnotationBuilder</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MapperAnnotationBuilder</span>(config, type);</span><br><span class="line">                parser.parse();</span><br><span class="line">                <span class="comment">// 标记加载完成</span></span><br><span class="line">                loadCompleted = <span class="literal">true</span>;</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                <span class="comment">// 若加载未完成，从 knownMappers 中移除</span></span><br><span class="line">                <span class="keyword">if</span> (!loadCompleted) &#123;</span><br><span class="line">                    knownMappers.remove(type);</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>以上做的事情时将<code>package</code>下的<code>interface</code>全部注册到<code>MapperRegistry</code>中，在<code>MapperRegistry</code>中持有<code>knownMappers</code>去存储这些<code>interface</code>的信息。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2019/12/25/15772037542887.jpg" alt="-w889"><br>到现在为止，只是处理了<code>interface</code>，那么真实映射的那些XML配置文件时在哪里处理的呢。可以跟进到<code>parser.parse();</code>中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">parse</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 判断当前 Mapper 接口是否应加载过。</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">resource</span> <span class="operator">=</span> type.toString();</span><br><span class="line">        <span class="keyword">if</span> (!configuration.isResourceLoaded(resource)) &#123;</span><br><span class="line">            <span class="comment">// 加载对应的 XML Mapper 文件</span></span><br><span class="line">            loadXmlResource();</span><br><span class="line">            <span class="comment">// 标记该 Mapper 接口已经加载过</span></span><br><span class="line">            configuration.addLoadedResource(resource);</span><br><span class="line">            <span class="comment">// 设置 namespace 属性</span></span><br><span class="line">            assistant.setCurrentNamespace(type.getName());</span><br><span class="line">            <span class="comment">// 解析 @CacheNamespace 注解</span></span><br><span class="line">            parseCache();</span><br><span class="line">            <span class="comment">// 解析 @CacheNamespaceRef 注解</span></span><br><span class="line">            parseCacheRef();</span><br><span class="line">            <span class="comment">// 遍历每个方法，解析其上的注解</span></span><br><span class="line">            Method[] methods = type.getMethods();</span><br><span class="line">            <span class="keyword">for</span> (Method method : methods) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">// issue #237</span></span><br><span class="line">                    <span class="keyword">if</span> (!method.isBridge()) &#123;</span><br><span class="line">                        <span class="comment">// 执行解析</span></span><br><span class="line">                        parseStatement(method);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IncompleteElementException e) &#123;</span><br><span class="line">                    <span class="comment">// 解析失败，添加到 configuration 中</span></span><br><span class="line">                    configuration.addIncompleteMethod(<span class="keyword">new</span> <span class="title class_">MethodResolver</span>(<span class="built_in">this</span>, method));</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="comment">// 解析待定的方法</span></span><br><span class="line">        parsePendingMethods();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>loadXmlResource()</code> 做的事情主要是对于<code>Mapper XML</code>文件的处理，具体怎么解析在下一篇文章中介绍。<code>loadedResources</code>用来存储已加载资源的信息。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2019/12/25/15772038493445.jpg" alt="-w917"><br>最后解析后的语句信息保存在<code>mappedStatements</code>中，<code>mappedStatements</code>是一个<code>HashMap</code>，<code>key</code>是<code>namespace.id</code> 的字符,具体执行的<code>SQL</code>语句信息就是<code>value</code>，在实际<code>debug</code>中，发现同时也存储了相同一份为<code>id</code>的<code>Entry</code>。不知道<code>MyBatis</code>为什么要这样做。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2019/12/25/15772808581776.jpg" alt="-w917"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>回过头来，我们再看如何去生成一个<code>SqlSessionFactory</code>，从<code>SqlSessionFactoryBuilder</code>的<code>build</code>方法中我们可以找到答案。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> SqlSessionFactory <span class="title function_">build</span><span class="params">(Reader reader, String environment, Properties properties)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 创建 XMLConfigBuilder 对象</span></span><br><span class="line">            <span class="type">XMLConfigBuilder</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">XMLConfigBuilder</span>(reader, environment, properties);</span><br><span class="line">            <span class="comment">// 执行 XML 解析</span></span><br><span class="line">            <span class="comment">// 创建 DefaultSqlSessionFactory 对象</span></span><br><span class="line">            <span class="keyword">return</span> build(parser.parse());</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">&quot;Error building SqlSession.&quot;</span>, e);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            ErrorContext.instance().reset();</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                reader.close();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">                <span class="comment">// Intentionally ignore. Prefer previous error.</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><code>build</code>方法做的事情就是读取配置文件XML转换为<code>Configuration</code>实例。其中最关键的两句代码代表来这个过程的两个阶段。</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2019/12/25/15772814984148.jpg" alt="-w915"></p><ol><li>创建 <code>XMLConfigBuilder</code> 对象，就是读取全局配置<code>XML</code>文件到<code>DOM</code>对象的过程，这个过程中包含对<code>XML</code>配置的校验。</li><li>从<code>DOM</code>对象到<code>Configuration</code>对象的过程，解析全局配置<code>XML</code>各个标签，以及解析<code>mapper</code>标签中声明的<code>mapper.xml</code>文件。</li></ol><h2 id="参考引用"><a href="#参考引用" class="headerlink" title="参考引用"></a>参考引用</h2><p><a href="https://mybatis.org/mybatis-3/zh/getting-started.html">MyBatis 文档</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文将会介绍&lt;code&gt;MyBatis&lt;/code&gt;配置文件解析部分的代码解读，从创建一个&lt;code&gt;SqlSessionFactory&lt;/code&gt;作为入口，引入MyBatis配置文件的说明。简要说明配置文件中常用标签的用法和说明，根据每个标签，详细介绍&lt;code&gt;MyBatis&lt;/code&gt;是如何解析这些标签的。&lt;/p&gt;
    
    </summary>
    
    
      <category term="框架" scheme="https://lazyallen.github.io/categories/%E6%A1%86%E6%9E%B6/"/>
    
    
      <category term="MyBatis" scheme="https://lazyallen.github.io/tags/MyBatis/"/>
    
      <category term="源码" scheme="https://lazyallen.github.io/tags/%E6%BA%90%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>当执行“Java -jar springbootapp.jar --server.port=8081” 命令后会发生什么</title>
    <link href="https://lazyallen.github.io/2019/11/21/spring-boot-properties-binder/"/>
    <id>https://lazyallen.github.io/2019/11/21/spring-boot-properties-binder/</id>
    <published>2019-11-20T16:00:00.000Z</published>
    <updated>2022-12-07T07:04:47.585Z</updated>
    
    <content type="html"><![CDATA[<p>本篇文章的起因是同事问我下面的<code>Jar</code>的启动命令中，- 和 –的参数有什么区别。下意识觉得区别是 在 <code>xxx.jar</code> 之前的参数叫做 <code>VM 参数</code> ，传入<code>JVM</code>中，而后面的参数 <code>Program 参数</code>，是传入<code>jar</code>中的，对应的就是<code>main(String[] args)</code>中的args数组，但是说到有什么别的明显区别我倒是说不上来。</p><span id="more"></span><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -Xloggc:/logs/governor-service-gc.log -verbose.gc -XX:+PrintGCDateStamps -javaagent:/agent/apm-javaagent.jar -Dskywalking.collector.backend_service=11.11.11.11:1111 -Dskywalking.agent.service_name=uat-mgp-governor-xxxx -Dskywalking.agent.authentication=c8fxxxx17c46bd -jar xxx-microservice.jar --ENV_PROFILE=uat --LIFE_CIRCLE=dev --server.port=11111 --eureka.instance.ip-address=$&#123;NODE_IP&#125; --eureka.instance.non-secure-port=$&#123;NODE_PORT_11111&#125; --jasypt.encryptor.password=xxx -Dsun.net.inetaddr.ttl=3</span><br></pre></td></tr></table></figure><p>由此，我心中也有了疑问：</p><ul><li>为什么 <code>Program 参数</code>的写法是 – </li><li>假如都是配置<code>server.port</code>，<code>VM 参数</code> 和 <code>Program 参数</code>的优先级是？</li><li><code>Spring Boot</code>是如何处理两者的呢？</li></ul><h2 id="VM-args-和-Program-args-有什么区别"><a href="#VM-args-和-Program-args-有什么区别" class="headerlink" title="VM args 和 Program args 有什么区别"></a>VM args 和 Program args 有什么区别</h2><p>如何启动一个Java的应用？首先我们可以在<a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BABDJJFI">Oracle的文档</a>中找到答案。</p><blockquote><p>java [options] -jar filename [args]</p><p>options:Command-line options separated by spaces. See Options.</p><p>args: The arguments passed to the main() method separated by spaces.</p></blockquote><p>JVM提供了<code>Standard Options</code>、<code>Non-Standard Options</code> 等6种不同的Options用作不同的场景。其中<code>Standard Options</code>是最常见的Options，例如 <code>-jar filename</code>、<code>-version</code>、<code>-help</code> 等Options。<code>-Dproperty=value</code> 也是<code>Standard Options</code>，文档中的用法解释是：</p><blockquote><p>Sets a system property value. The property variable is a string with no spaces that represents the name of the property. The value variable is a string that represents the value of the property. If value is a string with spaces, then enclose it in quotation marks (for example -Dfoo=”foo bar”).</p></blockquote><blockquote><p>设置一个系统属性值。属性变量是一个没有空格的字符串，代表属性的名称。value变量是一个表示属性值的字符串。如果value是一个带空格的字符串，则用引号括起来（例如-Dfoo=”foo bar”）</p></blockquote><p>对此，明确了一点，类似<code>-Dkey=value</code>的<code>VM参数</code>最后会成为<code>System properties</code>。</p><p>可以看到，在文档中并没有规定 <code>args</code> 的写法，那么为什么在案例中需要在参数前加上<code>--</code>呢。由于启动的是一个<code>Spring Boot</code>项目，尝试去<a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config">Spring Boot文档</a>中寻找答案。<br>在<code>4.2.2. Accessing Command Line Properties</code>中有说明，<code>Spring Boot</code>的参数均以<code>--</code> 开头，成为 <code>Command Line Properties</code>，作为外部化参数的一种。说到外部化参数，那是不是<code>Spring Boot</code>还有其他的外部化参数？<br><code>Spring Boot</code> 允许外部化配置，以便在不同的环境中使用相同的应用程序代码。可以使用属性文件、 YAML 文件、环境变量和命令行参数来外部化配置，也可以使用<code>@value</code>注释将属性值直接注入 bean，可以通过 Spring 的 <code>Environment</code> 抽象访问属性值，也可以通过<code>@configurationproperties</code>绑定到结构化对象。<br>对于这么多的外部化配置，就会存在相同的属性覆盖的情况，而<code>Spring Boot</code>按照17种不同的外部化配置规约了不同的优先级，这里引用和本篇文章相关的参数。</p><blockquote><p>4.Command line arguments<br>9.Java System properties(System.getProperties()).<br>10.OS environment variables.<br>15.Application properties packaged inside your jar (application.properties and YAML variants).<br>17.Default properties (specified by setting SpringApplication.setDefaultProperties).</p></blockquote><p>到这边答案就出来了，对于<code>java -Dkey=value -jar springboot.jar --key=value</code>这句启动命令：</p><ul><li>前者<code>-Dkey=value</code>会将<code>key:value</code>键值对写入System properties</li><li>后者<code>--key=value</code>属于Spring Boot特定的Command line arguments</li><li>以上两种参数对Spring Boot而言都是外置化参数，通常情况下，Command line arguments 的优先级显然比 System properties 和 配置文件 的优先级高</li></ul><h2 id="举个栗子"><a href="#举个栗子" class="headerlink" title="举个栗子"></a>举个栗子</h2><p>我们可以尝试去配置<code>server.port</code>这个参数去验证</p><ul><li>application.properties:server.port=8088</li><li>VM options= -Dserver.port=8081</li><li>Program arguments= –server.port=8082</li></ul><p>启动类如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.player.command.demo;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.ApplicationArguments;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.ApplicationRunner;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.core.env.Environment;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CommandDemoApplication</span> <span class="keyword">implements</span> <span class="title class_">ApplicationRunner</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> LoggerFactory.getLogger(CommandDemoApplication.class);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">SERVER_PORT</span> <span class="operator">=</span> <span class="string">&quot;server.port&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    Environment env;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="comment">//Customizing SpringApplication disable command args</span></span><br><span class="line"><span class="comment">//        SpringApplication app = new SpringApplication(CommandDemoApplication.class);</span></span><br><span class="line"><span class="comment">//        app.setAddCommandLineProperties(false);</span></span><br><span class="line"><span class="comment">//        app.run(args);</span></span><br><span class="line">        SpringApplication.run(CommandDemoApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">(ApplicationArguments args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        logger.info(<span class="string">&quot;COMMAND ARGS:args:&#123;&#125;&quot;</span>,args);</span><br><span class="line">        logger.info(<span class="string">&quot;COMMAND ARGS:SourceArgs:&#123;&#125;&quot;</span>,args.getSourceArgs());</span><br><span class="line">        logger.info(<span class="string">&quot;COMMAND ARGS:OptionNames:&#123;&#125;&quot;</span>,args.getOptionNames());</span><br><span class="line">        logger.info(<span class="string">&quot;COMMAND ARGS:NonOptionArgs:&#123;&#125;&quot;</span>,args.getNonOptionArgs());</span><br><span class="line">        logger.info(<span class="string">&quot;COMMAND ARGS:OptionValues for server.port :&#123;&#125;&quot;</span>,args.getOptionValues(SERVER_PORT));</span><br><span class="line">        logger.info(<span class="string">&quot;---------------------------------------&quot;</span>);</span><br><span class="line">        logger.info(<span class="string">&quot;SYSTEM PROPERTIES: server.port:&#123;&#125;&quot;</span>,System.getProperty(SERVER_PORT));</span><br><span class="line">        logger.info(<span class="string">&quot;---------------------------------------&quot;</span>);</span><br><span class="line">        logger.info(<span class="string">&quot;SPRING ENV:server.port:&#123;&#125;&quot;</span>,env.getProperty(SERVER_PORT));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里我们通过实现<code>ApplicationRunner</code>传入的<code>ApplicationArguments args</code> 就是<code>Program arguments</code>，启动后查看日志，可见最后应用使用的<code>port</code>为<code>8082</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">2020-09-17 23:34:02.392  INFO 6774 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8082 (http) with context path &#x27;&#x27;</span><br><span class="line">2020-09-17 23:34:02.400  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : Started CommandDemoApplication in 1.988 seconds (JVM running for 2.659)</span><br><span class="line">2020-09-17 23:34:02.401  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : COMMAND ARGS:args:org.springframework.boot.DefaultApplicationArguments@456abb66</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : COMMAND ARGS:SourceArgs:--server.port=8082</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : COMMAND ARGS:OptionNames:[server.port]</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : COMMAND ARGS:NonOptionArgs:[]</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : COMMAND ARGS:OptionValues for server.port :[8082]</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : ---------------------------------------</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : SYSTEM PROPERTIES: server.port:8081</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : ---------------------------------------</span><br><span class="line">2020-09-17 23:34:02.403  INFO 6774 --- [           main] c.l.p.c.demo.CommandDemoApplication      : SPRING ENV:server.port:8082</span><br><span class="line">2020-09-17 23:34:06.942  INFO 6774 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService &#x27;applicationTaskExecutor&#x27;</span><br></pre></td></tr></table></figure><h2 id="Think-more…"><a href="#Think-more…" class="headerlink" title="Think more…"></a>Think more…</h2><p>如果仔细看开头的案例，会发现<code>-Dsun.net.inetaddr.ttl=3</code>是作为<code>Program arguments</code> 传入应用的，那么这个参数会生效吗？按以上的说法，<code>Spring Boot</code>虽然可以接收这样的<code>Program arguments</code>，但由于其使用的是 <code>-D</code> 的写法，<code>Spring Boot</code>应该是<code>不认识</code>这个参数的。那么如果传入的是<code>--sun.net.inetaddr.ttl=3</code>会不会生效呢，笔者通过实验，发现的确通过获取<code>Spring</code>的环境参数，的确能获取到这个值，但从<code>System properties</code> 中则是null的。在<a href="https://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html#nct">文档</a>中，对于<code>sun.net.inetaddr.ttl</code>的描述是<code>This is a sun private system property</code>，这是一个私有的<code>System properties</code>，笔者认为虽然能从<code>Spring</code> 的环境参数中获取到这个值，但这个值应该是不会生效的，因为在<code>System properties</code>中这个值依旧是null。</p><p>所以，不能简单的认为<code>--key=value</code>和<code>-Dkey=value</code>是差不多的，在<code>Spring Boot</code>中，除了优先级不同之外，<code>--key=value</code>并不能去变相的代替<code>-Dkey=value</code>，比如说企图在<code>Program arguments</code>中传入类似 <code>--gc=xxx</code> 的GC参数的骚操作肯定是不行的。</p><p>那么问题又来了，为什么<code>--server.port</code>就可以去代替并覆盖<code>-Dserver.port</code>呢？解答这个问题，我们可以尝试去找出，<code>Spring Boot</code>内置<code>web容器</code>中的<code>port</code>是从哪里来的？</p><h2 id="Spring-Boot是如何处理-–args-参数的"><a href="#Spring-Boot是如何处理-–args-参数的" class="headerlink" title="Spring Boot是如何处理 –args 参数的"></a>Spring Boot是如何处理 –args 参数的</h2><p>通过Debug发现入口在<code>SpringApplication</code>的<code>run()</code>方法中，这里我们只展示关键入口和生效步骤。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">public</span> ConfigurableApplicationContext <span class="title function_">run</span><span class="params">(String... args)</span> &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">//在启动时，根据Program arguments 构造出DefaultApplicationArguments</span></span><br><span class="line"><span class="type">ApplicationArguments</span> <span class="variable">applicationArguments</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultApplicationArguments</span>(args);</span><br><span class="line"><span class="comment">//准备环境</span></span><br><span class="line"><span class="type">ConfigurableEnvironment</span> <span class="variable">environment</span> <span class="operator">=</span> prepareEnvironment(listeners, applicationArguments);</span><br><span class="line">configureIgnoreBeanInfo(environment);</span><br><span class="line"><span class="type">Banner</span> <span class="variable">printedBanner</span> <span class="operator">=</span> printBanner(environment);</span><br><span class="line">context = createApplicationContext();</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">configurePropertySources</span><span class="params">(ConfigurableEnvironment environment, String[] args)</span> &#123;</span><br><span class="line"><span class="type">MutablePropertySources</span> <span class="variable">sources</span> <span class="operator">=</span> environment.getPropertySources();</span><br><span class="line"><span class="keyword">if</span> (<span class="built_in">this</span>.defaultProperties != <span class="literal">null</span> &amp;&amp; !<span class="built_in">this</span>.defaultProperties.isEmpty()) &#123;</span><br><span class="line"><span class="comment">//可以看到，defaultProperties是放在最后的，优先级最低</span></span><br><span class="line">sources.addLast(<span class="keyword">new</span> <span class="title class_">MapPropertySource</span>(<span class="string">&quot;defaultProperties&quot;</span>, <span class="built_in">this</span>.defaultProperties));</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//this.addCommandLineProperties默认为true，但也可以配置为false，这时command line 就会失效</span></span><br><span class="line"><span class="keyword">if</span> (<span class="built_in">this</span>.addCommandLineProperties &amp;&amp; args.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">name</span> <span class="operator">=</span> CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;</span><br><span class="line"><span class="keyword">if</span> (sources.contains(name)) &#123;</span><br><span class="line">PropertySource&lt;?&gt; source = sources.get(name);</span><br><span class="line"><span class="type">CompositePropertySource</span> <span class="variable">composite</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CompositePropertySource</span>(name);</span><br><span class="line">composite.addPropertySource(</span><br><span class="line"><span class="keyword">new</span> <span class="title class_">SimpleCommandLinePropertySource</span>(<span class="string">&quot;springApplicationCommandLineArgs&quot;</span>, args));</span><br><span class="line">composite.addPropertySource(source);</span><br><span class="line">sources.replace(name, composite);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> &#123;</span><br><span class="line">sources.addFirst(<span class="keyword">new</span> <span class="title class_">SimpleCommandLinePropertySource</span>(args));</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">public</span> <span class="title function_">SimpleCommandLinePropertySource</span><span class="params">(String... args)</span> &#123;</span><br><span class="line">        <span class="comment">//可以看到SimpleCommandLineArgsParser是用来解析command line 的</span></span><br><span class="line"><span class="built_in">super</span>(<span class="keyword">new</span> <span class="title class_">SimpleCommandLineArgsParser</span>().parse(args));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//解析command line的逻辑</span></span><br><span class="line"><span class="keyword">public</span> CommandLineArgs <span class="title function_">parse</span><span class="params">(String... args)</span> &#123;</span><br><span class="line"><span class="type">CommandLineArgs</span> <span class="variable">commandLineArgs</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CommandLineArgs</span>();</span><br><span class="line"><span class="keyword">for</span> (String arg : args) &#123;</span><br><span class="line"><span class="keyword">if</span> (arg.startsWith(<span class="string">&quot;--&quot;</span>)) &#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">optionText</span> <span class="operator">=</span> arg.substring(<span class="number">2</span>);</span><br><span class="line">String optionName;</span><br><span class="line"><span class="type">String</span> <span class="variable">optionValue</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"><span class="type">int</span> <span class="variable">indexOfEqualsSign</span> <span class="operator">=</span> optionText.indexOf(<span class="string">&#x27;=&#x27;</span>);</span><br><span class="line"><span class="keyword">if</span> (indexOfEqualsSign &gt; -<span class="number">1</span>) &#123;</span><br><span class="line">optionName = optionText.substring(<span class="number">0</span>, indexOfEqualsSign);</span><br><span class="line">optionValue = optionText.substring(indexOfEqualsSign + <span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> &#123;</span><br><span class="line">optionName = optionText;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (optionName.isEmpty()) &#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;Invalid argument syntax: &quot;</span> + arg);</span><br><span class="line">&#125;</span><br><span class="line">commandLineArgs.addOptionArg(optionName, optionValue);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> &#123;</span><br><span class="line">commandLineArgs.addNonOptionArg(arg);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> commandLineArgs;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>大概流程是，在应用启动时，<code>Spring Boot</code>会根据默认的规则去解析<code>command line</code>，构造成<code>SimpleCommandLinePropertySource</code>，最后加入<code>Spring Environment</code>的<code>propertySources</code> 中。这里的<code>PropertySource</code>很重要，<code>PropertySource</code> 可以简单理解为配置数据源的抽象，上文所讲的各种外部配置都可以看作为数据源。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/09/18/20200918-at-1235-am.png" alt="2020-09-18 at 12.35 A"></p><h2 id="Tomcat的port是从哪里获取的"><a href="#Tomcat的port是从哪里获取的" class="headerlink" title="Tomcat的port是从哪里获取的"></a>Tomcat的port是从哪里获取的</h2><p>当启动<code>Spring Web Application</code>时，控制台通常都会打印出当前容器的<code>port</code>端口。例如：<code>o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8082 (http)</code> ，所以可以直接从<code>TomcatWebServer</code>这个类入手。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">initialize</span><span class="params">()</span> <span class="keyword">throws</span> WebServerException &#123;</span><br><span class="line">   <span class="comment">//从getPortsDescription(false)获得port </span></span><br><span class="line">logger.info(<span class="string">&quot;Tomcat initialized with port(s): &quot;</span> + getPortsDescription(<span class="literal">false</span>));</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getPort</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 可以看到是从protocolHandler中拿出来的，实际通常是Http11NioProtocol，而它又委托给了AbstractEndpoint</span></span><br><span class="line">        <span class="keyword">if</span> (protocolHandler <span class="keyword">instanceof</span> AbstractProtocol&lt;?&gt;) &#123;</span><br><span class="line">            <span class="keyword">return</span> ((AbstractProtocol&lt;?&gt;) protocolHandler).getPort();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// Fall back for custom protocol handlers not based on AbstractProtocol</span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">port</span> <span class="operator">=</span> getProperty(<span class="string">&quot;port&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (port <span class="keyword">instanceof</span> Integer) &#123;</span><br><span class="line">            <span class="keyword">return</span> ((Integer) port).intValue();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// Usually means an invalid protocol has been configured</span></span><br><span class="line">        <span class="keyword">return</span> -<span class="number">1</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"><span class="comment">     * Server socket port.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">port</span> <span class="operator">=</span> -<span class="number">1</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getPort</span><span class="params">()</span> &#123; <span class="keyword">return</span> port; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setPort</span><span class="params">(<span class="type">int</span> port )</span> &#123; <span class="built_in">this</span>.port=port; &#125;</span><br></pre></td></tr></table></figure><p>可以看到这里的<code>port</code>的默认值是<code>-1</code>，在<code>Spring Boot</code>中，当你想维持一个<code>WebApplicationContext</code>，但又不想处理任何连接，就可以通过将<code>port</code>设置为<code>-1</code>去实现。看到这里笔者发现并没有其他地方引用了<code>setPort()</code>这个方法去设置这个值，这里有个技巧是可以在<code>setPort()</code>上打个断点，然后通过调用栈查看调用的上下文。发现这个参数是从<code>ServerProperties</code>中获取的。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/09/18/20200918-at-809-pm.png" alt="2020-09-18 at 8.09 P"></p><h2 id="Spring-Boot的配置绑定"><a href="#Spring-Boot的配置绑定" class="headerlink" title="Spring Boot的配置绑定"></a>Spring Boot的配置绑定</h2><p><code>ServerProperties</code> 从字面含义是服务参数配置，这个类存在于<code>Spring Boot autoconfigure</code> 项目中，在这个项目中有许多其他的<code>Properties</code>类。<code>Spring Boot</code>特性之一就是为了简化开发，为此提供了一系列的通用配置给到开发者配置，真正做到开箱即用，在<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties">Common Application properties</a>可以查看具体配置列表。<br>到这里，我们只要关注的问题就变成了，<code>ServerProperties</code> 中<code>port</code>参数是如何设置的，答案是<code>Spring Boot</code>的配置绑定过程。<code>Spring Boot</code>提供了一系列的参数配置，开发者只需要简单在配置文件中配置一行配置，这行配置的含义就可以生效，肯定其中<code>Spring Boot</code>在背后做了一些事情，而这个过程就是配置绑定。通俗的可以认为，这个过程就是<code>Spring Boot</code>帮你把配置文件或其他外部化配置的参数 一一映射绑定到 对应的<code>Properties</code>中，比如<code>server.port</code>对应的就是<code>ServerProperties</code>的<code>port</code>参数。这里笔者简要分析入口和关键部分逻辑，感兴趣可以自行Debug。<br>首先在<code>ConfigurationProperties</code>的注解上可以猜到入口是<code>ConfigurationPropertiesBindingPostProcessor</code>。<code>ConfigurationPropertiesBindingPostProcessor</code>实现了<code>BeanPostProcessor</code>接口，重写了<code>postProcessBeforeInitialization()</code>，这个方法就是配置绑定的入口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">postProcessBeforeInitialization</span><span class="params">(Object bean, String beanName)</span> <span class="keyword">throws</span> BeansException &#123;</span><br><span class="line">bind(ConfigurationPropertiesBean.get(<span class="built_in">this</span>.applicationContext, bean, beanName));</span><br><span class="line"><span class="keyword">return</span> bean;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> Object <span class="title function_">bindDataObject</span><span class="params">(ConfigurationPropertyName name, Bindable&lt;?&gt; target, BindHandler handler,</span></span><br><span class="line"><span class="params">Context context, <span class="type">boolean</span> allowRecursiveBinding)</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (isUnbindableBean(name, target, context)) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line">Class&lt;?&gt; type = target.getType().resolve(Object.class);</span><br><span class="line"><span class="keyword">if</span> (!allowRecursiveBinding &amp;&amp; context.isBindingDataObject(type)) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//函数式接口，延迟执行</span></span><br><span class="line"><span class="type">DataObjectPropertyBinder</span> <span class="variable">propertyBinder</span> <span class="operator">=</span> (propertyName, propertyTarget) -&gt; bind(name.append(propertyName),</span><br><span class="line">propertyTarget, handler, context, <span class="literal">false</span>, <span class="literal">false</span>);</span><br><span class="line"><span class="keyword">return</span> context.withDataObject(type, () -&gt; &#123;</span><br><span class="line"><span class="keyword">for</span> (DataObjectBinder dataObjectBinder : <span class="built_in">this</span>.dataObjectBinders) &#123;</span><br><span class="line">     <span class="comment">//  真正执行的逻辑，可以看到上面的函数也传入进来了,这里</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">instance</span> <span class="operator">=</span> dataObjectBinder.bind(name, target, context, propertyBinder);</span><br><span class="line"><span class="keyword">if</span> (instance != <span class="literal">null</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> instance;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">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="keyword">private</span> &lt;T&gt; <span class="type">boolean</span> <span class="title function_">bind</span><span class="params">(BeanSupplier&lt;T&gt; beanSupplier, DataObjectPropertyBinder propertyBinder,</span></span><br><span class="line"><span class="params">BeanProperty property)</span> &#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">propertyName</span> <span class="operator">=</span> property.getName();</span><br><span class="line"><span class="type">ResolvableType</span> <span class="variable">type</span> <span class="operator">=</span> property.getType();</span><br><span class="line">Supplier&lt;Object&gt; value = property.getValue(beanSupplier);</span><br><span class="line">Annotation[] annotations = property.getAnnotations();</span><br><span class="line"><span class="comment">//真正执行绑定的逻辑</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">bound</span> <span class="operator">=</span> propertyBinder.bindProperty(propertyName,</span><br><span class="line">Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));</span><br><span class="line"><span class="keyword">if</span> (bound == <span class="literal">null</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (property.isSettable()) &#123;</span><br><span class="line">property.setValue(beanSupplier, bound);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (value == <span class="literal">null</span> || !bound.equals(value.get())) &#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;No setter found for property: &quot;</span> + property.getName());</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>DataObjectBinder</code>接口有两个实现，其中跟进到<code>JavaBeanBinder#bind()</code>方法中，这里的逻辑不通过Debug根本发现不了执行逻辑。通过Debug发现在<code>bind()</code>方法中传入的<code>propertyBinder</code>中间接持有<code>PropertySources</code>参数，走到这边大概能猜到其实配置绑定的数据源就是<code>PropertySources</code>，会根据优先级从<code>PropertySources</code>中设置对应的配置参数。在引用中有一篇更为详细的Debug博客值得阅读。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/09/18/20200918-at-842-pm.png" alt="2020-09-18 at 8.42 P"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过以上，可以确定几点：</p><ul><li><code>Spring Boot</code>在应用启动时会维护一个<code>PropertySources</code> 变量存储所有外部化配置的信息；</li><li>同时在配置绑定的过程中，会从<code>PropertySources</code> 中根据优先级获取对应的配置参数，作为有效的配置，最后绑定在<code>*Properties</code>类的属性。</li><li>内嵌的<code>tomcat</code>容器在启动时，<code>port</code>信息也是从<code>ServerProperties</code>中获取的，理所当然使用的也是根据优先级顺序得到的有效<code>port</code>值。</li></ul><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><ul><li><a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BABDJJFI">Launches a Java application.</a></li><li><a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config">Externalized Configuration</a></li><li><a href="https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties">Common Application properties</a></li><li><a href="https://www.xiefayang.com/2019/04/01/Spring%20Boot%20%E9%85%8D%E7%BD%AE%E7%BB%91%E5%AE%9A%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/">Spring Boot 2.0源码解析-配置绑定</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本篇文章的起因是同事问我下面的&lt;code&gt;Jar&lt;/code&gt;的启动命令中，- 和 –的参数有什么区别。下意识觉得区别是 在 &lt;code&gt;xxx.jar&lt;/code&gt; 之前的参数叫做 &lt;code&gt;VM 参数&lt;/code&gt; ，传入&lt;code&gt;JVM&lt;/code&gt;中，而后面的参数 &lt;code&gt;Program 参数&lt;/code&gt;，是传入&lt;code&gt;jar&lt;/code&gt;中的，对应的就是&lt;code&gt;main(String[] args)&lt;/code&gt;中的args数组，但是说到有什么别的明显区别我倒是说不上来。&lt;/p&gt;
    
    </summary>
    
    
      <category term="源码" scheme="https://lazyallen.github.io/categories/%E6%BA%90%E7%A0%81/"/>
    
    
      <category term="Spring Boot" scheme="https://lazyallen.github.io/tags/Spring-Boot/"/>
    
  </entry>
  
  <entry>
    <title>从String类的不可变谈到分布式一致性</title>
    <link href="https://lazyallen.github.io/2019/10/11/state-in-code-design-and-service-design/"/>
    <id>https://lazyallen.github.io/2019/10/11/state-in-code-design-and-service-design/</id>
    <published>2019-10-10T16:00:00.000Z</published>
    <updated>2022-12-07T07:05:00.139Z</updated>
    
    <content type="html"><![CDATA[<p>本篇文章记录的是，在程序设计和服务层面，我对于「状态」的理解，状态就像「资本主义」，口里说的不想要，现实中又摆脱不掉。</p><span id="more"></span><h2 id="有状态对象和无状态对象：String是否线程安全"><a href="#有状态对象和无状态对象：String是否线程安全" class="headerlink" title="有状态对象和无状态对象：String是否线程安全"></a>有状态对象和无状态对象：String是否线程安全</h2><p>通常，我们会讨论StringBuffer是线程安全的，而StringBuilder是线程不安全的。那么String本身是否是线程安全呢？<br>我认为String类是线程安全的，因为String是一个不可变对象（Immutable Object），不可变对象天生支持线程安全。在「Effective Java」中，对于不可变对象的解释如下：</p><blockquote><p>不可变对象(Immutable Object)：对象一旦被创建后，对象所有的状态及属性在其生命周期内不会发生任何变化。</p></blockquote><p>那么String是如何体现它的不可变的呢，因为String的本质是一个不可变的char数组。从定义可以看出：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">String</span></span><br><span class="line">    <span class="keyword">implements</span> <span class="title class_">java</span>.io.Serializable, Comparable&lt;String&gt;, CharSequence &#123;</span><br><span class="line">    <span class="comment">/** The value is used for character storage. */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">char</span> value[];</span><br><span class="line">    ...</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>也就是说，每次new一个String的时候，总是会在JVM的堆中开辟一块内存去存一个不可变的数组。那么对String进行写操作时，是如何处理的呢。比如在String的substring方法可以看到。</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="keyword">public</span> String <span class="title function_">substring</span><span class="params">(<span class="type">int</span> beginIndex)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (beginIndex &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StringIndexOutOfBoundsException</span>(beginIndex);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">int</span> <span class="variable">subLen</span> <span class="operator">=</span> value.length - beginIndex;</span><br><span class="line">        <span class="keyword">if</span> (subLen &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StringIndexOutOfBoundsException</span>(subLen);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> (beginIndex == <span class="number">0</span>) ? <span class="built_in">this</span> : <span class="keyword">new</span> <span class="title class_">String</span>(value, beginIndex, subLen);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>调用substring方法最后返回的是new了一个新的String实例。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">String</span><span class="params">(<span class="type">char</span> value[], <span class="type">int</span> offset, <span class="type">int</span> count)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (offset &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StringIndexOutOfBoundsException</span>(offset);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (count &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (count &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StringIndexOutOfBoundsException</span>(count);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (offset &lt;= value.length) &#123;</span><br><span class="line">                <span class="built_in">this</span>.value = <span class="string">&quot;&quot;</span>.value;</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// Note: offset or count might be near -1&gt;&gt;&gt;1.</span></span><br><span class="line">        <span class="keyword">if</span> (offset &gt; value.length - count) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">StringIndexOutOfBoundsException</span>(offset + count);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>.value = Arrays.copyOfRange(value, offset, offset+count);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>在String的构造方法可以看到，每次new一个String实例，调用的是Arrays.copyOfRange方法，这是一个native方法，作用就是拷贝内存块。<br><strong>String里面的写操作最后返回的都是在内存中重新开辟了一块内存地址，而并不是在原有的地址上进行写操作。</strong> 通俗一点讲就是，把String实例比作是一块画板，写操作比作是在画板上画画，对于String而言，每次写操作都是重新换了一块画板并在新画板上画画，而上一块画板永远保持它的样子，写操作时在新画板上发生的。每一块画板永远保持的一个样子，不会改变，这就是String的不可变性。反过来说，假如我们所有的写操作都是在同一块画板上进行的，那么它就是可变的，因为在它的生命周期里面，变化随时都会发生。<br>当说一个东西不可变的时，也可以说它是没有状态的（Stateless）<br>在<a href="http://ifeve.com/thread-safety-and-immutability/">线程安全及不可变性</a>中提供了一个不可变类的例子。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ImmutableValue</span>&#123;</span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> <span class="variable">value</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">ImmutableValue</span><span class="params">(<span class="type">int</span> value)</span>&#123;</span><br><span class="line"><span class="built_in">this</span>.value = value;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getValue</span><span class="params">()</span>&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">this</span>.value;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个不可变类有几个特点：</p><ul><li>成员变量value是通过构造函数赋值的</li><li>没有set方法<br>以上保证了实例化的对象是没有公开的方法去修改它本身的。</li></ul><p>当多个线程同时访问同一个资源，并且其中的一个或者多个线程对这个资源进行了写操作，才会产生<a href="http://ifeve.com/race-conditions-and-critical-sections">竞态条件</a>多个线程同时读同一个资源不会产生竞态条件。关键点在于对资源进行写操作后会改变状态，比如自增操作，在并发环境下，很难保证线程读到的永远是最新的状态，有可能由于线程切换和缓存不一致，而共享资源也是有状态的，就会出现两个线程读到的值是一样的情况（类似画板，两个画家在画画的时候均以为当前的画板是最新的，在同一个画板上同时画了两笔，恰好两笔都画在了一个地方，但在外人看来画板上只是添加了一笔而已），这就是线程不安全。那如何解决这个问题呢，只要解决一个关键问题就可以：<code>保证画家画画的时候永远看到的是最新的画作</code>，有两种方案：</p><ul><li>线程同步共享资源的状态：从画家入手，保证同一时刻只有一位画家在画画</li><li>将共享资源去状态化：画家得到的画板永远都是基于上一份画板拷贝而成的，对于所有的画家而言，他们任何时刻看到的画板都是最新的<br>而我们以上讲的不可变对象，采用的就是方案2，方案2在计算机设计领域有另外一个名字：写时复制（Copy-on-write，简称COW）。</li></ul><blockquote><p>写入时复制（英语：Copy-on-write，简称COW）是一种计算机程序设计领域的优化策略。其核心思想是，如果有多个调用者（callers）同时请求相同资源（如内存或磁盘上的数据存储），他们会共同获取相同的指针指向相同的资源，直到某个调用者试图修改资源的内容时，系统才会真正复制一份专用副本（private copy）给该调用者，而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的（transparently）。此作法主要的优点是如果调用者没有修改该资源，就不会有副本（private copy）被创建，因此多个调用者只是读取操作时可以共享同一份资源。</p></blockquote><p>String 类的设计包含了COW的思想，除此之外，在Java语言中，<code>安全失败</code>的本质其实也是COW思想。<br>要说到<code>安全失败</code>，我们先谈什么是<code>快速失败</code>，两者均是Collection集合类中的概念：</p><blockquote><p>fail-fast机制：当遍历一个集合对象时，如果集合对象的结构被修改了，就会抛出ConcurrentModificationExcetion异常。</p></blockquote><p>以ArrayList为例，简单说ArrayList继承自AbstractList类，AbstractList内部有一个字段modCount，代表修改的次数。ArrayList类的add、remove操作都会使得modCount自增。当使用ArrayList.iterator()返回一个迭代器对象时。迭代器对象有一个属性expectedModCount，它被赋值为该方法调用时modCount的值。这意味着，这个值是modCount在这个时间点的快照值，expectedModCount值在iterator对象内部不会再发送变化，<a href="https://segmentfault.com/a/1190000016694292">具体可以阅读这篇文章</a>。调用next()迭代会比照两个值是否一致，否则丢出异常。<br>显然，modCount就是ArrayList的状态，而expectedModCount就是ArrayList的状态快照，在并发环境上，我们希望共享资源是不变的（共享资源有状态会带来线程不安全）。为什么在并发环境下推荐使用iterator.remove()而非list.remove()的原因就是，iterator.remove()会同步更新expectedModCount的值与modCount保持一致，而list.remove()只会更新modCount的值，expectedModCount没有同步更新，所以才会丢出异常。<br>除此之外，你可能会想到，那我把COW思想运用在容器里面，是不是也能保证容器的并发安全。是的，fail-safe安全失败概念本质就是COW思想，具体我们不在这里展开。</p><p>我们再回来不可变对象的讨论上来，至此，可以得出一个结论：不可变对象是天生支持线程安全的。</p><h2 id="有状态服务和无状态服务：购物车功能的实现方案"><a href="#有状态服务和无状态服务：购物车功能的实现方案" class="headerlink" title="有状态服务和无状态服务：购物车功能的实现方案"></a>有状态服务和无状态服务：购物车功能的实现方案</h2><blockquote><p>无状态服务（stateless service）对单次请求的处理，不依赖其他请求，也就是说，处理一次请求所需的全部信息，要么都包含在这个请求里，要么可以从外部获取到（比如说数据库），服务器本身不存储任何信息<br>有状态服务（stateful service）则相反，它会在自身保存一些数据，先后的请求是有关联的.</p></blockquote><p>为了好理解，举一个购物车功能的设计例子：在电商网站购物时，用户可以把自己想买的物品放入购物车。之后在某一个时间统一下单结账。购物车是有状态的，这一秒我把一本书加入了购物车，下一秒我可能就不要了。实现这个功能最常见的两种方案就是Seesion和Cookies去存储状态，那么这两种方案有什么区别呢。<br>我们假设Cookes永远不过期（现实一般不会这样做），在单机架构下，两者并无明显差异。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/16/you-zhuang-tai-he-wu-zhuang-tai.png" alt="有状态和无状态"><br>将购物车的信息存储在服务端，那么这个服务就是有状态的，购物车信息存储在Client端，服务并不保存任何状态，称为无状态服务。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/16/you-zhuang-tai-he-wu-zhuang-tai.jpg" alt="有状态和无状态"></p><p>但在集群架构中，对于无状态服务，横向扩展非常方便，而有状态服务则需要考虑同步状态的问题。如上图，当<code>stateful service2</code> 扩展到集群中时，需要将其他两台机的session同步到<code>stateful service2</code> 中，不然就可能出现购物车有东西的用户请求到<code>stateful service2</code>后，购物车被「清空」的情况。<br>当然，实际情况中对有状态服务进行扩展也有其他的方式，比如在负载均衡时将用户的IP和目标服务器绑定，用户所有的请求都由一台机器处理；或者使用「共享seesion」，其本质也只是将状态剥离到了其他服务中。<br>所以，无状态服务对横行扩展是友好的。</p><h2 id="CAP理论"><a href="#CAP理论" class="headerlink" title="CAP理论"></a>CAP理论</h2><p>但现实世界中，几乎所有的场景都是有状态的：</p><ul><li>买火车票：一个人买到车票的时段是应该完全分开的；</li><li>银行存钱：存进去1块钱，账户余额就必须加1；</li><li>秒杀场景：商品数量卖光了，不能出现超卖情况；</li></ul><p>假如以上场景只是在一台单机上运行，状态维持在一个地方。而如果是分布式集群架构，状态的一致性则必须保证。<br>在理论计算机科学中，CAP理论指出，对于一个分布式系统来说，不能同时满足三个性质：</p><ul><li>一致性（Consistency） （等同于所有节点访问同一份最新的数据副本）</li><li>可用性（Availability）（每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据）</li><li>分区容错性（Partition tolerance）（以实际效果而言，分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性，就意味着发生了分区的情况，必须就当前操作在C和A之间做出选择[3]。）</li></ul><p>比如有一个服务在全球有5个节点，正常情况下，节点互相通信保持状态一致。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/16/15840822157854.jpg" alt="-w611"></p><p>突然有一天由于不可抗拒力，中美之间的网络不可互相通信了。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/16/15840823118138.jpg" alt="-w598"></p><p>这是，对于整个系统来说，就是发生了分区，为了保证系统可以继续提供服务，有两个选择：</p><ul><li>保证一致性：比如停掉China的两个节点，牺牲了中国地区用户的可用性；</li><li>保证可用性：继续提供服务，但这时两个分区的数据会出现不一致的情况；</li></ul><p>CAP理论指出的观点是：对于一个分布式系统，如果一致性如果达到100%，那么可用性只能接近100%，反之亦然。</p><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><ul><li><a href="https://www.cnblogs.com/dolphin0520/p/10693891.html">深入理解Java中的不可变对象</a></li><li><a href="http://ifeve.com/thread-safety-and-immutability/">线程安全及不可变性</a></li><li><a href="https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD">写入时复制</a></li><li><a href="https://segmentfault.com/a/1190000016694292">快速失败与安全失败</a></li><li><a href="https://www.iteye.com/topic/960532">精通有状态vs无状态(Stateful vs Stateless)—Immutable模式之姐妹篇</a></li><li><a href="http://liaojiacan.me/2019/05/27/%E3%80%90%E9%AB%98%E5%8F%AF%E7%94%A8%E6%9E%B6%E6%9E%84%E3%80%91%E7%90%86%E8%A7%A3%E6%9C%89%E7%8A%B6%E6%80%81%E6%9C%8D%E5%8A%A1%E5%92%8C%E6%97%A0%E7%8A%B6%E6%80%81%E6%9C%8D%E5%8A%A1/index.html">理解有状态服务和无状态服务</a></li><li><a href="https://www.sofastack.tech/projects/sofa-jraft/overview/">一致性状态机</a></li><li><a href="https://zh.wikipedia.org/wiki/%E7%8A%B6%E6%80%81%E6%9C%BA%E5%A4%8D%E5%88%B6">状态机复制</a></li><li><a href="https://www.infoq.cn/article/raft-paper">Raft 一致性算法论文译文</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本篇文章记录的是，在程序设计和服务层面，我对于「状态」的理解，状态就像「资本主义」，口里说的不想要，现实中又摆脱不掉。&lt;/p&gt;
    
    </summary>
    
    
      <category term="杂谈" scheme="https://lazyallen.github.io/categories/%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="分布式" scheme="https://lazyallen.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>XXL-JOB 分布式任务调度平台调研使用分析</title>
    <link href="https://lazyallen.github.io/2019/09/24/xxl-job-survey/"/>
    <id>https://lazyallen.github.io/2019/09/24/xxl-job-survey/</id>
    <published>2019-09-23T16:00:00.000Z</published>
    <updated>2022-12-07T07:05:53.739Z</updated>
    
    <content type="html"><![CDATA[<p>对于XXL-JOB 的使用和功能介绍在其<a href="https://www.xuxueli.com/xxl-job">文档</a>中非常清晰，且非常容易上手，本篇文章是把官方文档众多内容简单梳理后而成，目的在于<strong>尽可能</strong>简单清晰用通俗化的语言把XXL-JOB 工具介绍给一个小白用户。</p><span id="more"></span><h2 id="什么是分布式任务调度"><a href="#什么是分布式任务调度" class="headerlink" title="什么是分布式任务调度"></a>什么是分布式任务调度</h2><p>在日常开发中，可能会碰到类似「每天定时推送一些业务数据通知到用户」的场景，这时能想到最简单的解决方案时用JDK定时任务的Timer。但随业务发展，单 体应用不仅有挂掉的风险，且不一定能Hold大量的业务数据。这时最简单的方法就是加机器（横向扩展），这也带来了很多问题。</p><ul><li>分布式：怎么保证任务正确的执行：比如5台机器，同一时间只要1台机器执行</li><li>故障处理：机器在执行任务的时候挂掉了怎么办？</li><li>任务管理：如果有几千个任务该如何管理？</li><li>日志监控：怎么知道任务的执行情况？<br>等等多个问题。<br>而分布式调度平台的产生就是去解决以上问题。目前比较知名的几个分布式任务调度平台有：Quartz、ElasticJob、LTS(Light Task Scheduler)、XXL-JOB.<h2 id="XXL-JOB-简介"><a href="#XXL-JOB-简介" class="headerlink" title="XXL-JOB 简介"></a>XXL-JOB 简介</h2><blockquote><p>XXL-JOB是一个分布式任务调度平台，其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线，开箱即用。<br>该工具的开发者是来自美团点评的许雪里，XXL是作者名的缩写而成。</p></blockquote><h2 id="亮点"><a href="#亮点" class="headerlink" title="亮点"></a>亮点</h2>文档介绍几十个特性，这里我摘选几个比较重要的亮点特性：</li><li>中心化架构：和ElasticJob和LTS去中心化架构不同，XXL-JOB是中心化架构，由调度中心和执行器组成。</li><li>后台管理WEB：管理页面可以管理执行器、CRUD任务、查看调度日志和任务日志</li><li>Rolling实时日志：支持在线查看调度结果，并且支持以Rolling方式实时查看执行器输出的完整的执行日志；</li><li>GLUE：提供Web IDE，支持在线开发任务逻辑代码，动态发布，实时编译生效，省略部署上线的过程。支持30个版本的历史版本回溯。</li><li>任务依赖：支持配置子任务依赖，当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔；</li><li>路由策略：执行器集群部署时提供丰富的路由策略，包括：第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等；</li><li>阻塞处理策略：调度过于密集执行器来不及处理时的处理策略，策略包括：单机串行（默认）、丢弃后续调度、覆盖之前调度；</li><li>全异步：任务调度流程全异步化设计实现，如异步调度、异步运行、异步回调等，有效对密集调度进行流量削峰，理论上支持任意时长任务的运行；<h2 id="运行部署的几个步骤"><a href="#运行部署的几个步骤" class="headerlink" title="运行部署的几个步骤"></a>运行部署的几个步骤</h2>文档中的「<a href="https://www.xuxueli.com/xxl-job/#%E4%BA%8C%E3%80%81%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8">快速入门</a>」已经非常具体了。大概分为</li></ul><ol><li>初始化调度数据库</li><li>部署调度中心集群：xxl-job-admin</li><li>部署执行器集群：xxl-job-core。可以直接使用，也可以将现有项目改造为执行器，只要引入<code>xxl-job-core</code>的Maven依赖即可。</li><li>登录调度中心访问地址，管理配置。<br>注意：本篇运行部署的版本是v2.12<h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809285614844.jpg" alt="-w450"><br>从架构图上理解，XXL-JOB分为两个部分，同时也对应两个项目。</li></ol><ul><li>调度中心（xxl-job-admin）<ul><li>执行器管理</li><li>任务管理</li><li>日志管理</li><li>其他</li></ul></li><li>执行器（xxl-job-core）<ul><li>主动注册</li><li>任务执行</li><li>任务回调</li><li>日志服务<br>同时，从架构图也可以看出来基本的工作执行流程如下：<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809286832223.jpg" alt="-w450"></li></ul></li></ul><ol><li>任务执行器根据配置的调度中心的地址，自动注册到调度中心</li><li>达到任务触发条件，调度中心下发任务</li><li>执行器基于线程池执行任务，并把执行结果放入内存队列中、把执行日志写入日志文件中</li><li>执行器的回调线程消费内存队列中的执行结果，主动上报给调度中心</li><li>当用户在调度中心查看任务日志，调度中心请求任务执行器，任务执行器读取任务日志文件并返回日志详情<h2 id="统一术语"><a href="#统一术语" class="headerlink" title="统一术语"></a>统一术语</h2></li></ol><ul><li>调度中心：负责管理、调度、监控任务的全周期。可以理解为所有执行器的老大，所有的执行器都有老大调遣；</li><li>执行器：可以理解为具体干活的一类小弟，小弟要主动去认老大（自动注册），老大发出指令后执行（任务执行），执行后反馈任务执行结果（回调线程），同时也提供给老大查询任务执行过程记录的能力（日志服务）；</li><li>任务：一件事情，由一个或一组执行器执行；</li><li>调度：老大交给小弟办一件事的具体描述；<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809789864403.jpg" alt="-w450"></li></ul><h2 id="注册方式：自动注册和手动录入"><a href="#注册方式：自动注册和手动录入" class="headerlink" title="注册方式：自动注册和手动录入"></a>注册方式：自动注册和手动录入</h2><p>在调度中心的执行器管理页面，提供了两种执行器注册方式。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809767494257.jpg" alt="-w450"><br>这里需要明白4个字段的含义：</p><ul><li>执行器：这里的执行器实际指的是「执行器集群」的概念，并非单个机器节点，同时这也是执行任务的基本单位，也就是说调度中心并不能「直接」指定某台机器去执行任务，但可以「间接」通过定义任务路由的方式实现；</li><li>AppName:「执行器集群」的唯一标志，每个执行器机器集群的唯一标示, 任务注册以 “执行器” 为最小粒度进行注册; 每个任务通过其绑定的执行器可感知对应的执行器机器列表;</li><li>注册方式：分为自动和手动，如果是自动就不用填机器地址，同时执行器配置要必填调度中心地址和AppName用于主动注册；如果是手动，需要填写机器地址；</li><li>机器地址：具体执行任务的机器节点。一个AppName维护着包含多个机器节点的列表；<br>无论是自动注册还是手动注册，都需要用户在执行器管理页面声明一个执行器，这里我们着重说一下自动注册的配置和流程。<h3 id="主动注册配置说明："><a href="#主动注册配置说明：" class="headerlink" title="主动注册配置说明："></a>主动注册配置说明：</h3></li><li>首先需要在执行器管理页面新建一个注册方式为「自动注册」的执行器；</li><li>部署执行器时需要填写<code>xxl.job.admin.addresses</code>和<code>xxl.job.executor.appname</code>两个配置字段表明开启自动注册，这两个字段时选填的，如果为空则表明关闭自动注册；<h3 id="执行器注册和摘除流程说明："><a href="#执行器注册和摘除流程说明：" class="headerlink" title="执行器注册和摘除流程说明："></a>执行器注册和摘除流程说明：</h3>任务注册的心跳周期Beat默认为30s。</li><li>执行器节点启动之后会以一倍Beat进行执行器注册, 调度中心以一倍Beat进行动态任务发现; </li><li>“执行器” 在进行任务注册时将会周期性维护一条注册记录，即机器地址和AppName的绑定关系; “调度中心” 从而可以动态感知每个AppName在线的机器列表，具体信息见<code>xxl_job_registry</code>表。</li></ul><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809789173585.jpg" alt="-w450"></p><ul><li>执行器摘除分为主动摘除和过期摘除：<ul><li>过期摘除：注册信息的失效时间为三倍Beat，也就是90s，执行器销毁时，将会主动上报调度中心并摘除对应的执行器机器信息，提高心跳注册的实时性；</li><li>主动摘除：在WEB页面主动删除执行器。<h2 id="新建一个任务"><a href="#新建一个任务" class="headerlink" title="新建一个任务"></a>新建一个任务</h2>新建好执行器后，接下来就是定义一个任务分配给执行器执行。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809790762351.jpg" alt="-w450"><h3 id="运行模式：Bean模式和Glue模式"><a href="#运行模式：Bean模式和Glue模式" class="headerlink" title="运行模式：Bean模式和Glue模式"></a>运行模式：Bean模式和Glue模式</h3>如果把业务代码比作奔驰在路上的跑车，如果我们要给轮胎加一个零件，有两种方案，一是把车停下来把零件加上去后再启动，这就是Bean模式，二是不停车，车在跑的同时直接在论坛上把零件「粘」上去，这也是Glue（中文含义就是胶水）的意义来源。<br>两者区别：</li></ul></li><li>Bean模式的业务代码保存在执行器，Glue模式的业务代码保存在调度中心，运行在执行器；</li><li>Bean模式更新代码需呀停机，Glue模式支持通过Web IDE在线更新，实时编译和生效；</li><li>Glue模式提供版本回滚功能，Glue模式每次执行前都要判断为最新版本，否则需要冲洗构造任务线程；</li><li>Glue模式支持跨语言，Shell、Python、Nodejs</li></ul><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809814100565.jpg" alt="-w450"><br>Glue Java 模式是通过 GLUE Class loader 加载源码的方式，加载源码可以注入 spring 当中其他的一些 server 组件，很方便的接触 spring 其他服务。你修改的时候，下次任务执行，会是你当前这份源码重新实例化，注入一些新的服务进行执行。</p><h3 id="开发一个JOB"><a href="#开发一个JOB" class="headerlink" title="开发一个JOB"></a>开发一个JOB</h3><p>类级别：在XXL-JOB中，所以的JOB都需要继承<code>IJobHandler</code>并重写<code>execute</code>方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">IJobHandler</span> &#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/** success */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> ReturnT&lt;String&gt; SUCCESS = <span class="keyword">new</span> <span class="title class_">ReturnT</span>&lt;String&gt;(<span class="number">200</span>, <span class="literal">null</span>);</span><br><span class="line"><span class="comment">/** fail */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> ReturnT&lt;String&gt; FAIL = <span class="keyword">new</span> <span class="title class_">ReturnT</span>&lt;String&gt;(<span class="number">500</span>, <span class="literal">null</span>);</span><br><span class="line"><span class="comment">/** fail timeout */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> ReturnT&lt;String&gt; FAIL_TIMEOUT = <span class="keyword">new</span> <span class="title class_">ReturnT</span>&lt;String&gt;(<span class="number">502</span>, <span class="literal">null</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="comment"> * execute handler, invoked when executor receives a scheduling request</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> param</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> Exception</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> ReturnT&lt;String&gt; <span class="title function_">execute</span><span class="params">(String param)</span> <span class="keyword">throws</span> Exception;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * init handler, invoked when JobThread init</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> <span class="keyword">throws</span> InvocationTargetException, IllegalAccessException &#123;</span><br><span class="line"><span class="comment">// do something</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * destroy handler, invoked when JobThread destroy</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">destroy</span><span class="params">()</span> <span class="keyword">throws</span> InvocationTargetException, IllegalAccessException &#123;</span><br><span class="line"><span class="comment">// do something</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>方法级别：前提是Spring容器，直接在方法上添加<code>@XxlJob(&quot;demoJobHandler&quot;)</code>就行。<br><strong>无论是类级别还是方法级别，执行方式格式要求为 <code>public ReturnT&lt;String&gt; execute(String param)</code>。</strong></p><h3 id="官方提供的几种示例任务"><a href="#官方提供的几种示例任务" class="headerlink" title="官方提供的几种示例任务"></a>官方提供的几种示例任务</h3><h4 id="demoJobHandler：简单示例任务，任务内部模拟耗时任务逻辑，用户可在线体验Rolling-Log等功能；"><a href="#demoJobHandler：简单示例任务，任务内部模拟耗时任务逻辑，用户可在线体验Rolling-Log等功能；" class="headerlink" title="demoJobHandler：简单示例任务，任务内部模拟耗时任务逻辑，用户可在线体验Rolling Log等功能；"></a>demoJobHandler：简单示例任务，任务内部模拟耗时任务逻辑，用户可在线体验Rolling Log等功能；</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 1、简单任务示例（Bean模式）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@XxlJob(&quot;demoJobHandler&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">demoJobHandler</span><span class="params">(String param)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        XxlJobLogger.log(<span class="string">&quot;XXL-JOB, Hello World.&quot;</span>+param);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">5</span>; i++) &#123;</span><br><span class="line">            XxlJobLogger.log(<span class="string">&quot;beat at:&quot;</span> + i);</span><br><span class="line">            TimeUnit.SECONDS.sleep(<span class="number">2</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> ReturnT.SUCCESS;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h4 id="shardingJobHandler：分片示例任务，任务内部模拟处理分片参数，可参考熟悉分片任务；"><a href="#shardingJobHandler：分片示例任务，任务内部模拟处理分片参数，可参考熟悉分片任务；" class="headerlink" title="shardingJobHandler：分片示例任务，任务内部模拟处理分片参数，可参考熟悉分片任务；"></a>shardingJobHandler：分片示例任务，任务内部模拟处理分片参数，可参考熟悉分片任务；</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 2、分片广播任务</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@XxlJob(&quot;shardingJobHandler&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">shardingJobHandler</span><span class="params">(String param)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 分片参数</span></span><br><span class="line">        ShardingUtil.<span class="type">ShardingVO</span> <span class="variable">shardingVO</span> <span class="operator">=</span> ShardingUtil.getShardingVo();</span><br><span class="line">        XxlJobLogger.log(<span class="string">&quot;分片参数：当前分片序号 = &#123;&#125;, 总分片数 = &#123;&#125;&quot;</span>, shardingVO.getIndex(), shardingVO.getTotal());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 业务逻辑</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; shardingVO.getTotal(); i++) &#123;</span><br><span class="line">            <span class="keyword">if</span> (i == shardingVO.getIndex()) &#123;</span><br><span class="line">                XxlJobLogger.log(<span class="string">&quot;第 &#123;&#125; 片, 命中分片开始处理&quot;</span>, i);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                XxlJobLogger.log(<span class="string">&quot;第 &#123;&#125; 片, 忽略&quot;</span>, i);</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> ReturnT.SUCCESS;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h4 id="httpJobHandler：通用HTTP任务Handler；业务方只需要提供HTTP链接即可，不限制语言、平台；"><a href="#httpJobHandler：通用HTTP任务Handler；业务方只需要提供HTTP链接即可，不限制语言、平台；" class="headerlink" title="httpJobHandler：通用HTTP任务Handler；业务方只需要提供HTTP链接即可，不限制语言、平台；"></a>httpJobHandler：通用HTTP任务Handler；业务方只需要提供HTTP链接即可，不限制语言、平台；</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 4、跨平台Http任务</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@XxlJob(&quot;httpJobHandler&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">httpJobHandler</span><span class="params">(String param)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// request</span></span><br><span class="line">        <span class="type">HttpURLConnection</span> <span class="variable">connection</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="type">BufferedReader</span> <span class="variable">bufferedReader</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// connection</span></span><br><span class="line">            <span class="type">URL</span> <span class="variable">realUrl</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">URL</span>(param);</span><br><span class="line">            connection = (HttpURLConnection) realUrl.openConnection();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// connection setting</span></span><br><span class="line">            connection.setRequestMethod(<span class="string">&quot;GET&quot;</span>);</span><br><span class="line">            connection.setDoOutput(<span class="literal">true</span>);</span><br><span class="line">            connection.setDoInput(<span class="literal">true</span>);</span><br><span class="line">            connection.setUseCaches(<span class="literal">false</span>);</span><br><span class="line">            connection.setReadTimeout(<span class="number">5</span> * <span class="number">1000</span>);</span><br><span class="line">            connection.setConnectTimeout(<span class="number">3</span> * <span class="number">1000</span>);</span><br><span class="line">            connection.setRequestProperty(<span class="string">&quot;connection&quot;</span>, <span class="string">&quot;Keep-Alive&quot;</span>);</span><br><span class="line">            connection.setRequestProperty(<span class="string">&quot;Content-Type&quot;</span>, <span class="string">&quot;application/json;charset=UTF-8&quot;</span>);</span><br><span class="line">            connection.setRequestProperty(<span class="string">&quot;Accept-Charset&quot;</span>, <span class="string">&quot;application/json;charset=UTF-8&quot;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// do connection</span></span><br><span class="line">            connection.connect();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//Map&lt;String, List&lt;String&gt;&gt; map = connection.getHeaderFields();</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">// valid StatusCode</span></span><br><span class="line">            <span class="type">int</span> <span class="variable">statusCode</span> <span class="operator">=</span> connection.getResponseCode();</span><br><span class="line">            <span class="keyword">if</span> (statusCode != <span class="number">200</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;Http Request StatusCode(&quot;</span> + statusCode + <span class="string">&quot;) Invalid.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// result</span></span><br><span class="line">            bufferedReader = <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">InputStreamReader</span>(connection.getInputStream(), <span class="string">&quot;UTF-8&quot;</span>));</span><br><span class="line">            <span class="type">StringBuilder</span> <span class="variable">result</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">            String line;</span><br><span class="line">            <span class="keyword">while</span> ((line = bufferedReader.readLine()) != <span class="literal">null</span>) &#123;</span><br><span class="line">                result.append(line);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">String</span> <span class="variable">responseMsg</span> <span class="operator">=</span> result.toString();</span><br><span class="line"></span><br><span class="line">            XxlJobLogger.log(responseMsg);</span><br><span class="line">            <span class="keyword">return</span> ReturnT.SUCCESS;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            XxlJobLogger.log(e);</span><br><span class="line">            <span class="keyword">return</span> ReturnT.FAIL;</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">                <span class="keyword">if</span> (bufferedReader != <span class="literal">null</span>) &#123;</span><br><span class="line">                    bufferedReader.close();</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (connection != <span class="literal">null</span>) &#123;</span><br><span class="line">                    connection.disconnect();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e2) &#123;</span><br><span class="line">                XxlJobLogger.log(e2);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h4 id="commandJobHandler：通用命令行任务Handler；业务方只需要提供命令行即可；如-“pwd”命令；"><a href="#commandJobHandler：通用命令行任务Handler；业务方只需要提供命令行即可；如-“pwd”命令；" class="headerlink" title="commandJobHandler：通用命令行任务Handler；业务方只需要提供命令行即可；如 “pwd”命令；"></a>commandJobHandler：通用命令行任务Handler；业务方只需要提供命令行即可；如 “pwd”命令；</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 3、命令行任务</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@XxlJob(&quot;commandJobHandler&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">commandJobHandler</span><span class="params">(String param)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">command</span> <span class="operator">=</span> param;</span><br><span class="line">        <span class="type">int</span> <span class="variable">exitValue</span> <span class="operator">=</span> -<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">        <span class="type">BufferedReader</span> <span class="variable">bufferedReader</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// command process</span></span><br><span class="line">            <span class="type">Process</span> <span class="variable">process</span> <span class="operator">=</span> Runtime.getRuntime().exec(command);</span><br><span class="line">            <span class="type">BufferedInputStream</span> <span class="variable">bufferedInputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedInputStream</span>(process.getInputStream());</span><br><span class="line">            bufferedReader = <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">InputStreamReader</span>(bufferedInputStream));</span><br><span class="line"></span><br><span class="line">            <span class="comment">// command log</span></span><br><span class="line">            String line;</span><br><span class="line">            <span class="keyword">while</span> ((line = bufferedReader.readLine()) != <span class="literal">null</span>) &#123;</span><br><span class="line">                XxlJobLogger.log(line);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// command exit</span></span><br><span class="line">            process.waitFor();</span><br><span class="line">            exitValue = process.exitValue();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            XxlJobLogger.log(e);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (bufferedReader != <span class="literal">null</span>) &#123;</span><br><span class="line">                bufferedReader.close();</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">if</span> (exitValue == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> IJobHandler.SUCCESS;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ReturnT</span>&lt;String&gt;(IJobHandler.FAIL.getCode(), <span class="string">&quot;command exit value(&quot;</span>+exitValue+<span class="string">&quot;) is failed&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h3 id="路由策略"><a href="#路由策略" class="headerlink" title="路由策略"></a>路由策略</h3><p>当执行器集群部署时，提供丰富的路由策略，包括；</p><ul><li>FIRST（第一个）：固定选择第一个机器；</li><li>LAST（最后一个）：固定选择最后一个机器；</li><li>ROUND（轮询）</li><li>RANDOM（随机）：随机选择在线的机器；</li><li>CONSISTENT_HASH（一致性HASH）：每个任务按照Hash算法固定选择某一台机器，且所有任务均匀散列在不同机器上。</li><li>LEAST_FREQUENTLY_USED（最不经常使用）：使用频率最低的机器优先被选举；</li><li>LEAST_RECENTLY_USED（最近最久未使用）：最久为使用的机器优先被选举；</li><li>FAILOVER（故障转移）：按照顺序依次进行心跳检测，第一个心跳检测成功的机器选定为目标执行器并发起调度；</li><li>BUSYOVER（忙碌转移）：按照顺序依次进行空闲检测，第一个空闲检测成功的机器选定为目标执行器并发起调度；</li><li>SHARDING_BROADCAST(分片广播)：广播触发对应集群中所有机器执行一次任务，同时系统自动传递分片参数；可根据分片参数开发分片任务；</li></ul><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809819649558.jpg" alt="-w450"><br>从任务触发到任务执行在调度中心阶段有几个步骤</p><ul><li>匹配执行器</li><li>从注册中心加载在线机器节点列表</li><li>根据路由策略去匹配一组机器</li><li>通知这个机器去执行任务<br>基本前面几个路由策略比较容易理解，这里我们着重说一下后面三个路由策略。<h4 id="故障转移和忙碌转移"><a href="#故障转移和忙碌转移" class="headerlink" title="故障转移和忙碌转移"></a>故障转移和忙碌转移</h4><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809823334099.jpg" alt="-w450"><br>调度器在调度时会请求执行器是否处于处于故障或者忙碌状态，对应着<code>ExecutorBiz</code>的开放出来的两个方法。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ExecutorBiz</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * beat</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">beat</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * idle beat</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> jobId</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> ReturnT&lt;String&gt; <span class="title function_">idleBeat</span><span class="params">(<span class="type">int</span> jobId)</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="分片广播"><a href="#分片广播" class="headerlink" title="分片广播"></a>分片广播</h4><p>有时候一台机器处理不够，需要多台机器分片处理。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809827117089.jpg" alt="-w450"><br>可以参照上面的分片广播任务查看如何使用。</p><h3 id="阻塞处理策略"><a href="#阻塞处理策略" class="headerlink" title="阻塞处理策略"></a>阻塞处理策略</h3><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809829662644.jpg" alt="-w450"></p><p>当我们有一些耗时任务，触发的频率超过它的执行器所执行的那些速度的时候，如上图，红色的触发请求进来，但是前面的还在堆积着执行，这时候怎么办？第一条就是默认的单机串行，会把请求入队列，等前面的执行完了之后，挨个把所有的触发的任务全都执行掉。第二个就是丢弃后续的调度，红色的进来了，发现前面已经有了，或者是当前已经 JOB 运行了，直接把后面的标记失败，不进行后面的执行了。最后一个就是覆盖之前调度的，它发现前面队列里面的数据或者任务执行的情况下，把队列清空，把清空的数据全都标记失败，然后把执行的 JOB 也标记失败，让自己来运行。</p><h3 id="触发规则"><a href="#触发规则" class="headerlink" title="触发规则"></a>触发规则</h3><p>现在提供的触发规则主要有 3 种。</p><ul><li>第一种是 Cron 表达式，每一个任务需要配一个 Cron 表达式。</li><li>第二种是任务依赖，你可以为每一个任务配置一个子任务，当副任务执行完成之后，可以触发子任务，这样关联的方式进行触发执行。</li><li>第三种就是事件触发，其实就是类似于 Mq 的场景，代码里面有一个业务逻辑，触发了一个任务执行。<h3 id="任务状态"><a href="#任务状态" class="headerlink" title="任务状态"></a>任务状态</h3></li><li>成功（SUCCESS）</li><li>失败 （FAIL）</li><li>超时 （FAIL_TIMEOUT）</li><li>进行中<h2 id="调度日志和任务日志"><a href="#调度日志和任务日志" class="headerlink" title="调度日志和任务日志"></a>调度日志和任务日志</h2>调度日志：调度平台调度任务的日志，如果任务失败会保留出失败的日志。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809831398387.jpg" alt="-w450"><br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809831766681.jpg" alt="-w450"><br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809831930855.jpg" alt="-w450"></li></ul><p>任务日志：具体任务执行的日志，下面那张图是一个日志页面展示（貌似是读不到日志），真实应该展示的应该是下面的日志内容。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809832647686.jpg" alt="-w450"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">2020-02-05 23:29:54 [com.xxl.job.core.thread.JobThread#run]-[124]-[Thread-6] &lt;br&gt;----------- xxl-job job execute start -----------&lt;br&gt;----------- Param:</span><br><span class="line">2020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[20]-[Thread-6] 分片参数：当前分片序号 = 0, 总分片数 = 2</span><br><span class="line">2020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[25]-[Thread-6] 第 0 片, 命中分片开始处理</span><br><span class="line">2020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[27]-[Thread-6] 第 1 片, 忽略</span><br><span class="line">2020-02-05 23:29:54 [com.xxl.job.core.thread.JobThread#run]-[164]-[Thread-6] &lt;br&gt;----------- xxl-job job execute end(finish) -----------&lt;br&gt;----------- ReturnT:ReturnT [code=200, msg=null, content=null]</span><br><span class="line">2020-02-05 23:29:55 [com.xxl.job.core.thread.TriggerCallbackThread#callbackLog]-[190]-[xxl-job, executor TriggerCallbackThread] &lt;br&gt;----------- xxl-job job callback finish.</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="用户管理"><a href="#用户管理" class="headerlink" title="用户管理"></a>用户管理</h2><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809830330474.jpg" alt="-w450"><br>两种角色：</p><ul><li>管理员：拥有所有的执行器的权限</li><li>普通用户：由管理分配指定的执行器的权限<h2 id="目前发现的几个小问题"><a href="#目前发现的几个小问题" class="headerlink" title="目前发现的几个小问题"></a>目前发现的几个小问题</h2><h3 id="报表页面统计简单"><a href="#报表页面统计简单" class="headerlink" title="报表页面统计简单"></a>报表页面统计简单</h3>运行报表页面只有任务数量、调度次数、执行器数量的统计，无法根据不同执行器展现特定执行器的数据报表。<h3 id="不同用户的报表页面数据没有隔离"><a href="#不同用户的报表页面数据没有隔离" class="headerlink" title="不同用户的报表页面数据没有隔离"></a>不同用户的报表页面数据没有隔离</h3><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809837001285.jpg" alt="-w450"></li></ul><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809836580697.jpg" alt="-w450"><br>对于allen用户，报表页面的数据和管理员登录后所展示的数据一样。<br>但执行器管理和任务管理以及调度日志，是做了数据隔离的。</p><h3 id="手动输入执行器总显示Online"><a href="#手动输入执行器总显示Online" class="headerlink" title="手动输入执行器总显示Online"></a>手动输入执行器总显示Online</h3><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809839793863.jpg" alt="-w450"><br>对于手动录入的执行器，总显示Online。<br>这样的好处是，用户可以手动控制上线的机器，只要修改机器列表即可。</p><h2 id="是否可以覆盖需求"><a href="#是否可以覆盖需求" class="headerlink" title="是否可以覆盖需求"></a>是否可以覆盖需求</h2><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/02/06/15809841544862.jpg" alt="-w450"></p><ol><li>是否可以指定节点运行？<br>A：XXL-JOB运行的基本单位是一个执行器集群，但是可以通过指定不同的路由策略达到指定机器节点的目的。</li><li>JOB可以被高频调用<br>A：参照<a href="https://github.com/xuxueli/xxl-job/issues/225">压测报告</a><br>XXLJOB有过持续20小时，80W次调度100%成功率的测试报告。</li><li>支持大数据量的任务调度<br>A：XXL-JOB支持分片广播，是支持大数据量的任务调度的，数据量的多少应该取决于执行器集群的数量吧</li><li>调度策略简单明了？<br>A：简单明了</li><li>提供API<br><a href="https://www.xuxueli.com/xxl-job/#5.12%20%E6%89%A7%E8%A1%8C%E5%99%A8API%E6%9C%8D%E5%8A%A1">API接口</a><br>提供了调度中心和执行器的API。</li></ol>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于XXL-JOB 的使用和功能介绍在其&lt;a href=&quot;https://www.xuxueli.com/xxl-job&quot;&gt;文档&lt;/a&gt;中非常清晰，且非常容易上手，本篇文章是把官方文档众多内容简单梳理后而成，目的在于&lt;strong&gt;尽可能&lt;/strong&gt;简单清晰用通俗化的语言把XXL-JOB 工具介绍给一个小白用户。&lt;/p&gt;
    
    </summary>
    
    
      <category term="中间件" scheme="https://lazyallen.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="分布式" scheme="https://lazyallen.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="任务调度" scheme="https://lazyallen.github.io/tags/%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6/"/>
    
  </entry>
  
  <entry>
    <title>单元测试:从Junit到Powermock</title>
    <link href="https://lazyallen.github.io/2019/08/11/unit-test-littile-share/"/>
    <id>https://lazyallen.github.io/2019/08/11/unit-test-littile-share/</id>
    <published>2019-08-10T16:00:00.000Z</published>
    <updated>2022-12-07T07:05:13.216Z</updated>
    
    <content type="html"><![CDATA[<p>本文介绍的是Java语言的单元测试框架，分别介绍Junit、Mockito、Powermock三种工具的特点，并附上了用于演示的Demo案例。</p><span id="more"></span><h2 id="什么是单元测试"><a href="#什么是单元测试" class="headerlink" title="什么是单元测试"></a>什么是单元测试</h2><p>单元测试是指，对软件中的最⼩可测试单元在与程序其他部分相隔离的情况下进⾏<br>检查和验证的⼯作，这⾥的最⼩可测试单元通常是指函数或者类。</p><h2 id="单元测试的好处"><a href="#单元测试的好处" class="headerlink" title="单元测试的好处"></a>单元测试的好处</h2><p>单元测试通常由开发⼯程师完成，⼀般会伴随开发代码⼀起递交⾄代码库。单元测试属于最严格<br>的软件测试⼿段，是最接近代码底层实现的验证⼿段，可以在软件开发的早期以最⼩的成本保证<br>局部代码的质量。</p><h2 id="如何做好单元测试"><a href="#如何做好单元测试" class="headerlink" title="如何做好单元测试"></a>如何做好单元测试</h2><h3 id="需要测试哪些东西："><a href="#需要测试哪些东西：" class="headerlink" title="需要测试哪些东西："></a>需要测试哪些东西：</h3><ul><li>结果是否正确</li><li>边界条件<ul><li>空值或者不完整的值</li><li>格式错误的数据</li><li>完全伪造或者不一致的输入数据</li><li>意料之外的值</li></ul></li><li>检查反向关联<ul><li>为了检查数据是否插入成功，检查能否查询出来</li></ul></li><li>检查异常：强制检查异常情况</li><li>性能特性</li></ul><h3 id="什么是好的单元测试："><a href="#什么是好的单元测试：" class="headerlink" title="什么是好的单元测试："></a>什么是好的单元测试：</h3><ul><li>自动化</li><li>独立性</li><li>可重复</li></ul><h3 id="单元测试的三个步骤"><a href="#单元测试的三个步骤" class="headerlink" title="单元测试的三个步骤"></a>单元测试的三个步骤</h3><ul><li>准备数据、行为</li><li>测试目标模块</li><li>验证测试结果</li></ul><h2 id="在Spring中使用Junit进行单元测试"><a href="#在Spring中使用Junit进行单元测试" class="headerlink" title="在Spring中使用Junit进行单元测试"></a>在Spring中使用Junit进行单元测试</h2><h3 id="阿里代码规约手册中几条关于单元测试的强制规范"><a href="#阿里代码规约手册中几条关于单元测试的强制规范" class="headerlink" title="阿里代码规约手册中几条关于单元测试的强制规范"></a>阿里代码规约手册中几条关于单元测试的强制规范</h3><ul><li>不允许使用<code>syetem.out</code>进行人肉验证，必须使用<code>assert</code>进行验证</li><li>保持单元测试的独立性，每一个测试案例互不影响</li><li>核心业务，核心代码，核心模块的新增代码必须保证单元测试通过</li></ul><h3 id="Junit介绍和入门"><a href="#Junit介绍和入门" class="headerlink" title="Junit介绍和入门"></a>Junit介绍和入门</h3><p><code>Junit</code>是一套框架（用于<code>JAVA</code>语言），由 <code>Erich Gamma</code>和 <code>Kent Beck</code> 编写的一个回归测试框架<code>（regression testing framework）</code>，即用于白盒测试。现阶段的最新版本号是<code>4.12</code>，<code>JUnit5</code>目前正在测试中，所以这里还是以<code>JUnit4</code>为准。</p><h3 id="使用assertThat语法"><a href="#使用assertThat语法" class="headerlink" title="使用assertThat语法"></a>使用assertThat语法</h3><p><code>JUnit4.4</code>引入了<code>Hamcrest</code>框架(匹配器框架)，<code>Hamcest</code>提供了一套匹配符<code>Matcher</code>，这些匹配符更接近自然语言，可读性高，更加灵活。使用全新的断言语法：<code>assertThat</code>，结合<code>Hamcest</code>提供的匹配符，只用这一个方法，就可以实现所有的测试。</p><p><code>assertThat</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">assertThat(T actual, Matcher&lt;T&gt; matcher);</span><br><span class="line">assertThat(String reason, T actual, Matcher&lt;T&gt; matcher);</span><br></pre></td></tr></table></figure><p>其中<code>actual</code>为需要测试的变量，<code>matcher</code>为使用<code>Hamcrest</code>的匹配符来表达变量<code>actual</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">assertThat( testedNumber, allOf( greaterThan(<span class="number">8</span>), lessThan(<span class="number">16</span>) ) );</span><br><span class="line"><span class="comment">//allOf匹配符表明如果接下来的所有条件必须都成立测试才通过，相当于“与”（&amp;&amp;）2、</span></span><br><span class="line">assertThat( testedNumber, anyOf( greaterThan(<span class="number">16</span>), lessThan(<span class="number">8</span>) ) );</span><br><span class="line"><span class="comment">//anyOf匹配符表明如果接下来的所有条件只要有一个成立则测试通过，相当于“或”（||）</span></span><br><span class="line">assertThat( testedString, containsString( <span class="string">&quot;developerWorks&quot;</span> ) );</span><br><span class="line"><span class="comment">//containsString匹配符表明如果测试的字符串testedString包含子字符串&quot;developerWorks&quot;则测试通过</span></span><br><span class="line">assertThat( testedNumber, greaterThan(<span class="number">16.0</span>) );</span><br><span class="line"><span class="comment">//greaterThan匹配符表明如果所测试的数值testedNumber大于16.0则测试通过</span></span><br><span class="line">assertThat( iterableObject, hasItem ( <span class="string">&quot;element&quot;</span> ) );</span><br><span class="line"><span class="comment">//hasItem匹配符表明如果测试的迭代对象iterableObject含有元素“element”项则测试通过</span></span><br></pre></td></tr></table></figure><h3 id="常见注解介绍"><a href="#常见注解介绍" class="headerlink" title="常见注解介绍"></a>常见注解介绍</h3><p>引入依赖：</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="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>junit<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>junit<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>4.12<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<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">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.hamcrest<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>hamcrest-library<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>1.3<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<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></pre></td></tr></table></figure><p>新建一个测试用的类：<code>Calculator</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Objects;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Calculator</span> &#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Double <span class="title function_">addition</span><span class="params">(Double x,Double y)</span>&#123;</span><br><span class="line"><span class="keyword">if</span> (Objects.isNull(x) || Objects.isNull(y)) &#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">NullPointerException</span>(<span class="string">&quot;参数不能为空&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> x+y;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Double <span class="title function_">division</span><span class="params">(Double x, <span class="type">int</span> y)</span>&#123;</span><br><span class="line"><span class="keyword">if</span> (<span class="number">0</span>==y)&#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;除数y不能为0&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> x/y;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Double <span class="title function_">multiplication</span><span class="params">(Double x, Double y)</span>&#123;</span><br><span class="line"><span class="keyword">if</span> (Objects.isNull(x) || Objects.isNull(y)) &#123;</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">NullPointerException</span>(<span class="string">&quot;参数不能为空&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> x*y;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">version</span><span class="params">()</span>&#123;</span><br><span class="line">System.out.printf(<span class="string">&quot;v1.0&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>生成对应的测试类:<code>CalculatorTest</code>。</p><p>小技巧：在<code>IDEA</code>中，使用<code>ctrl + shift +  T</code> 的快捷键可以快速生成测试类。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Before;</span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.mockito.InjectMocks;</span><br><span class="line"><span class="keyword">import</span> org.mockito.Mock;</span><br><span class="line"><span class="keyword">import</span> org.mockito.MockitoAnnotations;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.*;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.*;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountantTest</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Mock</span></span><br><span class="line">Calculator calculator;</span><br><span class="line"></span><br><span class="line"><span class="meta">@InjectMocks</span></span><br><span class="line">Accountant accountant;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Before</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"><span class="comment">// 初始化测试用例类中由Mockito的注解标注的所有模拟对象</span></span><br><span class="line">MockitoAnnotations.initMocks(<span class="built_in">this</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateSalary</span><span class="params">()</span> &#123;</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">20.0</span>);</span><br><span class="line">when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(<span class="number">16.0</span>);</span><br><span class="line"><span class="type">Double</span> <span class="variable">salary</span> <span class="operator">=</span> accountant.calculateSalary(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">assertThat(salary,is(<span class="number">16.0</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>@Test</code>注解有两个可选的参数，分别为<code>timeout</code>和<code>expectd</code>。除<code>@Test</code>注解之外，还有如下注解：</p><ul><li><code>@Before</code> 注解的作用是使被标记的方法在测试类里每个方法执行前调用；同理 <code>After</code> 使被标记方法在当前测试类里每个方法执行后调用。</li><li><code>@BeforeClass</code> 注解的作用是使被标记的方法在当前测试类被实例化前调用；同理 <code>@AfterClass</code> 使被标记的方法在测试类被实例化后调用。</li><li><code>@Ignore</code> 注解的作用是使被标记方法暂时不执行。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.assertThat;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Calculatortest2</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">Calculatortest2</span><span class="params">()</span> &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;Constructor&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@BeforeClass</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">beforeThis</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;BeforeClass&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@AfterClass</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">afterThis</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;AfterClass&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Before</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;Before&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@After</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">tearDown</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;After&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">evaluate</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"><span class="type">Calculator</span> <span class="variable">calculator</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Calculator</span>();</span><br><span class="line"><span class="type">int</span> <span class="variable">sum</span> <span class="operator">=</span> calculator.addition(<span class="number">1</span>,<span class="number">1</span>);</span><br><span class="line">assertThat(sum, is(<span class="number">2</span>));</span><br><span class="line">System.out.println(<span class="string">&quot;Test evaluate&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">idiot</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;Test idiot&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Ignore</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">ignoreMe</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;Ignore&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>结果如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">BeforeClass</span><br><span class="line">Constructor</span><br><span class="line">Before</span><br><span class="line">Test idiot</span><br><span class="line">After</span><br><span class="line">Constructor</span><br><span class="line">Before</span><br><span class="line">Test evaluate</span><br><span class="line">After</span><br><span class="line">AfterClass</span><br><span class="line"></span><br><span class="line">Process finished with exit code 0</span><br></pre></td></tr></table></figure><p>从控制台输出可以得到以下两点信息：</p><ul><li>测试类在测试每一个<code>case</code>时，会重新实例化一次测试类，为的是保证每个<code>case</code>是独立隔离的。</li><li><code>BeforeClass</code>是在测试类初始化之前执行，<code>Before</code>是在每一个<code>case</code>运行前执行，我们可以用该注解在测试类初始化的时候准备一些测试的数据。</li></ul><h3 id="使用Mockito模拟来Mock对象"><a href="#使用Mockito模拟来Mock对象" class="headerlink" title="使用Mockito模拟来Mock对象"></a>使用Mockito模拟来Mock对象</h3><h3 id="为什么需要Mock"><a href="#为什么需要Mock" class="headerlink" title="为什么需要Mock"></a>为什么需要Mock</h3><p>在做单元测试的时候，经常出现这样的情况，在需要测试中模块中包含其他依赖的模块，有时候去对这个依赖的模块做单元测试比较高，或者无法进行单元测试。例如，在<code>Java Web</code>项目中通常是分层的，我们需要对<code>Service</code>层进行单元测试，由于<code>Service</code>需要依赖<code>Dao</code>层，我们又不希望对<code>Service</code>的同时又为<code>Dao</code>层做一些初始化的工作从而保证测试通过，就可以将<code>Dao</code>层<code>Mock</code>出来。<code>Mock</code>出来的对象不是真实的对象，而是具备和真实对象相同行为的对象。比如<code>List mockList = mock(List.class)</code>，这个<code>mockList</code>并不是真实的<code>List</code>，但同样又<code>add()</code>,<code>clear()</code>,<code>size()</code>等方法。</p><h3 id="Mockito介绍和入门"><a href="#Mockito介绍和入门" class="headerlink" title="Mockito介绍和入门"></a>Mockito介绍和入门</h3><p><code>Mockito</code> 是 <code>Mock</code> 数据的测试框架，简化了对有外部依赖的类的单元测试。</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></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>org.mockito<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>mockito-core<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>2.23.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<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></pre></td></tr></table></figure><p>这里我们写一个会计师类：<code>Accountant</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Accountant</span> &#123;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Double</span> <span class="variable">tax</span> <span class="operator">=</span> <span class="number">0.2</span>;</span><br><span class="line">Calculator calculator;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Double <span class="title function_">calculateSalary</span><span class="params">(Double a, Double b)</span>&#123;</span><br><span class="line"><span class="type">Double</span> <span class="variable">entireSalary</span> <span class="operator">=</span> calculator.addition(a,b);</span><br><span class="line"><span class="type">Double</span> <span class="variable">deservedSalary</span> <span class="operator">=</span> calculator.multiplication(entireSalary,(<span class="number">1</span>-tax));</span><br><span class="line"><span class="keyword">return</span> deservedSalary;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Double <span class="title function_">calculateOddMonthSalary</span><span class="params">(Double a, Double b)</span>&#123;</span><br><span class="line"><span class="type">int</span> <span class="variable">month</span> <span class="operator">=</span> DateUtils.getCurrentMonth();</span><br><span class="line"><span class="keyword">if</span>(month%<span class="number">2</span>!=<span class="number">0</span>)&#123;</span><br><span class="line"><span class="type">Double</span> <span class="variable">entireSalary</span> <span class="operator">=</span> calculator.addition(a,b);</span><br><span class="line"><span class="type">Double</span> <span class="variable">deservedSalary</span> <span class="operator">=</span> calculator.multiplication(entireSalary,(<span class="number">1</span>-tax));</span><br><span class="line"><span class="keyword">return</span> deservedSalary;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0.0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String <span class="title function_">sayHello</span><span class="params">()</span>&#123;</span><br><span class="line">System.out.println(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;hello&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">printSayHello</span><span class="params">()</span>&#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">hello</span> <span class="operator">=</span> <span class="built_in">this</span>.sayHello();</span><br><span class="line">System.out.println(hello);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">Accountant</span><span class="params">(Calculator calculator)</span> &#123;</span><br><span class="line"><span class="built_in">this</span>.calculator = calculator;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">Accountant</span><span class="params">()</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对应的测试类：</p><p>小技巧：静态导入 <code>org.mockito.Mockito.*</code>;</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Before;</span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.mockito.InjectMocks;</span><br><span class="line"><span class="keyword">import</span> org.mockito.MockitoAnnotations;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.assertThat;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.ArgumentMatchers.anyDouble;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.mock;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.when;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountantTest2</span> &#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Calculator calculator;</span><br><span class="line">Accountant accountant;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Before</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">calculator = mock(Calculator.class);</span><br><span class="line">accountant = <span class="keyword">new</span> <span class="title class_">Accountant</span>(calculator);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateSalaryUseMockMethod</span><span class="params">()</span> &#123;</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">20.0</span>);</span><br><span class="line">when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(<span class="number">80.0</span>);</span><br><span class="line"><span class="type">Double</span> <span class="variable">salary</span> <span class="operator">=</span> accountant.calculateSalary(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">assertThat(salary,is(<span class="number">80.0</span>));</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Mockito</code> 支持通过静态方法<code>mock()</code> 来 <code>Mock</code> 对象，或者通过 <code>@Mock</code> 注解，来创建 <code>Mock</code> 对象。</p><p>假如说，你需要使用<code>@Mock</code>注解，则需要初始化测试用例类中由<code>Mockito</code>的注解标注的所有模拟对象，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Before;</span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.mockito.InjectMocks;</span><br><span class="line"><span class="keyword">import</span> org.mockito.Mock;</span><br><span class="line"><span class="keyword">import</span> org.mockito.MockitoAnnotations;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.*;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.*;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountantTest</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Mock</span></span><br><span class="line">Calculator calculator;</span><br><span class="line"></span><br><span class="line"><span class="meta">@InjectMocks</span></span><br><span class="line">Accountant accountant;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Before</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"><span class="comment">// 初始化测试用例类中由Mockito的注解标注的所有模拟对象</span></span><br><span class="line">MockitoAnnotations.initMocks(<span class="built_in">this</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateSalary</span><span class="params">()</span> &#123;</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">20.0</span>);</span><br><span class="line">when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(<span class="number">16.0</span>);</span><br><span class="line"><span class="type">Double</span> <span class="variable">salary</span> <span class="operator">=</span> accountant.calculateSalary(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">assertThat(salary,is(<span class="number">16.0</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你也可以使用<code>@RunWith(MockitoJUnitRunner.class)</code>去代替<code>MockitoAnnotations.initMocks(this)</code>;</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Before;</span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.junit.runner.RunWith;</span><br><span class="line"><span class="keyword">import</span> org.mockito.InjectMocks;</span><br><span class="line"><span class="keyword">import</span> org.mockito.Matchers;</span><br><span class="line"><span class="keyword">import</span> org.mockito.Mock;</span><br><span class="line"><span class="keyword">import</span> org.mockito.MockitoAnnotations;</span><br><span class="line"><span class="keyword">import</span> org.mockito.junit.MockitoJUnitRunner;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.ArrayList;</span><br><span class="line"><span class="keyword">import</span> java.util.Iterator;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.assertThat;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.ArgumentMatchers.anyDouble;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.*;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RunWith(MockitoJUnitRunner.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountantTest3</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Mock</span></span><br><span class="line">Calculator calculator;</span><br><span class="line"></span><br><span class="line"><span class="meta">@InjectMocks</span></span><br><span class="line">Accountant accountant;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateSalary</span><span class="params">()</span> &#123;</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">20.0</span>);</span><br><span class="line">when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(<span class="number">16.0</span>);</span><br><span class="line"><span class="type">Double</span> <span class="variable">salary</span> <span class="operator">=</span> accountant.calculateSalary(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">assertThat(salary,is(<span class="number">16.0</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test(expected = NullPointerException.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateSalary1</span><span class="params">()</span>&#123;</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">100.0</span>).thenThrow(<span class="keyword">new</span> <span class="title class_">NullPointerException</span>());</span><br><span class="line"><span class="type">Double</span> <span class="variable">mockResult</span> <span class="operator">=</span> calculator.addition(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">System.out.println(<span class="string">&quot;mockResult&quot;</span>+mockResult);</span><br><span class="line">calculator.addition(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通常建议使用<code>@RunWith(MockitoJUnitRunner.class)</code>的方式去初始化测试用例类中由<code>Mockito</code>的注解标注的所有模拟对象。</p><p><code>mock</code> 出来的对象拥有和源对象同样的方法和属性，<code>when()</code> 和 <code>thenReturn()</code> 方法是对源对象的配置，怎么理解，就是说在第一步 <code>mock()</code> 时，<code>mock</code> 出来的对象还不具备被 <code>Mock</code> 对象实例的行为特征，而 <code>when(...).thenReturn(...)</code> 就是根据条件去配置源对象的预期行为。</p><p>有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。典型的运用就是使用<code>mock</code>迭代器。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testMockIterator</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="type">Iterator</span> <span class="variable">i</span> <span class="operator">=</span> mock(Iterator.class);</span><br><span class="line">when(i.next()).thenReturn(<span class="string">&quot;hello&quot;</span>,<span class="string">&quot;world&quot;</span>);</span><br><span class="line"><span class="comment">//when(i.next()).thenReturn(&quot;hello&quot;).thenReturn(&quot;world&quot;);</span></span><br><span class="line"><span class="type">String</span> <span class="variable">result</span> <span class="operator">=</span> i.next() + <span class="string">&quot; &quot;</span> + i.next();</span><br><span class="line">assertThat(result,is(<span class="string">&quot;hello world&quot;</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>除了对方法调用结果是否正确的测试，有时还需要验证一些方法的行为，比如验证方法被调用的次数，验证方法的入参等，<code>Mockito</code> 通过 <code>verify()</code> 方法实现这些场景的测试需求。这被称为“行为测试”。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testVerify</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="type">Calculator</span> <span class="variable">mock</span> <span class="operator">=</span> mock(Calculator.class);</span><br><span class="line">when(mock.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">2.0</span>);</span><br><span class="line"></span><br><span class="line">mock.addition(<span class="number">1.0</span>,<span class="number">1.0</span>);</span><br><span class="line">mock.version();</span><br><span class="line">mock.version();</span><br><span class="line"></span><br><span class="line"><span class="comment">//比如说写Controller的单元测试，验证service方法是否调用</span></span><br><span class="line">verify(mock, times(<span class="number">2</span>)).version();</span><br><span class="line">verify(mock, never()).multiplication(anyDouble(),anyDouble());</span><br><span class="line">verify(mock, atLeastOnce()).version();</span><br><span class="line">verify(mock, atLeast(<span class="number">2</span>)).version();</span><br><span class="line">verify(mock, atMost(<span class="number">3</span>)).version();;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Mockito</code> 支持通过 <code>@Spy</code> 注解或 <code>spy()</code> 方法包裹实际对象，除非明确指定对象，否则都会调用包裹后的对象。这种方式实现了对实际对象的部分自定义修改。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testDifferMockSpy</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="type">List</span> <span class="variable">mock</span> <span class="operator">=</span> mock(ArrayList.class);</span><br><span class="line">mock.add(<span class="string">&quot;one&quot;</span>);</span><br><span class="line">verify(mock).add(<span class="string">&quot;one&quot;</span>);</span><br><span class="line">System.out.println(<span class="string">&quot;mock[0]:&quot;</span>+mock.get(<span class="number">0</span>));</span><br><span class="line"></span><br><span class="line"><span class="type">List</span> <span class="variable">spy</span> <span class="operator">=</span> spy(<span class="keyword">new</span> <span class="title class_">ArrayList</span>());</span><br><span class="line">spy.add(<span class="string">&quot;one&quot;</span>);</span><br><span class="line">verify(spy).add(<span class="string">&quot;one&quot;</span>);</span><br><span class="line">System.out.println(<span class="string">&quot;spy[0]:&quot;</span>+spy.get(<span class="number">0</span>));</span><br><span class="line">when(spy.size()).thenReturn(<span class="number">100</span>);</span><br><span class="line">System.out.println(<span class="string">&quot;spy.size:&quot;</span>+spy.size());</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="PowerMockito解决了什么问题"><a href="#PowerMockito解决了什么问题" class="headerlink" title="PowerMockito解决了什么问题"></a>PowerMockito解决了什么问题</h3><p><code>Mockito</code> 因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中，但是Mock 工具不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟，<code>PowerMock</code> 是在 <code>EasyMock</code> 以及 <code>Mockito</code> 基础上的扩展，通过定制类加载器等技术，<code>PowerMock</code> 实现了之前提到的所有模拟功能，使其成为大型系统上单元测试中的必备工具。</p><h3 id="Maven依赖"><a href="#Maven依赖" class="headerlink" title="Maven依赖"></a>Maven依赖</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="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>org.powermock<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>powermock-module-junit4<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>2.0.0-RC.3<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<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">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.powermock<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>powermock-api-mockito2<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>2.0.0-RC.3<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<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></pre></td></tr></table></figure><h3 id="mock静态方法"><a href="#mock静态方法" class="headerlink" title="mock静态方法"></a>mock静态方法</h3><p>通常，在一些工具类中会又很多静态方法，比如，我们新建一个工具类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDate;</span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DateUtils</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">int</span> <span class="title function_">getCurrentMonth</span><span class="params">()</span>&#123;</span><br><span class="line"><span class="type">LocalDate</span> <span class="variable">now</span> <span class="operator">=</span> LocalDate.now();</span><br><span class="line"><span class="keyword">return</span> now.getMonthValue();</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>相应的测试类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.lazyallen.blog;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javafx.beans.binding.When;</span><br><span class="line"><span class="keyword">import</span> org.junit.Before;</span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.junit.runner.RunWith;</span><br><span class="line"><span class="keyword">import</span> org.mockito.InjectMocks;</span><br><span class="line"><span class="keyword">import</span> org.mockito.Mock;</span><br><span class="line"><span class="keyword">import</span> org.powermock.api.mockito.PowerMockito;</span><br><span class="line"><span class="keyword">import</span> org.powermock.core.classloader.annotations.PrepareForTest;</span><br><span class="line"><span class="keyword">import</span> org.powermock.modules.junit4.PowerMockRunner;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.hamcrest.Matchers.is;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.junit.Assert.assertThat;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.ArgumentMatchers.anyDouble;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> org.mockito.Mockito.*;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> allen</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2019-06-09</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RunWith(PowerMockRunner.class)</span></span><br><span class="line"><span class="meta">@PrepareForTest(&#123;DateUtils.class,Accountant.class&#125;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountantTest4</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Mock</span></span><br><span class="line">Calculator calculator;</span><br><span class="line"></span><br><span class="line"><span class="meta">@InjectMocks</span></span><br><span class="line">Accountant accountant;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Before</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span>&#123;</span><br><span class="line">PowerMockito.mockStatic(DateUtils.class);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCalculateOddMonthSalary</span><span class="params">()</span>&#123;</span><br><span class="line">PowerMockito.when(DateUtils.getCurrentMonth()).thenReturn(<span class="number">7</span>);</span><br><span class="line">when(calculator.addition(anyDouble(),anyDouble())).thenReturn(<span class="number">20.0</span>);</span><br><span class="line">when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(<span class="number">16.0</span>);</span><br><span class="line"><span class="type">Double</span> <span class="variable">salary</span> <span class="operator">=</span> accountant.calculateOddMonthSalary(<span class="number">10.0</span>,<span class="number">10.0</span>);</span><br><span class="line">assertThat(salary,is(<span class="number">16.0</span>));</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只需要注意两点：</p><ul><li>@RunWith(PowerMockRunner.class)</li><li>@PrepareForTest({DateUtils.class,Accountant.class})</li></ul><h3 id="mock私有方法"><a href="#mock私有方法" class="headerlink" title="mock私有方法"></a>mock私有方法</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testPrivate</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"><span class="type">Accountant</span> <span class="variable">accountant1</span> <span class="operator">=</span> PowerMockito.spy(<span class="keyword">new</span> <span class="title class_">Accountant</span>());</span><br><span class="line">PowerMockito.when(accountant1, <span class="string">&quot;sayHello&quot;</span>).thenReturn(<span class="string">&quot;你好&quot;</span>);</span><br><span class="line">accountant1.printSayHello();</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=""[https://github.com/hehonghui/mockito-doc-zh#10](https://github.com/hehonghui/mockito-doc-zh#10)"">Mockito框架中文文档</a></li><li><a href=""[https://github.com/lazyallen/blog-unit-test](https://github.com/lazyallen/blog-unit-test)"">代码示例</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文介绍的是Java语言的单元测试框架，分别介绍Junit、Mockito、Powermock三种工具的特点，并附上了用于演示的Demo案例。&lt;/p&gt;
    
    </summary>
    
    
      <category term="工具" scheme="https://lazyallen.github.io/categories/%E5%B7%A5%E5%85%B7/"/>
    
    
      <category term="Java" scheme="https://lazyallen.github.io/tags/Java/"/>
    
      <category term="UnitTest" scheme="https://lazyallen.github.io/tags/UnitTest/"/>
    
  </entry>
  
  <entry>
    <title>浅析Docker 独立容器的网络通信模式</title>
    <link href="https://lazyallen.github.io/2019/04/17/use-docker-bridge-network-driver/"/>
    <id>https://lazyallen.github.io/2019/04/17/use-docker-bridge-network-driver/</id>
    <published>2019-04-16T16:00:00.000Z</published>
    <updated>2022-12-07T07:05:25.896Z</updated>
    
    <content type="html"><![CDATA[<p>很早就接触过Docker容器相关的概念，之前还使用过Jenkins提供的k8s插件进行容器编排。但说实话，自己工作还是生活中使用的比较少，基础处于我知道是怎么样的，但缺乏实践经验。</p><span id="more"></span><h2 id="搭建xxl-job-admin-docker-container-遇到的疑问"><a href="#搭建xxl-job-admin-docker-container-遇到的疑问" class="headerlink" title="搭建xxl-job-admin docker container 遇到的疑问"></a>搭建xxl-job-admin docker container 遇到的疑问</h2><p>在阅读xxl-job的文档时，发现其提供Docker容器部署的方式，于是就在本机装好了Docker环境，实践一番。提前说明一下，本次实验使用的Docker版本为19.03.8。<br>xxl-job-admin的容器部署很简单，一共就分两步：</p><ul><li>先部署一个MySql，部署完需要执行好xxl-job相应的SQL脚本</li><li>再部署xxl-job-admin，其中MySql链接指向刚刚部署好的MySql实例</li></ul><p>部署命令也很简单，一共就3条：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker network create simple-network</span><br><span class="line"></span><br><span class="line">docker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql </span><br><span class="line"></span><br><span class="line">docker run --network simple-network -e PARAMS=&quot;--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false --spring.datasource.password=123456&quot; -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:2.1.2</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们在结尾再解释为什么是这样写的，我先提出我的困惑。和单机部署应用进程都处在同一个网络环境中不同的是，在同一台主机上的两个容器，他们的网络环境是不是隔离的？如果不是隔离的，容器之间是怎么通信的，容器和宿主机之间又是怎么通行的呢？</p><h2 id="Docker-network-drivers-介绍"><a href="#Docker-network-drivers-介绍" class="headerlink" title="Docker network drivers 介绍"></a>Docker network drivers 介绍</h2><p>在解答疑惑之前，我们需要先了解一下Docker的network驱动，通常情况下，默认是以下几种：</p><ul><li>bridge：默认网络驱动模式，通常如果不特别指定别的网络驱动模式，一般默认就是bridge。</li><li>host：在独立容器中，使用host网络驱动会移除容器和容器主机之间的网络隔离性。也就是说，这个容器和主机的网络环境完全一样。不过目前只在Docker 17.06以上版本支持，且不支持Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.</li><li>overlay：overlay 网络将多个 Docker 守护进程连接在一起，并使集群服务能够相互通信。您还可以使用 overlay 网络来实现 swarm 集群和独立容器之间的通信，或者不同 Docker 守护进程上的两个独立容器之间的通信。该策略实现了在这些容器之间进行操作系统级别路由的需求。[^cnkirito_footnote]</li><li>macvlan：Macvlan 网络允许为容器分配 MAC 地址，使其显示为网络上的物理设备。 Docker 守护进程通过其 MAC 地址将流量路由到容器。对于希望直连到物理网络的传统应用程序而言，使用 macvlan 模式一般是最佳选择，而不应该通过 Docker 宿主机的网络进行路由。[^cnkirito_footnote]</li><li>none：对于此容器，禁用所有联网。通常与自定义网络驱动程序一起使用。none 模式不适用于集群服务。[^cnkirito_footnote]</li></ul><p>对于运行在一台主机的单独容器而言，只需要关心以上的bridge、host、none。<br>查看当前容器下已有的网络驱动</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">➜  ~ docker network ls</span><br><span class="line">NETWORK ID          NAME                DRIVER              SCOPE</span><br><span class="line">04675183a24d        bridge              bridge              local</span><br><span class="line">245b36b452af        host                host                local</span><br><span class="line">43a8d2311d38        none                null                local</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看到，默认在一个Docker容器中包含三种网络驱动，其中<code>SCOPE</code>指的是网络范围，可以是local或swarm范围。区别在于local是在主机范围内提供连接和网络服务（例如DNS或IPAM）。swarm可跨群群集提供连接和网络服务。swarm网络在整个群集中具有相同的网络ID，而local范围网络在每个主机上具有唯一的网络ID。</p><p>接下来我们主要介绍host并着重介绍bridge网络驱动。</p><h2 id="Docker-Host-Network-Driver"><a href="#Docker-Host-Network-Driver" class="headerlink" title="Docker Host Network Driver"></a>Docker Host Network Driver</h2><p>如果在创建容器时指定<code>--net=host</code>，host网络中的所有容器都可以在宿主机上相互通信。从网络角度来看，这等于在没有容器的主机上运行的多个进程。因为它们使用相同的主机环境，所以没有两个容器能够绑定到相同的TCP端口，如果在同一主机上调度了多个容器，则可能出现端口冲突的情况。<br><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/04/02/15855902431392.jpg" alt=""></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">#Create containers on the host network</span><br><span class="line">host $ docker run --rm -itd --net host --name C1 alpine sh</span><br><span class="line"></span><br><span class="line">#Show host eth0</span><br><span class="line">host $ ip -o -4 address show dev eth0 |cut -d’ ‘ -f1-7</span><br><span class="line">2: eth0    inet 172.31.21.213/20</span><br><span class="line"></span><br><span class="line">#Show eth0 from C1</span><br><span class="line">host $ docker exec -it C1 sh</span><br><span class="line">C1 # ip -o -4 address show dev eth0 |cut -d&#x27; &#x27; -f1-7</span><br><span class="line">2: eth0    inet 172.31.21.213/20</span><br></pre></td></tr></table></figure><p>可见，C1和宿主机的公用一套网络环境，所以eth0网卡的IP均为<code>172.31.21.213</code>。在host网络下，容器网络环境和宿主机网络没有任何隔离性，容器和容器之间，容器和宿主机之间的网络通信不存在任何障碍。</p><h2 id="Docker-Bridge-Network-Driver"><a href="#Docker-Bridge-Network-Driver" class="headerlink" title="Docker Bridge Network Driver"></a>Docker Bridge Network Driver</h2><p>容器创建时倘若没有特别指定其他网络驱动，则默认使用的是Bridge 桥接网络驱动。而在docker中，bridge分为系统默认和用户自定义的两种；</p><h3 id="Default-Docker-Bridge-Network"><a href="#Default-Docker-Bridge-Network" class="headerlink" title="Default Docker Bridge Network"></a>Default Docker Bridge Network</h3><p>可以看到上面执行执行docker network ls 时，第一行展示的network<br>就是一个名叫Bridge的Bridge网络驱动。<br>比如启动一个名叫bb-default-bridge的容器，在容器内部，其IP为172.17.0.2</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">➜  ~ docker run --rm -it --name bb-default-bridge busybox sh</span><br><span class="line">/ # ifconfig</span><br><span class="line">eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02</span><br><span class="line">          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0</span><br><span class="line">          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1</span><br><span class="line">          RX packets:10 errors:0 dropped:0 overruns:0 frame:0</span><br><span class="line">          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class="line">          collisions:0 txqueuelen:0</span><br><span class="line">          RX bytes:828 (828.0 B)  TX bytes:0 (0.0 B)</span><br></pre></td></tr></table></figure><p>那如何反映，该容器使用的是默认的Bridge网络驱动呢,docker提供了docker network inspect命令展现使用该网络驱动下的所有容器。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line">➜  ~ docker network inspect bridge</span><br><span class="line">[</span><br><span class="line">    &#123;</span><br><span class="line">        &quot;Name&quot;: &quot;bridge&quot;,</span><br><span class="line">        &quot;Id&quot;: &quot;04675183a24d5f702da598aa3fc03c3a68684ff66b45e4c9eddbbf80c94031d7&quot;,</span><br><span class="line">        &quot;Created&quot;: &quot;2020-03-30T14:00:38.974673641Z&quot;,</span><br><span class="line">        &quot;Scope&quot;: &quot;local&quot;,</span><br><span class="line">        &quot;Driver&quot;: &quot;bridge&quot;,</span><br><span class="line">        &quot;EnableIPv6&quot;: false,</span><br><span class="line">        &quot;IPAM&quot;: &#123;</span><br><span class="line">            &quot;Driver&quot;: &quot;default&quot;,</span><br><span class="line">            &quot;Options&quot;: null,</span><br><span class="line">            &quot;Config&quot;: [</span><br><span class="line">                &#123;</span><br><span class="line">                    &quot;Subnet&quot;: &quot;172.17.0.0/16&quot;,</span><br><span class="line">                    &quot;Gateway&quot;: &quot;172.17.0.1&quot;</span><br><span class="line">                &#125;</span><br><span class="line">            ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;Internal&quot;: false,</span><br><span class="line">        &quot;Attachable&quot;: false,</span><br><span class="line">        &quot;Ingress&quot;: false,</span><br><span class="line">        &quot;ConfigFrom&quot;: &#123;</span><br><span class="line">            &quot;Network&quot;: &quot;&quot;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;ConfigOnly&quot;: false,</span><br><span class="line">        &quot;Containers&quot;: &#123;</span><br><span class="line">            &quot;3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d&quot;: &#123;</span><br><span class="line">                &quot;Name&quot;: &quot;bb-default-bridge&quot;,</span><br><span class="line">                &quot;EndpointID&quot;: &quot;6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5&quot;,</span><br><span class="line">                &quot;MacAddress&quot;: &quot;02:42:ac:11:00:02&quot;,</span><br><span class="line">                &quot;IPv4Address&quot;: &quot;172.17.0.2/16&quot;,</span><br><span class="line">                &quot;IPv6Address&quot;: &quot;&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;Options&quot;: &#123;</span><br><span class="line">            &quot;com.docker.network.bridge.default_bridge&quot;: &quot;true&quot;,</span><br><span class="line">            &quot;com.docker.network.bridge.enable_icc&quot;: &quot;true&quot;,</span><br><span class="line">            &quot;com.docker.network.bridge.enable_ip_masquerade&quot;: &quot;true&quot;,</span><br><span class="line">            &quot;com.docker.network.bridge.host_binding_ipv4&quot;: &quot;0.0.0.0&quot;,</span><br><span class="line">            &quot;com.docker.network.bridge.name&quot;: &quot;docker0&quot;,</span><br><span class="line">            &quot;com.docker.network.driver.mtu&quot;: &quot;1500&quot;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;Labels&quot;: &#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>可见，你同时可以发现，在Containers.IPv4Address的内容和容器内获取到的IP是一致的，且com.docker.network.bridge.name所指是网桥名称为docker0。</p><p>实验宿主机的IP为192.168.1.10，那在bb-default-bridge中能不能访问到宿主机呢</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">/ # ping 192.168.1.105</span><br><span class="line">PING 192.168.1.105 (192.168.1.105): 56 data bytes</span><br><span class="line">64 bytes from 192.168.1.105: seq=0 ttl=37 time=4.473 ms</span><br><span class="line">64 bytes from 192.168.1.105: seq=1 ttl=37 time=0.773 ms</span><br><span class="line">64 bytes from 192.168.1.105: seq=2 ttl=37 time=1.157 ms</span><br></pre></td></tr></table></figure><p>结果发现是可以的，反过来宿主机ping容器则ping不通（是不是有点类似Windows中的NAT网络类型）。如果宿主机需要访问容器时，可以在创建容器时使用 -p 参数将宿主机端口和容器端口映射。<br>假如再创建一个bb-default-bridge-1，依旧使用的是默认的Bridge网络驱动。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&quot;Containers&quot;: &#123;</span><br><span class="line">            &quot;3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d&quot;: &#123;</span><br><span class="line">                &quot;Name&quot;: &quot;bb-default-bridge&quot;,</span><br><span class="line">                &quot;EndpointID&quot;: &quot;6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5&quot;,</span><br><span class="line">                &quot;MacAddress&quot;: &quot;02:42:ac:11:00:02&quot;,</span><br><span class="line">                &quot;IPv4Address&quot;: &quot;172.17.0.2/16&quot;,</span><br><span class="line">                &quot;IPv6Address&quot;: &quot;&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;453703548c1416316e776d9dab9a87afb5d006fe23da87ca25416cdad390bffc&quot;: &#123;</span><br><span class="line">                &quot;Name&quot;: &quot;bb-default-bridge-1&quot;,</span><br><span class="line">                &quot;EndpointID&quot;: &quot;e18409cabc6a10510529c8ded1a6c7f0ceba057473585418b36e1c7406e67af7&quot;,</span><br><span class="line">                &quot;MacAddress&quot;: &quot;02:42:ac:11:00:03&quot;,</span><br><span class="line">                &quot;IPv4Address&quot;: &quot;172.17.0.3/16&quot;,</span><br><span class="line">                &quot;IPv6Address&quot;: &quot;&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br></pre></td></tr></table></figure><p>会发现，两者均可以互相ping通。也就说，在同一个宿主机的不同容器之间，使用默认Bridge network driver 是可以通过IP相互访问对方的。</p><h3 id="User-Defined-Bridge-Networks"><a href="#User-Defined-Bridge-Networks" class="headerlink" title="User-Defined Bridge Networks"></a>User-Defined Bridge Networks</h3><p>用户自定的Bridge网络驱动和默认的Bridge网络驱动类似，那么如何创建一个用户自定义的Bridge网络驱动呢。最简单的方法就是docker network create 语句，更多用法可以参照<a href="https://docs.docker.com/engine/reference/commandline/network/">docker network Command-line reference</a>。<br>比如创建一个叫lazyallen的User-Defined Bridge Network，其中-d是drive的缩写，且是可选项，若不指定默认创建的是Bridge类型的网络驱动。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">➜  ~ docker network create -d bridge lazyallen</span><br><span class="line">75561be3d1901b8213b795c3c9113392375fe6d57aea2eb1e9b097e2bdc0c0bd</span><br><span class="line">➜  ~ docker network ls</span><br><span class="line">NETWORK ID          NAME                DRIVER              SCOPE</span><br><span class="line">04675183a24d        bridge              bridge              local</span><br><span class="line">245b36b452af        host                host                local</span><br><span class="line">75561be3d190        lazyallen           bridge              local</span><br><span class="line">43a8d2311d38        none                null                local</span><br></pre></td></tr></table></figure><p>可见，的确出现了一个叫lazyallen的Bridge Network。和默认的Bridge网络驱动相比，在用法上的差别就是 User-Defined Bridge Networks 提供了容器间自动的DNS解析。<br>什么意思呢？我们同样创建两个容器，让他们都使用lazyallen Bridge Driver,使用 –network<br> 可指定network driver</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run --rm -it --name bb-user-defined-bridge-0 --network lazyallen busybox sh</span><br><span class="line">docker run --rm -it --name bb-user-defined-bridge-1 --network lazyallen busybox sh</span><br></pre></td></tr></table></figure><p>容器在网络通信时，会将容器名直接DNS解析为目标容器IP。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">PING bb-user-defined-bridge-0 (172.18.0.2): 56 data bytes</span><br><span class="line">64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.176 ms</span><br><span class="line">64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.175 ms</span><br><span class="line">64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.185 ms</span><br></pre></td></tr></table></figure><p>这样最大的好处就是，容器之间不用再使用IP作为hardcode嵌入到配置中，只要使用容器名称作为引用即可，就算因为网络环境导致容器的IP动态变化，也不用再修改配置。</p><p>同样，下面这张图片就可以很好解释Bridge network driver</p><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/04/02/15858380387811.jpg" alt=""></p><h2 id="回过头解释"><a href="#回过头解释" class="headerlink" title="回过头解释"></a>回过头解释</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker network create simple-network</span><br><span class="line"></span><br><span class="line">docker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql </span><br><span class="line"></span><br><span class="line">docker run --network simple-network -e PARAMS=&quot;--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false --spring.datasource.password=123456&quot; -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:2.1.2</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>通过以上解释，这里就显而易见地做了3件事</p><ul><li>创建了一个叫simple-network的自定义Bridge 网络驱动</li><li>创建两个容器时均指定simple-network网络驱动</li><li>后端应用的配置文件将MySql的地址使用xxl-mysql引用</li></ul><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ul><li><a href="https://docs.docker.com/network/bridge/">Use bridge networks</a></li><li><a href="https://success.docker.com/article/networking#challengesofnetworkingcontainersandmicroservices">Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks</a></li><li><a href="https://www.cnkirito.moe/docker-network-bridge/">Docker Network—Bridge 模式</a></li></ul><p>[^cnkirito_footnote]: 引用自文章<a href="https://www.cnkirito.moe/docker-network-bridge/">「Docker Network—Bridge 模式」</a>的翻译</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;很早就接触过Docker容器相关的概念，之前还使用过Jenkins提供的k8s插件进行容器编排。但说实话，自己工作还是生活中使用的比较少，基础处于我知道是怎么样的，但缺乏实践经验。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Docker" scheme="https://lazyallen.github.io/categories/Docker/"/>
    
    
      <category term="Bridge" scheme="https://lazyallen.github.io/tags/Bridge/"/>
    
      <category term="Network" scheme="https://lazyallen.github.io/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>解惑：究竟什么是IOC（控制反转）？</title>
    <link href="https://lazyallen.github.io/2019/04/10/what-is-ioc/"/>
    <id>https://lazyallen.github.io/2019/04/10/what-is-ioc/</id>
    <published>2019-04-09T16:00:00.000Z</published>
    <updated>2022-12-07T07:05:38.406Z</updated>
    
    <content type="html"><![CDATA[<p>本文是我对于控制反转这个概念的理解，从接触IOC到很长的一段时间，我都以为自己理解了控制反转的概念，但实际回过头问自己究竟什么是控制反转，又很难清晰解释。通过不断反问自己问题，搜寻相关的文章，最后才找到了本文的答案。其中另外一个感触是，对于技术当中的概念术语，应该尽可能参考英文原文的解释才是最直接有效的理解方式，而很多中文解释其实大多参杂了那个人对于这个概念的理解，吃别人嘴里吐出来的东西营养价值就不多了。</p><span id="more"></span><h2 id="IOC就是把对象的控制权交给容器？"><a href="#IOC就是把对象的控制权交给容器？" class="headerlink" title="IOC就是把对象的控制权交给容器？"></a>IOC就是把对象的控制权交给容器？</h2><p><code>IOC</code>作为<code>Spring</code>的特性之一，经常会拿来和<code>Spring</code>一起讨论。正如我第一次接触到<code>IOC</code>的时候，也是在学习<code>Spring</code>的时候。很长一段时间我对于IOC的理解，和搜索引擎的结果的都差不多。<br>比如，在<a href="https://zhuanlan.zhihu.com/p/38038068">你真的思考过IOC容器吗？</a>这片文章中，作者对于<code>IOC</code>的理解如下：</p><blockquote><p>Ioc是把对象的控制权交给框架或容器，容器中存储了众多我们需要的对象，然后我们就无需再手动的在代码中创建对象。需要什么对象就直接告诉容器我们需要什么对象，容器会把对象根据一定的方式注入到我们的代码中。注入的过程被称为DI。</p></blockquote><p>通过对于这段话的理解，<code>IOC</code>做的事情就是<code>无需再手动的在代码中创建对象。需要什么对象就直接告诉容器我们需要什么对象</code> 。貌似听起来挺有道理，控制反转，就是将创建对象的控制权从对象本身反转到了Spring <code>IOC</code>容器。可是，我解释不来我心中的另外一个疑问：难道这样做的目的仅仅就是帮助开发者减少一些创建对象的代码吗？<br>我对此表示否定，感觉以上文章更多介绍的是<code>IOC</code>容器，而并非<code>IOC</code> 控制反转 概念本身。</p><h2 id="脱离Spring：从Martin-Flower的文章找答案"><a href="#脱离Spring：从Martin-Flower的文章找答案" class="headerlink" title="脱离Spring：从Martin Flower的文章找答案"></a>脱离Spring：从Martin Flower的文章找答案</h2><p><code>IOC</code>概念出自于<code>Martin Flower</code>的一篇文章，<a href="https://insights.thoughtworks.cn/injection/">Inversion of Control Containers and the Dependency Injection pattern</a>，我找到了一篇<code>ThoughtWorks</code>的译文，<a href="https://insights.thoughtworks.cn/injection/">IoC容器和Dependency Injection模式</a>。<br>在这篇文章中，作者用一个<code>MovieFinder</code>的例子介绍了控制反转这个概念：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MovieLister</span>...</span><br><span class="line">    <span class="keyword">private</span> MovieFinder finder;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MovieLister</span><span class="params">()</span></span><br><span class="line">    &#123;</span><br><span class="line">        finder = <span class="keyword">new</span> <span class="title class_">ColonDelimitedMovieFinder</span>(<span class="string">&quot;movies1.txt&quot;</span>);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/29/15854942576555.jpg" alt=""><br>作者同时提出一个问题：</p><blockquote><p>这个实现类的名字就说明：我将要从一个逗号分隔的文本文件中获得影片列表。你不必操心具体的实现细节，只要设想这样一个实现类就可以了。如果这个类只由我自己使用，一切都没问题。但是，如果我的朋友叹服于这个精彩的功能，也想使用我的程序，那又会怎么样呢？如果他们也把影片清单保存在一个逗号分隔的文本文件中，并且也把这个文件命名为” movie1.txt “，那么一切还是没问题。如果他们只是给这个文件改改名，我也可以从一个配置文件获得文件名，这也很容易。但是，如果他们用完全不同的方式——例如<code>SQL 数据库</code>、<code>XML 文件</code>、<code>web service</code>，或者另一种格式的文本文件——来存储影片清单呢？在这种情况下，我们需要用另一个类来获取数据。</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MovieLister</span>...</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MovieLister</span><span class="params">(MovieFinder finder)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">this</span>.finder = finder;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><img src="https://lazyallen-blog-1251933423.cos.ap-guangzhou.myqcloud.com/2020/03/29/15854943441208.jpg" alt=""></p><blockquote><p>Dependency Injection模式的基本思想是：用一个单独的对象（装配器）来获得MovieFinder的一个合适的实现，并将其实例赋给MovieLister类的一个字段。</p></blockquote><p>在<a href="https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5">WIKI</a> 中，我找到关于控制反转最贴切的解释：</p><blockquote><p>Class A中用到了Class B的对象b，一般情况下，需要在A的代码中显式的new一个B的对象。<br>采用依赖注入技术之后，A的代码只需要定义一个私有的B对象，不需要直接new来获得这个对象，而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件（如XML）来指定。</p></blockquote><h2 id="IOC的目的：依赖解耦"><a href="#IOC的目的：依赖解耦" class="headerlink" title="IOC的目的：依赖解耦"></a>IOC的目的：依赖解耦</h2><p>与其回答什么是控制反转，更好的出发点是，控制反转有什么好处，能带来什么？<br>答案就是：<strong>解耦</strong><br>我们举<a href="https://stackoverflow.com/questions/3058/what-is-inversion-of-control">这篇回答</a>中的例子来说明。</p><p>假如我们需要在文本框对输入的文本进行拼写校验，如下代码可以满足我们的需求。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TextEditor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SpellChecker checker;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TextEditor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.checker = <span class="keyword">new</span> <span class="title class_">SpellChecker</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>但现实情况中，文本框中的文本并非一定是英文，有可能是中文，也有可能是其他语言或者混合语言。</strong>那么以上逻辑就需要扩展出新的<code>checker</code>，可是当前的<code>checker</code>是在<code>TextEditor</code>中创建出来，我们需要换一个<code>check</code>的话需要新写一个类。<br>到这里，<code>checker</code>作为<code>TextEditor</code>的依赖，关系就耦合在一起，一旦下层依赖需要发生变动，就一定会影响到上层的代码。暂且不论这样写好不好，类比于现实生活，如同在输出框中输出的不一定是英文，强行将<code>checker</code>和<code>TextEditor</code>绑定在一起就是不合适的（不是说不对，只能说不合适）。<br>那么最简单的方式当然就是，把创建依赖的逻辑放置到外层，通过注入的方式将依赖设置到属性当中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TextEditor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> IocSpellChecker checker;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TextEditor</span><span class="params">(IocSpellChecker checker)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.checker = checker;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样做的好处是，极大方便了<code>checker</code>的扩展，也解除了<code>TextEditor</code>和<code>checker</code>的耦合关系。和主动创建相比，这时依赖必须先创建再注入到对象属性当中。这时，我们只要针对不同的语言环境，<code>new</code>出不同的<code>SpellChecker</code>，而不用改写<code>TextEditor</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"><span class="type">SpellChecker</span> <span class="variable">sc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpellChecker</span>; <span class="comment">// dependency</span></span><br><span class="line"><span class="type">TextEditor</span> <span class="variable">textEditor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TextEditor</span>(sc);</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>那么我对于控制反转的理解是：控制反转，<code>反转的是依赖的创建权</code>，由本来自己的依赖自己主动创建，反转成了依赖在外部创建，创建好了再注入进来的模式，其中，<code>DI</code>（依赖注入）是实现控制反转的方式而已。<br>所以，这里解答了我对于之前关于IOC理解的一个误区，我以前一直以为<code>IOC</code>就如文章前头说，反转的实例创建的创建权，讲创建权交给了容器而已，并没有理解到更本质的含义。<code>IOC</code>其实更多可以解释为一种模式或者写法，这种写法带来的好处就是解除依赖的耦合。而<code>Spring IOC</code>只是套用了这种模式，简化了依赖创建的部分，将对象和对象的依赖统一管理起来，更方便开发者使用。</p><h2 id="相关引用"><a href="#相关引用" class="headerlink" title="相关引用"></a>相关引用</h2><ul><li><a href="https://insights.thoughtworks.cn/injection/">IoC容器和Dependency Injection模式</a></li><li><a href="https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5">WIKI-控制反转</a> </li><li><a href="https://stackoverflow.com/questions/3058/what-is-inversion-of-control">What is Inversion of Control?</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文是我对于控制反转这个概念的理解，从接触IOC到很长的一段时间，我都以为自己理解了控制反转的概念，但实际回过头问自己究竟什么是控制反转，又很难清晰解释。通过不断反问自己问题，搜寻相关的文章，最后才找到了本文的答案。其中另外一个感触是，对于技术当中的概念术语，应该尽可能参考英文原文的解释才是最直接有效的理解方式，而很多中文解释其实大多参杂了那个人对于这个概念的理解，吃别人嘴里吐出来的东西营养价值就不多了。&lt;/p&gt;
    
    </summary>
    
    
      <category term="解惑" scheme="https://lazyallen.github.io/categories/%E8%A7%A3%E6%83%91/"/>
    
    
      <category term="IOC" scheme="https://lazyallen.github.io/tags/IOC/"/>
    
  </entry>
  
</feed>
