<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Doublemine</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://notes.wanghao.work/"/>
  <updated>2019-07-03T03:40:55.496Z</updated>
  <id>https://notes.wanghao.work/</id>
  
  <author>
    <name>夏末</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Kubernetes集群之路（二）etcd集群部署</title>
    <link href="https://notes.wanghao.work/2018-04-26-kubernetes%E9%9B%86%E7%BE%A4%E4%B9%8B%E8%B7%AFetcd%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html"/>
    <id>https://notes.wanghao.work/2018-04-26-kubernetes集群之路etcd集群部署.html</id>
    <published>2018-04-26T12:40:08.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.worldvectorlogo.com/logos/etcd.svg" alt></p><div class="note info"><p>在前面我们生成了所有kubernetes相关的TLS证书，kubernetes集群自身所有配置相关信息都存储在etcd之中，而flannel也将网络子网网段注册到etcd之中并为集群中节点的pod提供了加入同一局域网的能力。因此接下来我们安装部署etcd集群。</p></div><p>因为flannel插件也依赖于etcd存储信息，所以我们首先需要安装etcd集群，使之实现高可用。</p><p>在开始之前请确保在上一篇文章中生成的TLS证书都分发到需要部署的<code>所有机器节点</code>的以下位置：</p><ol><li><code>/etc/kubernetes/ssl/etcd.pem</code></li><li><code>/etc/kubernetes/ssl/etcd-key.pem</code></li><li><code>/etc/kubernetes/ssl/ca.pem</code></li></ol><a id="more"></a><h4 id="部署etcd"><a href="#部署etcd" class="headerlink" title="部署etcd"></a>部署etcd</h4><p>我们采用纯二进制安装etcd,因此不使用默认的包管理器中的安装文件。在每台需要部署的etcd的节点上，通过官方仓库下载你需要的版本的etcd二进制安装包：</p><ul><li><a href="https://github.com/coreos/etcd/releases" target="_blank" rel="noopener">https://github.com/coreos/etcd/releases</a></li></ul><h5 id="下载安装二进制文件"><a href="#下载安装二进制文件" class="headerlink" title="下载安装二进制文件"></a>下载安装二进制文件</h5><p>目前最新版本是<a href="https://github.com/coreos/etcd/releases/tag/v3.3.4" target="_blank" rel="noopener">v3.3.4</a>（截止到我写这篇文章的时候），而kubernetes v1.10验证过的版本为<a href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.10.md#external-dependencies" target="_blank" rel="noopener"><code>3.1.12</code></a>,如果没有特殊需求请尽量使用验证版本（如果你是不升级不舒服斯基当我没说）找到对应的系统架构并直接下载:  <a href="https://github.com/coreos/etcd/releases/download/v3.1.12/etcd-v3.1.12-linux-amd64.tar.gz" target="_blank" rel="noopener">https://github.com/coreos/etcd/releases/download/v3.1.12/etcd-v3.1.12-linux-amd64.tar.gz</a></p><p>在所有需要安装etcd节点执行以下命令来安装<code>etcd和etcdctl</code>(注意:etcd目前不支持降级，如果你初始安装版本过高，后续像降级到验证版是比较麻烦的):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">wget https://github.com/coreos/etcd/releases/download/v3.1.12/etcd-v3.1.12-linux-amd64.tar.gz</span><br><span class="line">tar zxvf etcd-v3.1.12-linux-amd64.tar.gz</span><br><span class="line"><span class="built_in">cd</span> etcd-v3.1.12-linux-amd64</span><br><span class="line">sudo mv etcd etcdctl /usr/<span class="built_in">local</span>/bin/</span><br></pre></td></tr></table></figure><h5 id="配置systemd-unit"><a href="#配置systemd-unit" class="headerlink" title="配置systemd unit"></a>配置systemd unit</h5><p>接着，我们需要编辑对应的systemd unit service文件，我们需要新建一个<code>etcd.service</code>文件并放置于以下路径：<code>/usr/lib/systemd/system/etcd.service</code>并键入以下内容：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=Etcd Server</span><br><span class="line">After=network.target</span><br><span class="line">After=network-online.target</span><br><span class="line">Wants=network-online.target</span><br><span class="line">Documentation=https://github.com/coreos</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=notify</span><br><span class="line">WorkingDirectory=/var/lib/etcd/</span><br><span class="line">EnvironmentFile=-/etc/etcd/etcd.conf</span><br><span class="line">ExecStart=/usr/local/bin/etcd \</span><br><span class="line">  --name $&#123;ETCD_NAME&#125; \</span><br><span class="line">  --cert-file=$&#123;ETCD_TRUST_CERT_FILE&#125; \</span><br><span class="line">  --key-file=$&#123;ETCD_TRUST_CERT_KEY&#125; \</span><br><span class="line">  --peer-cert-file=$&#123;ETCD_TRUST_CERT_FILE&#125; \</span><br><span class="line">  --peer-key-file=$&#123;ETCD_TRUST_CERT_KEY&#125; \</span><br><span class="line">  --trusted-ca-file=$&#123;ETCD_TRUST_CA_FILE&#125; \</span><br><span class="line">  --peer-trusted-ca-file=$&#123;ETCD_TRUST_CA_FILE&#125; \</span><br><span class="line">  --initial-advertise-peer-urls $&#123;ETCD_INITIAL_ADVERTISE_PEER_URLS&#125; \</span><br><span class="line">  --listen-peer-urls $&#123;ETCD_LISTEN_PEER_URLS&#125; \</span><br><span class="line">  --listen-client-urls $&#123;ETCD_LISTEN_CLIENT_URLS&#125; \</span><br><span class="line">  --advertise-client-urls $&#123;ETCD_ADVERTISE_CLIENT_URLS&#125; \</span><br><span class="line">  --initial-cluster-token $&#123;ETCD_INITIAL_CLUSTER_TOKEN&#125; \</span><br><span class="line">  --initial-cluster $&#123;ETCD_CLUSTER_NODE_LIST&#125; \</span><br><span class="line">  --initial-cluster-state new \</span><br><span class="line">  --data-dir=$&#123;ETCD_DATA_DIR&#125;</span><br><span class="line">Restart=on-failure</span><br><span class="line">RestartSec=5</span><br><span class="line">LimitNOFILE=65536</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><div class="note warning"><ol><li><code>WorkingDirectory</code>:指定 etcd 的工作目录和数据目录为 <code>/var/lib/etcd</code>，需在启动服务前创建这个目录。</li><li>为了保证通信安全，需要指定 etcd 的公私钥(cert-file和key-file)、Peers 通信的公私钥和CA证书(peer-cert-file、peer-key-file、peer-trusted-ca-file)、客户端的CA证书（trusted-ca-file）。</li><li><code>--initial-cluster-state</code> 值为 <code>new</code> 时，<code>--name</code> 的参数值必须位于 <code>--initial-cluster</code> 列表中。</li><li>我们将其中一些参数的设置抽取为环境变量，以便于我们修改参数的时候不需要再次systemctl daemon-reload。</li><li>带有<code>--peer-xxx</code>前缀的配置为etcd与其它etcd节点通信的相关配置，不带有的该前缀的则为客户端（例如：etcdctl）与etcd节点（作为服务器）通信的相关配置。</li></ol></div><p>对应的，我们在<code>/etc/etcd/etcd.conf</code>路径中新建一个<code>etcd.conf</code>文件并键入以下内容:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"># [member]</span><br><span class="line">ETCD_NAME=node1</span><br><span class="line">ETCD_DATA_DIR=&quot;/var/lib/etcd&quot;</span><br><span class="line">ETCD_LISTEN_PEER_URLS=&quot;https://0.0.0.0:2380&quot;</span><br><span class="line">ETCD_LISTEN_CLIENT_URLS=&quot;https://0.0.0.0:2379&quot;</span><br><span class="line"></span><br><span class="line">ETCD_TRUST_CA_FILE=&quot;/etc/kubernetes/ssl/ca.pem&quot;</span><br><span class="line">ETCD_TRUST_CERT_FILE=&quot;/etc/kubernetes/ssl/etcd.pem&quot;</span><br><span class="line">ETCD_TRUST_CERT_KEY=&quot;/etc/kubernetes/ssl/etcd-key.pem&quot;</span><br><span class="line"></span><br><span class="line">#[cluster]</span><br><span class="line">ETCD_INITIAL_ADVERTISE_PEER_URLS=&quot;https://10.138.148.161:2380&quot;</span><br><span class="line">ETCD_INITIAL_CLUSTER_TOKEN=&quot;etcd-cluster&quot;</span><br><span class="line">ETCD_ADVERTISE_CLIENT_URLS=&quot;https://10.138.148.161:2379&quot;</span><br><span class="line"></span><br><span class="line">ETCD_CLUSTER_NODE_LIST=&quot;node1=https://10.138.148.161:2380,node2=https://10.138.196.180:2380,node3=https://10.138.212.68:2380&quot;</span><br></pre></td></tr></table></figure><div class="note warning"><ol><li>这是节点IP为10.138.148.161的环境变量配置文件内容，对于其他节点，修改对应的<code>ETCD_NAME</code>为对应的node1、node2、node3，并将<code>ETCD_INITIAL_ADVERTISE_PEER_URLS</code>和<code>ETCD_INITIAL_ADVERTISE_PEER_URLS</code>修改为对应的节点的<code>ip</code>。</li><li>此处需要特别说明的是：<code>ETCD_CLUSTER_NODE_LIST</code>中的ip必须在生成etcd TLS证书时在<code>etcd-csr.json</code>中的<code>hosts</code>字段中指定（Subject Alternative Name（SAN）），否则可能会得到<code>(error &quot;remote error: tls: bad certificate&quot;, ServerName &quot;&quot;)</code>这样的错误。</li><li>所有需要加入的节点都需要在<code>ETCD_CLUSTER_NODE_LIST</code>中指定，并正确配置其<code>ETCD_NAME</code>。</li></ol></div><hr><h5 id="验证etcd安装"><a href="#验证etcd安装" class="headerlink" title="验证etcd安装"></a>验证etcd安装</h5><p>在所有的节点上完成了上述两步之后，我们分别执行以下命令来启动etcd（初始可能会阻塞一段时间）:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl start etcd</span><br></pre></td></tr></table></figure><p>如果配置正确，那么上述命令执行结果应该是任何输出的。如果结果有错，请参照上述配置和环境变量文件检查配置。一旦我们顺利启动<code>etcd</code>服务，我们还需要正确检查我们的<code>etcd</code>集群是否可用，在<code>etcd</code>集群中任一节点中执行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">etcdctl --endpoint https://127.0.0.1:2379   \</span><br><span class="line">--ca-file=/etc/kubernetes/ssl/ca.pem        \</span><br><span class="line">--cert-file=/etc/kubernetes/ssl/etcd.pem    \</span><br><span class="line">--key-file=/etc/kubernetes/ssl/etcd-key.pem \</span><br><span class="line">cluster-health</span><br></pre></td></tr></table></figure><p>在一切正常情况下，你会得到类似如下的输出结果:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">member 245a74588a3e85d0 is healthy: got healthy result from https://xxx.xxx.xxx.xxx:2379</span><br><span class="line">member 953bacee4a009939 is healthy: got healthy result from https://xxx.xxx.xxx.xxx:2379</span><br><span class="line">member f43a05b0ce1a8ed6 is healthy: got healthy result from https://xxx.xxx.xxx.xxx:2379</span><br><span class="line">cluster is healthy</span><br></pre></td></tr></table></figure><hr><h4 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h4><p>需要特别说明的是：<code>etcd</code>集群是否和<code>kubernetes</code>部署在同样的服务器节点上是<code>可选的</code>。也就是说<code>etcd</code>集群可以脱离<code>kubernetes</code>部署的集群而单独部署在其他单独的服务器上，且并不需要和<code>kubernetes</code>节点数对应。经过我的实践如果有条件的话请务必：</p><ul><li>将etcd部署在kubernetes的Node节点之外负载比较低的服务器节点上。</li><li>etcd的集群数量尽量为奇数，以确保某些情况下部分etcd节点挂掉的选举问题。</li></ul><hr><p>至此，我们的<code>etcd</code>集群已经顺利安装完成。接下来安装flannel插件。</p><h4 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h4><ul><li><a href="https://jimmysong.io/kubernetes-handbook/practice/install-kubernetes-on-centos.html" target="_blank" rel="noopener">在CentOS上部署kubernetes集群</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;img src=&quot;https://cdn.worldvectorlogo.com/logos/etcd.svg&quot; alt&gt;&lt;/p&gt;
&lt;div class=&quot;note info&quot;&gt;&lt;p&gt;在前面我们生成了所有kubernetes相关的TLS证书，kubernetes集群自身所有配置相关信息都存储在etcd之中，而flannel也将网络子网网段注册到etcd之中并为集群中节点的pod提供了加入同一局域网的能力。因此接下来我们安装部署etcd集群。&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;因为flannel插件也依赖于etcd存储信息，所以我们首先需要安装etcd集群，使之实现高可用。&lt;/p&gt;
&lt;p&gt;在开始之前请确保在上一篇文章中生成的TLS证书都分发到需要部署的&lt;code&gt;所有机器节点&lt;/code&gt;的以下位置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/etc/kubernetes/ssl/etcd.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/kubernetes/ssl/etcd-key.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/kubernetes/ssl/ca.pem&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
    
      <category term="docker" scheme="https://notes.wanghao.work/tags/docker/"/>
    
      <category term="kubernetes" scheme="https://notes.wanghao.work/tags/kubernetes/"/>
    
  </entry>
  
  <entry>
    <title>Kubernetes集群之路（一）TLS证书配置</title>
    <link href="https://notes.wanghao.work/2018-03-26-Kubernetes%E9%9B%86%E7%BE%A4%E4%B9%8B%E8%B7%AF%E4%B9%8BTLS%E8%AF%81%E4%B9%A6%E9%85%8D%E7%BD%AE.html"/>
    <id>https://notes.wanghao.work/2018-03-26-Kubernetes集群之路之TLS证书配置.html</id>
    <published>2018-03-26T12:40:08.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://upload.wikimedia.org/wikipedia/commons/6/67/Kubernetes_logo.svg" alt></p><div class="note default"><p>Kubernetes是Google开源的容器化集群管理系统，其提供的应用部署、扩展、服务发现等机制对于微服务化架构应用有着十分重要的作用。</p><p>本系列文章基于以下版本来讲述如何使用二进制方式安装Kubernetes集群顺便讲述下踩坑的心路历程：</p><ul><li>Kubernetes version: <code>v1.10</code></li><li>System: <code>CentOS Linux 7</code></li><li>Kernel: <code>Linux 3.10.0</code></li></ul></div><p>Kubernetes系统的各个组件需要使用TLS证书对其通信加密以及授权认证，所以在部署之前我们需要先生成相关的TLS证书以便后续操作能够顺利进行。</p><a id="more"></a><hr><blockquote><p>在后续安装部署中，将不使用kube-apiserver的HTTP非安全端口，所有组件都启用TLS双向认证通信。因此TLS证书配置是在安装配置Kubernetes系统中最容易出错和难于排查问题的一步，所以请务必耐心仔细。</p></blockquote><p>在开始前，为了模拟集群节点，我们假定需要在以下三台Linux主机上部署Kubernetes:</p><ul><li><code>10.138.148.161</code>：作为<code>master</code>节点</li><li><code>10.138.196.180</code>：作为<code>Node</code>节点</li><li><code>10.138.212.68</code>：作为<code>Node</code>节点</li></ul><div class="note danger"><p>同一台主机上可以同时部署master和Node节点相关组件，即同时作为控制节点和工作节点，不过这么做可能导致master节点负载过高而失去响应进而导致整个集群出现无法预知的问题。</p></div><hr><h3 id="安装CFSSL证书生成工具"><a href="#安装CFSSL证书生成工具" class="headerlink" title="安装CFSSL证书生成工具"></a>安装<code>CFSSL</code>证书生成工具</h3><div class="note info"><p>我们将使用<code>Cloudflare</code>的PKI工具集<a href="https://github.com/cloudflare/cfssl" target="_blank" rel="noopener">cloudflare/cfssl</a>来生成集群所需要的各种<code>TLS</code>证书。</p></div><p>执行以下命令直接下载二进制文件进行安装:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O cfssl </span><br><span class="line">chmod +x cfssl </span><br><span class="line">sudo mv cfssl /usr/<span class="built_in">local</span>/bin</span><br><span class="line"></span><br><span class="line">wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -O cfssljson </span><br><span class="line">chmod +x cfssljson </span><br><span class="line">sudo mv cfssljson /usr/<span class="built_in">local</span>/bin</span><br><span class="line"></span><br><span class="line">wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -O cfssl-certinfo </span><br><span class="line">chmod +x cfssl-certinfo </span><br><span class="line">sudo mv cfssl-certinfo /usr/<span class="built_in">local</span>/bin</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> PATH=/usr/<span class="built_in">local</span>/bin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure><h3 id="创建CA根证书（Certificate-Authority）"><a href="#创建CA根证书（Certificate-Authority）" class="headerlink" title="创建CA根证书（Certificate Authority）"></a>创建CA根证书（Certificate Authority）</h3><p>CA（Certificate Authority）是自签名的根证书，用来签名后续创建的其它 TLS 证书；<br>确认<code>CFSSL</code>工具安装成功之后，我们先通过<code>CFSSL</code>工具来创建模版配置json文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cfssl <span class="built_in">print</span>-defaults config &gt; config.json</span><br><span class="line">cfssl <span class="built_in">print</span>-defaults csr &gt; csr.json</span><br></pre></td></tr></table></figure><h4 id="创建CA配置文件"><a href="#创建CA配置文件" class="headerlink" title="创建CA配置文件"></a>创建CA配置文件</h4><p>这将生成两个模版json文件，后续<code>CFSSL</code>将读取json文件内容并生成对应的<code>pem</code>文件。我们先复制<code>config.json</code>为<code>ca-config.json</code>文件并做如下修改:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"signing"</span>: &#123;</span><br><span class="line">        <span class="attr">"default"</span>: &#123;</span><br><span class="line">            <span class="attr">"expiry"</span>: <span class="string">"99999h"</span></span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">"profiles"</span>: &#123;</span><br><span class="line">            <span class="attr">"kubernetes"</span>: &#123;</span><br><span class="line">                <span class="attr">"expiry"</span>: <span class="string">"99999h"</span>,</span><br><span class="line">                <span class="attr">"usages"</span>: [</span><br><span class="line">                    <span class="string">"signing"</span>,</span><br><span class="line">                    <span class="string">"key encipherment"</span>,</span><br><span class="line">                    <span class="string">"server auth"</span>,</span><br><span class="line">                    <span class="string">"client auth"</span></span><br><span class="line">                ]</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><div class="note warning"><p><code>profiles</code>：可以定义多个profiles，分别指定不同的过期时间、使用场景等参数；后续在签名证书时使用某个特定的profile。</p><p><code>signing</code>：表示该证书可用于签名(签发)其它证书，生成的 ca.pem 证书中 CA=TRUE。</p><p><code>server auth</code>：表示`client可以用该 CA（生成的ca.pem） 对server提供的证书进行验证。</p><p><code>client auth</code>：表示server可以用该CA(生成的ca.pem）对client提供的证书进行验证。</p></div><h4 id="创建CA证书签名请求"><a href="#创建CA证书签名请求" class="headerlink" title="创建CA证书签名请求"></a>创建CA证书签名请求</h4><p>我们复制<code>csr.json</code>为<code>ca-csr.json</code>并做以下修改:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"kubernetes"</span>,</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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><div class="note warning"><p><code>CN</code>(<code>Common Name</code>):后续<code>kube-apiserver</code>组件将从证书中提取该字段作为请求的用户名；</p><p><code>O</code>(<code>Organtzation</code>):后续<code>kube-apiserver</code>组件将从证书中提取该字段作为请求的用户所属的用户组；</p></div><h4 id="生成CA证书和私钥"><a href="#生成CA证书和私钥" class="headerlink" title="生成CA证书和私钥"></a>生成CA证书和私钥</h4><p>执行以下命令来生成CA证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -initca ca-csr.json | cfssljson -bare ca</span><br><span class="line">ls ca*</span><br><span class="line">ca.csr  ca-csr.json  ca-key.pem  ca.pem</span><br></pre></td></tr></table></figure><p>这样，我们就生成了CA证书和私钥了，因为我们需要双向<code>TLS</code>认证，所以需要拷贝<code>ca-key.pem</code>和<code>ca.pem</code>到所有要部署的机器的<code>/etc/kubernetes/ssl</code>目录下备用。</p><h3 id="创建kubernetes组件认证授权证书"><a href="#创建kubernetes组件认证授权证书" class="headerlink" title="创建kubernetes组件认证授权证书"></a>创建kubernetes组件认证授权证书</h3><p>因为我们准备部署的kubernetes组件是使用<code>TLS</code>双向认证的，包括<code>kube-apiserver</code>不打算使用HTTP端口，因此，我们需要生成以下的证书以供后续组件部署的时候备用：</p><div class="note info"><ul><li><code>etcd</code>证书：etcd集群之间通信加密使用的<code>TLS</code>证书。</li><li><code>kube-apiserver</code>证书：配置<code>kube-apiserver</code>组件的证书。</li><li><code>kube-controller-manager</code>证书：用于和<code>kube-apiserver</code>通信认证的证书。</li><li><code>kube-scheduler</code>证书：用于和<code>kube-apiserver</code>通信认证的证书。</li><li><code>kubelet</code>证书【可选,非必需】：用于和<code>kube-apiserver</code>通信认证的证书，如果使用<code>TLS Bootstarp</code>认证方式，将没有必要配置。</li><li><code>kube-proxy</code>证书【可选,非必需】：用于和<code>kube-apiserver</code>通信认证的证书，如果使用<code>TLS Bootstarp</code>认证方式，将没有必要配置。</li></ul></div><p>下面我们将逐个创建对应的<code>TLS</code>证书，并做相应的简短说明：</p><h4 id="创建etcd证书："><a href="#创建etcd证书：" class="headerlink" title="创建etcd证书："></a>创建<code>etcd</code>证书：</h4><p>首选我们创建<code>etcd</code>证书签名请求(CSR)，拷贝<code>csr.json</code>为<code>etcd-csr.json</code>并做以下修改:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"etcd"</span>,</span><br><span class="line">    <span class="attr">"hosts"</span>: [</span><br><span class="line">      <span class="string">"127.0.0.1"</span>,</span><br><span class="line">      <span class="string">"10.138.212.68"</span>,</span><br><span class="line">      <span class="string">"10.138.196.180"</span>,</span><br><span class="line">      <span class="string">"10.138.148.161"</span>,</span><br><span class="line">        <span class="string">"master"</span>,</span><br><span class="line">        <span class="string">"node1"</span>,</span><br><span class="line">        <span class="string">"node2"</span></span><br><span class="line">    ],</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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><div class="note danger"><p>此处需要指定<code>host</code>字段的值，该值为所有需要部署etcd节点的<code>ip 域名 或者 hostname</code>，etcd需要使用<code>Subject Alternative Name（SAN）</code>来校验集群以及防止滥用。如果你不清楚应该使用哪个ip，默认情况下使用<code>ip a</code>查看<code>eth0</code>即可。此处指定的<code>ip</code>与后续指定的<code>etcd的systemd</code>配置<code>initial-cluster</code>相关。</p><p>相关阅读: <a href="https://github.com/coreos/etcd/issues/2056" target="_blank" rel="noopener">Option to accept TLS client certificates even if they lack correct Subject Alternative Names</a></p></div><p>生成<code>etcd</code>证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes etcd-csr.json | cfssljson -bare etcd</span><br><span class="line">ls etcd*</span><br><span class="line">etcd.csr  etcd-csr.json  etcd-key.pem etcd.pem</span><br></pre></td></tr></table></figure><p>将生成的<code>etch-key.pem</code>和<code>etcd.pem</code>拷贝到所有需要部署<code>etcd</code>集群的服务器<code>/etc/etcd/ssl</code>目录下备用。</p><h4 id="创建kube-apiserver证书"><a href="#创建kube-apiserver证书" class="headerlink" title="创建kube-apiserver证书"></a>创建<code>kube-apiserver</code>证书</h4><p>创建<code>kube-apiserver</code>证书签名请求配置文件，拷贝<code>csr.json</code>为<code>kubernetes-csr.json</code>并做以下修改：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"CN"</span>: <span class="string">"kubernetes"</span>,</span><br><span class="line">    <span class="attr">"hosts"</span>: [</span><br><span class="line">      <span class="string">"127.0.0.1"</span>,</span><br><span class="line">      <span class="string">"10.138.212.68"</span>,</span><br><span class="line">      <span class="string">"10.138.196.180"</span>,</span><br><span class="line">      <span class="string">"10.138.148.161"</span>,</span><br><span class="line">      <span class="string">"10.254.0.1"</span>,</span><br><span class="line">      <span class="string">"kubernetes"</span>,</span><br><span class="line">      <span class="string">"kubernetes.default"</span>,</span><br><span class="line">      <span class="string">"kubernetes.default.svc"</span>,</span><br><span class="line">      <span class="string">"kubernetes.default.svc.cluster"</span>,</span><br><span class="line">      <span class="string">"kubernetes.default.svc.cluster.local"</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"key"</span>: &#123;</span><br><span class="line">        <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">        <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"names"</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">            <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">            <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">            <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">            <span class="attr">"OU"</span>: <span class="string">"System"</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><div class="note warning"><p>此处指定了<code>host</code>字段来表示授权使用该证书的<code>ip或域名</code>列表，因此上述配置文件指定了要部署的kubernetes三台服务器ip（实际上只需要指定打算部署master节点的ip即可）以及<code>kube-apiserver</code>注册的名为<code>kubernetes</code>服务的服务ip（一般默认为后续配置<code>kube-apiserve</code>组件的时候指定的<code>—service-cluster-ip-range</code>网段的第一个ip。）如果你不清楚怎么操作，可以留空<code>host</code>字段。</p><p>如果你指定了<code>host</code>字段，这里如果有 <code>VIP</code> 的，也是需要填写的。</p></div><p>生成<code>kube-apiserver</code>证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare apiserver</span><br><span class="line">ls apiserver*</span><br><span class="line">apiserver.csr  apiserver-key.pem  apiserver.pem</span><br></pre></td></tr></table></figure><p>我们将该证书拷贝到需要部署到<code>master</code>节点上的<code>/etc/kubernetes/ssl</code>上备用。</p><div class="note info"><p>因为我们master节点的组件之间的通信使用<code>非HTTP</code>的安全端口，所以同样也需要<code>TLS</code>认证授权，因此我们也需要配置<code>kube-controller-manager</code>和<code>kube-scheduler</code>的证书来供这两个组件访问<code>kube-apiserver</code>.如果你的集群master节点组件使用HTTP非安全端口通信，那么可以不需要配置这两个证书。</p></div><h4 id="创建kube-controller-manager证书"><a href="#创建kube-controller-manager证书" class="headerlink" title="创建kube-controller-manager证书"></a>创建<code>kube-controller-manager</code>证书</h4><p>复制<code>car.json</code>为<code>kube-controller-manager-csr.json</code>并做以下修改：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"system:kube-controller-manager"</span>,</span><br><span class="line">  <span class="attr">"hosts"</span>: [],</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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>kube-apiserver</code>将提取<code>CN</code>作为客户端组件(kube-controller-manager)的用户名(system:kube-controller-manager)，<code>kube-apiserver</code>预定义的RBAC使用ClusterRoleBinding <code>system:kube-controller-manager</code>将<code>用户system:kube-controller-manager</code>与<code>ClusterRole system:kube-controller-manager</code>绑定。</p><p>生成<code>kube-controller-manager</code>证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-controller-manager-csr.json | cfssljson -bare controller-manager</span><br><span class="line">ls controller-manager*</span><br><span class="line">controller-manager.csr  controller-manager-key.pem  controller-manager.pem</span><br></pre></td></tr></table></figure><p>将证书拷贝到需要部署<code>kube-controller-manager</code>的master节点<code>/etc/kubernetes/ssl</code>上备用。</p><h4 id="创建kube-scheduler-证书"><a href="#创建kube-scheduler-证书" class="headerlink" title="创建kube-scheduler`证书"></a>创建kube-scheduler`证书</h4><p>与<code>kube-controller-manager</code>一样，<code>kube-scheduler</code>同样也需要<code>TLS</code>证书来访问<code>kube-apiserver</code>。此处不再赘述。直接上<code>kube-scheduler-csr.json</code>文件内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"system:kube-scheduler"</span>,</span><br><span class="line">  <span class="attr">"hosts"</span>: [],</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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></code>kube-scheduler <code>将提取</code>CN作为客户端的用户名<code>,这里是</code>system:kube-scheduler<code>。 kube-apiserver 预定义的 RBAC 使用的 ClusterRoleBindings</code>system:kube-scheduler <code>将</code>用户system:kube-scheduler <code>与</code>ClusterRole system:kube-scheduler `绑定。</p><p>生成<code>kube-scheduler</code>证书以及私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-scheduler-csr.json | cfssljson -bare scheduler</span><br><span class="line">ls scheduler*</span><br><span class="line">scheduler.csr  scheduler-key.pem  scheduler.pem</span><br></pre></td></tr></table></figure><p>将证书拷贝到需要部署<code>kube-scheduler</code>的master节点<code>/etc/kubernetes/ssl</code>上备用。</p><p>至此，<code>master</code>节点上的证书生成就全部完成了，接下来是生成<code>worker</code>节点的证书，需要注意的是：生成<code>worker</code>证书是可选的，如果你使用<code>TLS Bootstarpping</code>那么你可以跳过以下步骤<code>worker</code>证书生成工作。直接转到部署的实际操作环节。关于<code>TLS</code>证书和<code>TLS Bootstarpping</code>认证方式的区别，后续考虑单独写一遍文章展开来讲。</p><hr><h4 id="创建kubelet证书"><a href="#创建kubelet证书" class="headerlink" title="创建kubelet证书"></a>创建<code>kubelet</code>证书</h4><p>拷贝<code>car.json</code>为<code>kubelet-csr.json</code>并做以下修改:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"system:node:node"</span>,</span><br><span class="line">  <span class="attr">"hosts"</span>: [],</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"system:nodes"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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><div class="note warning"><p><code>O</code>为用户组，kubernetes RBAC定义了ClusterRoleBinding将Group system:nodes和CLusterRole system:node关联起来。</p><p>注意:在<code>kubernetes v1.8+</code>以上版本，将不会自动创建<code>binding</code>,因此我们后续需要手动创建绑定关系。</p></div><p>生成<code>kubelet</code>证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubelet-csr.json | cfssljson -bare kubelet</span><br><span class="line">ls kubelet*</span><br><span class="line">kubelet.csr  kubelet-csr.json  kubelet-key.pem  kubelet.pem</span><br></pre></td></tr></table></figure><p>将生成的证书和秘钥拷贝到所有需要部署的worker节点上的<code>/etc/kubernetes/ssl</code>下备用。</p><h4 id="创建kube-proxy证书"><a href="#创建kube-proxy证书" class="headerlink" title="创建kube-proxy证书"></a>创建<code>kube-proxy</code>证书</h4><p>拷贝<code>car.json</code>为<code>kube-proxy-csr.json</code>并做以下修改:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"CN"</span>: <span class="string">"system:kube-proxy"</span>,</span><br><span class="line">  <span class="attr">"hosts"</span>: [],</span><br><span class="line">  <span class="attr">"key"</span>: &#123;</span><br><span class="line">    <span class="attr">"algo"</span>: <span class="string">"rsa"</span>,</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">2048</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"names"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"C"</span>: <span class="string">"CN"</span>,</span><br><span class="line">      <span class="attr">"ST"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"L"</span>: <span class="string">"Shanghai"</span>,</span><br><span class="line">      <span class="attr">"O"</span>: <span class="string">"k8s"</span>,</span><br><span class="line">      <span class="attr">"OU"</span>: <span class="string">"System"</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><div class="note warning"><p><code>CN</code> 指定该证书的 User为 system:kube-proxy。Kubernetes RBAC定义了ClusterRoleBinding将<code>system:kube-proxy用户</code>与<code>system:node-proxier 角色</code>绑定。system:node-proxier具有kube-proxy组件访问ApiServer的相关权限。</p></div><p>生成<code>kube-proxy</code>证书和私钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-proxy-csr.json | cfssljson -bare kube-proxy</span><br><span class="line">ls kube-proxy*</span><br><span class="line">kube-proxy.csr  kube-proxy-csr.json  kube-proxy-key.pem  kube-proxy.pem</span><br></pre></td></tr></table></figure><p>将生成的证书和私钥拷贝到所有需要部署<code>worker</code>节点的<code>/etc/kubernetes/ssl</code>下备用。</p><p>在完成证书分发之后，这样我们的证书相关的生成工作就完成了。接下来开始配置各个组件。</p><p>参考资料：</p><ul><li><a href="https://kubernetes.io/docs/admin/authorization/rbac/" target="_blank" rel="noopener">Using RBAC Authorization</a></li><li><a href="https://wiki.shileizcc.com/display/KUB/Kubernetes+HA+Cluster+Build" target="_blank" rel="noopener">Kubernetes HA Cluster Build</a></li><li><a href="https://jimmysong.io/kubernetes-handbook/practice/install-kubernetes-on-centos.html" target="_blank" rel="noopener">在CentOS上部署kubernetes集群</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/6/67/Kubernetes_logo.svg&quot; alt&gt;&lt;/p&gt;
&lt;div class=&quot;note default&quot;&gt;&lt;p&gt;Kubernetes是Google开源的容器化集群管理系统，其提供的应用部署、扩展、服务发现等机制对于微服务化架构应用有着十分重要的作用。&lt;/p&gt;
&lt;p&gt;本系列文章基于以下版本来讲述如何使用二进制方式安装Kubernetes集群顺便讲述下踩坑的心路历程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes version: &lt;code&gt;v1.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;System: &lt;code&gt;CentOS Linux 7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kernel: &lt;code&gt;Linux 3.10.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes系统的各个组件需要使用TLS证书对其通信加密以及授权认证，所以在部署之前我们需要先生成相关的TLS证书以便后续操作能够顺利进行。&lt;/p&gt;
    
    </summary>
    
    
      <category term="docker" scheme="https://notes.wanghao.work/tags/docker/"/>
    
      <category term="kubernetes" scheme="https://notes.wanghao.work/tags/kubernetes/"/>
    
  </entry>
  
  <entry>
    <title>lua读取redis数据的null判断</title>
    <link href="https://notes.wanghao.work/2018-01-26-lua%E8%AF%BB%E5%8F%96redis%E6%95%B0%E6%8D%AE%E7%9A%84null%E5%88%A4%E6%96%AD.html"/>
    <id>https://notes.wanghao.work/2018-01-26-lua读取redis数据的null判断.html</id>
    <published>2018-01-26T12:40:08.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>最近在配合移动端调试的时候，被抓去debug一个在清除redis缓存之后才会出现的网关错误。于是打开服务器上的log定位到类似错误:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[error] 7#7: *12030 lua entry thread aborted: runtime error: /data/share/apps/lua/access_check.lua:133: bad argument #1 to &apos;decode&apos; (string expected, got userdata)</span><br></pre></td></tr></table></figure><p>该段代码的主要作用是在<code>openresty</code>中<code>lua</code>读取<code>redis</code>中数据并解码为<code>json</code>：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">local</span> access_token = redis_client:read_by_key(token_key)</span><br><span class="line">   <span class="keyword">if</span> access_token == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line">       <span class="comment">-- do something...</span></span><br><span class="line">       <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">   <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">   <span class="keyword">local</span> obj_token = cjson.decode(access_token)</span><br><span class="line">   <span class="comment">-- do something</span></span><br></pre></td></tr></table></figure><p>通过查询资料得知原因：<strong><code>lua</code>读取<code>redis</code>数据返回结果为空时，返回的结果不是<code>nil</code>而是<code>userdata</code>类型的<code>ngx.null</code>。</strong></p><hr><a id="more"></a><h4 id="为什么要这么设计？"><a href="#为什么要这么设计？" class="headerlink" title="为什么要这么设计？"></a><a href="https://github.com/openresty/lua-resty-redis/issues/90" target="_blank" rel="noopener">为什么要这么设计？</a></h4><blockquote><p>因为<code>nil</code>在<code>lua</code>中有特殊的意义，如果一个变量被设置为<code>nil</code>相当于告知该变量<code>未定义</code>(不存在)一样，如果把<code>redis</code>查询的结果为空设置为<code>nil</code>，而该查询的<code>key</code>对应在<code>redis</code>中又是存在的，就无法把<code>查询为空</code>和<code>未定义</code>区分开来了，这样显然是不合理的。所以必须使用一个<code>userdata</code>类型的值来表示这个查询记录为空，但是又不等同于<code>未定义变量</code>（ngx.null)。</p></blockquote><hr><p>因此，代码做如下修改即可:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">local</span> access_token = redis_client:read_by_key(token_key)</span><br><span class="line">   <span class="keyword">if</span> access_token == ngx.null <span class="keyword">or</span> access_token == <span class="literal">nil</span> <span class="keyword">then</span></span><br><span class="line">       <span class="comment">-- do something...</span></span><br><span class="line">       <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">   <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">   <span class="keyword">local</span> obj_token = cjson.decode(access_token)</span><br><span class="line">   <span class="comment">-- do something</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近在配合移动端调试的时候，被抓去debug一个在清除redis缓存之后才会出现的网关错误。于是打开服务器上的log定位到类似错误:&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;[error] 7#7: *12030 lua entry thread aborted: runtime error: /data/share/apps/lua/access_check.lua:133: bad argument #1 to &amp;apos;decode&amp;apos; (string expected, got userdata)&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;该段代码的主要作用是在&lt;code&gt;openresty&lt;/code&gt;中&lt;code&gt;lua&lt;/code&gt;读取&lt;code&gt;redis&lt;/code&gt;中数据并解码为&lt;code&gt;json&lt;/code&gt;：&lt;/p&gt;
&lt;figure class=&quot;highlight lua&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;local&lt;/span&gt; access_token = redis_client:read_by_key(token_key)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;   &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; access_token == &lt;span class=&quot;literal&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;then&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       &lt;span class=&quot;comment&quot;&gt;-- do something...&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;false&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;   &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;   &lt;span class=&quot;keyword&quot;&gt;local&lt;/span&gt; obj_token = cjson.decode(access_token)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;   &lt;span class=&quot;comment&quot;&gt;-- do something&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;通过查询资料得知原因：&lt;strong&gt;&lt;code&gt;lua&lt;/code&gt;读取&lt;code&gt;redis&lt;/code&gt;数据返回结果为空时，返回的结果不是&lt;code&gt;nil&lt;/code&gt;而是&lt;code&gt;userdata&lt;/code&gt;类型的&lt;code&gt;ngx.null&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
    
    </summary>
    
    
      <category term="openresty" scheme="https://notes.wanghao.work/tags/openresty/"/>
    
      <category term="lua" scheme="https://notes.wanghao.work/tags/lua/"/>
    
      <category term="Redis" scheme="https://notes.wanghao.work/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>聊聊Mybatis Update操作返回值</title>
    <link href="https://notes.wanghao.work/2017-09-06-%E8%81%8A%E8%81%8AMybatis-Update%E6%93%8D%E4%BD%9C%E8%BF%94%E5%9B%9E%E5%80%BC.html"/>
    <id>https://notes.wanghao.work/2017-09-06-聊聊Mybatis-Update操作返回值.html</id>
    <published>2017-09-06T20:00:03.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<div class="note info"><p>后端的数据持久化使用的是 Mybatis ，在做高并发下账户增减余额的时候，打算使用乐观锁来解决这个问题。在获取update操作的返回值时遇到了一个问题，似乎 Mybatis 进行 update 操作得到的 <code>int</code> 返回值并不是影响的行数。这下就尴尬了。</p></div><p>一般而言，我们知道当我们使用 Mybatis 在 <code>mapper</code> 接口中定义 <code>insert</code> <code>delete</code> 等操作，定义一个 <code>int</code> 类型的返回值，通过该值是否为 0 来判断数据库中受影响的行数进而判断操作是否成功。</p><a id="more"></a><hr><p>到底 <code>update</code> 返回值代表什么呢？我们来验证一下便知道了，假设有如下一张表以及两条数据：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja7y057g3j20x004yjst.jpg" alt title="数据库更新之前记录"></p><p>我们来编写一个简单的单元测试用例来验证下,首先使用 mybatis 简单的写个 mapper 进行更新操作，其中 xml 中的内容为：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja7tn625wj210q0ekgnn.jpg" alt title="mapper 更新语句"></p><p>数据库连接配置为：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja7wi2iv9j20se0k076a.jpg" alt title="数据库连接配置"></p><p>接来下，我们来编写一个简单的单元测试来验证下：<strong>update 的返回值是不是受影响的记录的条数</strong>，对应的单元测试代码如下：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja83t5sf3j20sg0oggoq.jpg" alt title="单元测试代码"></p><p>由单元测试代码可以得知，我们将要把数据库中两条记录的 <code>phone</code> 字段的值由 <code>12345678</code> 修改为 <code>66666666</code> ,正常情况下，<code>resultCode</code> 将会返回 2 。因为 <code>update</code> 操作影响到数据库中这 2 条记录，这和我们期望 2 是相符合的。那么一切正常的情况下，这次单元测试将会通过，那么我们运行看看结果：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja8bdifgjj20jq0423yx.jpg" alt title="单元测试通过"></p><p>单元测试通过了，再查看数据库中的记录：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja8ifpsyyj20x206u3zr.jpg" alt title="更新后的数据库记录"></p><p>这说明 mybatis 的 update 更新操作返回值的确是返回受影响的行数……真的是这样吗？</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja8m25otsj204305h0sm.jpg" alt></p><p>我们知道，当数据库中的记录被修改之后，再次执行重复的 <code>update</code> 操作将不会影响到新的行数，为了验证我说的话，我们试试：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja8rrcju0j20ne03i74t.jpg" alt></p><p>那么，按照这个逻辑：我们再次执行这个单元测试应该是，<code>resultCode</code> 返回的应该是 <code>0</code> ，和我们的期望的数字 <code>2</code> 不一致，将会导致测试不通过。再次运行单元测试：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja8xe6fwgj20ju050t9b.jpg" alt title="单元测试依然通过"></p><p>居然还是 <code>passed</code> ,看到这里聪明的你已经看出来了，<font color="red">默认情况下，mybatis 的 <code>update</code> 操作返回值是记录的  <code>matched</code> 的条数，并不是影响的记录条数。</font></p><hr><p>严格意义上来将，这并不是 mybatis 的返回值，mybatis 仅仅只是返回的数据库连接驱动（通常是 <code>JDBC</code> ）的返回值，也就是说，如果驱动告知更新 2 条记录受影响，那么我们将得到 mybatis 的返回值就会是 2 和 mybatis 本身是没有关系的。</p><p>道理我都懂，如果我们非得要 mybatis 的 <code>update</code> 操作明确的返回受影响的记录条数，有没有什么办法呢？</p><p>当然是有的。</p><p>通过对 <code>JDBC</code> URL 显式的指定 <strong><code>useAffectedRows</code></strong>选项，我们将可以得到受影响的记录的条数：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jdbc:mysql:<span class="comment">//$&#123;jdbc.host&#125;/$&#123;jdbc.db&#125;?useAffectedRows=true</span></span><br></pre></td></tr></table></figure><p>我们对我们的数据库连接配置稍做修改，添加 <code>useAffectedRows</code> 字段：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja9nv1e5xj20u80kcdhz.jpg" alt title="修改之后的数据库连接配置"></p><p>此时，mybatis 的 update 操作返回的应该是受影响的条数了，我们再次运行单元测试试试看：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fja9psbcjpj21hy08eabi.jpg" alt title="单元测试不通过"></p><p><code>update</code> 操作返回的是受影响的记录条数，我们知道为 <code>0</code> 和我们预期的 <code>2</code> 不一致，自然而然单元测试不通过咯～。</p><h5 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h5><ul><li><a href="http://mybatis-user.963551.n3.nabble.com/Return-number-of-changed-rows-td3888464.html" target="_blank" rel="noopener">“Return number of changed rows”</a></li><li><a href="http://www.peristblog.com/c?id=10164" target="_blank" rel="noopener">“mybatis实现乐观锁”</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;后端的数据持久化使用的是 Mybatis ，在做高并发下账户增减余额的时候，打算使用乐观锁来解决这个问题。在获取update操作的返回值时遇到了一个问题，似乎 Mybatis 进行 update 操作得到的 &lt;code&gt;int&lt;/code&gt; 返回值并不是影响的行数。这下就尴尬了。&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;一般而言，我们知道当我们使用 Mybatis 在 &lt;code&gt;mapper&lt;/code&gt; 接口中定义 &lt;code&gt;insert&lt;/code&gt; &lt;code&gt;delete&lt;/code&gt; 等操作，定义一个 &lt;code&gt;int&lt;/code&gt; 类型的返回值，通过该值是否为 0 来判断数据库中受影响的行数进而判断操作是否成功。&lt;/p&gt;
    
    </summary>
    
    
      <category term="MySQL" scheme="https://notes.wanghao.work/tags/MySQL/"/>
    
      <category term="Mybatis" scheme="https://notes.wanghao.work/tags/Mybatis/"/>
    
      <category term="JDBC" scheme="https://notes.wanghao.work/tags/JDBC/"/>
    
  </entry>
  
  <entry>
    <title>Gradle、Maven项目相互转换</title>
    <link href="https://notes.wanghao.work/2017-08-21-Gradle%E3%80%81Maven%E9%A1%B9%E7%9B%AE%E7%9B%B8%E4%BA%92%E8%BD%AC%E6%8D%A2.html"/>
    <id>https://notes.wanghao.work/2017-08-21-Gradle、Maven项目相互转换.html</id>
    <published>2017-08-21T23:01:21.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[  <div class="note info"><p>在开发Android项目的时候，使用的是<code>Gradle</code>构建工具，喜欢它的灵活和方便，在转向Java后端开发的时候更多时候使用的是<code>Maven</code>构建工具，然而看着漫天的尖括号，心里实在是难受。虽然只是一个构建工具，本着折腾的心，我还是更认可和看好<code>Gradle</code>。然而很多时候你的队友并没有习惯去使用或者快速熟悉<code>Gradle</code>构建工具，那么这个时候就需要将<code>Gradle</code>项目转换为Maven项目了，或者将Maven项目转换为<code>Gradle</code>项目了。</p></div><a id="more"></a><h3 id="安装Gradle／Maven"><a href="#安装Gradle／Maven" class="headerlink" title="安装Gradle／Maven"></a>安装Gradle／Maven</h3><p>首先是安装构建工具，这个没啥好说的。</p><h5 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h5><p>打开Powershell或者Cmder执行以下命令完成安装：</p><figure class="highlight powershell"><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">choco install gradle</span><br><span class="line">choco install maven</span><br></pre></td></tr></table></figure><p><code>choco</code>为windows下的一款包管理工具，可以方便安装管理配置一些常见的软件包，如果你没有安装<code>choco</code>的话，请移步：<a href="https://chocolatey.org/" target="_blank" rel="noopener">https://chocolatey.org/</a></p><h4 id="Mac"><a href="#Mac" class="headerlink" title="Mac"></a>Mac</h4><p>打开Terminal，执行以下命令安装：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">brew install gradle</span><br><span class="line">brew install maven</span><br></pre></td></tr></table></figure><h3 id="Maven-to-Gradle"><a href="#Maven-to-Gradle" class="headerlink" title="Maven to Gradle"></a>Maven to Gradle</h3><p>需要特别说明的是，<code>Gradle</code>对<code>Maven</code>的支持是比较完善的，因此，转换也是非常的简单，在<code>pom.xml</code>文件所在的目录下执行：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">gradle init     # 根据pom.xml内容生成对应的gradle配置</span><br><span class="line">gradle build    # 开启gradle构建</span><br></pre></td></tr></table></figure><hr><h3 id="Gradle-to-Maven"><a href="#Gradle-to-Maven" class="headerlink" title="Gradle to Maven"></a>Gradle to Maven</h3><p><code>Gradle</code>项目转<code>Maven</code>项目需要借助一个Gradle插件，在项目的<code>module</code>的<code>build.gradle</code>文件中加入以下配置即可：</p><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">apply <span class="string">plugin:</span> <span class="string">'maven'</span></span><br></pre></td></tr></table></figure><p>通过双击<code>Idea</code>的Gradle Tasks GUI:</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fj5qcx3csoj20j20ee0ua.jpg" alt></p><p>或者执行命令来完成转换:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gradle install</span><br></pre></td></tr></table></figure><p>完成之后，将会在当前Module项目的<code>build</code>目录下的<code>poms</code>文件夹下生成<code>pom-default.xml</code>，将其拷贝到项目的根目录下即可。</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fj5pn78lbhj20ik0g4jsk.jpg" alt></p><ul><li>参考文档：<a href="https://guides.gradle.org/migrating-from-maven/" target="_blank" rel="noopener">https://guides.gradle.org/migrating-from-maven/</a></li></ul><hr><p>通过实际测试，这样的生成的<code>pom-default.xml</code>文件是不能用于直接<code>maven</code>构建的，因为生成的<code>pom-default.xml</code>文件中的<code>groupId</code>还需要我们手动指定下。这样显然是不清真的，于是我们可以在<code>build.gradle</code>文件中将其事先定义好，这样生成的pom文件就不用我们再手动更改了：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fj5pvzyw2xj20ni0dign1.jpg" alt></p><p>然而这样我们还是觉得麻烦，毕竟需要手动复制到项目根目录，再重新命名。我们还可以通过Hook Gradle中Maven插件的<code>install</code>Task来完成自动的复制和命名,编辑<code>build.gradle</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></pre></td><td class="code"><pre><span class="line">task convert2Maven &#123;</span><br><span class="line">    doLast &#123;</span><br><span class="line">        file(<span class="string">"$buildDir/poms/pom-default.xml"</span>).renameTo(file(<span class="string">"$rootDir/pom.xml"</span>))</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">install.dependsOn(convert2Maven)</span><br></pre></td></tr></table></figure><p>此时，再执行<code>gradle install</code>这个task就可以看到gradle已经自动为我们在项目的根目录下生成好了<code>pom.xml</code>文件啦。</p>]]></content>
    
    <summary type="html">
    
      &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;在开发Android项目的时候，使用的是&lt;code&gt;Gradle&lt;/code&gt;构建工具，喜欢它的灵活和方便，在转向Java后端开发的时候更多时候使用的是&lt;code&gt;Maven&lt;/code&gt;构建工具，然而看着漫天的尖括号，心里实在是难受。虽然只是一个构建工具，本着折腾的心，我还是更认可和看好&lt;code&gt;Gradle&lt;/code&gt;。然而很多时候你的队友并没有习惯去使用或者快速熟悉&lt;code&gt;Gradle&lt;/code&gt;构建工具，那么这个时候就需要将&lt;code&gt;Gradle&lt;/code&gt;项目转换为Maven项目了，或者将Maven项目转换为&lt;code&gt;Gradle&lt;/code&gt;项目了。&lt;/p&gt;&lt;/div&gt;
    
    </summary>
    
    
      <category term="Gradle" scheme="https://notes.wanghao.work/tags/Gradle/"/>
    
      <category term="Maven" scheme="https://notes.wanghao.work/tags/Maven/"/>
    
  </entry>
  
  <entry>
    <title>Docker非Root运行</title>
    <link href="https://notes.wanghao.work/2017-07-11-Docker%E9%9D%9ERoot%E7%94%A8%E6%88%B7%E8%BF%90%E8%A1%8C.html"/>
    <id>https://notes.wanghao.work/2017-07-11-Docker非Root用户运行.html</id>
    <published>2017-07-11T10:56:01.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<div class="note info"><p>Docker Engine的Deamon进程是以root权限运行的，如果是普通用户要与之交互，需要使用<code>sudo</code>命令来提权与之交互。之前使用Docker官方的安装脚本安装完成之后，会给出一个提示将当前非root用户添加到doker组之中，以避免每次都需要输入<code>sudo</code>的麻烦。</p></div><p>然而随着Docker版本的迭代和官网的安装方式的更改，现在官方给出的安装方式是添加仓库源地址，然后使用默认的<code>apt</code>或者<code>yum</code>包管理工具来完成后安装。并不再提示用户添加非root用户到组。</p><p>默认情况下，完成Docker Engine的安装之后，Docker将会自动创建一个名为<code>docker</code>的用户组，所以<code>root</code>用户和在<code>docker</code>组中的用户都可以免去<code>sudo</code>来与Docker Engine交互。知道原理之后就简单了：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo usermod -aG docker $&#123;whoami&#125; #添加当前用户到docker组</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;Docker Engine的Deamon进程是以root权限运行的，如果是普通用户要与之交互，需要使用&lt;code&gt;sudo&lt;/code&gt;命令来提权与之交互。之前使用Docker官方的安装脚本安装完成之后，会给出一个提示将当前非r
      
    
    </summary>
    
    
      <category term="Docker" scheme="https://notes.wanghao.work/tags/Docker/"/>
    
  </entry>
  
  <entry>
    <title>Spring Boot使用SpringFox自动生成Api Doc</title>
    <link href="https://notes.wanghao.work/2017-06-30-Spring-Boot%E4%BD%BF%E7%94%A8Swagger%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90Api-Doc.html"/>
    <id>https://notes.wanghao.work/2017-06-30-Spring-Boot使用Swagger自动生成Api-Doc.html</id>
    <published>2017-06-30T10:56:01.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<div class="note info"><p>在做Android开发的时候，对于Api接口的对接有着深刻的体会：后端通过Markdown或者Word写好Api文档，然后通过类似Samba或者Dropbox这样的服务与移动端实现文档共享。有的时候因为接口出了问题，中间还得来回修改对接，效率低下不说，要是后端手抖写错参数而没有意识到，移动端埋头一顿调试。。。说多了都是泪。</p></div><p>为了避免同时维护代码和文档来保持两者之间的同步而带来的额外负担，同事推荐了<a href="http://apidocjs.com/" target="_blank" rel="noopener"><code>ApiDoc</code></a>来生成文档，虽然生成的文档界面比较清爽然而前提是必须得按照规定的语法写上详细的注释，才能生成对应的文档，虽然写注释本身是一件好事，不过有能够自动生成的方法为啥不使用呢?</p><a id="more"></a><hr><p>与<code>Apidoc</code>类似，<code>Swagger</code>也是一个用来文档化Resetful Api的项目，不过开源社区的支持应该是所有类似项目中最为完善的，因此除了可以使用<a href="https://github.com/swagger-api/swagger-editor" target="_blank" rel="noopener">Swagger Editor</a>来编写Api文档之外，你还可以使用其它对应的自动化生成工具，以此来避免同时维护文档和代码的麻烦：</p><ul><li><a href="https://github.com/springfox/springfox" target="_blank" rel="noopener">Springfox</a> 是为Spring而打造的自动化生成接口文档的其中一个Java实现</li><li><a href="https://github.com/marcgibbons/django-rest-swagger" target="_blank" rel="noopener">Django Reset Swagger</a> 则是为Django而打造的Python实现。</li></ul><p>这篇文章将从头创建一个Spring Boot项目并使用Springfox来生成对应的接口文档，来说明使用Springfox是多么的简单。首先创建Spring Boot项目：</p><h3 id="创建Spring-Boot项目"><a href="#创建Spring-Boot项目" class="headerlink" title="创建Spring Boot项目"></a>创建Spring Boot项目</h3><h4 id="Eclipse"><a href="#Eclipse" class="headerlink" title="Eclipse"></a>Eclipse</h4><p>如果你是使用Eclipse的话，那么：</p><p><img src="https://camo.githubusercontent.com/8caa3693b4268c095c001089313d687f647d551a/687474703a2f2f696d67322e77696b69612e6e6f636f6f6b69652e6e65742f5f5f636232303133303831393134323932382f6361726466696768742f696d616765732f7468756d622f352f35352f476f2d686f6d652d796f7572652d6472756e6b2e6a70672f35303070782d476f2d686f6d652d796f7572652d6472756e6b2e6a7067" alt></p><h4 id="IntelliJ-IDEA"><a href="#IntelliJ-IDEA" class="headerlink" title="IntelliJ IDEA"></a>IntelliJ IDEA</h4><p>我们使用IDEA的<code>Spring initializr</code>向导来简化初始化创建项目，如图所示：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh3qqzjjr9j21gu0w4td1.jpg" alt></p><p>点击下一步根据个人的喜好来配置喜欢的JVM语言和构建工具，此处我选择<code>Kotlin</code>和<code>Gradle</code>，一切都是为了爽：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh3qv1abohj21gu0w4jum.jpg" alt></p><p>点击下一步选择需要集成的依赖项，此处我们简单演示下Resetful Api文档生成，所以选择Web即可，如图：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh45g74sdaj21gu0w4agg.jpg" alt></p><p>点击Next直至完成。这样，我们就完成了Spring Boot项目的创建了。</p><hr><h3 id="添加Springfox依赖"><a href="#添加Springfox依赖" class="headerlink" title="添加Springfox依赖"></a>添加Springfox依赖</h3><p>编辑根目录下的<code>build.gradle</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></pre></td><td class="code"><pre><span class="line">dependencies &#123;</span><br><span class="line">    .... <span class="comment">/*some depends...*/</span></span><br><span class="line">    compile <span class="string">"io.springfox:springfox-swagger2:$springfoxVersion"</span></span><br><span class="line">    compile <span class="string">"io.springfox:springfox-swagger-ui:$springfoxVersion"</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ext &#123;</span><br><span class="line">    springfoxVersion = <span class="string">'2.7.0'</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="配置Springfox"><a href="#配置Springfox" class="headerlink" title="配置Springfox"></a>配置Springfox</h3><p>Springfox通过<code>Docket</code>对象来定义生成的Api的一些属性，因此我们来创建一个Configure类来专门做Springfox的配置。创建一个<code>Swagger2Configure.kt</code>文件，并添加以下内容：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableSwagger2</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Swagger2Configure</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span> <span class="function"><span class="keyword">fun</span> <span class="title">petApi</span><span class="params">()</span></span>: Docket &#123;</span><br><span class="line">        <span class="keyword">return</span> Docket(DocumentationType.SWAGGER_2)</span><br><span class="line">                .apiInfo(generateApiInfo()) <span class="comment">/*定制swagger ui显示的版本信息*/</span></span><br><span class="line">                .useDefaultResponseMessages(<span class="literal">false</span>)</span><br><span class="line">                .select()</span><br><span class="line">                <span class="comment">/*移除默认的Error Controller*/</span></span><br><span class="line">                .apis(Predicates.not(RequestHandlerSelectors.basePackage(<span class="string">"org.springframework.boot"</span>)))</span><br><span class="line">                .paths(PathSelectors.any())</span><br><span class="line">                .build()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">generateApiInfo</span><span class="params">()</span></span>: ApiInfo &#123;</span><br><span class="line">        <span class="keyword">return</span> ApiInfoBuilder().title(<span class="string">"Spring Boot Api Doc"</span>)</span><br><span class="line">                .contact(Contact(<span class="string">"Doublemine"</span>, <span class="string">"https://notes.wanghao.work"</span>, <span class="string">"doublemine.w@gmail.com"</span>))</span><br><span class="line">                .description(<span class="string">"This is a sample api doc description"</span>)</span><br><span class="line">                .build()</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>上述示例只演示了最基本的配置，如果想查看完整的示例解释，请移步<a href="http://springfox.github.io/springfox/docs/current/#configuration-explained" target="_blank" rel="noopener">Configuration explained</a>,至此，Springfox的配置就完成了。就是这么简单。</p><hr><h3 id="创建接口"><a href="#创建接口" class="headerlink" title="创建接口"></a>创建接口</h3><p>我们创建一个简单的UserController来模拟获取用户信息，<code>UserController.kt</code>：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping(<span class="meta-string">"/user/"</span>)</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">open</span> <span class="class"><span class="keyword">class</span> <span class="title">UserController</span> : <span class="type">BaseController</span></span>() &#123;</span><br><span class="line">    <span class="meta">@RequestMapping(value = <span class="meta-string">"/info/&#123;id&#125;"</span>, method = arrayOf(RequestMethod.GET)</span>)</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">getUserInfoById</span><span class="params">(<span class="meta">@PathVariable</span> id: <span class="type">Int</span>)</span></span>: User &#123;</span><br><span class="line">        <span class="keyword">return</span> User(id, <span class="string">"小白"</span>, <span class="number">35</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对应的Model<code>User.kt</code>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">User</span></span>(<span class="keyword">val</span> id: <span class="built_in">Int</span>, <span class="keyword">var</span> name: String, <span class="keyword">var</span> age: <span class="built_in">Int</span>)</span><br></pre></td></tr></table></figure><p>至此就完成了简单的接口，接着我们启动项目并访问<a href="http://localhost:8080/swagger-ui.html" target="_blank" rel="noopener">http://localhost:8080/swagger-ui.html</a> ，一切正常的话，你将会看到以下页面：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh4gacpr8wj21sk0su0wr.jpg" alt></p><p>一般来说这样已经能够满足我们的基本需要了，如果还需要更为详细的文档，Springfox也提供的注解来简化配置过程，我们接下来稍微修改下<code>UserController.kt</code>：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Api(tags = arrayOf(<span class="meta-string">"用户信息"</span>)</span>)</span><br><span class="line"><span class="meta">@RequestMapping(<span class="meta-string">"/user/"</span>)</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">open</span> <span class="class"><span class="keyword">class</span> <span class="title">UserController</span> : <span class="type">BaseController</span></span>() &#123;</span><br><span class="line">    <span class="meta">@ApiOperation(<span class="meta-string">"获取用户信息"</span>, notes = <span class="meta-string">"根据用户Id在来查询用户信息"</span>)</span></span><br><span class="line">    <span class="meta">@RequestMapping(value = <span class="meta-string">"/info/&#123;id&#125;"</span>, method = arrayOf(RequestMethod.GET)</span>)</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">getUserInfoById</span><span class="params">(<span class="meta">@PathVariable</span> id: <span class="type">Int</span>)</span></span>: User &#123;</span><br><span class="line">        <span class="keyword">return</span> User(id, <span class="string">"小白"</span>, <span class="number">35</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们重启项目查看下：</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh4gfu3btij21qo0qi786.jpg" alt></p><p>可以发现文档添加了对应的中文，要查看全部可用的注解以及其作用，请移步官方文档：</p><ul><li><a href="http://springfox.github.io/springfox/docs/current/#support-for-documentation-from-property-file-lookup" target="_blank" rel="noopener"> Support for documentation from property file lookup</a></li><li><a href="https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X" target="_blank" rel="noopener">Swagger-Core Annotations</a></li></ul><p>简单集成使用到这里👌咯，后续再写一写生成静态文档相关的内容吧。Just for Fun！</p>]]></content>
    
    <summary type="html">
    
      &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;在做Android开发的时候，对于Api接口的对接有着深刻的体会：后端通过Markdown或者Word写好Api文档，然后通过类似Samba或者Dropbox这样的服务与移动端实现文档共享。有的时候因为接口出了问题，中间还得来回修改对接，效率低下不说，要是后端手抖写错参数而没有意识到，移动端埋头一顿调试。。。说多了都是泪。&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;为了避免同时维护代码和文档来保持两者之间的同步而带来的额外负担，同事推荐了&lt;a href=&quot;http://apidocjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;ApiDoc&lt;/code&gt;&lt;/a&gt;来生成文档，虽然生成的文档界面比较清爽然而前提是必须得按照规定的语法写上详细的注释，才能生成对应的文档，虽然写注释本身是一件好事，不过有能够自动生成的方法为啥不使用呢?&lt;/p&gt;
    
    </summary>
    
    
      <category term="Gradle" scheme="https://notes.wanghao.work/tags/Gradle/"/>
    
      <category term="Swagger" scheme="https://notes.wanghao.work/tags/Swagger/"/>
    
      <category term="Spring Boot" scheme="https://notes.wanghao.work/tags/Spring-Boot/"/>
    
      <category term="Kotlin" scheme="https://notes.wanghao.work/tags/Kotlin/"/>
    
  </entry>
  
  <entry>
    <title>解决Git refusing to merge unrelated histories问题</title>
    <link href="https://notes.wanghao.work/2017-06-28-%E8%A7%A3%E5%86%B3Git-refusing-to-merge-unrelated-histories%E9%97%AE%E9%A2%98.html"/>
    <id>https://notes.wanghao.work/2017-06-28-解决Git-refusing-to-merge-unrelated-histories问题.html</id>
    <published>2017-06-28T10:40:36.000Z</published>
    <updated>2019-07-03T03:40:55.500Z</updated>
    
    <content type="html"><![CDATA[<p>最近在使用brew更新了git之后，发现在与Github上的新创建的<code>repo</code>建立关联的之后，进行<code>pull</code>操作会出现类似于下面的这种错误：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"> * branch            master     -&gt; FETCH_HEAD</span><br><span class="line"> * [new branch]      master     -&gt; origin/master</span><br><span class="line">fatal: refusing to merge unrelated histories</span><br></pre></td></tr></table></figure><p>通过查阅资料显示，GIt从版本<code>2.9.0</code>开始，预设行为不允许合并没有共同祖先的分支，需要加上<code>--allow-unrelated-histories</code>选项进行pull操作才不会出现此类错误信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git pull origin master --allow-unrelated-histories</span><br></pre></td></tr></table></figure><p>相关参考：</p><ul><li><a href="https://git-scm.com/docs/git-merge/2.9.0" target="_blank" rel="noopener">Git merge (2.9.0)</a></li><li><a href="https://stackoverflow.com/questions/37937984/git-refusing-to-merge-unrelated-histories" target="_blank" rel="noopener">StackoverFlow: Git refusing to merge unrelated histories</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近在使用brew更新了git之后，发现在与Github上的新创建的&lt;code&gt;repo&lt;/code&gt;建立关联的之后，进行&lt;code&gt;pull&lt;/code&gt;操作会出现类似于下面的这种错误：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;
      
    
    </summary>
    
    
      <category term="Git" scheme="https://notes.wanghao.work/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>Docker Compose：链接外部容器的几种方式</title>
    <link href="https://notes.wanghao.work/2017-06-12-Docker-Compose-%E9%93%BE%E6%8E%A5%E5%A4%96%E9%83%A8%E5%AE%B9%E5%99%A8%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F.html"/>
    <id>https://notes.wanghao.work/2017-06-12-Docker-Compose-链接外部容器的几种方式.html</id>
    <published>2017-06-12T23:04:41.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<div class="note info"><p>在Docker中，容器之间的链接是一种很常见的操作：它提供了访问其中的某个容器的网络服务而不需要将所需的端口暴露给Docker Host主机的功能。Docker Compose中对该特性的支持同样是很方便的。然而，如果需要链接的容器没有定义在同一个<code>docker-compose.yml</code>中的时候，这个时候就稍微麻烦复杂了点。</p></div><a id="more"></a><p>在不使用Docker Compose的时候，将两个容器链接起来使用<code>—link</code>参数，相对来说比较简单，以<code>nginx</code>镜像为例子：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run --rm --name test1 -d nginx  #开启一个实例test1</span><br><span class="line">docker run --rm --name test2 --link test1 -d nginx #开启一个实例test2并与test1建立链接</span><br></pre></td></tr></table></figure><p>这样，<code>test2</code>与<code>test1</code>便建立了链接，就可以在<code>test2</code>中使用访问<code>test1</code>中的服务了。</p><p>如果使用Docker Compose，那么这个事情就更简单了，还是以上面的<code>nginx</code>镜像为例子，编辑<code>docker-compose.yml</code>文件为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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="attr">version:</span> <span class="string">"3"</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr">  test2:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">    depends_on:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">test1</span></span><br><span class="line"><span class="attr">    links:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">test1</span></span><br><span class="line"><span class="attr">  test1:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br></pre></td></tr></table></figure><p>最终效果与使用普通的Docker命令<code>docker run xxxx</code>建立的链接并无区别。这只是一种最为理想的情况。</p><hr><ol><li>如果容器没有定义在同一个<code>docker-compose.yml</code>文件中，应该如何链接它们呢？</li><li>又如果定义在<code>docker-compose.yml</code>文件中的容器需要与<code>docker run xxx</code>启动的容器链接，需要如何处理？</li></ol><p>针对这两种典型的情况，下面给出我个人测试可行的办法：</p><hr><h3 id="方式一：让需要链接的容器同属一个外部网络"><a href="#方式一：让需要链接的容器同属一个外部网络" class="headerlink" title="方式一：让需要链接的容器同属一个外部网络"></a>方式一：让需要链接的容器同属一个外部网络</h3><p>我们还是使用nginx镜像来模拟这样的一个情景：假设我们需要将两个使用Docker Compose管理的nignx容器（<code>test1</code>和<code>test2</code>）链接起来，使得<code>test2</code>能够访问<code>test1</code>中提供的服务，这里我们以能ping通为准。</p><p>首先，我们定义容器<code>test1</code>的<code>docker-compose.yml</code>文件内容为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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="attr">version:</span> <span class="string">"3"</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr">  test2:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">    container_name:</span> <span class="string">test1</span></span><br><span class="line"><span class="attr">    networks:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">default</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">app_net</span></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"><span class="attr">  app_net:</span></span><br><span class="line"><span class="attr">    external:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>容器<code>test2</code>内容与<code>test1</code>基本一样，只是多了一个<code>external_links</code>,需要特别说明的是：<strong>最近发布的Docker版本已经不需要使用external_links来链接容器，容器的DNS服务可以正确的作出判断</strong>，因此如果你你需要兼容较老版本的Docker的话，那么容器<code>test2</code>的<code>docker-compose.yml</code>文件内容为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="attr">version:</span> <span class="string">"3"</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr">  test2:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">    networks:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">default</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">app_net</span></span><br><span class="line"><span class="attr">    external_links:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">test1</span></span><br><span class="line"><span class="attr">    container_name:</span> <span class="string">test2</span></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"><span class="attr">  app_net:</span></span><br><span class="line"><span class="attr">    external:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>否则的话，<code>test2</code>的<code>docker-compose.yml</code>和<code>test1</code>的定义完全一致，不需要额外多指定一个<code>external_links</code>。相关的问题请参见stackoverflow上的相关问题：<a href="https://stackoverflow.com/questions/39067295/docker-compose-external-container" target="_blank" rel="noopener">docker-compose + external container</a></p><p>正如你看到的那样，这里两个容器的定义里都使用了同一个外部网络<code>app_net</code>,因此，我们需要在启动这两个容器之前通过以下命令再创建外部网络：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker network create app_net</span><br></pre></td></tr></table></figure><p>之后，通过<code>docker-compose up -d</code>命令启动这两个容器，然后执行<code>docker exec -it test2 ping test1</code>,你将会看到如下的输出：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">docker exec -it test2 ping test1</span><br><span class="line">PING test1 (172.18.0.2): 56 data bytes</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.091 ms</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.146 ms</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.150 ms</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=3 ttl=64 time=0.145 ms</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=4 ttl=64 time=0.126 ms</span><br><span class="line">64 bytes from 172.18.0.2: icmp_seq=5 ttl=64 time=0.147 ms</span><br></pre></td></tr></table></figure><p>证明这两个容器是成功链接了，反过来在<code>test1</code>中ping<code>test2</code>也是能够正常ping通的。</p><p>如果我们通过<code>docker run --rm --name test3 -d nginx</code>这种方式来先启动了一个容器(<code>test3</code>)并且没有指定它所属的外部网络，而需要将其与<code>test1</code>或者<code>test2</code>链接的话，这个时候手动链接外部网络即可：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker network connect app_net test3</span><br></pre></td></tr></table></figure><p>这样，三个容器都可以相互访问了。</p><hr><h3 id="方式二：更改需要链接的容器的网络模式"><a href="#方式二：更改需要链接的容器的网络模式" class="headerlink" title="方式二：更改需要链接的容器的网络模式"></a>方式二：更改需要链接的容器的网络模式</h3><p>通过更改你想要相互链接的容器的网络模式为<code>bridge</code>,并指定需要链接的外部容器（<code>external_links</code>)即可。与同属外部网络的容器可以相互访问的链接方式一不同，这种方式的访问是单向的。</p><p>还是以nginx容器镜像为例子，如果容器实例<code>nginx1</code>需要访问容器实例<code>nginx2</code>，那么<code>nginx2</code>的<code>doker-compose.yml</code>定义为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">"3"</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr">  nginx2:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">    container_name:</span> <span class="string">nginx2</span></span><br><span class="line"><span class="attr">    network_mode:</span> <span class="string">bridge</span></span><br></pre></td></tr></table></figure><p>与其对应的，<code>nginx1</code>的<code>docker-compose.yml</code>定义为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">"3"</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr">  nginx1:</span></span><br><span class="line"><span class="attr">    image:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">    external_links:</span></span><br><span class="line"><span class="bullet">      -</span> <span class="string">nginx2</span></span><br><span class="line"><span class="attr">    container_name:</span> <span class="string">nginx1</span></span><br><span class="line"><span class="attr">    network_mode:</span> <span class="string">bridge</span></span><br></pre></td></tr></table></figure><blockquote><p>需要特别说明的是，这里的<code>external_links</code>是不能省略的，而且<code>nginx1</code>的启动必须要在<code>nginx2</code>之后，否则可能会报找不到容器<code>nginx2</code>的错误。</p></blockquote><p>接着我们使用ping来测试下连通性：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ docker exec -it nginx1 ping nginx2  # nginx1 to nginx2</span><br><span class="line">PING nginx2 (172.17.0.4): 56 data bytes</span><br><span class="line">64 bytes from 172.17.0.4: icmp_seq=0 ttl=64 time=0.141 ms</span><br><span class="line">64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.139 ms</span><br><span class="line">64 bytes from 172.17.0.4: icmp_seq=2 ttl=64 time=0.145 ms</span><br><span class="line"></span><br><span class="line">$ docker exec -it nginx2 ping nginx1 #nginx2 to nginx1</span><br><span class="line">ping: unknown host</span><br></pre></td></tr></table></figure><p>以上也能充分证明这种方式是属于单向联通的。</p><p>在实际应用中根据自己的需要灵活的选择这两种链接方式，如果想偷懒的话，大可选择第二种。不过我更推荐第一种，不难看出无论是联通性还是灵活性，较为更改网络模式的第二种都更为友好。</p>]]></content>
    
    <summary type="html">
    
      &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;在Docker中，容器之间的链接是一种很常见的操作：它提供了访问其中的某个容器的网络服务而不需要将所需的端口暴露给Docker Host主机的功能。Docker Compose中对该特性的支持同样是很方便的。然而，如果需要链接的容器没有定义在同一个&lt;code&gt;docker-compose.yml&lt;/code&gt;中的时候，这个时候就稍微麻烦复杂了点。&lt;/p&gt;&lt;/div&gt;
    
    </summary>
    
    
      <category term="Docker" scheme="https://notes.wanghao.work/tags/Docker/"/>
    
      <category term="Docker Compose" scheme="https://notes.wanghao.work/tags/Docker-Compose/"/>
    
  </entry>
  
  <entry>
    <title>MySQL API Drivers安装小记</title>
    <link href="https://notes.wanghao.work/2017-05-20-%E8%A7%A3%E5%86%B3Mysql%E7%9A%84Python%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85%E5%A4%B1%E8%B4%A5.html"/>
    <id>https://notes.wanghao.work/2017-05-20-解决Mysql的Python驱动安装失败.html</id>
    <published>2017-05-20T02:12:46.000Z</published>
    <updated>2019-07-03T03:40:55.500Z</updated>
    
    <content type="html"><![CDATA[<h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>最近需要使用Django写点东西，由于自己的macbook上没有也不打算安装MySQL而是以Docker的MySQL镜像替代，Django文档提供了三种MySQL驱动供选择，官方推荐的是<a href="https://pypi.python.org/pypi/mysqlclient" target="_blank" rel="noopener"><code>mysqlclient</code></a>,由于我本地没有安装MySQL，所以是没有Native Driver的以至于在安装MySQL驱动的时候遇到了点小问题，在此记录下。</p><hr><a id="more"></a><h4 id="过程"><a href="#过程" class="headerlink" title="过程"></a>过程</h4><p>安装<code>mysqlclient</code>：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install mysqlclient</span><br></pre></td></tr></table></figure><p>然而得到错误信息如下：</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><span class="line">7</span><br><span class="line">8</span><br><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">Collecting mysqlclient</span><br><span class="line">  Using cached mysqlclient-1.3.10.tar.gz</span><br><span class="line">    Complete output from command python setup.py egg_info:</span><br><span class="line">    /bin/sh: mysql_config: command not found</span><br><span class="line">    Traceback (most recent call last):</span><br><span class="line">      File "&lt;string&gt;", line 1, in &lt;module&gt;</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-mcd2q8ly/mysqlclient/setup.py", line 17, in &lt;module&gt;</span><br><span class="line">        metadata, options = get_config()</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-mcd2q8ly/mysqlclient/setup_posix.py", line 44, in get_config</span><br><span class="line">        libs = mysql_config("libs_r")</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-mcd2q8ly/mysqlclient/setup_posix.py", line 26, in mysql_config</span><br><span class="line">        raise EnvironmentError("%s not found" % (mysql_config.path,))</span><br><span class="line">    OSError: mysql_config not found</span><br><span class="line"></span><br><span class="line">    ----------------------------------------</span><br><span class="line">Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-mcd2q8ly/mysqlclient/</span><br></pre></td></tr></table></figure><p>因为没有安装MySQL，所以在安装<code>mysqlclient</code>之前还需要安装Connector，如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install mysql-connector-c</span><br></pre></td></tr></table></figure><p>之后安装再安装<code>mysqlclient</code>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install mysqlclient</span><br></pre></td></tr></table></figure><p>然后又就报错了，错误信息如下：</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><span class="line">7</span><br><span class="line">8</span><br><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">Collecting mysqlclient</span><br><span class="line">  Using cached mysqlclient-1.3.10.tar.gz</span><br><span class="line">    Complete output from command python setup.py egg_info:</span><br><span class="line">    Traceback (most recent call last):</span><br><span class="line">      File "&lt;string&gt;", line 1, in &lt;module&gt;</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-smwmu1qn/mysqlclient/setup.py", line 17, in &lt;module&gt;</span><br><span class="line">        metadata, options = get_config()</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-smwmu1qn/mysqlclient/setup_posix.py", line 54, in get_config</span><br><span class="line">        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-smwmu1qn/mysqlclient/setup_posix.py", line 54, in &lt;listcomp&gt;</span><br><span class="line">        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]</span><br><span class="line">      File "/private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-smwmu1qn/mysqlclient/setup_posix.py", line 12, in dequote</span><br><span class="line">        if s[0] in "\"'" and s[0] == s[-1]:</span><br><span class="line">    IndexError: string index out of range</span><br><span class="line"></span><br><span class="line">    ----------------------------------------</span><br><span class="line">Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/ll/d2yyxp555ss1gm613y33hfy80000gn/T/pip-build-smwmu1qn/mysqlclient/</span><br></pre></td></tr></table></figure><hr><h4 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h4><p>通过查找资料得出可能的结论是通过brew安装的<code>mysql-connector-c</code>配置可能不正确，打开<code>/usr/local/bin/mysql_config</code>脚本修改其中的部分内容:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create options</span></span><br><span class="line">libs=<span class="string">"-L<span class="variable">$pkglibdir</span>"</span></span><br><span class="line">libs=<span class="string">"<span class="variable">$libs</span>  -l"</span></span><br></pre></td></tr></table></figure><p>修改为：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> Create options</span></span><br><span class="line">libs="-L$pkglibdir"</span><br><span class="line">libs="$libs  -lmysqlclient -lssl -lcrypto"</span><br></pre></td></tr></table></figure><p>保存，再次安装<code>mysqlclient</code>应该就会正常安装了。接着就可以使用Django和运行在Docker中的MySQL愉快的Coding了～</p>]]></content>
    
    <summary type="html">
    
      &lt;h4 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h4&gt;&lt;p&gt;最近需要使用Django写点东西，由于自己的macbook上没有也不打算安装MySQL而是以Docker的MySQL镜像替代，Django文档提供了三种MySQL驱动供选择，官方推荐的是&lt;a href=&quot;https://pypi.python.org/pypi/mysqlclient&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;mysqlclient&lt;/code&gt;&lt;/a&gt;,由于我本地没有安装MySQL，所以是没有Native Driver的以至于在安装MySQL驱动的时候遇到了点小问题，在此记录下。&lt;/p&gt;
&lt;hr&gt;
    
    </summary>
    
    
      <category term="Python" scheme="https://notes.wanghao.work/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>Android Studio生成Java方法描述符</title>
    <link href="https://notes.wanghao.work/2017-03-21-Android-Studio%E6%94%AF%E6%8C%81%E7%94%9F%E6%88%90Java-Native%E6%96%B9%E6%B3%95%E7%AD%BE%E5%90%8D.html"/>
    <id>https://notes.wanghao.work/2017-03-21-Android-Studio支持生成Java-Native方法签名.html</id>
    <published>2017-03-21T21:32:50.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<div class="note info"><p>在日常的Android NDK开发中，会不可避免的用到C与Java代码相互调用的情况。Java调用C的方法还好，C调用Java的方法就比较麻烦了。需要编写看着就头疼的Java方法描述符才能正确的调用Java方法。 </p></div><p>其中常见的Java方法域和描述符如下表所示：</p><table><thead><tr><th style="text-align:center">Java类型</th><th style="text-align:center">签名</th></tr></thead><tbody><tr><td style="text-align:center">Boolean</td><td style="text-align:center">Z</td></tr><tr><td style="text-align:center">Byte</td><td style="text-align:center">B</td></tr><tr><td style="text-align:center">Char</td><td style="text-align:center">C</td></tr><tr><td style="text-align:center">Short</td><td style="text-align:center">S</td></tr><tr><td style="text-align:center">Int</td><td style="text-align:center">I</td></tr><tr><td style="text-align:center">Long</td><td style="text-align:center">J</td></tr><tr><td style="text-align:center">Float</td><td style="text-align:center">F</td></tr><tr><td style="text-align:center">Double</td><td style="text-align:center">D</td></tr><tr><td style="text-align:center">Fully-qualified-class</td><td style="text-align:center">Lfully-qualified-class</td></tr><tr><td style="text-align:center">type[]</td><td style="text-align:center">[type</td></tr><tr><td style="text-align:center">Method type</td><td style="text-align:center">(arg-type)ret-type</td></tr></tbody></table><p>通过上述对照表，我们可以通过C代码查找一个为<code>String</code>类型的Java静态字段，例如:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">jfieldID staticJavaFieldId;</span><br><span class="line">staticJavaFieldId = env-&gt;GetStaticFieldID(clazz, <span class="string">"mStaticField"</span>, <span class="string">"Ljava/lang/String;"</span>);</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* do something...</span></span><br><span class="line"><span class="comment">**/</span></span><br></pre></td></tr></table></figure><a id="more"></a><hr><p>借助<code>javap</code>我们可以很方便的得知一个<code>class</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">$ javap -s -p com.xiamo.test.Message</span><br><span class="line">  </span><br><span class="line">Compiled from <span class="string">"Message.java"</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">com</span>.<span class="title">xiamo</span>.<span class="title">test</span>.<span class="title">Message</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> java.lang.String TAG;</span><br><span class="line">    descriptor: Ljava/lang/String;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> CHECK_POINT;</span><br><span class="line">    descriptor: I</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> ERROR_NOT_SERVER;</span><br><span class="line">    descriptor: I</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">int</span> errorCode;</span><br><span class="line">    descriptor: I</span><br><span class="line">  <span class="keyword">public</span> java.lang.String message;</span><br><span class="line">    descriptor: Ljava/lang/String;</span><br><span class="line">  <span class="keyword">public</span> com.xiamo.test.Message();</span><br><span class="line">    descriptor: ()V</span><br></pre></td></tr></table></figure><p>但是每次需要查看对应类的方法描述符的时候都需要手动敲一次命令，这样显然不够清真。好在<code>Android Studio</code>为我们提供了<code>External Tools</code>。我们可以用它来自定义这个操作简化我们的双手。</p><h3 id="设置External-Tools"><a href="#设置External-Tools" class="headerlink" title="设置External Tools"></a>设置External Tools</h3><p>打开<code>Android Studio</code>的设置页面，在<code>Tools</code>选项卡中选中<code>External Tools</code>，如下图所示：</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/image/jni/external%20tools.png" alt title="选择External Tools"></p><p>点击右侧区域的<code>+</code>新增一个<code>Tools</code>,在选卡中填入如下图所示的参数:</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/image/jni/config.png" alt title="设置External Tools"></p><ul><li><code>Name</code> 为你要设置的<code>External Tools</code>的名字，便于你自己标识就行，此处我设置为<code>JNI Descriptor Generator</code></li><li><code>Program</code>为<code>Tools</code>执行的命令的路径，如果你需要替换为你自己JDK中的<code>javap</code>修改这个值就行，此处使用<code>Android Studio</code>自带的<code>JDK</code>路径，填入<code>$JDKPath$/bin/javap</code></li><li><code>Parameters</code>为命令执行的参数，我们要获取方法描述符，所以设置为：<code>-s -p $FileClass$</code></li><li><code>Working directory</code>为上述设置好的工具执行的目录，设置为<code>$ModuleFileDir$/build/intermediates/classes/debug</code></li></ul><p>点击保存，我们的<code>External Tools</code>就设置好啦。这个时候在<code>Tools</code>—&gt;<code>External Tools</code>中就可以看到我们设置好的<code>Tools</code>了。需要注意的是这个时候点击改工具查看当前我们选中的Java源文件的文件操作符，是可能会报错找不到指定的class文件。</p><p>这是因为我们指定的<code>Working directory</code>中还没有生成class文件，选择<code>Build</code>选项中的<code>Make Project</code>，等待make完成，再次点击<code>Tools</code>—&gt;<code>External Tools—&gt;</code> <code>JNI Descriptor Generator</code> 即可生成对应Java源文件的文件描述符了。这样我们就可以愉快的调用使用C调用Java中的方法咯。</p>]]></content>
    
    <summary type="html">
    
      &lt;div class=&quot;note info&quot;&gt;&lt;p&gt;在日常的Android NDK开发中，会不可避免的用到C与Java代码相互调用的情况。Java调用C的方法还好，C调用Java的方法就比较麻烦了。需要编写看着就头疼的Java方法描述符才能正确的调用Java方法。 &lt;/p&gt;&lt;/div&gt;
&lt;p&gt;其中常见的Java方法域和描述符如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:center&quot;&gt;Java类型&lt;/th&gt;
&lt;th style=&quot;text-align:center&quot;&gt;签名&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Boolean&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Z&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Byte&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Char&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Short&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;S&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Int&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Long&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;J&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Float&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Double&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Fully-qualified-class&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Lfully-qualified-class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;type[]&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;[type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:center&quot;&gt;Method type&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;(arg-type)ret-type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;通过上述对照表，我们可以通过C代码查找一个为&lt;code&gt;String&lt;/code&gt;类型的Java静态字段，例如:&lt;/p&gt;
&lt;figure class=&quot;highlight c&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;jfieldID staticJavaFieldId;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;staticJavaFieldId = env-&amp;gt;GetStaticFieldID(clazz, &lt;span class=&quot;string&quot;&gt;&quot;mStaticField&quot;&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&quot;Ljava/lang/String;&quot;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;* do something...&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;**/&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
      <category term="Android Studio" scheme="https://notes.wanghao.work/tags/Android-Studio/"/>
    
      <category term="JNI" scheme="https://notes.wanghao.work/tags/JNI/"/>
    
  </entry>
  
  <entry>
    <title>FFmpeg折腾笔记之编译FFmpeg</title>
    <link href="https://notes.wanghao.work/2017-01-13-FFmpeg%E6%8A%98%E8%85%BE%E7%AC%94%E8%AE%B0%E4%B9%8B%E7%BC%96%E8%AF%91FFmpeg.html"/>
    <id>https://notes.wanghao.work/2017-01-13-FFmpeg折腾笔记之编译FFmpeg.html</id>
    <published>2017-01-13T21:58:04.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>​        又有很久没有写新的文章了，感觉再不写点什么东西博客草都长满了。于是打算折腾下FFmpeg，记录下折腾的过程，以熟悉Android NDK开发的基本操作。过程中有地方有错误欢迎指出，如果你对这个方面有所了解，欢迎讨论指教。</p><h4 id="编译环境"><a href="#编译环境" class="headerlink" title="编译环境"></a>编译环境</h4><ul><li><a href="https://ffmpeg.org/releases/ffmpeg-2.8.11.tar.bz2" target="_blank" rel="noopener">FFmpeg 2.8.11 “Feynman”</a></li><li>Mac OS X </li><li>NDK  <code>14.0.3770861</code></li></ul><p>下载完成FFmpeg源码之后，先对源码根目录中的<code>configure</code>文件进行修改以适应Android平台。因为默认编译出来的动态库文件版本号在<code>.so</code>之后，例如：<code>libavcodec.so.56.60.100</code>。Android平台对这种格式不能很好的识别（如果你不介意一个一个修改文件名的话）。通过<code>Vim</code>或者其他文本编辑器打开<code>configure</code>文件的第<code>2934</code>行（如果你下载的FFmpeg版本和我的一样的话）将:</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></pre></td><td class="code"><pre><span class="line">SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'</span><br><span class="line">LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'</span><br><span class="line">SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'</span><br><span class="line">SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'</span><br></pre></td></tr></table></figure><p>修改为:</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></pre></td><td class="code"><pre><span class="line">SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'  </span><br><span class="line">LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'  </span><br><span class="line">SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'  </span><br><span class="line">SLIB_INSTALL_LINKS='$(SLIBNAME)'</span><br></pre></td></tr></table></figure><p>后保存。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;​        又有很久没有写新的文章了，感觉再不写点什么东西博客草都长满了。于是打算折腾下FFmpeg，记录下折腾的过程，以熟悉Android NDK开发的基本操作。过程中有地方有错误欢迎指出，如果你对这个方面有所了解，欢迎讨论指教。&lt;/p&gt;
&lt;h4 id=&quot;编译环境&quot;
      
    
    </summary>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
      <category term="FFmpeg" scheme="https://notes.wanghao.work/tags/FFmpeg/"/>
    
      <category term="NDK" scheme="https://notes.wanghao.work/tags/NDK/"/>
    
  </entry>
  
  <entry>
    <title>Mac自带VNC Client</title>
    <link href="https://notes.wanghao.work/2016-11-24-Mac%E8%87%AA%E5%B8%A6VNC-Client.html"/>
    <id>https://notes.wanghao.work/2016-11-24-Mac自带VNC-Client.html</id>
    <published>2016-11-24T21:57:28.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>由于有需要远程VNC的需要，苦苦寻找一款Mac上比较好使的VNC Client，结果经人指点，发现原来MacOS自带了VNC Client，只是没有在Launchpad中显示罢了。使用<code>command</code>+<code>space</code> 调出<code>Spotlight搜索</code>键入<code>screen Sharing.app</code>即可。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;由于有需要远程VNC的需要，苦苦寻找一款Mac上比较好使的VNC Client，结果经人指点，发现原来MacOS自带了VNC Client，只是没有在Launchpad中显示罢了。使用&lt;code&gt;command&lt;/code&gt;+&lt;code&gt;space&lt;/code&gt; 调出&lt;co
      
    
    </summary>
    
    
      <category term="Mac" scheme="https://notes.wanghao.work/tags/Mac/"/>
    
      <category term="Tips" scheme="https://notes.wanghao.work/tags/Tips/"/>
    
  </entry>
  
  <entry>
    <title>开启Git大小写敏感</title>
    <link href="https://notes.wanghao.work/2016-11-24-%E5%BC%80%E5%90%AFGit%E5%A4%A7%E5%B0%8F%E5%86%99%E6%95%8F%E6%84%9F.html"/>
    <id>https://notes.wanghao.work/2016-11-24-开启Git大小写敏感.html</id>
    <published>2016-11-24T21:35:57.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>最近公司开发的项目的时候，自己或者队友都经常会手抖针对某些文件的命名规范大小写写错。（毕竟入冬了。。）尤其是包名小写手抖写成大写了。。又由于我设置Android Studio新建文件自动添加到Git，一来二往，导致推上服务器之后发现自己命名有问题，顺手改成小写，这个时候问题就来了。</p><p>由于Git默认是大小写不敏感的，导致我这边虽然修改了，然而队友那边还是老样子，可能会导致后续的提交出现问题让项目无法编译通过，虽然不是什么大问题，稍微小改一下就行。然而鉴于这种问题出现了几次，每次都手动修改还是挺闹心的。我们还是来动手让Git区分大小写吧。</p><p>如果只是想应用于当前项目，那么在当前项目中使用执行以下<code>Git命令</code>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config core.ignorecase false</span><br></pre></td></tr></table></figure><p>当然，如果想一劳永逸的话，推荐还是做一个全局配置:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global core.ignorecase false</span><br></pre></td></tr></table></figure><p>这样之后的项目都不用担心大小写都问题闹心了。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近公司开发的项目的时候，自己或者队友都经常会手抖针对某些文件的命名规范大小写写错。（毕竟入冬了。。）尤其是包名小写手抖写成大写了。。又由于我设置Android Studio新建文件自动添加到Git，一来二往，导致推上服务器之后发现自己命名有问题，顺手改成小写，这个时候问题
      
    
    </summary>
    
    
      <category term="Git" scheme="https://notes.wanghao.work/tags/Git/"/>
    
      <category term="Tips" scheme="https://notes.wanghao.work/tags/Tips/"/>
    
  </entry>
  
  <entry>
    <title>基于LeanCloud、Telegram实现多说评论推送</title>
    <link href="https://notes.wanghao.work/2016-10-26-%E5%9F%BA%E4%BA%8ELeanCloud%E3%80%81Telegram%E5%AE%9E%E7%8E%B0%E5%A4%9A%E8%AF%B4%E8%AF%84%E8%AE%BA%E6%8E%A8%E9%80%81.html"/>
    <id>https://notes.wanghao.work/2016-10-26-基于LeanCloud、Telegram实现多说评论推送.html</id>
    <published>2016-10-26T00:19:53.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>众所周知多说评论是不支持主动推送的，也就是说如果访客主动留言评论，多说是不会给我们邮件推送提醒的。但是我们总不能每隔一段时间去检查下博客的评论留言吧，这样效率也太低了ヾ(。￣□￣)ﾂ゜゜゜于是趁着最近闲的蛋疼折腾出了这么个东东，能够实现访客主动评论的时候，在5分钟之内响应并通过Telegram推送到手机上方便查看和浏览。</p><hr><p>嗯，相关的源码和基本的部署说明丢在Github上了,感兴趣的可以<font color="red"><strong><a href="https://github.com/Doublemine/TelegramBot" target="_blank" rel="noopener">戳此查看</a></strong></font>。</p><ul><li><a href="https://github.com/Doublemine/TelegramBot" target="_blank" rel="noopener">Telegram Bot For DuoShuo</a></li></ul><p>目前保证基本的功能能够稳定的运行，后续再添加一些其它的功能吧。嗯，详细的配置说明先挖个坑，改天再写。懒癌犯了，先休息~</p><p><img src="https://ws1.sinaimg.cn/large/694830ebgy1fh3r3jys56g208c08cx6p.jpg" alt title="休息"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;众所周知多说评论是不支持主动推送的，也就是说如果访客主动留言评论，多说是不会给我们邮件推送提醒的。但是我们总不能每隔一段时间去检查下博客的评论留言吧，这样效率也太低了ヾ(。￣□￣)ﾂ゜゜゜于是趁着最近闲的蛋疼折腾出了这么个东东，能够实现访客主动评论的时候，在5分钟之内响应并
      
    
    </summary>
    
      <category term="Python" scheme="https://notes.wanghao.work/categories/Python/"/>
    
    
      <category term="LeanCloud" scheme="https://notes.wanghao.work/tags/LeanCloud/"/>
    
      <category term="Python" scheme="https://notes.wanghao.work/tags/Python/"/>
    
      <category term="Telegram" scheme="https://notes.wanghao.work/tags/Telegram/"/>
    
  </entry>
  
  <entry>
    <title>当Android应用被强杀之后</title>
    <link href="https://notes.wanghao.work/2016-09-10-AndroidProcessbeKilledweShoudIDo.html"/>
    <id>https://notes.wanghao.work/2016-09-10-AndroidProcessbeKilledweShoudIDo.html</id>
    <published>2016-09-10T19:00:20.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>Android应用在后台运行的时候很容易被强杀，尤其是在国内XX助手、XX管家、XX大师之类的应用大行其道之下。如果我们开发的应用没有被用户加入白名单，很大程度上会被系统因为内存不足或者用户主动给应用在后台运行的时候杀掉。这个时候问题就来了：</p><ul><li><strong>如何保证我们的应用在被强杀之后用户再次回到应用来保证应用的稳定性而不Crash？</strong></li></ul><p>为了说明上述问题，我们来简单模拟这样一个过程:</p><blockquote><p>APP –&gt; ActivityA –&gt; ActivityB –&gt;ActivityC –&gt; Pressed Home</p></blockquote><p>假设APP在Activity C页面用户按下<code>Home</code>键应用退到后台运行。这个时候启动DDMS，选中该APP的进程，Kill。然后我们从运行APP历史列表中选中该APP并将其置于前台，这个时候回到该应用的界面还是Activity C。再点击返回按钮回到ActivityB，在某些性能比较差一点的机器上可能会出现短暂的黑屏然后才会显示出ActivityB。这是因为该Activity实例其实在Kill该APP进程的时候已经被销毁了，但是Android系统虽然销毁了Activity实例，却并没有销毁该APP的Activity栈。因此我们点击返回按钮还是会回到ActivityB。但是需要重新构建该ActivityB的实例。</p><p>这样看貌似并没有什么问题，然而事情并不会这么简单（废话，不然我写这篇博客干嘛。。），如果ActivityB中引用了静态变量并尝试获取其值的时候，这个时候是会出现NPE的。</p><a id="more"></a><p>我们简单来总结下上述过程:</p><ol><li><p>当应用在后台被Kill，整个APP进程都被销毁，所有变量都被清空，包括Application的实例。</p></li><li><p>虽然所有变量和实例都被销毁，但是Activity栈并没有被清空，所以我们回到应用还能得知页面的打开顺序。</p></li><li><p>当应用被强杀时，会自动调用<code>onSaveInstance</code>方法去保存一些核心变量，然而这在面对N多的页面的时候显然不是一件省心的事情，而且你也不能保证你的队友也会这么做。。</p></li><li><p>在某些性能比较低或者页面逻辑比较复杂的页面会黑屏是因为需要重建ActivityB的实例，也就是需要重走Activity的声明周期<code>OnCreate</code>，性能差点的机器上自然就会有短暂的黑屏了。</p></li></ol><p>如果APP中没有静态变量的引用，那就不会出现NEP，但是一旦引用了静态变量，这个时候可能就比较危险了。（静态变量包括全局的登录状态，全局的用户配置、标志位之类的数据）当然了，如果你能将所有的静态变量修改到单例中去，并将其持久化，为NULL的时候再去取的话，原则上来说这样也可以避免NPE。然而要是这样做的话很大程度上会减缓开发的进度，而且指不定哪个队友就给你挖坑了。然后你怎么挂的都不知道。。</p><p>为了一劳永逸的解决这个问题，我们需要冷静下来思考一下:<strong>既然APP被强杀了，为啥还要回到原先的页面中去而不是重走启动APP的流程?</strong></p><p>我们虽然不能阻止Android系统销毁实例却保存Activity栈，也不想多写那些持久化或者Cache静态变量代码（这将是一件费力而且不太讨好的事情）所以我们唯一能做的就是检测应用是否被强杀，并且在被强杀之后重走启动流程而不是回到原先的逻辑当中。</p><p>以下给出我的一种实现方式，如果你有更好的想法，欢迎和我交流:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">BaseActivity</span> <span class="keyword">extends</span> <span class="title">AppCompatActivity</span> </span>&#123;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">int</span> FLAG;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">int</span> FLAG_NOT_INIT = <span class="number">200</span>;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(@Nullable Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line">    <span class="keyword">if</span> (FLAG != FLAG_NOT_INIT) &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">this</span>.getClass() == LauncherActivity.class) &#123;</span><br><span class="line">        FLAG = FLAG_NOT_INIT;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        reLauncher();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们实例化上述场景，给出一种对应关系:</p><p>ActivityA –&gt; 启动页</p><p>ActivityB –&gt; 主页</p><p>ActivityC –&gt; 详情页</p><p>其中将ActivityB的<code>launchMode</code>设置为<code>singleTask</code>，并且在BaseActivity中使用静态变量FLAG进行判断当前应用是否被强杀，如果被强杀则利用ActivityB的<code>launchMode</code>特性清空栈并重新初始化即可。</p><h6 id="有关Activity的启动模式相关文章，可参阅Activity启动模式图文详解"><a href="#有关Activity的启动模式相关文章，可参阅Activity启动模式图文详解" class="headerlink" title="有关Activity的启动模式相关文章，可参阅Activity启动模式图文详解"></a>有关Activity的启动模式相关文章，可参阅<a href="http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0520/2897.html" target="_blank" rel="noopener">Activity启动模式图文详解</a></h6>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Android应用在后台运行的时候很容易被强杀，尤其是在国内XX助手、XX管家、XX大师之类的应用大行其道之下。如果我们开发的应用没有被用户加入白名单，很大程度上会被系统因为内存不足或者用户主动给应用在后台运行的时候杀掉。这个时候问题就来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如何保证我们的应用在被强杀之后用户再次回到应用来保证应用的稳定性而不Crash？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了说明上述问题，我们来简单模拟这样一个过程:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;APP –&amp;gt; ActivityA –&amp;gt; ActivityB –&amp;gt;ActivityC –&amp;gt; Pressed Home&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设APP在Activity C页面用户按下&lt;code&gt;Home&lt;/code&gt;键应用退到后台运行。这个时候启动DDMS，选中该APP的进程，Kill。然后我们从运行APP历史列表中选中该APP并将其置于前台，这个时候回到该应用的界面还是Activity C。再点击返回按钮回到ActivityB，在某些性能比较差一点的机器上可能会出现短暂的黑屏然后才会显示出ActivityB。这是因为该Activity实例其实在Kill该APP进程的时候已经被销毁了，但是Android系统虽然销毁了Activity实例，却并没有销毁该APP的Activity栈。因此我们点击返回按钮还是会回到ActivityB。但是需要重新构建该ActivityB的实例。&lt;/p&gt;
&lt;p&gt;这样看貌似并没有什么问题，然而事情并不会这么简单（废话，不然我写这篇博客干嘛。。），如果ActivityB中引用了静态变量并尝试获取其值的时候，这个时候是会出现NPE的。&lt;/p&gt;
    
    </summary>
    
      <category term="Android" scheme="https://notes.wanghao.work/categories/Android/"/>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>Android之Keystore文件签名</title>
    <link href="https://notes.wanghao.work/2016-07-03-Android%E4%B9%8BKeystore%E6%96%87%E4%BB%B6%E7%AD%BE%E5%90%8D.html"/>
    <id>https://notes.wanghao.work/2016-07-03-Android之Keystore文件签名.html</id>
    <published>2016-07-03T20:58:16.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p>Android应用开发的时候会经常使用到第三方SDK，例如地图、推送、支付以及分享等。而这些第三方服务SDK为了应用不被滥用都会有签名验证机制。我们通过上传Android应用的签名来在服务端配置以验证应用的合法性。本文将讲述下在Android Stduio中常用的签名操作。</p><h3 id="默认KeyStore"><a href="#默认KeyStore" class="headerlink" title="默认KeyStore"></a>默认KeyStore</h3><h4 id="默认KeyStore存储路径"><a href="#默认KeyStore存储路径" class="headerlink" title="默认KeyStore存储路径"></a>默认KeyStore存储路径</h4><p>在Android Stduio中系统默认内置了一个签名文件<code>debug.keystore</code>，用于我们在debug下的默认App签名。如果没有在Gradle文件中特殊指定，那么Android Studio将自动使用默认的<code>debug.keystore</code>文件为项目App生成Debug版本的签名。</p><a id="more"></a><ul><li><p>在Mac/Linux系统中，<code>debug.keystore</code>文件默认储存在<code>~/.android/</code>路径下。</p></li><li><p>在Windows系统中，<code>debug.keystore</code>文件将默认存储在<code>C:\Users\{USERNAME}\.android\</code>路径下。</p></li></ul><h4 id="获取默认KeyStore-SHA-1"><a href="#获取默认KeyStore-SHA-1" class="headerlink" title="获取默认KeyStore SHA-1"></a>获取默认KeyStore <code>SHA-1</code></h4><p>知道了Android Stduio 默认的<code>debug.keystore</code>之后，下一步我们将是要获取其指纹信息，以便于在第三方服务配置中填入Debug指纹信息。</p><ul><li>在Linux/Mac系统中，打开终端并输入以下命令:</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -list -v -keystore ~/.android/debug.keystore -<span class="built_in">alias</span> androiddebugkey -storepass android -keypass android</span><br></pre></td></tr></table></figure><ul><li>在Windows系统中，在CMD中输入以下命令:</li></ul><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -list -v -keystore "<span class="variable">%USERPROFILE%</span>\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android</span><br></pre></td></tr></table></figure><p>回车执行之后，你将会看到类似下面的<code>debug.keystore</code>输出提示:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">别名: androiddebugkey</span><br><span class="line">创建日期: 2015-11-18</span><br><span class="line">条目类型: PrivateKeyEntry</span><br><span class="line">证书链长度: 1</span><br><span class="line">证书[1]:</span><br><span class="line">所有者: CN=Android Debug, O=Android, C=US</span><br><span class="line">发布者: CN=Android Debug, O=Android, C=US</span><br><span class="line">序列号: b15af1</span><br><span class="line">有效期开始日期: Wed Nov 18 07:18:45 CST 2015, 截止日期: Fri Nov 10 07:18:45 CST 2045</span><br><span class="line">证书指纹:</span><br><span class="line">         MD5: FE:A1:9C:02:71:A2:DA:F9:7F:1C:2B:61:D7:65:89:44</span><br><span class="line">         SHA1: 01:DF:58:7D:04:3E:76:B5:92:98:37:0E:DD:70:61:01:70:F5:C9:8E</span><br><span class="line">         SHA256: 84:18:44:C2:BD:AD:5D:A8:88:A1:96:EF:A6:27:86:0A:36:44:31:38:F2:5F:B6:4E:F1:10:EE:93:D6:22:CD:59</span><br><span class="line">         签名算法名称: SHA256withRSA</span><br><span class="line">         版本: 3</span><br><span class="line"></span><br><span class="line">扩展:</span><br></pre></td></tr></table></figure><p>我们将其中的证书指纹填入到第三方服务DEBUG配置中即可。当然了，有的时候出于这样或者那样的原因考虑，我们并不想使用系统默认的KeyStore或者就想自己生成一个新的KeyStore，Debug环境与Release环境都使用同一个来减少配置的麻烦。这个时候我们就需要创建一个新的KeyStore文件了。</p><h3 id="创建新的KeyStore"><a href="#创建新的KeyStore" class="headerlink" title="创建新的KeyStore"></a>创建新的KeyStore</h3><p>我们使用JDK自带的Keytool命令行工具即可完成KeyStore密钥库文件的创建，此处需要说明的是，Android Stduio中自带的图形化界面<code>KeyStore</code>生成工具生成的<code>.jks</code>文件与Keytool生成的<code>.keystore</code>文件在使用上没有任何区别。</p><h4 id="生成KeyStore"><a href="#生成KeyStore" class="headerlink" title="生成KeyStore"></a>生成KeyStore</h4><p>在终端中键入以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -genkey -v -keystore &#123;FILENAME.keystore&#125; -<span class="built_in">alias</span> &#123;ALIAS&#125; -keyalg RSA -validity &#123;DURATION&#125;</span><br></pre></td></tr></table></figure><ul><li><code>{FILENAME.keystore}</code> 为生成的KeyStore的文件名</li><li><code>{ALIAS}</code> 为生成的KeyStore文件的别名</li><li><code>{DURATION}</code> 为该KeyStore文件的过期时间</li></ul><p>下面将以生成一个test.keystore文件为示例:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -genkey -v -keystore test.keystore -<span class="built_in">alias</span> <span class="built_in">test</span> -keyalg RSA -validity 365</span><br></pre></td></tr></table></figure><p>键入以上命令将生成一个以RSA算法加密的有效期365天的名为<code>test.keystore</code>的文件，该KeyStore文件的alias为 test。回车确认执行该命令之后，将会要求输入密钥库口令以及一些基本的信息，根据提示输入无误之后将会在当前终端所在目录生成指定的KeyStore文件。完整的示例如下所示:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">$ keytool -genkey -v -keystore test.keystore -<span class="built_in">alias</span> <span class="built_in">test</span> -keyalg RSA -validity 365</span><br><span class="line">输入密钥库口令:  android</span><br><span class="line">再次输入新口令: android</span><br><span class="line">您的名字与姓氏是什么?</span><br><span class="line">  [Unknown]:  Doublemine</span><br><span class="line">您的组织单位名称是什么?</span><br><span class="line">  [Unknown]:  Test</span><br><span class="line">您的组织名称是什么?</span><br><span class="line">  [Unknown]:  Test</span><br><span class="line">您所在的城市或区域名称是什么?</span><br><span class="line">  [Unknown]:  Test</span><br><span class="line">您所在的省/市/自治区名称是什么?</span><br><span class="line">  [Unknown]:  Test</span><br><span class="line">该单位的双字母国家/地区代码是什么?</span><br><span class="line">  [Unknown]:  Test</span><br><span class="line">CN=Doublemine, OU=Test, O=Test, L=Test, ST=Test, C=Test是否正确?</span><br><span class="line">  [否]:  y</span><br><span class="line"></span><br><span class="line">正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 365 天):</span><br><span class="line">         CN=Doublemine, OU=Test, O=Test, L=Test, ST=Test, C=Test</span><br><span class="line">输入 &lt;<span class="built_in">test</span>&gt; 的密钥口令</span><br><span class="line">        (如果和密钥库口令相同, 按回车):</span><br><span class="line">[正在存储test.keystore]</span><br></pre></td></tr></table></figure><p>这样我们就有了一个全新的KeyStore文件可以用于Android的App签名，有了KeyStore文件下一步当然就是获取我们生成的KeyStore文件的指纹信息咯~</p><h4 id="获取KeyStore指纹信息"><a href="#获取KeyStore指纹信息" class="headerlink" title="获取KeyStore指纹信息"></a>获取KeyStore指纹信息</h4><p>与获取默认的<code>debug.keystore</code>文件指纹信息类似，我们在终端中键入以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -v -list -keystore test.keystore -<span class="built_in">alias</span> <span class="built_in">test</span> -keypass android -storepass android</span><br></pre></td></tr></table></figure><p>即可获取到我们生成的KeyStore指纹信息，有的同学已经看出来了，只要将上述命令中的几个参数替换下，即可查看任意KeyStore的指纹信息:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -v -list -keystore &#123;FILENAME.keystore&#125; -<span class="built_in">alias</span> &#123;ALIAS&#125; -keypass &#123;KEYPASSWD&#125; -storepass &#123;STOREPASSWD&#125;</span><br></pre></td></tr></table></figure><ul><li><code>{FILENAME.keystore}</code>为keystore文件名</li><li><code>{ALIAS}</code>为KeyStore的别名</li><li><code>{KEYPASSWD}</code>为KeyStore的密钥口令</li><li><code>{STOREPASSWD}</code>为KeyStore的密钥库口令</li></ul><h3 id="应用KeyStore"><a href="#应用KeyStore" class="headerlink" title="应用KeyStore"></a>应用KeyStore</h3><p>前面我们忙活了大半天生成了KeyStore文件，并查看其指纹信息。但是如果我们不使用到我们的项目中，毕竟还是不会对我们的项目生效的~我们还需要在Gradle脚本中对其进行配置，我们的项目才会应用其KeyStore文件。</p><p>其中我们有两种较为普遍的方式在项目中配置我们的KeyStore文件，第一种比较简单粗暴，直接在gradle构建脚本中写入KeyStore信息,第二种则将KeyStore信息配置在一个单独的配置文件中，在gradle构建时动态读取。</p><h4 id="签名信息写入Gradle脚本"><a href="#签名信息写入Gradle脚本" class="headerlink" title="签名信息写入Gradle脚本"></a>签名信息写入Gradle脚本</h4><p>在Android Stduio中打开主moudle的<code>build.gradle</code>文件,在其中的<code>android</code>闭包中键入如下内容:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">signingConfigs &#123;</span><br><span class="line">    release &#123;</span><br><span class="line">      keyAlias <span class="string">'test'</span></span><br><span class="line">      keyPassword <span class="string">'android'</span></span><br><span class="line">      storeFile <span class="keyword">file</span>(<span class="string">'./keystore/test.keystore'</span>)</span><br><span class="line">      storePassword <span class="string">'android'</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>其中声明的release闭包中包含了<code>keyAlias</code>、<code>keyPassword</code>、<code>storeFile</code>、<code>storePassword</code>四个Property。其中含义分别为:</p><ul><li><code>keyAlias</code> keystore的<code>alias</code></li><li><code>keyPassword</code> KeyStore的密钥口令</li><li><code>storeFile</code>为KeyStore的文件存放路径，可以为相对或者绝对路径，此处使用的为相对路径</li><li><code>storePassword</code>为KeyStore的密钥库口令</li></ul><p>以上的Gradle DSL将会作用于我们的项目的Release版本，当我们在终端中个输入:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gradlew assembleRelease</span><br></pre></td></tr></table></figure><p>项目将会使用我们上面定义的<code>test.keystore</code>密钥库文件签名打包项目为Release发布版。</p><p>同样，如果我们不想使用默认的<code>debug.keystore</code>签名项目的Debug版本，我们亦可以重新生成一个KeyStore文件或者使用Release版本的签名该文件，放入debug闭包中即可:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">signingConfigs &#123;</span><br><span class="line">    release &#123;</span><br><span class="line">      keyAlias <span class="string">'test'</span></span><br><span class="line">      keyPassword <span class="string">'android'</span></span><br><span class="line">      storeFile <span class="keyword">file</span>(<span class="string">'./keystore/test.keystore'</span>)</span><br><span class="line">      storePassword <span class="string">'android'</span></span><br><span class="line">    &#125;</span><br><span class="line">    debug &#123;</span><br><span class="line">     keyAlias <span class="string">'test'</span></span><br><span class="line">      keyPassword <span class="string">'android'</span></span><br><span class="line">      storeFile <span class="keyword">file</span>(<span class="string">'./keystore/test.keystore'</span>)</span><br><span class="line">      storePassword <span class="string">'android'</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><h4 id="签名信息写入配置文件"><a href="#签名信息写入配置文件" class="headerlink" title="签名信息写入配置文件"></a>签名信息写入配置文件</h4><p>细心的同学可能发现了，虽然上面的把签名信息写入gradle脚本中比较方便省事，但是却在密钥文件的密钥密码泄露问题，任何能够看到此Moudle的build.gradle脚本的人都可以拿到KeyStore文件及其对应的密钥口令，可能会导致一些安全风险。因此在一些开源项目或者比较敏感的项目中，可能会存在类似的gradle配置:</p><p>在主moudle的build.gradle脚本的android闭包中:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">applicationVariants.all &#123;  </span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">project</span>.hasProperty(<span class="string">'keyAlias'</span>) &amp;&amp; <span class="keyword">project</span>.hasProperty(<span class="string">'storeFile'</span>) &amp;&amp;</span><br><span class="line">    <span class="keyword">project</span>.hasProperty(<span class="string">'storePassword'</span>) &amp;&amp;</span><br><span class="line">    <span class="keyword">project</span>.hasProperty(<span class="string">'keyPassword'</span>)) &#123;</span><br><span class="line">  android.signingConfigs.release.keyAlias = keyAlias</span><br><span class="line">  android.signingConfigs.release.storeFile = <span class="keyword">file</span>(storeFile)</span><br><span class="line">  android.signingConfigs.release.storePassword = storePassword</span><br><span class="line">  android.signingConfigs.release.keyPassword = keyPassword</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  android.buildTypes.release.signingConfig = <span class="keyword">null</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中<code>Variants</code>翻译中文为<code>变种</code>,<code>applicationVariants.all</code>属性含义为<code>app plugin</code>下所有的<code>Variant</code>的配置信息，可以将其看作为一个总览，可以方便的访问所有对象。<br><a href="http://tools.android.com/tech-docs/new-build-system/user-guide" target="_blank" rel="noopener">相关延伸阅读Gradle Plugin User Guide</a>，我们在其中通过<code>project.hasProperty</code>读取项目中的配置，并将其动态的赋值给<code>signingConfigs.release</code>下的相关属性。</p><p>然后我们通过在<code>gradle.properties</code>或者其它项目中能够被gradle的文件中定义以上属性并赋值即可：</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><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">storeFile=.<span class="regexp">/keystore/</span>test.keystore</span><br><span class="line">storePassword=android</span><br><span class="line">keyAlias=test</span><br><span class="line">keyPassword=android</span><br></pre></td></tr></table></figure><p>这样我们在项目团队协作时，将<code>gradle.properties</code>文件忽略即可。</p><p>Enjoy IT!</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Android应用开发的时候会经常使用到第三方SDK，例如地图、推送、支付以及分享等。而这些第三方服务SDK为了应用不被滥用都会有签名验证机制。我们通过上传Android应用的签名来在服务端配置以验证应用的合法性。本文将讲述下在Android Stduio中常用的签名操作。&lt;/p&gt;
&lt;h3 id=&quot;默认KeyStore&quot;&gt;&lt;a href=&quot;#默认KeyStore&quot; class=&quot;headerlink&quot; title=&quot;默认KeyStore&quot;&gt;&lt;/a&gt;默认KeyStore&lt;/h3&gt;&lt;h4 id=&quot;默认KeyStore存储路径&quot;&gt;&lt;a href=&quot;#默认KeyStore存储路径&quot; class=&quot;headerlink&quot; title=&quot;默认KeyStore存储路径&quot;&gt;&lt;/a&gt;默认KeyStore存储路径&lt;/h4&gt;&lt;p&gt;在Android Stduio中系统默认内置了一个签名文件&lt;code&gt;debug.keystore&lt;/code&gt;，用于我们在debug下的默认App签名。如果没有在Gradle文件中特殊指定，那么Android Studio将自动使用默认的&lt;code&gt;debug.keystore&lt;/code&gt;文件为项目App生成Debug版本的签名。&lt;/p&gt;
    
    </summary>
    
      <category term="Android" scheme="https://notes.wanghao.work/categories/Android/"/>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
      <category term="Android Studio" scheme="https://notes.wanghao.work/tags/Android-Studio/"/>
    
      <category term="KeyStore" scheme="https://notes.wanghao.work/tags/KeyStore/"/>
    
  </entry>
  
  <entry>
    <title>RxJava、Retrofit接收Error Response Body</title>
    <link href="https://notes.wanghao.work/2016-07-02-RxJava%E9%85%8D%E5%90%88Retofit%E6%8E%A5%E6%94%B6%E9%94%99%E8%AF%AFbody.html"/>
    <id>https://notes.wanghao.work/2016-07-02-RxJava配合Retofit接收错误body.html</id>
    <published>2016-07-02T22:10:53.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<p><code>RxJava</code>配合<code>Retrofit</code>能够大大简化Android项目中的网络请求代码量，使得逻辑更清晰，当然也可能会遇到一些问题。下面给出一种问题的解决方案。</p><h3 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h3><p>一个基本的RxJava配合Retrofit以及Lambda的网络调用看起来像这个样子的:</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><br><span class="line">Subscription subscription = mApi.getSimpleApi()</span><br><span class="line">       .subscribeOn(Schedulers.io())</span><br><span class="line">       .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line">       .subscribe(response -&gt; &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//do Something</span></span><br><span class="line"></span><br><span class="line">       &#125;, throwable -&gt; &#123;</span><br><span class="line"></span><br><span class="line">       <span class="comment">//Ops Error</span></span><br><span class="line"></span><br><span class="line">       &#125;);</span><br></pre></td></tr></table></figure><p>当<code>Retrofit</code>中的网络请求返回码状态码为<code>200</code>时，执行<code>do Something</code>中的逻辑处理正常的<br>业务流程，但是当服务器返回状态码为<code>非200</code>时，将会执行<code>Ops Error</code>中的业务流程而不会<br>执行<code>do Something</code>中的业务逻辑。</p><a id="more"></a><p>这样本没有什么问题，一般我们会在错误处理逻辑中在UI中给出错误提示，像这样:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Log.e(<span class="string">"Ops"</span>, <span class="string">"Error:"</span> + throwable.getMessage());</span><br></pre></td></tr></table></figure><p>但是这样的话我们只能获取到一服务器的错误响应码以及对应的简短的响应码错误说明，一般情况下我们服务器<br>都会包装错误信息为一个JSON，客户端解析错误信息必要的时候动态展示在UI上以提示用户。如果我们要拿到这样的JSON，使用<code>throwable.getMessage()</code>这样做显然是不行的。是不是使用RxJava配合Retrofit只能拿到这样的错误Throwable信息呢？</p><p>显然不是的，其实服务器返回的错误信息<code>非200</code>响应码的<code>Response Body</code>JSON对象包含在这个<code>throwable</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">throwable -&gt; &#123;</span><br><span class="line">          <span class="keyword">if</span>(throwable <span class="keyword">instanceof</span> HttpException)&#123;</span><br><span class="line">            HttpException httpException= (HttpException) throwable;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">              String errorBody= httpException.response().errorBody().string();</span><br><span class="line"></span><br><span class="line"><span class="comment">//<span class="doctag">TODO:</span> parse To JSON Obj</span></span><br><span class="line"></span><br><span class="line">            &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">              e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line"> <span class="comment">//Ops Print throwable</span></span><br><span class="line"></span><br><span class="line">        &#125;);</span><br></pre></td></tr></table></figure><p>在<code>Parse to JSON Obj</code>中将<code>errorBody</code>解析为JSON对象进行相应的处理即可。</p><p>然而，你不能让我每个地方都加上这样的一段代码吧，既然我们使用的是<code>RxJava</code>，我们可以让这种处理稍微看起来优雅点。以下以<code>Jackson</code>为例:</p><h4 id="自定义Action1"><a href="#自定义Action1" class="headerlink" title="自定义Action1"></a>自定义Action1</h4><p>由于<code>RxJava</code>的错误异常处理接受一个参数，并且没有返回值，因此我们可以定义一个<code>Action1</code>来替代默认的Error Action:</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">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">ErrorAction</span> <span class="keyword">implements</span> <span class="title">Action1</span>&lt;<span class="title">Throwable</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">call</span><span class="params">(Throwable throwable)</span> </span>&#123;</span><br><span class="line">    call(ErrorMessage.handle(throwable));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">call</span><span class="params">(ErrorMessage error)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中<code>ErrorMessage</code>为我们定义好的错误消息Model:</p><h4 id="定义Throwable-Handle"><a href="#定义Throwable-Handle" class="headerlink" title="定义Throwable Handle"></a>定义Throwable Handle</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@JsonIgnoreProperties</span>(ignoreUnknown = <span class="keyword">true</span>) <span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ResponseError</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">int</span> ERROR_CODE_IO_ERROR = <span class="number">2072</span>;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">int</span> ERROR_CODE_UN_KNOW = <span class="number">2073</span>;</span><br><span class="line">  <span class="meta">@JsonProperty</span>(<span class="string">"status_code"</span>) <span class="keyword">public</span> <span class="keyword">int</span> statusCode;</span><br><span class="line">  <span class="keyword">public</span> String message;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setStatusCode</span><span class="params">(<span class="keyword">int</span> statusCode)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.statusCode = statusCode;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setMessage</span><span class="params">(String message)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.message = message;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ResponseError <span class="title">handle</span><span class="params">(Throwable throwable)</span> </span>&#123;</span><br><span class="line">    ResponseError responseError = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (throwable <span class="keyword">instanceof</span> HttpException) &#123;</span><br><span class="line">      HttpException exception = (HttpException) throwable;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        responseError = <span class="keyword">new</span> ObjectMapper().readValue(exception.response().errorBody().string(),</span><br><span class="line">            ResponseError.class);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        responseError = <span class="keyword">new</span> ResponseError();</span><br><span class="line">        responseError.setMessage(e.getLocalizedMessage());</span><br><span class="line">        responseError.setStatusCode(ERROR_CODE_IO_ERROR);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      responseError = <span class="keyword">new</span> ResponseError();</span><br><span class="line">      responseError.setMessage(throwable.getMessage());</span><br><span class="line">      responseError.setStatusCode(ERROR_CODE_UN_KNOW);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> responseError;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，我们就完成了一个自定义<code>Action1</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">Subscription subscription = mApi.getSimpleApi()</span><br><span class="line">        .subscribeOn(Schedulers.io())</span><br><span class="line">        .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line">        .subscribe(response -&gt; &#123;</span><br><span class="line"></span><br><span class="line">         <span class="comment">//do Something</span></span><br><span class="line"></span><br><span class="line">        &#125;,  <span class="keyword">new</span> ErrorAction() &#123;</span><br><span class="line">              <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">call</span><span class="params">(ErrorMessage msg)</span> </span>&#123;</span><br><span class="line"><span class="comment">//Do Error               </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>Do Error</code>中拿到<code>ErrorMessage</code>对象，进行相应的对象操作即可~</p><p>Enjoy IT!</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;code&gt;RxJava&lt;/code&gt;配合&lt;code&gt;Retrofit&lt;/code&gt;能够大大简化Android项目中的网络请求代码量，使得逻辑更清晰，当然也可能会遇到一些问题。下面给出一种问题的解决方案。&lt;/p&gt;
&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;一个基本的RxJava配合Retrofit以及Lambda的网络调用看起来像这个样子的:&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Subscription subscription = mApi.getSimpleApi()&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       .subscribeOn(Schedulers.io())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       .observeOn(AndroidSchedulers.mainThread())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       .subscribe(response -&amp;gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;//do Something&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       &amp;#125;, throwable -&amp;gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       &lt;span class=&quot;comment&quot;&gt;//Ops Error&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;       &amp;#125;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;当&lt;code&gt;Retrofit&lt;/code&gt;中的网络请求返回码状态码为&lt;code&gt;200&lt;/code&gt;时，执行&lt;code&gt;do Something&lt;/code&gt;中的逻辑处理正常的&lt;br&gt;业务流程，但是当服务器返回状态码为&lt;code&gt;非200&lt;/code&gt;时，将会执行&lt;code&gt;Ops Error&lt;/code&gt;中的业务流程而不会&lt;br&gt;执行&lt;code&gt;do Something&lt;/code&gt;中的业务逻辑。&lt;/p&gt;
    
    </summary>
    
      <category term="Android" scheme="https://notes.wanghao.work/categories/Android/"/>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
      <category term="Android Studio" scheme="https://notes.wanghao.work/tags/Android-Studio/"/>
    
      <category term="RxJava" scheme="https://notes.wanghao.work/tags/RxJava/"/>
    
      <category term="Retrofit" scheme="https://notes.wanghao.work/tags/Retrofit/"/>
    
  </entry>
  
  <entry>
    <title>为NexT主题添加文章阅读量统计功能</title>
    <link href="https://notes.wanghao.work/2015-10-21-%E4%B8%BANexT%E4%B8%BB%E9%A2%98%E6%B7%BB%E5%8A%A0%E6%96%87%E7%AB%A0%E9%98%85%E8%AF%BB%E9%87%8F%E7%BB%9F%E8%AE%A1%E5%8A%9F%E8%83%BD.html"/>
    <id>https://notes.wanghao.work/2015-10-21-为NexT主题添加文章阅读量统计功能.html</id>
    <published>2015-10-21T21:57:00.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><hr><p>由于最近在折腾Android项目，需要用到一些与服务器交互、以及数据存储的相关功能，然后发现了<a href="https://leancloud.cn" target="_blank" rel="noopener">LeanCloud</a>这家服务提供商,使用下来还感觉还挺靠谱的(请给我广告费)。正好发现他们服务提供了<a href="https://leancloud.cn/docs/js_guide.html" target="_blank" rel="noopener">JavaScript SDK</a>，于是就想着尝试着实现Hexo博客文章的浏览数统计功能，之前虽然在使用不蒜子，但是不蒜子不能够在主页展示文章阅读量啊！对于博主这种有强迫症又想装X的人来说果断不能忍啊！</p><h3 id="修改NexT主题模版"><a href="#修改NexT主题模版" class="headerlink" title="修改NexT主题模版"></a><del>修改NexT主题模版</del></h3><p><del>本方法理论上对Hexo博客通用，由于博主使用的是NexT主题，所以当然针对NexT来说咯。</del><strong><font color="red">NexT主题目前已经合并这个Feature，因此如果你使用的是NexT主题，可以直接使用不用修改主题模版而直接在<code>_config.yml</code>中配置即可，请直接跳转查看<a href="#%E9%85%8D%E7%BD%AELeanCloud">配置LeanCloud</a></font></strong></p><a id="more"></a><h4 id="修改-config-yml文件"><a href="#修改-config-yml文件" class="headerlink" title="修改_config.yml文件"></a><del>修改<code>_config.yml</code>文件</del></h4><p><del>打开NexT主题的根目录下的<code>_config.yml</code>文件，在任意位置添加以下内容：</del></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">leancloud_visitors:</span><br><span class="line">  enable: <span class="literal">true</span></span><br><span class="line">  app_id: #&lt;AppID&gt;</span><br><span class="line">  app_key: #&lt;AppKEY&gt;</span><br></pre></td></tr></table></figure><h4 id="添加lean-analytics-swig文件"><a href="#添加lean-analytics-swig文件" class="headerlink" title="添加lean-analytics.swig文件"></a><del>添加<code>lean-analytics.swig</code>文件</del></h4><p><del>在主题的<code>layout\_scripts</code>路径下，新建一个<code>lean-analytics.swig</code>文件，并向里面添加以下内容</del></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- custom analytics part create by xiamo --&gt;</span><br><span class="line">&lt;script src=<span class="string">"https://cdn1.lncld.net/static/js/av-core-mini-0.6.1.js"</span>&gt;<span class="xml"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line">&lt;script&gt;AV.initialize(<span class="string">"&#123;&#123;theme.leancloud_visitors.app_id&#125;&#125;"</span>, <span class="string">"&#123;&#123;theme.leancloud_visitors.app_key&#125;&#125;"</span>);<span class="xml"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">showTime</span>(<span class="params">Counter</span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> query = <span class="keyword">new</span> AV.Query(Counter);</span><br><span class="line">$(<span class="string">".leancloud_visitors"</span>).each(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> url = $(<span class="keyword">this</span>).attr(<span class="string">"id"</span>).trim();</span><br><span class="line">query.equalTo(<span class="string">"url"</span>, url);</span><br><span class="line">query.find(&#123;</span><br><span class="line">success: <span class="function"><span class="keyword">function</span>(<span class="params">results</span>) </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (results.length == <span class="number">0</span>) &#123;</span><br><span class="line"><span class="keyword">var</span> content = $(<span class="built_in">document</span>.getElementById(url)).text() + <span class="string">': 0'</span>;</span><br><span class="line">$(<span class="built_in">document</span>.getElementById(url)).text(content);</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; results.length; i++) &#123;</span><br><span class="line"><span class="keyword">var</span> object = results[i];</span><br><span class="line"><span class="keyword">var</span> content = $(<span class="built_in">document</span>.getElementById(url)).text() + <span class="string">': '</span> + object.get(<span class="string">'time'</span>);</span><br><span class="line">$(<span class="built_in">document</span>.getElementById(url)).text(content);</span><br><span class="line">&#125;</span><br><span class="line">&#125;,</span><br><span class="line">error: <span class="function"><span class="keyword">function</span>(<span class="params">object, error</span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">"Error: "</span> + error.code + <span class="string">" "</span> + error.message);</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><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">addCount</span>(<span class="params">Counter</span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> Counter = AV.Object.extend(<span class="string">"Counter"</span>);</span><br><span class="line">url = $(<span class="string">".leancloud_visitors"</span>).attr(<span class="string">'id'</span>).trim();</span><br><span class="line">title = $(<span class="string">".leancloud_visitors"</span>).attr(<span class="string">'data-flag-title'</span>).trim();</span><br><span class="line"><span class="keyword">var</span> query = <span class="keyword">new</span> AV.Query(Counter);</span><br><span class="line">query.equalTo(<span class="string">"url"</span>, url);</span><br><span class="line">query.find(&#123;</span><br><span class="line">success: <span class="function"><span class="keyword">function</span>(<span class="params">results</span>) </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (results.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line"><span class="keyword">var</span> counter = results[<span class="number">0</span>];</span><br><span class="line">counter.fetchWhenSave(<span class="literal">true</span>);</span><br><span class="line">counter.increment(<span class="string">"time"</span>);</span><br><span class="line">counter.save(<span class="literal">null</span>, &#123;</span><br><span class="line">success: <span class="function"><span class="keyword">function</span>(<span class="params">counter</span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> content = $(<span class="built_in">document</span>.getElementById(url)).text() + <span class="string">': '</span> + counter.get(<span class="string">'time'</span>);</span><br><span class="line">$(<span class="built_in">document</span>.getElementById(url)).text(content);</span><br><span class="line">&#125;,</span><br><span class="line">error: <span class="function"><span class="keyword">function</span>(<span class="params">counter, error</span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'Failed to save Visitor num, with error message: '</span> + error.message);</span><br><span class="line">&#125;</span><br><span class="line">&#125;);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">var</span> newcounter = <span class="keyword">new</span> Counter();</span><br><span class="line">newcounter.set(<span class="string">"title"</span>, title);</span><br><span class="line">newcounter.set(<span class="string">"url"</span>, url);</span><br><span class="line">newcounter.set(<span class="string">"time"</span>, <span class="number">1</span>);</span><br><span class="line">newcounter.save(<span class="literal">null</span>, &#123;</span><br><span class="line">success: <span class="function"><span class="keyword">function</span>(<span class="params">newcounter</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">"newcounter.get('time')="</span>+newcounter.get(<span class="string">'time'</span>));</span><br><span class="line"><span class="keyword">var</span> content = $(<span class="built_in">document</span>.getElementById(url)).text() + <span class="string">': '</span> + newcounter.get(<span class="string">'time'</span>);</span><br><span class="line">$(<span class="built_in">document</span>.getElementById(url)).text(content);</span><br><span class="line">&#125;,</span><br><span class="line">error: <span class="function"><span class="keyword">function</span>(<span class="params">newcounter, error</span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'Failed to create'</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">error: <span class="function"><span class="keyword">function</span>(<span class="params">error</span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'Error:'</span> + error.code + <span class="string">" "</span> + error.message);</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="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">var</span> Counter = AV.Object.extend(<span class="string">"Counter"</span>);</span><br><span class="line"><span class="keyword">if</span> ($(<span class="string">'.leancloud_visitors'</span>).length == <span class="number">1</span>) &#123;</span><br><span class="line">addCount(Counter);</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> ($(<span class="string">'.post-title-link'</span>).length &gt; <span class="number">1</span>) &#123;</span><br><span class="line">showTime(Counter);</span><br><span class="line">&#125;</span><br><span class="line">&#125;);</span><br><span class="line">&lt;<span class="regexp">/script&gt;</span></span><br></pre></td></tr></table></figure><h4 id="修改post-swig文件"><a href="#修改post-swig文件" class="headerlink" title="修改post.swig文件"></a><del>修改<code>post.swig</code>文件</del></h4><p><del>在主题的<code>layout\_macro</code>路径下，打开<code>post.swig</code>文件，找到以下内容（大概88行）：</del></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;% if not is_index and theme.facebook_sdk.enable and theme.facebook_sdk.like_button %&#125;</span><br><span class="line">  &amp;nbsp; | &amp;nbsp;</span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"fb-like"</span> <span class="attr">data-layout</span>=<span class="string">"button_count"</span> <span class="attr">data-share</span>=<span class="string">"true"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">&#123;% endif %&#125;</span><br></pre></td></tr></table></figure><p><del>在其后面添加如下内容：</del></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;% if theme.leancloud_visitors.enable %&#125;</span><br><span class="line">   <span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">"&#123;&#123; url_for(post.path) &#125;&#125;"</span><span class="attr">class</span>=<span class="string">"leancloud_visitors"</span>  <span class="attr">data-flag-title</span>=<span class="string">"&#123;&#123; post.title &#125;&#125;"</span>&gt;</span></span><br><span class="line">            &amp;nbsp; | &amp;nbsp; &#123;&#123;__('post.visitors')&#125;&#125;</span><br><span class="line">            <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">  &#123;% endif %&#125;</span><br></pre></td></tr></table></figure><p><del>添加完毕之后，文件内容像这个样子：</del></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">      &#123;% if not is_index and theme.facebook_sdk.enable and theme.facebook_sdk.like_button %&#125;</span><br><span class="line">        &amp;nbsp; | &amp;nbsp;</span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"fb-like"</span> <span class="attr">data-layout</span>=<span class="string">"button_count"</span> <span class="attr">data-share</span>=<span class="string">"true"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      &#123;% endif %&#125;</span><br><span class="line">&#123;% if theme.leancloud_visitors.enable %&#125;</span><br><span class="line"> <span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">"&#123;&#123; url_for(post.path) &#125;&#125;"</span><span class="attr">class</span>=<span class="string">"leancloud_visitors"</span>  <span class="attr">data-flag-title</span>=<span class="string">"&#123;&#123; post.title &#125;&#125;"</span>&gt;</span></span><br><span class="line">        &amp;nbsp; | &amp;nbsp; &#123;&#123;__('post.visitors')&#125;&#125;</span><br><span class="line">        <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">&#123;% endif %&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">header</span>&gt;</span></span><br><span class="line">&#123;% endif %&#125;</span><br></pre></td></tr></table></figure><h4 id="修改layout-swig文件"><a href="#修改layout-swig文件" class="headerlink" title="修改layout.swig文件"></a><del>修改<code>layout.swig</code>文件</del></h4><p><del>在NexT根目录的<code>layout</code>路径下，打开 <code>_layout.swig</code>文件，在<code>&lt;/body&gt;</code>上方添加如下内容：</del><br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;% if theme.leancloud_visitors.enable %&#125;</span><br><span class="line">   &#123;% include '_scripts/lean-analytics.swig' %&#125;</span><br><span class="line">  &#123;%  endif %&#125;</span><br></pre></td></tr></table></figure></p><p><del>添加完成之后，文件内容像这个样子：</del></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;# LazyLoad #&#125;</span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">"text/javascript"</span> <span class="attr">src</span>=<span class="string">"&#123;&#123; url_for(theme.js) &#125;&#125;/lazyload.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">"text/javascript"</span>&gt;</span></span><br><span class="line"><span class="javascript">    $(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">      $(<span class="string">"#posts"</span>).find(<span class="string">'img'</span>).lazyload(&#123;</span></span><br><span class="line"><span class="javascript">        placeholder: <span class="string">"&#123;&#123; url_for(theme.images) &#125;&#125;/loading.gif"</span>,</span></span><br><span class="line"><span class="javascript">        effect: <span class="string">"fadeIn"</span></span></span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">  <span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  &#123;% if theme.leancloud_visitors.enable %&#125;</span><br><span class="line">   &#123;% include '_scripts/lean-analytics.swig' %&#125;</span><br><span class="line">  &#123;%  endif %&#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="修改zh-Hans-yml文件"><a href="#修改zh-Hans-yml文件" class="headerlink" title="修改zh-Hans.yml文件"></a><del>修改<code>zh-Hans.yml</code>文件</del></h4><p><del>在NexT目录的<code>languages</code>路径下的<code>zh-Hans.yml</code>文件，在<code>post:</code>结点下添加<code>visitors: 阅读次数</code>，像这个样子：</del><br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">post:</span><br><span class="line">  posted: 发表于</span><br><span class="line">  visitors: 阅读次数</span><br><span class="line">  updated: 更新于</span><br><span class="line">  in: 分类于</span><br><span class="line">  read_more: 阅读全文</span><br><span class="line">  untitled: 未命名</span><br><span class="line">  toc_empty: 此文章未包含目录</span><br></pre></td></tr></table></figure></p><p><del><strong> 如果你使用的是其它NexT的语言，请相应的添加该字段即可。</strong></del></p><p><del>至此NexT的修改工作就完成了，但是现在还是不能够使用文章阅读量这个统计功能的。这个功能依赖于<a href="https://leancloud.cn/" target="_blank" rel="noopener">LeanCloud</a>提供后端数据存取，因此我们需要注册一个<a href="https://leancloud.cn/login.html#/signup" target="_blank" rel="noopener">LeanCloud帐号</a>才能继续使用这个功能，<strong><a href="https://leancloud.cn/login.html#/signup" target="_blank" rel="noopener">点我快速注册</a>.</strong></del></p><hr><h3 id="配置LeanCloud"><a href="#配置LeanCloud" class="headerlink" title="配置LeanCloud"></a>配置<a href="https://leancloud.cn" target="_blank" rel="noopener">LeanCloud</a></h3><p>在注册完成LeanCloud帐号并验证邮箱之后，我们就可以登录我们的LeanCloud帐号，进行一番配置之后拿到<code>AppID</code>以及<code>AppKey</code>这两个参数即可正常使用文章阅读量统计的功能了。</p><h4 id="创建应用"><a href="#创建应用" class="headerlink" title="创建应用"></a>创建应用</h4><ul><li>我们新建一个应用来专门进行博客的访问统计的数据操作。首先，打开控制台，如下图所示：</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/open_consoloe.png" alt title="打开控制台"></p><ul><li>在出现的界面点击<code>创建应用</code>：</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/create_app.png" alt title="创建应用"></p><ul><li>在接下来的页面，新建的应用名称我们可以随意输入，即便是输入的不满意我们后续也是可以更改的:</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/creating_app.png" alt title="创建的新应用名称"></p><ul><li>这里为了演示的方便，我新创建一个取名为test的应用。创建完成之后我们点击新创建的应用的名字来进行该应用的参数配置：</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/create_class.png" alt title="打开应用参数配置界面"></p><ul><li>在应用的数据配置界面，左侧下划线开头的都是系统预定义好的表，为了便于区分我们新建一张表来保存我们的数据。点击左侧右上角的齿轮图标，新建Class：<br> 在弹出的选项中选择<code>创建Class</code>来新建Class用来专门保存我们博客的文章访问量等数据:<br> 点击<code>创建Class</code>之后，<del>理论上来说名字可以随意取名，只要你交互代码做相应的更改即可</del>，但是为了保证我们前面对NexT主题的修改兼容，此处的<strong><font color="green">新建Class名字必须为<font color="red"><code>Counter</code></font></font></strong>:</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/creating_class.png" alt title="权限配置"></p><ul><li>由于LeanCloud升级了默认的ACL权限，如果你想避免后续因为权限的问题导致次数统计显示不正常，建议在此处选择<code>无限制</code>。</li></ul><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/open_app_key.png" alt title="打开应用设置"></p><p>创建完成之后，左侧数据栏应该会多出一栏名为<code>Counter</code>的栏目，这个时候我们点击顶部的设置，切换到test应用的操作界面:<br>在弹出的界面中，选择左侧的<code>应用Key</code>选项，即可发现我们创建应用的<code>AppID</code>以及<code>AppKey</code>，有了它，我们就有权限能够通过主题中配置好的Javascript代码与这个应用的Counter表进行数据存取操作了:</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/opened_app_key.png" alt title="获取Appid、Appkey"></p><p>复制<code>AppID</code>以及<code>AppKey</code>并在NexT主题的<code>_config.yml</code>文件中我们相应的位置填入即可，正确配置之后文件内容像这个样子:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">leancloud_visitors:</span><br><span class="line">  enable: true</span><br><span class="line">  app_id: joaeuuc4hsqudUUwx4gIvGF6-gzGzoHsz</span><br><span class="line">  app_key: E9UJsJpw1omCHuS22PdSpKoh</span><br></pre></td></tr></table></figure><p>这个时候重新生成部署Hexo博客，应该就可以正常使用文章阅读量统计的功能了。需要特别说明的是：记录文章访问量的唯一标识符是文章的<code>发布日期</code>以及<code>文章的标题</code>，因此请确保这两个数值组合的唯一性，如果你更改了这两个数值，会造成文章阅读数值的清零重计。</p><h3 id="后台管理"><a href="#后台管理" class="headerlink" title="后台管理"></a>后台管理</h3><p>当你配置部分完成之后，初始的文章统计量显示为0，但是这个时候我们LeanCloud对应的应用的<code>Counter</code>表中并没有相应的记录，只是单纯的显示为0而已，当博客文章在配置好阅读量统计服务之后第一次打开时，便会自动向服务器发送数据来创建一条数据，该数据会被记录在对应的应用的<code>Counter</code>表中。</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/background.png" alt title="后台管理"></p><p>我们可以修改其中的<code>time</code>字段的数值来达到修改某一篇文章的访问量的目的（博客文章访问量快递提升人气的装逼利器）。双击具体的数值，修改之后回车即可保存。</p><ul><li><code>url</code>字段被当作唯一<code>ID</code>来使用，因此如果你不知道带来的后果的话请不要修改。</li><li><code>title</code>字段显示的是博客文章的标题，用于后台管理的时候区分文章之用，没有什么实际作用。</li><li>其他字段皆为自动生成，具体作用请查阅LeanCloud官方文档，如果你不知道有什么作用请不要随意修改。</li></ul><h3 id="Web安全"><a href="#Web安全" class="headerlink" title="Web安全"></a>Web安全</h3><p>因为AppID以及AppKey是暴露在外的，因此如果一些别用用心之人知道了之后用于其它目的是得不偿失的，为了确保只用于我们自己的博客，建议开启Web安全选项，这样就只能通过我们自己的域名才有权访问后台的数据了，可以进一步提升安全性。</p><p>选择应用的设置的<code>安全中心</code>选项卡:</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/open_safe_center.png" alt title="进入安全中心"></p><p>在<code>Web 安全域名</code>中填入我们自己的博客域名，来确保数据调用的安全:</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/bind_domain.png" alt title="锁定域名"></p><p>如果你不知道怎么填写安全域名而或者填写完成之后发现博客文章访问量显示不正常，打开浏览器调试模式，发现如下图的输出:</p><p><img src="http://7xkj6q.com1.z0.glb.clouddn.com/static/images/leancloud-page-anlysis/broswer_403.png" alt title="Web安全域名填写错误"></p><p>这说明你的安全域名填写错误，导致服务器拒绝了数据交互的请求，你可以更改为正确的安全域名或者你不知道如何修改请在本博文中留言或者放弃设置Web安全域名。</p><p>Enjoy it！</p>]]></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;hr&gt;
&lt;p&gt;由于最近在折腾Android项目，需要用到一些与服务器交互、以及数据存储的相关功能，然后发现了&lt;a href=&quot;https://leancloud.cn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LeanCloud&lt;/a&gt;这家服务提供商,使用下来还感觉还挺靠谱的(请给我广告费)。正好发现他们服务提供了&lt;a href=&quot;https://leancloud.cn/docs/js_guide.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JavaScript SDK&lt;/a&gt;，于是就想着尝试着实现Hexo博客文章的浏览数统计功能，之前虽然在使用不蒜子，但是不蒜子不能够在主页展示文章阅读量啊！对于博主这种有强迫症又想装X的人来说果断不能忍啊！&lt;/p&gt;
&lt;h3 id=&quot;修改NexT主题模版&quot;&gt;&lt;a href=&quot;#修改NexT主题模版&quot; class=&quot;headerlink&quot; title=&quot;修改NexT主题模版&quot;&gt;&lt;/a&gt;&lt;del&gt;修改NexT主题模版&lt;/del&gt;&lt;/h3&gt;&lt;p&gt;&lt;del&gt;本方法理论上对Hexo博客通用，由于博主使用的是NexT主题，所以当然针对NexT来说咯。&lt;/del&gt;&lt;strong&gt;&lt;font color=&quot;red&quot;&gt;NexT主题目前已经合并这个Feature，因此如果你使用的是NexT主题，可以直接使用不用修改主题模版而直接在&lt;code&gt;_config.yml&lt;/code&gt;中配置即可，请直接跳转查看&lt;a href=&quot;#%E9%85%8D%E7%BD%AELeanCloud&quot;&gt;配置LeanCloud&lt;/a&gt;&lt;/font&gt;&lt;/strong&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="Hexo" scheme="https://notes.wanghao.work/categories/Hexo/"/>
    
    
      <category term="Hexo" scheme="https://notes.wanghao.work/tags/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>Create a Simple Android Keyboard</title>
    <link href="https://notes.wanghao.work/2015-09-04-Create-a-Simple-Android-Keyboard.html"/>
    <id>https://notes.wanghao.work/2015-09-04-Create-a-Simple-Android-Keyboard.html</id>
    <published>2015-09-04T15:12:11.000Z</published>
    <updated>2019-07-03T03:40:55.496Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>&emsp;&emsp;由于最近做的一个Android项目需要用到用户的输入一些字符，常规的输入法输入非常的不方便。因此有必要自定义一个输入法来完成这个过程。此处给出一个简单的输入法Demo<br>来帮助理解自定义输入法的一些实现过程。</p><ul><li><strong>有关输入法的一些说明，请参见:<a href="http://notes.xiamo.tk/2015-04-01-Create-an-Android-IME.html" target="_blank" rel="noopener">Create a Android IME</a></strong></li></ul><a id="more"></a><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>&emsp;&emsp;磨刀不误砍柴工，创建一个Android是需要一点点准备工作的,选择一个好的IDE能够提高我们编码的效率：</p><ul><li>Android Studio（推荐）</li><li>Android SDK</li></ul><p>本博文的编写环境为Android Studio，如果你还在使用Eclipse的话，转到Android Studio上来吧！如果对上述IDE的下载感到茫然的话，推荐一个国内Android开发的好网站：<a href="http://www.androiddevtools.cn/" target="_blank" rel="noopener">http://www.androiddevtools.cn</a>。</p><h2 id="创建一个Android项目"><a href="#创建一个Android项目" class="headerlink" title="创建一个Android项目"></a>创建一个Android项目</h2><ul><li>打开Android Studio，创建一个新的Android项目，命名为<code>SimpleKeyboard</code>，如下图：</li></ul><p><img src="http://i3.piimg.com/37ae2bfdf945c18e.png" alt title="创建一个SimpleKeyboard项目"></p><ul><li>最小的Android支持版本我们在这里选择API 9也就是Android 2.3咯：</li></ul><p><img src="http://i3.piimg.com/f337c436d68dcc22.png" alt title="设置最小SDK支持版本"></p><ul><li>由于我们只是一个单独的键盘Demo，为了达到极致精简，还是不需要Activity了，因此我们选择<code>Add No Activity</code>：</li></ul><p><img src="http://i3.piimg.com/fed92e215ec17e4f.png" alt title="选择Add No Activity"></p><p>点击Finish，这样就完成了我们Android Keyboard项目的创建了。这个等待Gradle构建完成，因为我们这个项目是没有Activity的，因此我们需要按照如下图稍微配置一下：</p><p><img src="http://i3.piimg.com/4c72f6d152c54dee.png" alt></p><p><img src="http://i3.piimg.com/2195d3987d5dddd1.png" alt></p><h2 id="编辑AndroidManifest-xml文件"><a href="#编辑AndroidManifest-xml文件" class="headerlink" title="编辑AndroidManifest.xml文件"></a>编辑AndroidManifest.xml文件</h2><p>&emsp;&emsp;键盘在Android系统中被识别为一个输入法编辑器（IME），IME作为一个Service运行。只有在AndroidManifest.xml文件中通过<code>android.permission.BIND_INPUT_METHOD</code>权限声明的Service并且响应<code>android.view.im</code>这个元数据动作才能够被Android系统正确的识别为IME。因此，我们在<code>AndroidManifest.xml</code>文件中的application标签对中添加如下代码：</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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">service</span></span></span><br><span class="line"><span class="tag">          <span class="attr">android:name</span>=<span class="string">".SimpleIME"</span></span></span><br><span class="line"><span class="tag">          <span class="attr">android:label</span>=<span class="string">"@string/custom_ime"</span></span></span><br><span class="line"><span class="tag">          <span class="attr">android:permission</span>=<span class="string">"android.permission.BIND_INPUT_METHOD"</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">meta-data</span></span></span><br><span class="line"><span class="tag">              <span class="attr">android:name</span>=<span class="string">"android.view.im"</span></span></span><br><span class="line"><span class="tag">              <span class="attr">android:resource</span>=<span class="string">"@xml/method"</span> /&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">intent-filter</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.view.InputMethod"</span> /&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">intent-filter</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">service</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="创建method-xml"><a href="#创建method-xml" class="headerlink" title="创建method.xml"></a>创建method.xml</h2><blockquote><p>&emsp;&emsp;在上面的Service声明中，meta-data标签声明引用了一个叫做<code>method.xml</code>的文件，如果没有这个文件，那么Android系统将不能够识别我们的Service为一个有效的IME Service。这个文件包含了有关输入法及其子类的详细信息。</p></blockquote><p>在我们的Demo中，我们定义一个<code>subtyoe</code>来声明输入法的显示名称以及其语言环境：（如果没有res/xml目录的话，创建一个并将下面的内容添加到该目录的method.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></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">input-method</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">subtype</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:imeSubtypeLocale</span>=<span class="string">"zh_CN"</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:imeSubtypeMode</span>=<span class="string">"keyboard"</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:label</span>=<span class="string">"tk_xiamo_notes"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">input-method</span>&gt;</span></span><br></pre></td></tr></table></figure><p>如果你对label中直接写死字符串这种做法比较有强迫症的话，可以将其抽取到Strings.xml文件中。</p><h2 id="定义键盘布局"><a href="#定义键盘布局" class="headerlink" title="定义键盘布局"></a>定义键盘布局</h2><p>&emsp;&emsp;我们的键盘布局比较简单，仅仅包含了一个KeyboardView，因此在<code>layout</code>目录中创建一个<code>keyboard.xml</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></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">android.inputmethodservice.KeyboardView</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:id</span>=<span class="string">"@+id/keyboard"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_alignParentBottom</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:keyPreviewLayout</span>=<span class="string">"@layout/preview"</span> /&gt;</span></span><br></pre></td></tr></table></figure><p>其中需要说明的是：</p><ul><li><code>layout_alignParentBottom</code>属性设置为<code>true</code>来保证键盘会在设备屏幕的底端弹出而不是在其它什么地方弹出。</li><li><code>keyPreviewLayout</code>属性用于按下按键时暂短的按键预览。这里我们由于图方便省事，就用一个TextView来预览吧~</li></ul><p>在<code>layout</code>目录下创建一个preview.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></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">TextView</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:gravity</span>=<span class="string">"center"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:background</span>=<span class="string">"#A6A6A6"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:textStyle</span>=<span class="string">"bold"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:textSize</span>=<span class="string">"30sp"</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">TextView</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="定义键盘按键"><a href="#定义键盘按键" class="headerlink" title="定义键盘按键"></a>定义键盘按键</h2><blockquote><p>键盘按键所代表的键值以及其位置等详细信息都被指定在一个xml文件中，每个独立的键盘按键至少都必须包含一下两个属性：</p></blockquote><table><thead><tr><th>属性名</th><th>作用</th></tr></thead><tbody><tr><td>keyLabel</td><td>决定这个按键上显示的字符信息</td></tr><tr><td>codes</td><td>决定这个按键的对应的字符信息所代表的键值</td></tr></tbody></table><p><strong>例如：定义一个字母A的按键那么它的<code>codes</code>属性值应该为：97，<code>keyLabel</code>属性值应该为A。</strong></p><p>如果一个按键关联了多个键值，那么点击该按键输出的字符依赖于敲击该按键的次数。</p><p><strong>例如：如果一个按键拥有三个键值：63、33、58:</strong></p><table><thead><tr><th>敲击该按键的次数</th><th>输出的字符</th></tr></thead><tbody><tr><td>1次</td><td>?</td></tr><tr><td>2次</td><td>!</td></tr><tr><td>3次</td><td>:</td></tr></tbody></table><p>当然了，除了上述属性之外，一个按键还有一些其他的属性：</p><table><thead><tr><th>属性名</th><th>作用</th></tr></thead><tbody><tr><td>keyEdgeFlags</td><td>可以设置的值有：right或者left，该属性通常用在一行最左边或者最右边的按键上用于表示按键的排布</td></tr><tr><td>keyWidth</td><td>定义一个按键的宽度，通常该宽度值被定义为一个百分比值</td></tr><tr><td>isRepeatable</td><td>如果这个属性被设置为true的话，长按被设置为该属性的按键将会在长按这段时间中多次重复该按键的动作。通过，在删除键或者空格键中设置该属性为true</td></tr></tbody></table><p>键盘上的按键通过Row标识为一组按键，比较推荐的做法是限制每组中最多10枚按键，这样的话，每个按键的宽度等于键盘宽度的10%。在本Demo中，按键高度被设置为60dp，这个数值可以任意调整。但是推荐不要低于48dp。</p><p>在<code>res/xml</code>目录中，创建一个<code>qwerty.xml</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><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">Keyboard</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:horizontalGap</span>=<span class="string">"0px"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:keyHeight</span>=<span class="string">"60dp"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:keyWidth</span>=<span class="string">"10%p"</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:verticalGap</span>=<span class="string">"0px"</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Row</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"49"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"left"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"1"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"50"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"2"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"51"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"3"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"52"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"4"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"53"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"5"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"54"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"6"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"55"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"7"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"56"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"8"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"57"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"9"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"48"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"right"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"0"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Row</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"113"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"left"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"q"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"119"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"w"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"101"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"e"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"114"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"r"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"116"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"t"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"121"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"y"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"117"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"u"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"105"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"i"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"111"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"o"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"112"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"right"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"p"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Row</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"97"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"left"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"a"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"115"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"s"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"100"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"d"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"102"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"f"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"103"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"g"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"104"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"h"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"106"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"j"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"107"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"k"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"108"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"l"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"35,64"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"right"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"\#\@"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Row</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"-1"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"left"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"↑"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"122"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"z"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"120"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"x"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"99"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"c"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"118"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"v"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"98"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"b"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"110"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"n"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"109"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"m"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"46"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"."</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"63,33,58"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"right"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"\?!:"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Row</span> <span class="attr">android:rowEdgeFlags</span>=<span class="string">"bottom"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"44"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"left"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">","</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyWidth</span>=<span class="string">"10%p"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"47"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"/"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyWidth</span>=<span class="string">"10%p"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"32"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:isRepeatable</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"SPACE"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyWidth</span>=<span class="string">"40%p"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"-5"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:isRepeatable</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"Del"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyWidth</span>=<span class="string">"20%p"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Key</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:codes</span>=<span class="string">"-4"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyEdgeFlags</span>=<span class="string">"right"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyLabel</span>=<span class="string">"Done"</span></span></span><br><span class="line"><span class="tag">            <span class="attr">android:keyWidth</span>=<span class="string">"20%p"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">Keyboard</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="创建Service类"><a href="#创建Service类" class="headerlink" title="创建Service类"></a>创建Service类</h2><p>创建一个java类，命名为<code>SimpleIME.java</code>（与AndroidManifest.xml文件中的定义向对应）：</p><ul><li><code>SimpleIME</code>类应该继承至<code>InputMethodService</code>类。</li><li><code>SimpleIME</code>类应该实现<code>OnKeyboardActionListener</code>接口，该接口包含了键盘被点击或者被按下时回调的一些函数。</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><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> tk.xiamo.notes.simplekeyboard;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> android.inputmethodservice.InputMethodService;</span><br><span class="line"><span class="keyword">import</span> android.inputmethodservice.Keyboard;</span><br><span class="line"><span class="keyword">import</span> android.inputmethodservice.KeyboardView;</span><br><span class="line"><span class="keyword">import</span> android.media.AudioManager;</span><br><span class="line"><span class="keyword">import</span> android.view.KeyEvent;</span><br><span class="line"><span class="keyword">import</span> android.view.View;</span><br><span class="line"><span class="keyword">import</span> android.view.inputmethod.InputConnection;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SimpleIME</span> <span class="keyword">extends</span> <span class="title">InputMethodService</span> <span class="keyword">implements</span> <span class="title">KeyboardView</span>.<span class="title">OnKeyboardActionListener</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> KeyboardView keyboardView;</span><br><span class="line">    <span class="keyword">private</span> Keyboard keyboard;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> caps = <span class="keyword">false</span>;<span class="comment">//大小写转换</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * onKey方法用于处理键盘与其他应用程序输入域进行交互。</span></span><br><span class="line"><span class="comment">     * getCurrentInputConnection 这个方法被用于获取其它应用程序的输入域，一旦我们获取到这个Connection对象，我们就可以使用以下方法了：</span></span><br><span class="line"><span class="comment">     * commitText ：输出一个或者多个字符到输入域中。</span></span><br><span class="line"><span class="comment">     * deleteSurroundingText ：从输入域中删除一个或者多个字符。</span></span><br><span class="line"><span class="comment">     * sendKeyEvent ：发送一个事件（如KEYCODE_ENTER）到外部的应用程序。</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onKey</span><span class="params">(<span class="keyword">int</span> primaryCode, <span class="keyword">int</span>[] keyCodes)</span> </span>&#123;</span><br><span class="line">        InputConnection inputConnection = getCurrentInputConnection();</span><br><span class="line">        playClick(primaryCode);</span><br><span class="line">        <span class="keyword">switch</span> (primaryCode) &#123;</span><br><span class="line">            <span class="keyword">case</span> Keyboard.KEYCODE_DELETE:</span><br><span class="line">                inputConnection.deleteSurroundingText(<span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> Keyboard.KEYCODE_SHIFT:</span><br><span class="line">                caps = !caps;</span><br><span class="line">                keyboardView.setShifted(caps);</span><br><span class="line">                keyboardView.invalidateAllKeys();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> Keyboard.KEYCODE_DONE:</span><br><span class="line">                inputConnection.sendKeyEvent(<span class="keyword">new</span> KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="keyword">char</span> code = (<span class="keyword">char</span>) primaryCode;</span><br><span class="line">                <span class="keyword">if</span> (Character.isLetter(code) &amp;&amp; caps) &#123;</span><br><span class="line">                    code = Character.toUpperCase(code);</span><br><span class="line">                &#125;</span><br><span class="line">                inputConnection.commitText(String.valueOf(code), <span class="number">1</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="meta">@Override</span></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 当键盘被创建的时候，onCreateInputView方法将会被自动调用。所有的Service成员变量可以在该方法中被初始化。</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> View <span class="title">onCreateInputView</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        keyboardView = (KeyboardView) getLayoutInflater().inflate(R.layout.keyboard, <span class="keyword">null</span>);</span><br><span class="line">        keyboard = <span class="keyword">new</span> Keyboard(<span class="keyword">this</span>, R.xml.qwerty);</span><br><span class="line">        keyboardView.setKeyboard(keyboard);</span><br><span class="line">        keyboardView.setOnKeyboardActionListener(<span class="keyword">this</span>);</span><br><span class="line">        <span class="keyword">return</span> keyboardView;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> keyCode 按键的键码</span></span><br><span class="line"><span class="comment">     *                对指定的按键播放不同的按键声音</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">playClick</span><span class="params">(<span class="keyword">int</span> keyCode)</span> </span>&#123;</span><br><span class="line">        AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);</span><br><span class="line">        <span class="keyword">switch</span> (keyCode) &#123;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">32</span>:</span><br><span class="line">                am.playSoundEffect(AudioManager.FX_KEYPRESS_SPACEBAR);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> Keyboard.KEYCODE_DONE:</span><br><span class="line">            <span class="keyword">case</span> <span class="number">10</span>:</span><br><span class="line">                am.playSoundEffect(AudioManager.FX_KEYPRESS_RETURN);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> Keyboard.KEYCODE_DELETE:</span><br><span class="line">                am.playSoundEffect(AudioManager.FX_KEYPRESS_DELETE);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onText</span><span class="params">(CharSequence text)</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">swipeLeft</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">swipeRight</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">swipeDown</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">swipeUp</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPress</span><span class="params">(<span class="keyword">int</span> primaryCode)</span> </span>&#123;&#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onRelease</span><span class="params">(<span class="keyword">int</span> primaryCode)</span> </span>&#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>需要特别说明的是：一旦用户在键盘上按下了一个按键，onKey方法将会带着被按下的按键的所代表的键值参数而被调用，基于键值的不同，将会执行以下动作：</strong></p><table><thead><tr><th>键值</th><th>指定的动作</th></tr></thead><tbody><tr><td>KEYCODE_DELETE</td><td>将会使用deleteSurroundingText 方法删除光标左侧的一个字符。</td></tr><tr><td>KEYCODE_DONE</td><td>将会激发一个KEYCODE_ENTER 事件。</td></tr><tr><td>KEYCODE_SHIFT</td><td>caps变量的值将会被改变并且通过setShifted方法更新键盘的状态。整个键盘都会被重新绘制来保证状态改变之后按键的label标签可以被更新。其中：invalidateAllKeys 方法将可以重绘所有按键。</td></tr><tr><td>普通键值</td><td>将会被简单的转换为一个字符然后发送到文本输入域中，如果caps变量被设置为true，那么按键字符都将被转换为大写。</td></tr></tbody></table><h2 id="编译运行"><a href="#编译运行" class="headerlink" title="编译运行"></a>编译运行</h2><p>编译运行，在手机上运行的效果如下图：</p><p><img src="http://i1.piimg.com/e15673195304af50.jpg" alt title="键盘运行效果"></p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;&amp;emsp;&amp;emsp;由于最近做的一个Android项目需要用到用户的输入一些字符，常规的输入法输入非常的不方便。因此有必要自定义一个输入法来完成这个过程。此处给出一个简单的输入法Demo&lt;br&gt;来帮助理解自定义输入法的一些实现过程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有关输入法的一些说明，请参见:&lt;a href=&quot;http://notes.xiamo.tk/2015-04-01-Create-an-Android-IME.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Create a Android IME&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
    
    </summary>
    
      <category term="Android" scheme="https://notes.wanghao.work/categories/Android/"/>
    
    
      <category term="Android" scheme="https://notes.wanghao.work/tags/Android/"/>
    
      <category term="IME" scheme="https://notes.wanghao.work/tags/IME/"/>
    
  </entry>
  
</feed>
