<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>海阔天空的博客</title>
    <link>https://www.zhangaoo.com</link>
    <description>本博客主要用于Java，大数据，Golang等技术分享。同时也会分享一些生活的话题，比如摄影、码农理财、日常生活感受等。</description>
    <language>zh-CN</language>
    <item>
      <title>《优势谈判》读书笔记上</title>
      <link>https://www.zhangaoo.com/article/negotiation</link>
      <content:encoded>&lt;h2&gt;优势谈判&lt;/h2&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202077225715-negotiation.jpg" alt="202077225715-negotiation" /&gt;&lt;/p&gt; &lt;p&gt;两个月间断断续续的在看《优势谈判》这本书，截止到今天刚好看了一半左右，书中的实际案例让我历历在目，大到公司的招标谈判，小到去街边买东西讨价还价，都会让我想到这些案例，这些案例让我觉得谈判不仅是一门技术更是一门艺术。很多时候你不得不去分析揣测你的谈判对手策略、心理等。这完全就是一场没有硝烟的战争，买卖双方你攻我守，好不刺激。&lt;/p&gt; &lt;p&gt;每当实际运用书中的策略时，我总是有几分小激动，有的时候比较成功，有的时候运用的却比较失败。我发现大部分人虽然可能没看过这本书，但生活这本无形的书已经教会了他们书中的很多策略，尽管他们娴熟的应用着这些策略，但他们可能不知道，这些策略已经被人总结成了方法论。如果结合自身的实战并上升到理论层次的升华，相信很多人都是谈判高手。&lt;/p&gt; &lt;p&gt;回想一下自己的父母在菜市场讨价还价的激烈场景，可谓是道高一尺魔高一丈，交易达成，双方脸上都挂满了笑容，好像双方都在这场谈判中胜出了。但究竟是谁占了谁的便宜，谁落了谁的全套？接下来我讲结合自身的理解，来谈一谈我对一些策略的理解和分析。&lt;/p&gt; &lt;p&gt;策略“&lt;strong&gt;开出高于预期的条件&lt;/strong&gt;”。前段时间我闲鱼上出售还剩半年的游泳卡，那个时候我对这些谈判策略一无所知，当时我的游泳卡实际价值还有五百块，但迫于还有几天就到期了，到期之后必须在额外买十次才能继续使用，因此我打算六折也就是三百块出售，并且在闲鱼上标注了不讲价。我原本以为这是件很简单的交易。&lt;/p&gt; &lt;p&gt;后面陆陆续续有买家联系我，无一例外，他们都无视了不讲价的标签，一上来就和我砍价，我的内心是崩溃的。这里又涉及到一条谈判技巧，“&lt;strong&gt;永远不要接受第一次报价&lt;/strong&gt;”我的买家们都做到了这一条。我的这次交易最终基本锁定在了以为女性买家身上，这位买家也是以为谈判高手，她首先利用我急于出手的心里营造出一个可买可不买的氛围，然后表现的很不情愿的样子。这里涉及到的谈判技巧是“&lt;strong&gt;做不情愿的卖家和卖家&lt;/strong&gt;”。终于我还是动摇了，最终成交价格是两百六，足足被砍了百分之十三的价格。很明显这次交易中我是完败的。&lt;/p&gt; &lt;p&gt;看了这本书之后我应该如何避免这种完败的结局呢？首先我的标价不能等于我期望的价格，应该大于我期望的价格，比如我期望的价格是三百，那么我实际上可以标四百五或者四百的价格，即便被砍了一百块也还是满足我的期望。还要一种方式就是不直接标明价格，比如象征性的标一块来吸引眼球，然后再具体标注面谈。因为在闲鱼上的的确确看到了很多这样的案例。&lt;/p&gt; &lt;p&gt;在本书中有一条谈判技巧就是不要首先报价，让对方首先报价，因为首先报价的一方几乎可以肯定在谈判中是出于劣势的。因为在你报价后对方此时心里正窃喜，这报价比预期的要好很多，然后他还板着脸告诉你你的报价远远没有达到他的预期。他可以利用更多的技巧，比如“&lt;strong&gt;不情愿的买家或卖家，模糊的最高权威、黑脸白脸&lt;/strong&gt;”等策略来进一步得到更完美的价格，如果此时的你是一个和我上面卖游泳卡一样的谈判小白，大概率只能乖乖就范，在这次迷你谈判中彻彻底底的完败。&lt;/p&gt; &lt;p&gt;以上是我亲身经历的一个案例，只要我们在这个“江湖”上那么我们不可避免的都会别人谈判，掌握这些谈判的小技巧或策略有助于我们的“效益”最大化。原书还讲了比较多的谈判案例，上面案例只讲到很少的部分，有兴趣的同学可以找原书来升华一下自己的谈判理论高度。&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 07 Jul 2020 14:53:00 GMT</pubDate>
    </item>
    <item>
      <title>后端架构的演进</title>
      <link>https://www.zhangaoo.com/article/architecture-evolution</link>
      <content:encoded>&lt;h1&gt;后端架构的演进&lt;/h1&gt; &lt;p&gt;大部分公司都是从创业公司发展了，随着对应的其对应的技术架构也是一个逐步演进的过程。公司不同的发展阶段，不同的业务场景都会需要相应的技术架构来支撑。&lt;/p&gt; &lt;p&gt;俗话说得好，&lt;code&gt;没有最好只有最适合&lt;/code&gt;，需要提防过度优化，过度优化往往在成本以及技术给创业公司带来很多压力。而实际上很共功能都是摆设根本用不到太多。&lt;/p&gt; &lt;h2&gt;基本概念&lt;/h2&gt; &lt;p&gt;在介绍架构之前，为了避免部分读者对架构设计中的一些概念不了解，下面对几个最基础的概念进行介绍：&lt;/p&gt; &lt;p&gt;&lt;strong&gt;分布式&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;系统中的多个模块在不同服务器上部署，即可称为分布式系统，如Tomcat和数据库分别部署在不同的服务器上，或两个相同功能的Tomcat分别部署在不同服务器上。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;高可用&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;系统中部分节点失效时，其他节点能够接替它继续提供服务，则可认为系统具有高可用性&lt;/p&gt; &lt;p&gt;&lt;strong&gt;集群&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;一个特定领域的软件部署在多台服务器上并作为一个整体提供一类服务，这个整体称为集群。如 &lt;code&gt;Zookeeper&lt;/code&gt;中的 &lt;code&gt;Master&lt;/code&gt;和 &lt;code&gt;Slave&lt;/code&gt;分别部署在多台服务器上，共同组成一个整体提供集中配置服务。在常见的集群中，客户端往往能够连接任意一个节点获得服务，并且当集群中一个节点掉线时，其他节点往往能够自动的接替它继续提供服务，这时候说明集群具有高可用性&lt;/p&gt; &lt;p&gt;&lt;strong&gt;负载均衡&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;请求发送到系统时，通过某些方式把请求均匀分发到多个节点上，使系统中每个节点能够均匀的处理请求负载，则可认为系统是负载均衡的。&lt;/p&gt; &lt;h2&gt;架构演进&lt;/h2&gt; &lt;h3&gt;单点架构&lt;/h3&gt; &lt;img src="http://img.zhangaoo.com/2020329165427-single-node.jpg" alt="2020329165427-single-node" style="zoom:50%;" /&gt; &lt;p&gt;在数据量很小，业务规模也很小的时候一般使用一台数据库实例即可，比如业务还处于探索阶段，用户量很少的情况。此时 APP和数据库都是部署在一个节点上。只要APP或数据库其中之一出问题，那么服务就不可用。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;简单，开发调试方便，维护成本低廉&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;劣势：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;缺乏可靠性，单机实例一旦故障，既不可写，又不可读。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;第一次演进：APP与数据分开部署&lt;/h3&gt; &lt;p&gt;APP和数据库分别独占服务器资源，显著提高两者各自性能。随着用户数的增长，并发读写数据库成为瓶颈&lt;/p&gt; &lt;h3&gt;第二次演进：引入缓存&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032917746-single-node-cache.png" alt="202032917746-single-node-cache" /&gt;&lt;/p&gt; &lt;p&gt;在APP同服务器上或同JVM中增加本地缓存，并在外部增加分布式缓存，缓存热门商品信息或热门商品的html页面等。通过缓存能把绝大多数请求在读写数据库前拦截掉，大大降低数据库压力。其中涉及的技术包括：使用&lt;code&gt;memcached&lt;/code&gt;作为本地缓存，使用&lt;code&gt;Redis&lt;/code&gt;作为分布式缓存，还会涉及缓存一致性、缓存穿透/击穿、缓存雪崩、热点数据集中失效等问题。 缓存抗住了大部分的访问请求，随着用户数的增长，并发压力主要落在单机的APP上，响应逐渐变慢&lt;/p&gt; &lt;h3&gt;第三次演进：引入反向代理实现负载均衡&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020329183256-single-node-cache.jpg" alt="2020329183256-single-node-cache" /&gt;&lt;/p&gt; &lt;p&gt;在多台服务器上分别部署APP，使用反向代理软件（Nginx）把请求均匀分发到每个APP中。此处假设APP最多支持100个并发，Nginx最多支持50000个并发，那么理论上Nginx把请求分发到500个APP上，就能抗住50000个并发。其中涉及的技术包括：Nginx、HAProxy，两者都是工作在网络第七层的反向代理软件，主要支持http协议，还会涉及session共享、文件上传下载的问题。&lt;/p&gt; &lt;p&gt;反向代理使应用服务器可支持的并发量大大增加，但并发量的增长也意味着更多请求穿透到数据库，单机的数据库最终成为瓶颈&lt;/p&gt; &lt;p&gt;此时架构需要特别注意APP节点必须是无状态的，比如：APP不会在本地保存文件，而是把文件保存在第三方的组件，每个APP节点都能访问到。&lt;/p&gt; &lt;h3&gt;第四次演进：数据库读写分离&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020329184119-multiple-node-master-slave.jpg" alt="2020329184119-multiple-node-master-slave" /&gt;&lt;/p&gt; &lt;p&gt;把数据库划分为读库和写库，读库可以有多个，通过同步机制把写库的数据同步到读库，对于需要查询最新写入数据场景，可通过在缓存中多写一份，通过缓存获得最新数据。&lt;/p&gt; &lt;p&gt;其中涉及的技术包括：Mycat，它是数据库中间件，可通过它来组织数据库的分离读写和分库分表，客户端通过它来访问下层数据库，还会涉及数据同步，数据一致性的问题。&lt;/p&gt; &lt;p&gt;业务逐渐变多，不同业务之间的访问量差距较大，不同业务直接竞争数据库，相互影响性能&lt;/p&gt; &lt;p&gt;业界解决方案分两种：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;JDBC直连层：Sharding-JDBC、TDDL&lt;/li&gt; &lt;li&gt;Proxy代理层：Mycat、Mysql-proxy&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;第五次演进：数据库按业务分库&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020329185749-multiple-node-master-slave-service.jpg" alt="2020329185749-multiple-node-master-slave-service" /&gt;&lt;/p&gt; &lt;p&gt;把不同业务的数据保存到不同的数据库中，使业务之间的资源竞争降低，对于访问量大的业务，可以部署更多的服务器来支撑。这样同时导致跨业务的表无法直接做关联分析，需要通过其他途径来解决，但这不是本文讨论的重点，有兴趣的可以自行搜索解决方案。 随着用户数的增长，单机的写库会逐渐会达到性能瓶颈&lt;/p&gt; &lt;h3&gt;第六次演进：把大表拆分为小表&lt;/h3&gt; &lt;p&gt;比如针对评论数据，可按照商品ID进行hash，路由到对应的表中存储；针对支付记录，可按照小时创建表，每个小时表继续拆分为小表，使用用户ID或记录编号来路由数据。只要实时操作的表数据量足够小，请求能够足够均匀的分发到多台服务器上的小表，那数据库就能通过水平扩展的方式来提高性能。其中前面提到的Mycat也支持在大表拆分为小表情况下的访问控制。 这种做法显著的增加了数据库运维的难度，对DBA的要求较高。数据库设计到这种结构时，已经可以称为分布式数据库，但是这只是一个逻辑的数据库整体，数据库里不同的组成部分是由不同的组件单独来实现的，如分库分表的管理和请求分发，由Mycat实现，SQL的解析由单机的数据库实现，读写分离可能由网关和消息队列来实现，查询结果的汇总可能由数据库接口层来实现等等，这种架构其实是MPP（大规模并行处理）架构的一类实现。&lt;/p&gt; &lt;p&gt;目前开源和商用都已经有不少MPP数据库，开源中比较流行的有Greenplum、TiDB、Postgresql XC、HAWQ等，商用的如南大通用的GBase、睿帆科技的雪球DB、华为的LibrA等等，不同的MPP数据库的侧重点也不一样，如TiDB更侧重于分布式OLTP场景，Greenplum更侧重于分布式OLAP场景，这些MPP数据库基本都提供了类似Postgresql、Oracle、MySQL那样的SQL标准支持能力，能把一个查询解析为分布式的执行计划分发到每台机器上并行执行，最终由数据库本身汇总数据进行返回，也提供了诸如权限管理、分库分表、事务、数据副本等能力，并且大多能够支持100个节点以上的集群，大大降低了数据库运维的成本，并且使数据库也能够实现水平扩展。&lt;/p&gt; &lt;p&gt;数据库和Tomcat都能够水平扩展，可支撑的并发大幅提高，随着用户数的增长，最终单机的Nginx会成为瓶颈&lt;/p&gt; &lt;h3&gt;第七次演进：使用LVS或F5来使多个Nginx负载均衡&lt;/h3&gt; &lt;p&gt;由于瓶颈在Nginx，因此无法通过两层的Nginx来实现多个Nginx的负载均衡。图中的LVS和F5是工作在网络第四层的负载均衡解决方案，其中LVS是软件，运行在操作系统内核态，可对TCP请求或更高层级的网络协议进行转发，因此支持的协议更丰富，并且性能也远高于Nginx，可假设单机的LVS可支持几十万个并发的请求转发；F5是一种负载均衡硬件，与LVS提供的能力类似，性能比LVS更高，但价格昂贵。由于LVS是单机版的软件，若LVS所在服务器宕机则会导致整个后端系统都无法访问，因此需要有备用节点。可使用keepalived软件模拟出虚拟IP，然后把虚拟IP绑定到多台LVS服务器上，浏览器访问虚拟IP时，会被路由器重定向到真实的LVS服务器，当主LVS服务器宕机时，keepalived软件会自动更新路由器中的路由表，把虚拟IP重定向到另外一台正常的LVS服务器，从而达到LVS服务器高可用的效果。&lt;/p&gt; &lt;p&gt;此处需要注意的是，上图中从Nginx层到Tomcat层这样画并不代表全部Nginx都转发请求到全部的Tomcat，在实际使用时，可能会是几个Nginx下面接一部分的Tomcat，这些Nginx之间通过keepalived实现高可用，其他的Nginx接另外的Tomcat，这样可接入的Tomcat数量就能成倍的增加&lt;/p&gt; &lt;p&gt;由于LVS也是单机的，随着并发数增长到几十万时，LVS服务器最终会达到瓶颈，此时用户数达到千万甚至上亿级别，用户分布在不同的地区，与服务器机房距离不同，导致了访问的延迟会明显不同&lt;/p&gt; &lt;h3&gt;第八次演进：通过DNS轮询实现机房间的负载均衡&lt;/h3&gt; &lt;p&gt;在DNS服务器中可配置一个域名对应多个IP地址，每个IP地址对应到不同的机房里的虚拟IP。当用户访问www.taobao.com时，DNS服务器会使用轮询策略或其他策略，来选择某个IP供用户访问。此方式能实现机房间的负载均衡，至此，系统可做到机房级别的水平扩展，千万级到亿级的并发量都可通过增加机房来解决，系统入口处的请求并发量不再是问题 随着数据的丰富程度和业务的发展，检索、分析等需求越来越丰富，单单依靠数据库无法解决如此丰富的需求&lt;/p&gt; &lt;h3&gt;第九次演进：引入NoSQL数据库和搜索引擎等技术&lt;/h3&gt; &lt;p&gt;当数据库中的数据多到一定规模时，数据库就不适用于复杂的查询了，往往只能满足普通查询的场景。对于统计报表场景，在数据量大时不一定能跑出结果，而且在跑复杂查询时会导致其他查询变慢，对于全文检索、可变数据结构等场景，数据库天生不适用。因此需要针对特定的场景，引入合适的解决方案。如对于海量文件存储，可通过分布式文件系统HDFS解决，对于key value类型的数据，可通过HBase和Redis等方案解决，对于全文检索场景，可通过搜索引擎如ElasticSearch解决，对于多维分析场景，可通过Kylin或Druid等方案解决&lt;/p&gt; &lt;p&gt;当然，引入更多组件同时会提高系统的复杂度，不同的组件保存的数据需要同步，需要考虑一致性的问题，需要有更多的运维手段来管理这些组件等。&lt;/p&gt; &lt;p&gt;引入更多组件解决了丰富的需求，业务维度能够极大扩充，随之而来的是一个应用中包含了太多的业务代码，业务的升级迭代变得困难。&lt;/p&gt; &lt;p&gt;不同的NoSQL数据库使用也有很大差别，比如 Cassandra 适合HBase类似的BigTable大数据组件，其本身的设计就是反范式的，所以 Cassandra 不提供 JOIN 的能力，其建模推荐的方式是基于查询的，也就是先有业务场景具体查询再来设计数据库表。&lt;/p&gt; &lt;p&gt;Cassandra 是鼓励必要的数据冗余的，可能很多个查询都会有相同的列，Cassandra 是鼓励这么做的。&lt;/p&gt; &lt;h3&gt;第十次演进：大应用拆分为小应用&lt;/h3&gt; &lt;p&gt;按照业务板块来划分应用代码，使单个应用的职责更清晰，相互之间可以做到独立升级迭代。这时候应用之间可能会涉及到一些公共配置，可以通过分布式配置中心Zookeeper来解决。&lt;/p&gt; &lt;p&gt;不同应用之间存在共用的模块，由应用单独管理会导致相同代码存在多份，导致公共功能升级时全部应用代码都要跟着升级&lt;/p&gt; &lt;h3&gt;第十一次演进：复用的功能抽离成微服务&lt;/h3&gt; &lt;p&gt;如用户管理、订单、支付、鉴权等功能在多个应用中都存在，那么可以把这些功能的代码单独抽取出来形成一个单独的服务来管理，这样的服务就是所谓的微服务，应用和服务之间通过HTTP、TCP或RPC请求等多种方式来访问公共服务，每个单独的服务都可以由单独的团队来管理。&lt;/p&gt; &lt;p&gt;此外，可以通过Dubbo、SpringCloud等框架实现服务治理、限流、熔断、降级等功能，提高服务的稳定性和可用性。 不同服务的接口访问方式不同，应用代码需要适配多种访问方式才能使用服务，此外，应用访问服务，服务之间也可能相互访问，调用链将会变得非常复杂，逻辑变得混乱&lt;/p&gt; &lt;h3&gt;第十二次演进：引入企业服务总线ESB屏蔽服务接口的访问差异&lt;/h3&gt; &lt;p&gt;通过ESB统一进行访问协议转换，应用统一通过ESB来访问后端服务，服务与服务之间也通过ESB来相互调用，以此降低系统的耦合程度。这种单个应用拆分为多个应用，公共服务单独抽取出来来管理，并使用企业消息总线来解除服务之间耦合问题的架构，就是所谓的SOA（面向服务）架构，这种架构与微服务架构容易混淆，因为表现形式十分相似。个人理解，微服务架构更多是指把系统里的公共服务抽取出来单独运维管理的思想，而SOA架构则是指一种拆分服务并使服务接口访问变得统一的架构思想，SOA架构中包含了微服务的思想。&lt;/p&gt; &lt;p&gt;业务不断发展，应用和服务都会不断变多，应用和服务的部署变得复杂，同一台服务器上部署多个服务还要解决运行环境冲突的问题，此外，对于如大促这类需要动态扩缩容的场景，需要水平扩展服务的性能，就需要在新增的服务上准备运行环境，部署服务等，运维将变得十分困难&lt;/p&gt; &lt;h3&gt;第十三次演进：引入容器化技术实现运行环境隔离与动态服务管理&lt;/h3&gt; &lt;img src="http://img.zhangaoo.com/20203301226-micro-architecture.png" alt="20203301226-micro-architecture" style="zoom:80%;" /&gt; &lt;p&gt;目前最流行的容器化技术是Docker，最流行的容器管理服务是Kubernetes(K8S)，应用/服务可以打包为Docker镜像，通过K8S来动态分发和部署镜像。Docker镜像可理解为一个能运行你的应用/服务的最小的操作系统，里面放着应用/服务的运行代码，运行环境根据实际的需要设置好。把整个“操作系统”打包为一个镜像后，就可以分发到需要部署相关服务的机器上，直接启动Docker镜像就可以把服务起起来，使服务的部署和运维变得简单。&lt;/p&gt; &lt;p&gt;在大促的之前，可以在现有的机器集群上划分出服务器来启动Docker镜像，增强服务的性能，大促过后就可以关闭镜像，对机器上的其他服务不造成影响（在3.14节之前，服务运行在新增机器上需要修改系统配置来适配服务，这会导致机器上其他服务需要的运行环境被破坏）。&lt;/p&gt; &lt;p&gt;使用容器化技术后服务动态扩缩容问题得以解决，但是机器还是需要公司自身来管理，在非大促的时候，还是需要闲置着大量的机器资源来应对大促，机器自身成本和运维成本都极高，资源利用率低&lt;/p&gt; &lt;h3&gt;第十四次演进：以云平台承载系统&lt;/h3&gt; &lt;p&gt;系统可部署到公有云上，利用公有云的海量机器资源，解决动态硬件资源的问题，在大促的时间段里，在云平台中临时申请更多的资源，结合Docker和K8S来快速部署服务，在大促结束后释放资源，真正做到按需付费，资源利用率大大提高，同时大大降低了运维成本。 所谓的云平台，就是把海量机器资源，通过统一的资源管理，抽象为一个资源整体，在之上可按需动态申请硬件资源（如CPU、内存、网络等），并且之上提供通用的操作系统，提供常用的技术组件（如Hadoop技术栈，MPP数据库等）供用户使用，甚至提供开发好的应用，用户不需要关系应用内部使用了什么技术，就能够解决需求（如音视频转码服务、邮件服务、个人博客等）。在云平台中会涉及如下几个概念：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;IaaS：基础设施即服务。对应于上面所说的机器资源统一为资源整体，可动态申请硬件资源的层面；&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;PaaS：平台即服务。对应于上面所说的提供常用的技术组件方便系统的开发和维护；&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;SaaS：软件即服务。对应于上面所说的提供开发好的应用或服务，按功能或性能要求付费。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;DBaaS：数据库及服务。把RDB、NoSQL数据库抽象成服务，按需使用。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;至此，以上所提到的从高并发访问问题，到服务的架构和系统实施的层面都有了各自的解决方案，但同时也应该意识到，在上面的介绍中，其实是有意忽略了诸如跨机房数据同步、分布式事务实现等等的实际问题，这些问题以后有机会再拿出来单独讨论&lt;/p&gt; &lt;h2&gt;SOA、微服务、ESB比较&lt;/h2&gt; &lt;h3&gt;SOA&lt;/h3&gt; &lt;p&gt;SOA 全称是: Service Oriented Architecture，中文释义为 “面向服务的架构”，它是一种设计理念，其中包含多个服务， 服务之间通过相互依赖最终提供一系列完整的功能。各个服务通常以独立的形式部署运行，服务之间 通过网络进行调用。架构图如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020330121858-soa.jpg" alt="2020330121858-soa" style="zoom:40%;" /&gt; &lt;p&gt;跟 SOA 相提并论的还有一个 ESB(企业服务总线)，简单来说 ESB 就是一根管道，用来连接各个服务节点。ESB的存在是为了集成基于不同协议的不同服务，ESB 做了消息的转化、解释以及路由的工作，以此来让不同的服务互联互通; 随着我们业务的越来越复杂，会发现服务越来越多，SOA架构下，它们的调用关系会变成如下形式：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020330122054-soa-all.jpg" alt="2020330122054-soa-all" style="zoom:45%;" /&gt; &lt;p&gt;每个服务最多会有 n*(n-1)/2 调用&lt;/p&gt; &lt;p&gt;&lt;strong&gt;SOA所要解决的核心问题：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;系统间的集成 : 我们站在系统的角度来看，首先要解决各个系统间的通信问题，目的是将原先系统间散乱、无规划的网状结构，梳理成规整、可治理的星形结构，这步的实现往往需要引入一些概念和规范，比如 ESB、以及技术规范、服务管理规范; 这一步解决的核心问题是**【有序】**&lt;/li&gt; &lt;li&gt;系统的服务化 : 我们站在功能的角度，需要把业务逻辑抽象成可复用、可组装的服务，从而通过服务的编排实现业务的快速再生，目的是要把原先固有的业务功能抽象设计为通用的业务服务、实现业务逻辑的快速复用;这步要解决的核心问题是**【复用】**。&lt;/li&gt; &lt;li&gt;业务的服务化 : 我们站在企业的角度，要把企业职能抽象成可复用、可组装的服务，就要把原先职能化的企业架构转变为服务化的企业架构，以便进一步提升企业的对外服务的能力。“&lt;strong&gt;前面两步都是从技术层面来解决系统调用、系统功能复用的问题&lt;/strong&gt;”。而本步骤，则是以业务驱动把一个业务单元封装成一项服务。要解决的核心问题是 &lt;strong&gt;【高效】&lt;/strong&gt;。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;ESB(Enterprise Service Bus)&lt;/h3&gt; &lt;p&gt;很显然，上面的杂乱无章的调用关系基本是难以维护的，那这时候如果我们引入ESB的概念，项目调用就又会很清晰，如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/202033012252-esb.jpg" alt="202033012252-esb" style="zoom:44%;" /&gt; &lt;h3&gt;微服务&lt;/h3&gt; &lt;img src="http://img.zhangaoo.com/2020330122955-springcloud-micro.jpg" alt="2020330122955-springcloud-micro" style="zoom:75%;" /&gt; &lt;p&gt;微服务架构和 SOA 架构非常类似,微服务只是的 SOA 升华，只不过微服务架构强调的是“业务需要彻底的组件化及服务化”，原单个业务系统会被拆分为多个可以独立开发、设计、部署运行的小应用。&lt;/p&gt; &lt;p&gt;这些小应用间通过服务化完成交互和集成。 组件表示的就是一个可以独立更换和升级的单元，就像 PC 中的 CPU、内存、显卡、硬盘一样，独立且可以更换升级而不影响其他单元。若我们把 PC 中的各个组件以服务的方式构 建，那么这台 PC 只需要维护主板（可以理解为ESB）和一些必要的外部设备就可以。CPU、内存、硬盘等都是以组件方式提供服务，例如PC 需要调用 CPU 做计算处理，只需知道 CPU 这个组件的地址就可以了。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;微服务的特征：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通过服务实现组件化&lt;/li&gt; &lt;li&gt;按业务能力来划分服务和开发团队&lt;/li&gt; &lt;li&gt;去中心化&lt;/li&gt; &lt;li&gt;基础设施自动化(devops、自动化部署)&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;&lt;strong&gt;SOA 和微服务架构的差别&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;微服务不再强调传统 SOA 架构里面比较重的 ESB 企业服务总线，同时以 SOA 的思想进入到单个业务系统内部实 现真正的组件化。&lt;/p&gt; &lt;p&gt;Docker 容器技术的出现，为微服务提供了非常便利的条件，比如更小的部署单元，每个服务可以通过类似 Spring Boot 或者 Node 等技术独立运行。&lt;/p&gt; &lt;p&gt;还有一个点大家应该可以分析出来，SOA 注重的是系统集成，而微服务关注的是完全分离。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;服务网格（Service Mesh）架构解析&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;17 年年底，非侵入式的 Service Mesh 技术慢慢走向了成熟。Service Mesh ，中文释义“服务网格”，作为&lt;strong&gt;服务间通信的基础设施层&lt;/strong&gt;在系统中存在。&lt;/p&gt; &lt;p&gt;如果要用一句话来解释什么叫 Service Mesh，我们可以将它比作是&lt;strong&gt;应用程序或者说微服务间的 TCP/IP，负责服务间的网络调用、熔断、限流和监控&lt;/strong&gt;。我们都知道在编写应用程序时程序猿一般都无须关心 TCP/IP 这一层（比如提供 HTTP 协议的 Restful 应用），同样如果使用服务网格我们也就不需要关系服务间的那些原来是由应用程序或者其他框架实现的事情(熔断、限流、监控等)，现在只要交给 Service Mesh 就可以了。服务网格架构图如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020330123750-Service-Mesh.jpg" alt="2020330123750-Service-Mesh" style="zoom:50%;" /&gt; &lt;p&gt;目前流行的 Service Mesh 开源软件有 &lt;strong&gt;Linkerd&lt;/strong&gt;、&lt;strong&gt;Envoy&lt;/strong&gt; 和 &lt;strong&gt;Istio&lt;/strong&gt;，而最近 Buoyant（开源 Linkerd 的公司）又发布了基于 Kubernetes 的 Service Mesh 开源项目 &lt;strong&gt;Conduit&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;关于微服务和服务网格的区别，我这样理解：&lt;strong&gt;微服务更注重服务之间的生态，专注于服务治理等方面，而服务网格更专注于服务之间的通信，以及和 DevOps 更好的结合等&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;服务网格的特征&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;应用程序间通讯的中间层&lt;/li&gt; &lt;li&gt;轻量级网络代理&lt;/li&gt; &lt;li&gt;应用程序无感知&lt;/li&gt; &lt;li&gt;解耦应用程序的重试/超时、监控、追踪和服务发现&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;架构设计总结&lt;/h2&gt; &lt;p&gt;&lt;strong&gt;架构的调整是否必须按照上述演变路径进行？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;不是的，以上所说的架构演变顺序只是针对某个侧面进行单独的改进，在实际场景中，可能同一时间会有几个问题需要解决，或者可能先达到瓶颈的是另外的方面，这时候就应该按照实际问题实际解决。如在政府类的并发量可能不大，但业务可能很丰富的场景，高并发就不是重点解决的问题，此时优先需要的可能会是丰富需求的解决方案。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;对于将要实施的系统，架构应该设计到什么程度？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;对于单次实施并且性能指标明确的系统，架构设计到能够支持系统的性能指标要求就足够了，但要留有扩展架构的接口以便不备之需。对于不断发展的系统，如电商平台，应设计到能满足下一阶段用户量和性能指标要求的程度，并根据业务的增长不断的迭代升级架构，以支持更高的并发和更丰富的业务。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;服务端架构和大数据架构有什么区别？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;所谓的“大数据”其实是海量数据采集清洗转换、数据存储、数据分析、数据服务等场景解决方案的一个统称，在每一个场景都包含了多种可选的技术，如数据采集有Flume、Sqoop、Kettle等，数据存储有分布式文件系统HDFS、FastDFS，NoSQL数据库HBase、MongoDB等，数据分析有Spark技术栈、机器学习算法等。总的来说大数据架构就是根据业务的需求，整合各种大数据组件组合而成的架构，一般会提供分布式存储、分布式计算、多维分析、数据仓库、机器学习算法等能力。而服务端架构更多指的是应用组织层面的架构，底层能力往往是由大数据架构来提供。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;有没有一些架构设计的原则？&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;N+1设计。系统中的每个组件都应做到没有单点故障；&lt;/li&gt; &lt;li&gt;回滚设计。确保系统可以向前兼容，在系统升级时应能有办法回滚版本；&lt;/li&gt; &lt;li&gt;禁用设计。应该提供控制具体功能是否可用的配置，在系统出现故障时能够快速下线功能；&lt;/li&gt; &lt;li&gt;监控设计。在设计阶段就要考虑监控的手段；&lt;/li&gt; &lt;li&gt;多活数据中心设计。若系统需要极高的高可用，应考虑在多地实施数据中心进行多活，至少在一个机房断电的情况下系统依然可用；&lt;/li&gt; &lt;li&gt;采用成熟的技术。刚开发的或开源的技术往往存在很多隐藏的bug，出了问题没有商业支持可能会是一个灾难；&lt;/li&gt; &lt;li&gt;资源隔离设计。应避免单一业务占用全部资源；&lt;/li&gt; &lt;li&gt;架构应能水平扩展。系统只有做到能水平扩展，才能有效避免瓶颈问题；&lt;/li&gt; &lt;li&gt;非核心则购买。非核心功能若需要占用大量的研发资源才能解决，则考虑购买成熟的产品；&lt;/li&gt; &lt;li&gt;使用商用硬件。商用硬件能有效降低硬件故障的机率；&lt;/li&gt; &lt;li&gt;快速迭代。系统应该快速开发小功能模块，尽快上线进行验证，早日发现问题大大降低系统交付的风险；&lt;/li&gt; &lt;li&gt;无状态设计。服务接口应该做成无状态的，当前接口的访问不依赖于接口上次访问的状态。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Mon, 30 Mar 2020 04:40:00 GMT</pubDate>
    </item>
    <item>
      <title>BIO 与 NIO 初探</title>
      <link>https://www.zhangaoo.com/article/start-bio-nio</link>
      <content:encoded>&lt;h1&gt;BIO 与 NIO初探&lt;/h1&gt; &lt;h2&gt;BIO示例代码&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void testBIO() throws IOException {   ServerSocket serverSocket = new ServerSocket(8080);   while (true){     //Thread block 1     //Listens for a connection to be made to this socket and accepts it.     //The method blocks until a connection is made.     Socket conn = serverSocket.accept();     service.submit((Callable&amp;lt;String&amp;gt;)()-&amp;gt;{       byte []request = new byte[1024];       //Thread block 2       // This method blocks until input data is available,        // end of file is detected, or an exception is thrown.       conn.getInputStream().read(request);       System.out.println(new String(request));       return null;     });   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简要分析：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;当有连接请求时，socketServer通过accept方法获取一个socket&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;accept() 会获取一个连接，如果没有连接该方法会阻塞当前线程&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;取得socket后，将这个socket分给一个线程去处理。此时socket需要等待有效的请求数据到来后，才可以真正开始处理请求。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;socket交给线程后，这时socketServer才可以接收下一个连接请求。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;获得连接的顺序是和客户端请求到达服务器的先后顺序相关。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;NIO 示例代码&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    /**      * 此段代码只做示例用，功能并不完整，也没有考虑有所要处理的情况      * @throws IOException      */     @Test     public void testNIO() throws IOException {         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();         //显示的设置不使用BIO而是使用NIO模式         serverSocketChannel.configureBlocking(false);         serverSocketChannel.bind(new InetSocketAddress(8080));         System.out.println(&amp;quot;NIO 服务启动&amp;quot;);         //JDK 底层操作系统，多路复用，事件机制，操作系统会告诉JVM（selector和操作系统底层的多路复用机制打交道）         Selector selector = Selector.open();         //主线程循环检测操作系统是否有新的连接         while (true) {             //获取新链接             SocketChannel socketChannel = serverSocketChannel.accept();//非阻塞，不管有没有都要返回，没有返回null             if(socketChannel == null){                 continue;             }             //显示的设置不使用BIO而是使用NIO模式             socketChannel.configureBlocking(false);             //通过事件机制，当有数据过来的时候，再去处理，让 selector 帮我们去监控OP_READ事件，或者叫观察者模式（有数据传输）             socketChannel.register(selector, SelectionKey.OP_READ);              //查询事件             selector.select();             Set&amp;lt;SelectionKey&amp;gt; eventKeys = selector.selectedKeys();             //循环遍历事件             Iterator&amp;lt;SelectionKey&amp;gt; iterator = eventKeys.iterator();             while (iterator.hasNext()) {                 SelectionKey event = iterator.next();                 //SocketChannel channel = (SocketChannel)event.channel();//可以从时间知道是哪个连接                  if (event.isReadable()) {//一个连接有数据过来了                     service.submit((Callable&amp;lt;String&amp;gt;) () -&amp;gt; {                         //读写数据，处理请求，返回响应                         //ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(1024);//堆外内存，直接映射                         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//封装的数组，对外内存                         socketChannel.read(byteBuffer);                         //转为读取模式                         byteBuffer.flip();                         System.out.println(new String(byteBuffer.array()));                         //      socketChannel.write()                         return null;                     });                 }             }         }     }  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;基于事件驱动，当有连接请求，会将此连接注册到多路复用器上（selector）。&lt;/li&gt; &lt;li&gt;在多路复用器上可以注册监听事件，比如监听accept、read&lt;/li&gt; &lt;li&gt;通过监听，当真正有请求数据时，才来处理数据。&lt;/li&gt; &lt;li&gt;不会阻塞，会不停的轮询是否有就绪的事件，所以处理顺序和连接请求先后顺序无关，与请求数据到来的先后顺序有关&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;主要对比&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;code&gt;BIO&lt;/code&gt;一个连接，一个线程，非 &lt;code&gt;http&lt;/code&gt;请求，有可能只连接不发请求数据，此时线程是无用浪费的。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;code&gt;BIO&lt;/code&gt;处理依赖于连接建立；&lt;code&gt;NIO&lt;/code&gt;处理依赖于请求数据的到来。导致执行顺序不同。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;个线程处理一个请求 &lt;ol&gt; &lt;li&gt;&lt;strong&gt;BIO&lt;/strong&gt;：连接请求来，建立socket，等待请求数据到来（t1），处理时间（t2）&lt;/li&gt; &lt;li&gt;&lt;strong&gt;NIO&lt;/strong&gt;：连接请求来，注册到selector，设置读监听，等待请求数据（t1），处理时间（t2）&lt;/li&gt; &lt;li&gt;此时，两者用时皆为t1+t2，没有区别&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;一个线程处理两个请求 &lt;ol&gt; &lt;li&gt;第一个请求，等待请求数据（10），处理时间（1）&lt;/li&gt; &lt;li&gt;第二个请求，等待请求数据（1），处理时间（2）&lt;/li&gt; &lt;li&gt;&lt;strong&gt;BIO&lt;/strong&gt;：用时 10+1+1+2=14，第1个执行完用时10+1，等待第一个执行完处理第2个，用时1+2&lt;/li&gt; &lt;li&gt;&lt;strong&gt;NIO&lt;/strong&gt;：用时 1+2+7+1=11， 第二个数据先到，时间 1+2，此时第一个需要等时为10秒，还没到，还需等待7秒，时间为7+1&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;两个线程处理两个请求 &lt;ol&gt; &lt;li&gt;第一个请求，等待请求数据（10），处理时间（1）&lt;/li&gt; &lt;li&gt;第二个请求，等待请求数据（1），处理时间（2）&lt;/li&gt; &lt;li&gt;&lt;strong&gt;BIO&lt;/strong&gt;：用时 10+1+2=13，等待第1个请求10，交给工作线程一处理，此时同时接受第2个，等待1秒，处理时间2秒，此间线程一处理时间为一秒，在线程二结束之前就已经结束&lt;/li&gt; &lt;li&gt;&lt;strong&gt;NIO&lt;/strong&gt;：用时 1+2+7+1=11，第二个数据先到，时间 1+2，此时第一个还没到，还需等待7秒，时间为7+1如果两个请求顺序相反，则bio和nio一样，都是11秒&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;由此可见由于阻塞等待机制的不同，导致效率不同，主要优化点为，不必排队等待，先到先处理，就有可能效率高一点。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;BIO如果想要处理并发请求，则必须使用多线程，一般后端会用线程池来支持&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;NIO可以使用单线程，可以减少线程切换上下文的消耗。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;但是虽然单线程减少了线程切换的消耗，但是处理也变为线性的，也就是处理完一个请求，才能处理第二个。 这时，有这么两个场景&lt;/p&gt; &lt;ul&gt; &lt;li&gt;后端是密集型的计算，没有大量的IO操作，比如读些文件、数据库等&lt;/li&gt; &lt;li&gt;后端是有大量的IO操作。&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;当为第一种场景时：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;NIO单线&lt;/strong&gt;程则比较有优势， 理由是虽然是单线程，但是由于线程的计算是&lt;strong&gt;并发&lt;/strong&gt;计算，不是&lt;strong&gt;并行&lt;/strong&gt;计算，说到底，计算压力还是在&lt;strong&gt;CPU&lt;/strong&gt;上，一个线程计算，没有线程的多余消耗，显然比&lt;strong&gt;NIO多线程&lt;/strong&gt;要高效。BIO则必为多线程，否则将阻塞到天荒地老，但多线程是并发，不是并行，主要还是依靠CPU的线性计算，另外还有处理大量的线程上下文。&lt;/li&gt; &lt;li&gt;如果为第二种场景，多线程将有一定优势，多个线程把等待IO的时间能平均开。此时两者区别主要取决于以上分析的处理顺序了，显然NIO要更胜一筹。&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;NIO 框架&lt;/h2&gt; &lt;h3&gt;为什么要使用开源框架？&lt;/h3&gt; &lt;p&gt;这个问题几乎可以当做废话，框架肯定要比一些原生的API封装了更多地功能，重复造轮子在追求效率的情况并不是明智之举。那么先来说说NIO有什么缺点吧：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;NIO的类库和API还是有点复杂，比如Buffer的使用&lt;/li&gt; &lt;li&gt;Selector编写复杂，如果对某个事件注册后，业务代码过于耦合&lt;/li&gt; &lt;li&gt;需要了解很多多线程的知识，熟悉网络编程&lt;/li&gt; &lt;li&gt;面对断连重连、保丢失、粘包等，处理复杂&lt;/li&gt; &lt;li&gt;NIO存在BUG，根据网上言论说是selector空轮训导致CPU飙升，具体有兴趣的可以看看JDK的官网&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;那么有了这些问题，就急需一些大牛们开发通用框架来方便劳苦大众了。最受欢迎NIO框架就是MINA和Netty了。&lt;/p&gt; &lt;h3&gt;MINA VS Netty&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;MINA和Netty的主要贡献者都是同一个人——Trustin lee，韩国Line公司的。&lt;/li&gt; &lt;li&gt;MINA于2006年开发，到14、15年左右，基本停止维护&lt;/li&gt; &lt;li&gt;Nety开始于2009年，目前仍由苹果公司的norman maurer在主要维护。&lt;/li&gt; &lt;li&gt;Norman Maurer是《Netty in Action》一书的作者&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;讲了一大堆的废话之后，总结来说就是——Netty有前途，学它准没错。&lt;/p&gt; &lt;h3&gt;Netty介绍&lt;/h3&gt; &lt;p&gt;按照定义来说，Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;框架设计优雅，底层模型随意切换适应不同的网络协议要求&lt;/li&gt; &lt;li&gt;提供很多标准的协议、安全、编码解码的支持&lt;/li&gt; &lt;li&gt;解决了很多NIO不易用的问题&lt;/li&gt; &lt;li&gt;社区更为活跃，在很多开源框架中使用，如Dubbo、RocketMQ、Spark、Spring-data-redis等&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;主要支持的功能或者特性有：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020328112844-netty.jpg" alt="2020328112844-netty" /&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;底层核心有：Zero-Copy-Capable Buffer，非常易用的灵拷贝Buffer（这个内容很有意思，稍后专门来说）；统一的API；标准可扩展的时间模型&lt;/li&gt; &lt;li&gt;传输方面的支持有：管道通信、Http隧道、TCP与UDP&lt;/li&gt; &lt;li&gt;协议方面的支持有：基于原始文本和二进制的协议；解压缩；大文件传输；流媒体传输；protobuf编解码；安全认证；http和websocket&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;Netty服务器小例子&lt;/h3&gt; &lt;p&gt;基于Netty的服务器编程可以看做是Reactor模型：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032811311-Reactor.jpg" alt="202032811311-Reactor" /&gt;&lt;/p&gt; &lt;p&gt;即包含一个接收连接的线程池（也有可能是单个线程，boss线程池）以及一个处理连接的线程池（worker线程池）。boss负责接收连接，并进行IO监听；worker负责后续的处理。为了便于理解Netty，直接看看代码:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;  import java.net.InetSocketAddress; import java.nio.charset.Charset;  public class NettyNioServer {     public void serve(int port) throws InterruptedException {         final ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(&amp;quot;Hi\r\n&amp;quot;, Charset.forName(&amp;quot;UTF-8&amp;quot;)));   // 第一步，创建线程池         EventLoopGroup bossGroup = new NioEventLoopGroup(1);         EventLoopGroup workerGroup = new NioEventLoopGroup();          try{          // 第二步，创建启动类             ServerBootstrap b = new ServerBootstrap();             // 第三步，配置各组件             b.group(bossGroup, workerGroup)                     .channel(NioServerSocketChannel.class)                     .localAddress(new InetSocketAddress(port))                     .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {                         @Override                         protected void initChannel(SocketChannel socketChannel) throws Exception {                             socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){                                 @Override                                 public void channelActive(ChannelHandlerContext ctx) throws Exception {                                     ctx.writeAndFlush(buffer.duplicate()).addListener(ChannelFutureListener.CLOSE);                                 }                             });                         }                     });             // 第四步，开启监听             ChannelFuture f = b.bind().sync();             f.channel().closeFuture().sync();         } finally {             bossGroup.shutdownGracefully().sync();             workerGroup.shutdownGracefully().sync();         }     }      public static void main(String[] args) throws InterruptedException {         NettyNioServer server = new NettyNioServer();         server.serve(5555);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;代码非常少，而且想要换成阻塞IO，只需要替换Channel里面的工厂类即可：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class NettyOioServer {     public void serve(int port) throws InterruptedException {         final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(&amp;quot;Hi\r\b&amp;quot;, Charset.forName(&amp;quot;UTF-8&amp;quot;)));          EventLoopGroup bossGroup = new OioEventLoopGroup(1);         EventLoopGroup workerGroup = new OioEventLoopGroup();          try{             ServerBootstrap b = new ServerBootstrap();             b.group(bossGroup, workerGroup)//配置boss和worker                     .channel(OioServerSocketChannel.class) // 使用阻塞的SocketChannel          .... &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;p&gt;NIO在接收请求方式上，无疑是要高效于BIO，原因并非是不阻塞，我认为NIO一样是阻塞的，只是方式不同，先来的有效请求先处理，先阻塞时间短的。此时间可用于等待等待时间长的。&lt;/p&gt; &lt;p&gt;在处理请求上，NIO和BIO并没有什么不同，主要看线程池规划是否和理。NIO相对BIO在密集型计算的模型下，可以用更少的线程，甚至单线程&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 28 Mar 2020 03:33:00 GMT</pubDate>
    </item>
    <item>
      <title>Java 集合详解</title>
      <link>https://www.zhangaoo.com/article/java-map-list-array</link>
      <content:encoded>&lt;h1&gt;Java 集合详解&lt;/h1&gt; &lt;p&gt;收集了几张集合结构图，可对比看：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032184726-java-collection.png" alt="202032184726-java-collection" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032184848-java-collection-1.jpg" alt="202032184848-java-collection-1" /&gt;&lt;/p&gt; &lt;h2&gt;集合和数组的区别&lt;/h2&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032185135-collectin-array.png" alt="202032185135-collectin-array" /&gt;&lt;/p&gt; &lt;h2&gt;Collection集合接口的方法&lt;/h2&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032185344-collection-interface.png" alt="202032185344-collection-interface" /&gt;&lt;/p&gt; &lt;h2&gt;常用集合的分类&lt;/h2&gt; &lt;h3&gt;实现Collection接口的集合&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;├——-List 接口：元素按进入先后有序保存，可重复 │—————-├ LinkedList 接口实现类， 链表， 插入删除， 没有同步， 线程不安全 │—————-├ ArrayList 接口实现类， 数组， 随机访问， 没有同步， 线程不安全 │—————-└ Vector 接口实现类 数组， 同步， 线程安全 │ ———————-└ Stack 是Vector类的实现类 └——-Set 接口： 仅接收一次，不可重复，并做内部排序 ├—————-└HashSet 使用hash表（数组）存储元素 │————————└ LinkedHashSet 链表维护元素的插入次序 └ —————-TreeSet 底层实现为二叉树，元素排好序 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;实现Map接口集合&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;├———Hashtable 接口实现类， 同步， 线程安全 ├———HashMap 接口实现类 ，没有同步， 线程不安全- │—————–├ LinkedHashMap 双向链表和哈希表实现 │—————–└ WeakHashMap 引用的典型应用，可以作为简单的缓存表解决方案 ├ ——–TreeMap 红黑树对所有的key进行排序 └———IdentifyHashMap &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;List和Set集合详解&lt;/h2&gt; &lt;h3&gt;list和set的区别&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20203219254-list-set-difference.png" alt="20203219254-list-set-difference" /&gt;&lt;/p&gt; &lt;h3&gt;List&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;ArrayList&lt;/strong&gt;：底层数据结构是数组，查询快，增删慢，线程不安全，效率高，可以存储重复元素&lt;/li&gt; &lt;li&gt;&lt;strong&gt;LinkedList&lt;/strong&gt;： 底层数据结构是链表，查询慢，增删快，线程不安全，效率高，可以存储重复元素&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Vector&lt;/strong&gt;：底层数据结构是数组，查询快，增删慢，线程安全，效率低，可以存储重复元素&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321957-list-architecture.png" alt="2020321957-list-architecture" /&gt;&lt;/p&gt; &lt;h3&gt;Set&lt;/h3&gt; &lt;h4&gt;HashSet&lt;/h4&gt; &lt;p&gt;底层数据结构采用哈希表实现，元素无序且唯一，线程不安全，效率高，可以存储null元素，元素的唯一性是靠所存储元素类型是否重写 &lt;code&gt;hashCode()&lt;/code&gt; 和 &lt;code&gt;equals()&lt;/code&gt;方法来保证的，如果没有重写这两个方法，则无法保证元素的唯一性。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;具体实现唯一性的比较过程：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值，然后已经的所存储的元素的hashCode值比较，如果hashCode不相等，则所存储的两个对象一定不相等，此时存储当前的新的hashCode值处的元素对象；&lt;/li&gt; &lt;li&gt;如果hashCode相等，存储元素的对象还是不一定相等，此时会调用equals()方法判断两个对象的内容是否相等，如果内容相等，那么就是同一个对象，无需存储；&lt;/li&gt; &lt;li&gt;如果比较的内容不相等，那么就是不同的对象，就该存储了，此时就要采用哈希的解决地址冲突算法，在当前hashCode值处类似一个新的链表， 在同一个hashCode值的后面存储存储不同的对象，这样就保证了元素的唯一性。&lt;/li&gt; &lt;li&gt;HashSet采用哈希算法，底层用数组存储数据。默认初始化容量16，加载因子0.75。&lt;/li&gt; &lt;li&gt;Object类中的hashCode()的方法是所有子类都会继承这个方法，这个方法会用Hash算法算出一个Hash（哈希）码值返回，HashSet会用Hash码值去和数组长度取模， 模（这个模就是对象要存放在数组中的位置）相同时才会判断数组中的元素和要加入的对象的内容是否相同，如果不同才会添加进去。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Hash算法是一种散列算法：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Set hs = new HashSet(); o.hashCode(); o%当前总容量 (0–15) 不发生冲突——–&amp;gt;直接存放 发生冲突 o1.equals(o2)——(false)——-找一个空位添加   true -&amp;gt; 不添加   &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;覆盖hashCode()方法的原则：&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;一定要让那些我们认为相同的对象返回相同的 &lt;code&gt;hashCode&lt;/code&gt;值&lt;/li&gt; &lt;li&gt;尽量让那些我们认为不同的对象返回不同的hashCode值，否则，就会增加冲突的概率。&lt;/li&gt; &lt;li&gt;尽量的让hashCode值散列开（两值用异或运算可使结果的范围更广）&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;我们应该为保存到HashSet中的对象覆盖hashCode()和equals()，因为再将对象加入到HashSet中时，会首先调用hashCode方法计算出对象的hash值，接着根据此hash值调用HashMap中的hash方法，得到的值&amp;amp; (length-1)得到该对象在hashMap的transient Entry[] table中的保存位置的索引，接着找到数组中该索引位置保存的对象，并调用equals方法比较这两个对象是否相等，如果相等则不添加。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：所以要存入HashSet的集合对象中的自定义类必须覆盖hashCode(),equals()两个方法，才能保证集合中元素不重复。在覆盖equals()和hashCode()方法时， 要使相同对象的hashCode()方法返回相同值，覆盖equals()方法再判断其内容。为了保证效率，所以在覆盖hashCode()方法时， 也要尽量使不同对象尽量返回不同的Hash码值。&lt;/p&gt; &lt;p&gt;如果数组中的元素和要加入的对象的hashCode()返回了相同的Hash值（相同对象），才会用equals()方法来判断两个对象的内容是否相同。&lt;/p&gt; &lt;h4&gt;LinkedHashSet&lt;/h4&gt; &lt;p&gt;底层数据结构采用链表和哈希表共同实现，链表保证了元素的顺序与存储顺序一致，哈希表保证了元素的唯一性。线程不安全，效率高。&lt;/p&gt; &lt;h4&gt;TreeSet&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;底层数据结构采用二叉树来实现，元素唯一且已经排好序；&lt;/li&gt; &lt;li&gt;唯一性同样需要重写 &lt;code&gt;hashCode&lt;/code&gt;和&lt;code&gt;equals()&lt;/code&gt;方法，二叉树结构保证了元素的有序性。&lt;/li&gt; &lt;li&gt;根据构造方法不同，分为自然排序（无参构造）和比较器排序（有参构造），自然排序要求元素必须实现Compareable接口，并重写里面的compareTo()方法，元素通过比较返回的int值来判断排序序列，返回0说明两个对象相同，不需要存储；&lt;/li&gt; &lt;li&gt;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象，或者采用匿名内部类的方式new一个Comparator对象，重写里面的compare()方法；&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;小结&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;Set具有与Collection完全一样的接口，因此没有任何额外的功能，不像前面有两个不同的List。实际上Set就是Collection,只 是行为不同。(这是继承与多态思想的典型应用：表现不同的行为。)Set不保存重复的元素。&lt;/li&gt; &lt;li&gt;Set 存入Set的每个元素都必须是唯一的，因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;List和Set总结&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;List，Set都是继承自Collection接口，Map则不是&lt;/li&gt; &lt;li&gt;List特点：元素有放入顺序，元素可重复&lt;/li&gt; &lt;li&gt;Set特点：元素无放入顺序，元素不可重复，重复元素会覆盖掉，（注意：元素虽然无放入顺序，但是元素在set中的位置是有该元素的HashCode决定的，其位置其实是固定的，加入Set 的Object必须定义equals()方法 ，另外list支持for循环，也就是通过下标来遍历，也可以用迭代器，但是set只能用迭代，因为他无序，无法用下标来取得想要的值。）&lt;/li&gt; &lt;li&gt;Set：检索元素效率低下，删除和插入效率高，插入和删除不会引起元素位置改变。&lt;/li&gt; &lt;li&gt;List：和数组类似，List可以动态增长，查找元素效率高，插入删除元素效率低，因为会引起其他元素位置改变。&lt;/li&gt; &lt;li&gt;Arraylist 优点：ArrayList是实现了基于动态数组的数据结构,因为地址连续，一旦数据存储好了，查询操作效率会比较高（在内存里是连着放的）。&lt;/li&gt; &lt;li&gt;Arraylist 缺点：因为地址连续， ArrayList要移动数据,所以插入和删除操作效率比较低。&lt;/li&gt; &lt;li&gt;LinkedList 优点：LinkedList基于链表的数据结构,地址是任意的，所以在开辟内存空间的时候不需要等一个连续的地址，对于新增和删除操作add和remove，LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景&lt;/li&gt; &lt;li&gt;LinkedList 缺点：因为LinkedList要移动指针,所以查询操作性能比较低。&lt;/li&gt; &lt;li&gt;适用场景分析：当需要对数据进行对此访问的情况下选用ArrayList，当需要对数据进行多次增加删除修改时采用LinkedList。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;ArrayList与Vector的区别和适用场景&lt;/h4&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;ArrayList有三个构造方法：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public ArrayList(int initialCapacity);//构造一个具有指定初始容量的空列表。     public ArrayList();      //默认构造一个初始容量为10的空列表。     public ArrayList(Collection&amp;lt;? extends E&amp;gt; c);//构造一个包含指定 collection 的元素的列表 &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Vector有四个构造方法：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public Vector();//使用指定的初始容量和等于0的容量增量构造一个空向量。     public Vector(int initialCapacity);//构造一个空向量，使其内部数据数组的大小，其标准容量增量为零。     public Vector(Collection&amp;lt;? extends E&amp;gt; c);//构造一个包含指定 collection 中的元素的向量     public Vector(int initialCapacity,int capacityIncrement);//使用指定的初始容量和容量增量构造一个空的向量    &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;ArrayList和Vector都是用数组实现的，主要有这么三个区别：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Vector是多线程安全的，线程安全就是说多线程访问同一代码，不会产生不确定的结果。而ArrayList不是，这个可以从源码中看出，Vector类中的方法很多有synchronized进行修饰，这样就导致了Vector在效率上无法与ArrayList相比；&lt;/li&gt; &lt;li&gt;两个都是采用的线性连续空间存储元素，但是当空间不足的时候，两个类的增加方式是不同。 &lt;ul&gt; &lt;li&gt;Vector可以设置增长因子，而ArrayList不可以。&lt;/li&gt; &lt;li&gt;Vector是一种老的动态数组，是线程同步的，效率很低，一般不赞成使用。&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;适用场景分析&lt;/strong&gt;：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;Vector是线程同步的，所以它也是线程安全的，而ArrayList是线程异步的，是不安全的。如果不考虑到线程的安全因素，一般用ArrayList效率比较高。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;如果集合中的元素的数目大于目前集合数组的长度时，在集合中使用数据量比较大的数据，用Vector有一定的优势。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;TreeSet 是二差树（红黑树的树据结构）实现的，Treeset中的数据是自动排好序的，不允许放入null值&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;HashSet 是哈希表实现的,HashSet中的数据是无序的，可以放入null，但只能放入一个null，两者中的值都不能重复，就如数据库中唯一约束。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;HashSet要求放入的对象必须实现HashCode()方法，放入的对象，是以hashcode码作为标识的，而具有相同内容的String对象，hashcode是一样，所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;HashSet是基于Hash算法实现的，其性能通常都优于TreeSet。为快速查找而设计的Set，我们通常都应该使用HashSet，在我们需要排序的功能时，我们才使用TreeSet。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;何时使用&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032193417-use-scene.png" alt="202032193417-use-scene" /&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Map详解&lt;/h2&gt; &lt;p&gt;Map用于保存具有映射关系的数据，Map里保存着两组数据：key和value，它们都可以使任何引用类型的数据，但key不能重复。所以通过指定的key就可以取出对应的value。&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;请注意！！！&lt;/strong&gt;， Map 没有继承 Collection 接口， Map 提供 key 到 value 的映射，你可以通过“键”查找“值”。一个 Map 中不能包含相同的 key ，每个 key 只能映射一个 value 。 Map 接口提供 3 种集合的视图， Map 的内容可以被当作一组 key 集合，一组 value 集合，或者一组 key-value 映射。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Map&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032122915-map-method.png" alt="202032122915-map-method" /&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;HashMap和HashTable的比较&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321221124-hashmap-vs-hashtable.png" alt="2020321221124-hashmap-vs-hashtable" /&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Map的其它类&lt;/p&gt; &lt;p&gt;&lt;strong&gt;IdentityHashMap&lt;/strong&gt;和&lt;strong&gt;HashMap&lt;/strong&gt;的具体区别，IdentityHashMap使用 == 判断两个key是否相等，而HashMap使用的是equals方法比较key值。有什么区别呢？&lt;/p&gt; &lt;p&gt;对于==，如果作用于基本数据类型的变量，则直接比较其存储的 “值”是否相等； 如果作用于引用类型的变量，则比较的是所指向的对象的地址。&lt;/p&gt; &lt;p&gt;对于equals方法，注意：equals方法不能作用于基本数据类型的变量&lt;/p&gt; &lt;p&gt;如果没有对equals方法进行重写，则比较的是引用类型的变量所指向的对象的地址；&lt;/p&gt; &lt;p&gt;诸如String、Date等类对equals方法进行了重写的话，比较的是所指向的对象的内容。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321222156-other-map.jpg" alt="2020321222156-other-map" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;小结 &lt;ol&gt; &lt;li&gt;HashMap 非线程安全&lt;/li&gt; &lt;li&gt;HashMap：基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()]，为了优化HashMap空间的使用，您可以调优初始容量和负载因子。&lt;/li&gt; &lt;li&gt;TreeMap：非线程安全基于红黑树实现。TreeMap没有调优选项，因为该树总处于平衡状态。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;&lt;strong&gt;适用场景分析&lt;/strong&gt; &lt;ol&gt; &lt;li&gt;HashMap和HashTable:HashMap去掉了HashTable的contains方法，但是加上了containsValue()和containsKey()方法。HashTable同步的，而HashMap是非同步的，效率上比HashTable要高。HashMap允许空键值，而HashTable不允许。&lt;/li&gt; &lt;li&gt;HashMap：适用于Map中插入、删除和定位元素。&lt;/li&gt; &lt;li&gt;Treemap：适用于按自然顺序或自定义顺序遍历键(key)。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;线程安全集合类与非线程安全集合类 &lt;ol&gt; &lt;li&gt;LinkedList、ArrayList、HashSet是非线程安全的，Vector是线程安全的;&lt;/li&gt; &lt;li&gt;HashMap是非线程安全的，HashTable是线程安全的;&lt;/li&gt; &lt;li&gt;StringBuilder是非线程安全的，StringBuffer是线程安全的。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;数据结构 &lt;ol&gt; &lt;li&gt;ArrayXxx:底层数据结构是数组，查询快，增删慢&lt;/li&gt; &lt;li&gt;LinkedXxx:底层数据结构是链表，查询慢，增删快&lt;/li&gt; &lt;li&gt;HashXxx:底层数据结构是哈希表。依赖两个方法：hashCode()和equals()&lt;/li&gt; &lt;li&gt;TreeXxx:底层数据结构是二叉树。两种方式排序：自然排序和比较器排序&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Mon, 23 Mar 2020 08:21:00 GMT</pubDate>
    </item>
    <item>
      <title>Dubbo 和 SpringCloud 微服务架构对比</title>
      <link>https://www.zhangaoo.com/article/dubbo-vs-springcloud</link>
      <content:encoded>&lt;h1&gt;Dubbo 和 SpringCloud 微服务架构对比&lt;/h1&gt; &lt;p&gt;&lt;code&gt;Dubbo&lt;/code&gt; 出生于阿里系，是阿里巴巴服务化治理的核心框架，并被广泛应用于中国各互联网公司；只需要通过 Spring 配置的方式即可完成服务化，对于应用无入侵，设计的目的还是服务于自身的业务为主。&lt;/p&gt; &lt;p&gt;微服务架构是互联网很热门的话题，是互联网技术发展的必然结果。它提倡将单一应用程序划分成一组小的服务，服务之间互相协调、互相配合，为用户提供最终价值。&lt;/p&gt; &lt;p&gt;虽然微服务架构没有公认的技术标准和规范或者草案，但业界已经有一些很有影响力的开源微服务架构框架提供了微服务的关键思路，例如 &lt;code&gt;Dubbo&lt;/code&gt; 和 &lt;code&gt;Spring Cloud&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;各大互联网公司也有自研的微服务框架，但其模式都与这二者相差不大。&lt;/p&gt; &lt;h2&gt;&lt;strong&gt;微服务主要的优势&lt;/strong&gt;&lt;/h2&gt; &lt;h3&gt;&lt;strong&gt;降低复杂度&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;将原来耦合在一起的复杂业务拆分为单个服务，规避了原本复杂度无止境的积累。&lt;/p&gt; &lt;p&gt;每一个微服务专注于单一功能，并通过定义良好的接口清晰表述服务边界；每个服务开发者只专注服务本身，通过使用缓存、DAL 等各种技术手段来提升系统的性能，而对于消费方来说完全透明。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;可独立部署&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;由于微服务具备独立的运行进程，所以每个微服务可以独立部署。当业务迭代时只需要发布相关服务的迭代即可，降低了测试的工作量同时也降低了服务发布的风险。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;容错&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;在微服务架构下，当某一组件发生故障时，故障会被隔离在单个服务中。比如通过限流、熔断等方式降低错误导致的危害，保障核心业务正常运行。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;扩展&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;单块架构应用也可以实现横向扩展，就是将整个应用完整的复制到不同的节点。&lt;/p&gt; &lt;p&gt;当应用的不同组件在扩展需求上存在差异时，微服务架构便体现出其灵活性，因为每个服务可以根据实际需求独立进行扩展。&lt;/p&gt; &lt;p&gt;本文主要围绕微服务的技术选型、通讯协议、服务依赖模式、开始模式、运行模式等几方面来综合比较 Dubbo 和 Spring Cloud 这 2 种开发框架。&lt;/p&gt; &lt;p&gt;架构师可以根据公司的技术实力并结合项目的特点来选择某个合适的微服务架构平台，以此稳妥地实施项目的微服务化改造或开发进程。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;核心部件&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;微服务的核心要素在于服务的&lt;code&gt;发现、注册、路由、熔断、降级、分布式配置&lt;/code&gt;，基于上述几种必要条件对 Dubbo 和 Spring Cloud 做出对比。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;总体架构&lt;/strong&gt;&lt;/h3&gt; &lt;h4&gt;Dubbo 核心部件（如下图）&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321224658-dubbo-struct.jpg" alt="2020321224658-dubbo-struct" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;**Provider：**暴露服务的提供方，可以通过 jar 或者容器的方式启动服务。&lt;/li&gt; &lt;li&gt;**Consumer：**调用远程服务的服务消费方。&lt;/li&gt; &lt;li&gt;**Registry：**服务注册中心和发现中心。&lt;/li&gt; &lt;li&gt;**Monitor：**统计服务和调用次数，调用时间监控中心。（Dubbo 的控制台页面中可以显示，目前只有一个简单版本。）&lt;/li&gt; &lt;li&gt;**Container：**服务运行的容器。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;Spring Cloud总体架构（如下图）&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321224939-springcloud-struct.jpg" alt="2020321224939-springcloud-struct" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Service Provider：&lt;/strong&gt; 暴露服务的提供方。&lt;/li&gt; &lt;li&gt;**Service Consumer：**调用远程服务的服务消费方。&lt;/li&gt; &lt;li&gt;&lt;strong&gt;EureKa Server：&lt;/strong&gt; 服务注册中心和服务发现中心。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;从整体架构上来看，二者模式接近，都需要服务提供方，注册中心，服务消费方。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;对比&lt;/h3&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321225132-dubbo-compare-springcloud.jpg" alt="2020321225132-dubbo-compare-springcloud" /&gt;&lt;/p&gt; &lt;p&gt;Dubbo 只是实现了服务治理，而 Spring Cloud 子项目分别覆盖了微服务架构下的众多部件，服务治理只是其中的一个方面。&lt;/p&gt; &lt;p&gt;Dubbo 提供了各种 Filter，对于上述中“无”的要素，可以通过扩展 Filter 来完善。例如：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;**分布式配置：**可以使用淘宝的 diamond、百度的 disconf 来实现分布式配置管理。&lt;/li&gt; &lt;li&gt;**服务跟踪：**可以使用京东开源的 Hydra，或者扩展 Filter 用 Zippin 来做服务跟踪。&lt;/li&gt; &lt;li&gt;**批量任务：**可以使用当当开源的 Elastic-Job、tbschedule。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;点评：从核心要素来看，Spring Cloud 更胜一筹，在开发过程中只要整合 Spring Cloud 的子项目就可以顺利的完成各种组件的融合，而 Dubbo 却需要通过实现各种 Filter 来做定制，开发成本以及技术难度略高。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;通讯协议&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;基于通讯协议层面对 2 种框架支持的协议类型以及运行效率方面进行比较。&lt;/p&gt; &lt;h4&gt;Dubbo 使用 RPC 通讯协议，提供序列化方式&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;**Dubbo：**Dubbo 缺省协议采用单一长连接和 NIO 异步通讯，适合于小数据量大并发的服务调用，以及服务消费者机器数远大于服务提供者机器数的情况。&lt;/li&gt; &lt;li&gt;**RMI：*&lt;em&gt;RMI 协议采用 JDK 标准的 java.rmi.&lt;/em&gt; 实现，采用阻塞式短连接和 JDK 标准序列化方式。&lt;/li&gt; &lt;li&gt;**Hessian：**Hessian 协议用于集成 Hessian 的服务，Hessian 底层采用 HTTP 通讯，采用 Servlet 暴露服务，Dubbo 缺省内嵌 Jetty 作为服务器实现。&lt;/li&gt; &lt;li&gt;**HTTP：**采用 Spring 的 Http Invoker 实现。&lt;/li&gt; &lt;li&gt;**Webservice：**基于 CXF 的 frontend-simple 和 transports-http 实现。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;Spring Cloud 使用 HTTP 协议的 REST API&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;Spring Cloud 使用 HTTP 协议的 REST API。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;性能比较&lt;/h4&gt; &lt;p&gt;使用一个 Pojo 对象包含 10 个属性，请求 10 万次，Dubbo 和 Spring Cloud 在不同的线程数量下，每次请求耗时（ms）如下：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032123128-dubbo-springcloud-performance.jpg" alt="202032123128-dubbo-springcloud-performance" /&gt;&lt;/p&gt; &lt;p&gt;**说明：**客户端和服务端配置均采用阿里云的 ECS 服务器，4 核 8G 配置，Dubbo 采用默认的 Dubbo 协议。&lt;/p&gt; &lt;p&gt;**点评：**Dubbo 支持各种通信协议，而且消费方和服务方使用长链接方式交互，通信速度上略胜 Spring Cloud，如果对于系统的响应时间有严格要求，长链接更合适。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;服务依赖方式&lt;/strong&gt;&lt;/h3&gt; &lt;h4&gt;&lt;strong&gt;Dubbo&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;服务提供方与消费方通过接口的方式依赖，服务调用设计如下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;**Interface 层：**服务接口层，定义了服务对外提供的所有接口。&lt;/li&gt; &lt;li&gt;**Molel 层：**服务的 DTO 对象层。&lt;/li&gt; &lt;li&gt;**Business层：**业务实现层，实现 Interface 接口并且和 DB 交互。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;因此需要为每个微服务定义各自的 Interface 接口，并通过持续集成发布到私有仓库中。调用方应用对微服务提供的抽象接口存在强依赖关系，开发、测试、集成环境都需要严格的管理版本依赖。&lt;/p&gt; &lt;p&gt;通过 maven 的 install &amp;amp; deploy 命令把 Interface 和 Model 层发布到仓库中，服务调用方只需要依赖 Interface 和 Model 层即可。&lt;/p&gt; &lt;p&gt;在开发调试阶段只发布 Snapshot 版本，等到服务调试完成再发布 Release 版本，通过版本号来区分每次迭代的版本。通过 xml 配置方式即可接入 Dubbo，对程序无入侵。&lt;/p&gt; &lt;h4&gt;&lt;strong&gt;Spring Cloud&lt;/strong&gt;、&lt;/h4&gt; &lt;p&gt;服务提供方和服务消费方通过 Json 方式交互，因此只需要定义好相关 Json 字段即可，消费方和提供方无接口依赖。通过注解方式来实现服务配置，对于程序有一定入侵。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032123723-service-depend.jpg" alt="202032123723-service-depend" /&gt;&lt;/p&gt; &lt;p&gt;点评：Dubbo 服务依赖略重，需要有完善的版本管理机制，但是程序入侵少。而 Spring Cloud 通过 Json 交互，省略了版本管理的问题，但是具体字段含义需要统一管理，自身 Rest API 方式交互，为跨平台调用奠定了基础。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;组件运行流程&lt;/strong&gt;&lt;/h3&gt; &lt;h4&gt;&lt;strong&gt;Dubbo&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;下图中的每个组件都是需要部署在单独的服务器上，Gateway 用来接受前端请求、聚合服务，并批量调用后台原子服务。每个 Service 层和单独的 DB 交互。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032123101-dubbo-process.jpg" alt="202032123101-dubbo-process" /&gt;&lt;/p&gt; &lt;p&gt;Dubbo 组件运行：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;**Gateway：**前置网关，具体业务操作，Gateway 通过 Dubbo 提供的负载均衡机制自动完成。&lt;/li&gt; &lt;li&gt;**Service：**原子服务，只提供该业务相关的原子服务。&lt;/li&gt; &lt;li&gt;**Zookeeper：**原子服务注册到 ZK 上。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;&lt;strong&gt;Spring Cloud&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202032123129-springcloud-process.jpg" alt="202032123129-springcloud-process" /&gt;&lt;/p&gt; &lt;p&gt;Spring Cloud组件运行：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;所有请求都统一通过 API 网关（Zuul）来访问内部服务。&lt;/li&gt; &lt;li&gt;网关接收到请求后，从注册中心（Eureka）获取可用服务。&lt;/li&gt; &lt;li&gt;由 Ribbon 进行均衡负载后，分发到后端的具体实例。&lt;/li&gt; &lt;li&gt;微服务之间通过 Feign 进行通信处理业务。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;**点评：**业务部署方式相同，都需要前置一个网关来隔绝外部直接调用原子服务的风险。&lt;/p&gt; &lt;p&gt;Dubbo 需要自己开发一套 API 网关，而 Spring Cloud 则可以通过 Zuul 配置即可完成网关定制。使用方式上 Spring Cloud 略胜一筹。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;微服务架构组成以及注意事项&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;到底使用是 Dubbo 还是 Spring Cloud 并不重要，重点在于如何合理的利用微服务。下面是一张互联网通用的架构图，其中每个环节都是微服务的核心部分。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020321231549-internet-scene-sample.jpg" alt="2020321231549-internet-scene-sample" /&gt;&lt;/p&gt; &lt;h4&gt;架构分解&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;网关集群：数据的聚合、实现对接入客户端的身份认证、防报文重放与防数据篡改、功能调用的业务鉴权、响应数据的脱敏、流量与并发控制等。&lt;/li&gt; &lt;li&gt;业务集群：一般情况下移动端访问和浏览器访问的网关需要隔离，防止业务耦合。&lt;/li&gt; &lt;li&gt;Local Cache：由于客户端访问业务可能需要调用多个服务聚合，所以本地缓存有效的降低了服务调用的频次，同时也提示了访问速度。本地缓存一般使用自动过期方式，业务场景中允许有一定的数据延时。&lt;/li&gt; &lt;li&gt;服务层：原子服务层，实现基础的增删改查功能，如果需要依赖其他服务需要在 Service 层主动调用。&lt;/li&gt; &lt;li&gt;Remote Cache：访问 DB 前置一层分布式缓存，减少 DB 交互次数，提升系统的TPS。&lt;/li&gt; &lt;li&gt;DAL：数据访问层，如果单表数据量过大则需要通过 DAL 层做数据的分库分表处理。&lt;/li&gt; &lt;li&gt;MQ：消息队列用来解耦服务之间的依赖，异步调用可以通过 MQ 的方式来执行。&lt;/li&gt; &lt;li&gt;数据库主从：服务化过程中必经的阶段，用来提升系统的 TPS。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;注意事项：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;服务启动方式建议使用jar方式启动，启动速度快，更容易监控。&lt;/li&gt; &lt;li&gt;缓存、缓存、缓存，系统中能使用缓存的地方尽量使用缓存，通过合理的使用缓存可以有效的提高系统的TPS。&lt;/li&gt; &lt;li&gt;服务拆分要合理，尽量避免因服务拆分而导致的服务循环依赖。&lt;/li&gt; &lt;li&gt;合理的设置线程池，避免设置过大或者过小导致系统异常。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;Dubbo 出生于阿里系，是阿里巴巴服务化治理的核心框架，并被广泛应用于中国各互联网公司；只需要通过 Spring 配置的方式即可完成服务化，对于应用无入侵，设计的目的还是服务于自身的业务为主。&lt;/p&gt; &lt;p&gt;虽然阿里内部原因 Dubbo 曾经一度暂停维护版本，但是框架本身的成熟度以及文档的完善程度，完全能满足各大互联网公司的业务需求。&lt;/p&gt; &lt;p&gt;如果我们使用配置中心、分布式跟踪这些内容都需要自己去集成，这样无形中增加了使用 Dubbo 的难度。&lt;/p&gt; &lt;p&gt;Spring Cloud 是大名鼎鼎的 Spring 家族的产品， 专注于企业级开源框架的研发。&lt;/p&gt; &lt;p&gt;Spring Cloud 自从发布到现在，仍然在不断的高速发展，几乎考虑了服务治理的方方面面，开发起来非常的便利和简单。&lt;/p&gt; &lt;p&gt;Dubbo 于 2017 年开始又重启维护，发布了更新后的 2.5.7 版本，而 Spring Cloud 更新的非常快，目前已经更新到 Finchley.M2。&lt;/p&gt; &lt;p&gt;因此，企业需要根据自身的研发水平和所处阶段选择合适的架构来解决业务问题，不管是 Dubbo 还是 Spring Cloud 都是实现微服务有效的工具。&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 21 Mar 2020 15:24:00 GMT</pubDate>
    </item>
    <item>
      <title>JDK内置工具使用（jps、jstack、jmap、jstat）</title>
      <link>https://www.zhangaoo.com/article/jps-jstack-jmap-jstat</link>
      <content:encoded>&lt;h1&gt;JDK内置工具使用（jps、jstack、jmap、jstat）&lt;/h1&gt; &lt;h2&gt;jps&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;主要参数&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;-q 不输出类名、Jar名和传入main方法的参数 -m 输出传入main方法的参数 -l 输出main类或Jar的全限名 -v 输出传入JVM的参数 &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;查看简单信息&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;jps -lm 16163 com.zhangaoo.clockinout.Application 92805 sun.tools.jps.Jps -lm &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;查看详细信息&lt;/p&gt; &lt;p&gt;会把详细的JVM参数等信息都列出来&lt;/p&gt; &lt;pre&gt;&lt;code&gt; jps -lvm | grep clockinout 16163 com.zhangaoo.clockinout.Application -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:61210,suspend=y,server=n -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.profiles.active=dev -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=61209 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:/Users/zealzhangz/Library/Caches/IntelliJIdea2018.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8 &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;jstack&lt;/h2&gt; &lt;p&gt;&lt;code&gt;jstack&lt;/code&gt; 就是查看当前 &lt;code&gt;Java&lt;/code&gt;程序内线程详细堆栈信息的工具&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;h4&gt;主要参数&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;Options:     -F  to force a thread dump. Use when jstack &amp;lt;pid&amp;gt; does not respond (process is hung)(强制线程转储，此时线程会被挂起)     -m  to print both java and native frames (mixed mode)（打印Java和native接口的堆栈信息）     -l  long listing. Prints additional information about locks（打印关于锁的详细信息，如果有线程死锁可以使用jstack -l pid查看锁或资源的持有信息）     -h or -help to print this help message &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;利用 jstack 排除高CPU线程&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;首先利用上面的 jps -ml | grep tale 找到对应的进程如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;10815 tale-latest.jar --app.env=prod &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;然后使用 top 查看改进程下的所有线程情况&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ top -n 1 -H -p 10815 Tasks:  20 total,   1 running,  19 sleeping,   0 stopped,   0 zombie Cpu(s):  4.3%us,  1.1%sy,  0.0%ni, 94.6%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st Mem:   1019988k total,   781700k used,   238288k free,    83848k buffers Swap:        0k total,        0k used,        0k free,   283120k cached    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                 10838 zhanga    20   0 2407m 228m  13m R 97.8 23.0  22:57.07 java  10815 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.00 java # 同时用 jstack 记录堆栈信息 $ jstack 10815 &amp;gt; jstack-output.txt &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;观察到 10838 的线程几乎占用了所有CPU，将线程ID转化为16进制，因为jstack使用16进制表示&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ printf &amp;quot;%x\n&amp;quot; 10838 2a56 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后到刚刚记录的堆栈信息搜索 &lt;code&gt;2a56&lt;/code&gt;，发现如下堆栈信息：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;&amp;quot;worker@threadㄧ1&amp;quot; #18 prio=5 os_prio=0 tid=0x00007fd8d4009000 nid=0x2a56 runnable [0x00007fd8dd284000]    java.lang.Thread.State: RUNNABLE         at java.util.regex.Pattern$BnM.match(Pattern.java:5464)         at java.util.regex.Matcher.search(Matcher.java:1248)         at java.util.regex.Matcher.find(Matcher.java:637)         at java.util.regex.Matcher.replaceAll(Matcher.java:951)         at java.lang.String.replace(String.java:2240)         at com.vdurmont.emoji.EmojiParser.parseToUnicode(EmojiParser.java:129)         at com.tale.extension.Commons.emoji(Commons.java:240)         at com.tale.utils.TaleUtils.mdToHtml(TaleUtils.java:170)         at com.tale.extension.Theme.intro(Theme.java:299)         at com.tale.extension.Theme.intro(Theme.java:281)         at sun.reflect.GeneratedMethodAccessor108.invoke(Unknown Source)         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;分析如上调用堆栈，发现正则匹配相关逻辑，很可疑&lt;/li&gt; &lt;li&gt;定位到源码 &lt;code&gt;com.tale.utils.TaleUtils.mdToHtml&lt;/code&gt; 这个方法，从名字可知是 &lt;code&gt;Markdown&lt;/code&gt; 转 &lt;code&gt;HTML&lt;/code&gt; 的功能，通过测试发现 &lt;strong&gt;mdToHtml&lt;/strong&gt;方法执行需要花很长时间，至此就找到了出问题的源头，可以优化&lt;strong&gt;mdToHtml&lt;/strong&gt;方法，或者使用缓存来缓解这种情况&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;如果有其他问题，比如死锁等也可以使用此办法了排除，值得关注的线程状态有：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;死锁：Deadlock（重点关注）&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;执行中：Runnable&lt;/li&gt; &lt;li&gt;&lt;strong&gt;等待资源：Waiting on condition（重点关注）&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;&lt;strong&gt;等待获取监视器：Waiting on monitor entry（重点关注）&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;暂停：Suspended&lt;/li&gt; &lt;li&gt;对象等待中：Object.wait() 或 TIMED_WAITING&lt;/li&gt; &lt;li&gt;&lt;strong&gt;阻塞：Blocked（重点关注）&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;停止：Parked&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;&lt;strong&gt;jmap&lt;/strong&gt;&lt;/h2&gt; &lt;p&gt;&lt;code&gt;jmap&lt;/code&gt; 命令可以获得运行中的 &lt;code&gt;jvm&lt;/code&gt;的堆的快照，从而可以离线分析堆，以检查内存泄漏，检查一些严重影响性能的大对象的创建，检查系统中什么对象最多，各种对象所占内存的大小等等。&lt;/p&gt; &lt;h3&gt;打印heap的概要信息&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;jmap -heap PID &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020320174850-jmap-heap-summary.png" alt="2020320174850-jmap-heap-summary" /&gt;&lt;/p&gt; &lt;h3&gt;查看堆内存中的对象数目、大小&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ jmap -histo:live 19931 | less &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020320175018-heap-statistics.png" alt="2020320175018-heap-statistics" /&gt;&lt;/p&gt; &lt;h3&gt;内存dump文件&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ jhat  -port  9998  文件名.dump &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;该命令通常用来分析内存泄漏OOM，通常做法是：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 JVM 参数获取 dump 文件&lt;/li&gt; &lt;li&gt;进入Tomcat的'bin'目录，在'catalina.sh'文件里添加如下内容&lt;/li&gt; &lt;li&gt;&lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\jakarta-tomcat\webapps&lt;/code&gt;&lt;/li&gt; &lt;li&gt;然后使用MAT分析工具，如jhat命令，eclipse的mat插件。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;&lt;strong&gt;jstat&lt;/strong&gt;&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Jstat&lt;/code&gt;用于查看&lt;code&gt;gc&lt;/code&gt;垃圾回收使用情况：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;类的加载及卸载情况&lt;/li&gt; &lt;li&gt;查看新生代、老生代及持久代的垃圾收集情况，包括垃圾回收的次数及垃圾回收所占用的时间&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;➜  ~ jstat -help Usage: jstat -help|-options        jstat -&amp;lt;option&amp;gt; [-t] [-h&amp;lt;lines&amp;gt;] &amp;lt;vmid&amp;gt; [&amp;lt;interval&amp;gt; [&amp;lt;count&amp;gt;]] &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;option：我们经常使用的选项有gc、gcutil&lt;/li&gt; &lt;li&gt;vmid：java进程id&lt;/li&gt; &lt;li&gt;interval：间隔时间，单位为毫秒&lt;/li&gt; &lt;li&gt;count：打印次数&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Java7以及之前的Heap模型，java8以及之后已经没有Perm了，被Metaspace (元空间)取代：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;|&amp;lt;--Minor GC-&amp;gt;|     |&amp;lt;--------Major GC--------&amp;gt;| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||            |  |  |                          |                       || ||     Eden   |s0|s1|         Old Memory       |         Perm          || ||            |  |  |                          |                       || ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| |&amp;lt;--------------JVM Heap(-Xms -Xmx)-----------&amp;gt;|   -XX:PermSize |&amp;lt;-Young Gen(-Xmn)-&amp;gt;|                              -XX:MaxPermSize &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;堆内存 = 年轻代 + 年老代 + 永久代&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;年轻代 = Eden区 + 两个Survivor区（s0和s1）&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;类加载统计&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ jps -ml | grep clockinout 16163 com.zhangaoo.clockinout.Application $ jstat -class 16163        Loaded  Bytes  Unloaded  Bytes     Time     11413 21630.8        1     1.6       5.08 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;Loaded：加载 &lt;code&gt;class&lt;/code&gt; 的数量&lt;/li&gt; &lt;li&gt;Bytes：所占用空间大小&lt;/li&gt; &lt;li&gt;Unloaded：未加载数量&lt;/li&gt; &lt;li&gt;Bytes：未加载占用空间&lt;/li&gt; &lt;li&gt;Time：Time spent performing class load and unload operations&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;垃圾回收统计&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;jstat -gc 16163 1000 10 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT    8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54161.4   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54180.3   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54180.3   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762 8704.0 8704.0 4602.0  0.0   69952.0  54180.3   174784.0   29357.1   24320.0 22719.6 2816.0 2513.7    206    0.719   1      0.043    0.762  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;S0C：第一个幸存区的大小&lt;/li&gt; &lt;li&gt;S1C：第二个幸存区的大小&lt;/li&gt; &lt;li&gt;S0U：第一个幸存区的使用大小&lt;/li&gt; &lt;li&gt;S1U：第二个幸存区的使用大小&lt;/li&gt; &lt;li&gt;EC：伊甸园区的大小&lt;/li&gt; &lt;li&gt;EU：伊甸园区的使用大小&lt;/li&gt; &lt;li&gt;OC：老年代大小&lt;/li&gt; &lt;li&gt;OU：老年代使用大小&lt;/li&gt; &lt;li&gt;MC：方法区大小&lt;/li&gt; &lt;li&gt;MU：方法区使用大小&lt;/li&gt; &lt;li&gt;CCSC:压缩类空间大小&lt;/li&gt; &lt;li&gt;CCSU:压缩类空间使用大小&lt;/li&gt; &lt;li&gt;YGC：年轻代垃圾回收次数&lt;/li&gt; &lt;li&gt;YGCT：年轻代垃圾回收消耗时间&lt;/li&gt; &lt;li&gt;FGC：老年代垃圾回收次数&lt;/li&gt; &lt;li&gt;FGCT：老年代垃圾回收消耗时间&lt;/li&gt; &lt;li&gt;GCT：垃圾回收消耗总时间&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;堆内存统计&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ jstat -gccapacity 17265  NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC       MCMN     MCMX      MC     CCSMN    CCSMX     CCSC    YGC    FGC   87360.0  87360.0  87360.0 8704.0 8704.0  69952.0   174784.0   174784.0   174784.0   174784.0      0.0 1071104.0  24320.0      0.0 1048576.0   2816.0    206     1 &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 字段解释 NGCMN: Minimum new generation capacity (kB). NGCMX: Maximum new generation capacity (kB). NGC: Current new generation capacity (kB). S0C: Current survivor space 0 capacity (kB). S1C: Current survivor space 1 capacity (kB). EC: Current eden space capacity (kB). OGCMN: Minimum old generation capacity (kB). OGCMX: Maximum old generation capacity (kB). OGC: Current old generation capacity (kB). OC: Current old space capacity (kB). MCMN: Minimum metaspace capacity (kB). MCMX: Maximum metaspace capacity (kB). MC: Metaspace capacity (kB). CCSMN: Compressed class space minimum capacity (kB). CCSMX: Compressed class space maximum capacity (kB). CCSC: Compressed class space capacity (kB). YGC: Number of young generation GC events. FGC: Number of full GC events. &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;垃圾回收概要统计&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;jstat -gcutil 17265 1000 10  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT     52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762  52.87   0.00  80.00  16.80  93.42  89.26    206    0.719     1    0.043    0.762 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;S0：幸存1区当前使用比例&lt;/li&gt; &lt;li&gt;S1：幸存2区当前使用比例&lt;/li&gt; &lt;li&gt;E：伊甸园区使用比例&lt;/li&gt; &lt;li&gt;O：老年代使用比例&lt;/li&gt; &lt;li&gt;M：元数据区使用比例&lt;/li&gt; &lt;li&gt;CCS：压缩使用比例&lt;/li&gt; &lt;li&gt;YGC：年轻代垃圾回收次数&lt;/li&gt; &lt;li&gt;FGC：老年代垃圾回收次数&lt;/li&gt; &lt;li&gt;FGCT：老年代垃圾回收消耗时间&lt;/li&gt; &lt;li&gt;GCT：垃圾回收消耗总时间&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Fri, 20 Mar 2020 10:57:00 GMT</pubDate>
    </item>
    <item>
      <title>JVM 内存模型</title>
      <link>https://www.zhangaoo.com/article/jvm-model</link>
      <content:encoded>&lt;h1&gt;JVM 内存模型&lt;/h1&gt; &lt;p&gt;内存是非常重要的系统资源，是硬盘和 &lt;code&gt;CPU&lt;/code&gt; 的中间仓库及桥梁，承载着操作系统和应用程序的实时运行。 &lt;code&gt;JVM&lt;/code&gt;内存布局规定了&lt;code&gt;Java&lt;/code&gt; 在运行过程中内存申请、分配、管理的策略，保证了&lt;code&gt;JVM&lt;/code&gt; 的高效稳定运行，不同的 &lt;code&gt;JVM&lt;/code&gt;对于内存的划分方式和管理机制存在着部分差异，结合&lt;code&gt;JVM&lt;/code&gt;虚拟机规范，来探讨经典的&lt;code&gt;JVM&lt;/code&gt;内存布局。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/202031916852-jvm-model.png" alt="202031916852-jvm-model" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020319161221-jvm-model-2.png" alt="2020319161221-jvm-model-2" /&gt;&lt;/p&gt; &lt;h2&gt;Program Counter Register (程序计数寄存器)&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Register&lt;/code&gt; 的命名源于 &lt;code&gt;CPU&lt;/code&gt; 的寄存器，&lt;code&gt;CPU&lt;/code&gt;只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息，由于 &lt;code&gt;CPU&lt;/code&gt;时间片轮限制，众多线程在并发执行过程中，任何一个确定的时刻，一个处理器或者多核处理器中的一个内核，只会执行某个线程中的一条指令。这样必然导致经常中断或恢复，如何保证分毫无差呢?&lt;/p&gt; &lt;p&gt;每个线程在创建后，都会产生自己的程序计数器和栈帧，&lt;strong&gt;程序计数器用来存放执行指令的偏移量和行号指示器等&lt;/strong&gt;，线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响，此区域也不会发生内存溢出异常。&lt;/p&gt; &lt;h3&gt;定义&lt;/h3&gt; &lt;p&gt;程序计数器是一块较小的内存空间，可看作当前线程正在执行的字节码的行号指示器，如果当前线程正在执行的是：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Java方法：计数器记录的就是当前线程正在执行的字节码指令的地址&lt;/li&gt; &lt;li&gt;本地方法：那么程序计数器值为undefined&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;作用&lt;/h3&gt; &lt;p&gt;程序计数器有两个作用&lt;/p&gt; &lt;ul&gt; &lt;li&gt;字节码解释器通过改变程序计数器来依次读取指令，从而实现代码的流程控制，如：顺序执行、选择、循环、异常处理。&lt;/li&gt; &lt;li&gt;在多线程的情况下，程序计数器用于记录当前线程执行的位置，从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;特点&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;一块较小的内存空间&lt;/li&gt; &lt;li&gt;线程私有，每个线程都有一个独立的程序计数器。&lt;/li&gt; &lt;li&gt;是唯一一个不会出现 &lt;code&gt;OOM&lt;/code&gt;的内存区域。&lt;/li&gt; &lt;li&gt;生命周期随着线程的创建而创建，随着线程的结束而死亡。&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;Java虚拟机栈(JVM Stack)&lt;/h2&gt; &lt;h3&gt;定义&lt;/h3&gt; &lt;p&gt;相对于基于寄存器的运行环境来说，&lt;code&gt;JVM&lt;/code&gt; 是基于栈结构的运行环境，栈结构移植性更好，可控性更强，JVM中的虚拟机栈是描述 &lt;code&gt;Java&lt;/code&gt;方法执行的内存区域，它是线程私有的。&lt;/p&gt; &lt;p&gt;栈中的元素用于支持虚拟机进行方法调用，每个方法从开始调用到执行完成的过程，就是栈帧从入栈到出栈的过程。&lt;/p&gt; &lt;p&gt;在活动线程中，只有位于栈顶的帧才是有效的，称为当前栈帧，正在执行的方法称为当前方法，栈帧是方法运行的基本结构。&lt;/p&gt; &lt;p&gt;在执行引擎运行时，所有指令都只能针对当前栈帧进行操作，&lt;code&gt;StackOverflowError&lt;/code&gt; 表示请求的栈溢出，导致内存耗尽，通常出现在递归方法中。&lt;/p&gt; &lt;p&gt;&lt;code&gt;JVM&lt;/code&gt;能够横扫千军，虚拟机栈就是它的心腹大将，当前方法的栈帧，都是正在战斗的战场，其中的操作栈是参与战斗的士兵。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020319162524-stack.png" alt="2020319162524-stack" style="zoom:80%;" /&gt; &lt;p&gt;虚拟机栈通过压/出栈的方式，对每个方法对应的活动栈帧进行运算处理，方法正常执行结束，肯定会跳转到另一个栈帧上。&lt;/p&gt; &lt;p&gt;在执行的过程中，如果出现异常，会进行异常回溯，返回地址通过异常处理表确定，栈帧在整个JVM体系中的地位颇高,包括&lt;strong&gt;局部变量表、操作栈、动态连接、方法返回地址&lt;/strong&gt;等&lt;/p&gt; &lt;ul&gt; &lt;li&gt;局部变量表 &lt;ul&gt; &lt;li&gt;存放方法参数和局部变量&lt;/li&gt; &lt;li&gt;相对于类属性变量的准备阶段和初始化阶段来说，局部变量没有准备阶段，必须显式初始化。如果是非静态方法，则在 &lt;code&gt;index[0]&lt;/code&gt; 位置上存储的是方法所属对象的实例引用，随后存储的是参数和局部变量&lt;/li&gt; &lt;li&gt;字节码指令中的 &lt;code&gt;STORE&lt;/code&gt;指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;操作栈 &lt;ul&gt; &lt;li&gt;操作栈是一个初始状态为空的桶式结构栈&lt;/li&gt; &lt;li&gt;在方法执行过程中，会有各种指令往栈中写入和提取信息&lt;/li&gt; &lt;li&gt;&lt;code&gt;JVM&lt;/code&gt; 的执行引擎是基于栈的执行引擎，其中的栈指的就是操作栈&lt;/li&gt; &lt;li&gt;字节码指令集的定义都是基于栈类型的，栈的深度在方法元信息的&lt;code&gt;stack&lt;/code&gt;属性中&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;下面用一段简单的代码说明操作栈与局部变量表的交互&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;  public int simpleMethod(){     int x = 13;     int y = 14;     int z = x + y;     return z;   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;对应的字节码操作顺序如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020319215838-stack-bytecode.png" alt="2020319215838-stack-bytecode" style="zoom:67%;" /&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;第 &lt;code&gt;1&lt;/code&gt; 处说明:局部变量表就像个中药柜，里面有很多抽屉,依次编号为0, 1, 2,3,.,. n&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;字节码指令istore_ 1就是打开1号抽屉，把栈顶中的数13存进去&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;栈是一个很深的竖桶，任何时候只能对桶口元素进行操作，所以数据只能在栈顶进行存取&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;某些指令可以直接在抽屉里进行，比如inc指令，直接对抽屉里的数值进行+1操作&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;程序员面试过程中，常见的&lt;code&gt;i++&lt;/code&gt; 和 &lt;code&gt;++i&lt;/code&gt; 的区别，可以从字节码上对比出来&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20203192247-i-puls-plus-i.png" alt="20203192247-i-puls-plus-i" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;iload_ 1&lt;/code&gt;从局部变量表的第&lt;code&gt;1&lt;/code&gt;号抽屉里取出一个数，压入栈顶，下一步直接在抽屉里实现&lt;code&gt;+1&lt;/code&gt;的操作，而这个操作对栈顶元素的值没有影响，所以 &lt;code&gt;istore_ 2&lt;/code&gt; 只是把栈顶元素赋值给&lt;code&gt;a&lt;/code&gt;&lt;/li&gt; &lt;li&gt;表格右列，先在第&lt;code&gt;1&lt;/code&gt;号抽屉里执行&lt;code&gt;+1&lt;/code&gt;操作，然后通过&lt;code&gt;iload_ 1&lt;/code&gt; 把第&lt;code&gt;1&lt;/code&gt;号抽屉里的数压入栈顶，所以&lt;code&gt;istore_ 2&lt;/code&gt;存入的是&lt;code&gt;+1&lt;/code&gt;之后的值&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这里延伸一个信息，&lt;strong&gt;i++并非原子操作&lt;/strong&gt;。即使通过 &lt;strong&gt;volatile&lt;/strong&gt;关键字进行修饰，多个线程同时写的话，也会产生数据互相覆盖的问题.&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;动态连接&lt;/p&gt; &lt;p&gt;每个栈帧中包含一个在常量池中对当前方法的引用，目的是支持方法调用过程的动态连接&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;方法返回地址&lt;/p&gt; &lt;p&gt;方法执行时有两种退出情况&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;正常退出：正常执行到任何方法的返回字节码指令，如RETURN、IRETURN、ARETURN等&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;异常退出&lt;/p&gt; &lt;p&gt;无论何种退出情况，都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;退出可能有三种方式：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;返回值压入，上层调用栈帧&lt;/li&gt; &lt;li&gt;异常信息抛给能够处理的栈帧&lt;/li&gt; &lt;li&gt;&lt;code&gt;PC&lt;/code&gt; 计数器指向方法调用后的下一条指令&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Java 虚拟机栈是描述 Java 方法运行过程的内存模型，Java虚拟机栈会为每一个即将运行的Java方法创建“栈帧”，用于存储该方法在运行过程中所需要的一些信息&lt;/p&gt; &lt;ul&gt; &lt;li&gt;局部变量表：存放基本数据类型变量、引用类型的变量、returnAddress类型的变量&lt;/li&gt; &lt;li&gt;操作数栈&lt;/li&gt; &lt;li&gt;动态链接&lt;/li&gt; &lt;li&gt;当前方法的常量池指针&lt;/li&gt; &lt;li&gt;当前方法的返回地址&lt;/li&gt; &lt;li&gt;方法出口等信息&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;每一个方法从被调用到执行完成的过程，都对应着一个个栈帧在&lt;code&gt;JVM&lt;/code&gt; 栈中的入栈和出栈过程&lt;/p&gt; &lt;p&gt;&lt;strong&gt;注意：人们常说，Java的内存空间分为“栈”和“堆”，栈中存放局部变量，堆中存放对象。 这句话不完全正确！这里的“堆”可以这么理解，但这里的“栈”就是现在讲的虚拟机栈，或者说 Java 虚拟机栈中的局部变量表部分。真正的Java虚拟机栈是由一个个栈帧组成，而每个栈帧中都拥有：局部变量表、操作数栈、动态链接、方法出口信息。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;特点&lt;/h3&gt; &lt;p&gt;局部变量表的创建是在方法被执行的时候，随着栈帧的创建而创建，而且表的大小在编译期就确定，在创建的时候只需分配事先规定好的大小即可，在方法运行过程中，表的大小不会改变&lt;/p&gt; &lt;p&gt;Java虚拟机栈会出现两种异常：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;StackOverFlowError&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;若 &lt;code&gt;Java&lt;/code&gt;虚拟机栈的内存大小不允许动态扩展,那么当线程请求的栈深度大于虚拟机允许的最大深度时(但内存空间可能还有很多)，就抛出此异常&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;OutOfMemoryError&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;若Java虚拟机栈的内存大小允许动态扩展，且当线程请求栈时内存用完了，无法再动态扩展了，此时抛出&lt;code&gt;OutOfMemoryError&lt;/code&gt;异常&lt;/p&gt; &lt;p&gt;Java虚拟机栈也是线程私有的，每个线程都有各自的 &lt;code&gt;Java&lt;/code&gt; 虚拟机栈，而且随着线程的创建而创建，随着线程的死亡而死亡。&lt;/p&gt; &lt;h2&gt;本地方法栈(Native Method Stack)&lt;/h2&gt; &lt;p&gt;本地方法栈和 &lt;code&gt;Java&lt;/code&gt; 虚拟机栈实现的功能与抛出异常几乎相同，只不过虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务，本地方法区则为虚拟机使用到的 &lt;code&gt;Native&lt;/code&gt; 方法服务。&lt;/p&gt; &lt;p&gt;在 JVM 内存布局中，也是线程对象私有的，但是虚拟机栈“主内”，而本地方法栈“主外”，这个“内外”是针对JVM来说的，本地方法栈为Native方法服务。&lt;/p&gt; &lt;p&gt;线程开始调用本地方法时，会进入一个不再受JVM约束的世界，本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区，甚至可以调用寄存器，具有和&lt;code&gt;JVM&lt;/code&gt;相同的能力和权限&lt;/p&gt; &lt;p&gt;当大量本地方法出现时，势必会削弱&lt;code&gt;JVM&lt;/code&gt;对系统的控制力，因为它的出错信息都比较黑盒。对于内存不足的情况，本地方法栈还是会拋出native heap OutOfMemory。&lt;/p&gt; &lt;p&gt;最著名的本地方法应该是 &lt;code&gt;System.currentTimeMillis()&lt;/code&gt;，&lt;code&gt;JNI&lt;/code&gt; 使&lt;code&gt;Java&lt;/code&gt;深度使用&lt;code&gt;OS&lt;/code&gt;的特性功能，复用非&lt;code&gt;Java&lt;/code&gt;代码。&lt;/p&gt; &lt;p&gt;但是在项目过程中，如果大量使用其他语言来实现 &lt;code&gt;JNI&lt;/code&gt;,就会丧失跨平台特性，威胁到程序运行的稳定性 假如需要与本地代码交互，就可以用中间标准框架进行解耦，这样即使本地方法崩溃也不至于影响到&lt;code&gt;JVM&lt;/code&gt;的稳定&lt;/p&gt; &lt;p&gt;当然，如果要求极高的执行效率、偏底层的跨进程操作等，可以考虑设计为&lt;code&gt;JNI&lt;/code&gt;调用方式&lt;/p&gt; &lt;h2&gt;Java堆(Java Heap)&lt;/h2&gt; &lt;p&gt;Heap 是 OOM 故障最主要的发源地，它存储着几乎所有的实例对象，堆由垃圾收集器自动回收，堆区由各子线程共享使用。&lt;/p&gt; &lt;p&gt;通常情况下，它占用的空间是所有内存区域中最大的，但如果无节制地创建大量对象，也容易消耗完所有的空间。&lt;/p&gt; &lt;p&gt;堆的内存空间既可以固定大小，也可运行时动态地调整，通过如下参数设定初始值和最大值，比如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;-Xms256M. -Xmx1024M &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;其中-X表示它是JVM运行参数&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;ms&lt;/code&gt;是&lt;code&gt;memorystart&lt;/code&gt;的简称 最小堆容量&lt;/li&gt; &lt;li&gt;&lt;code&gt;mx&lt;/code&gt;是&lt;code&gt;memory max&lt;/code&gt;的简称 最大堆容量&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;但是在通常情况下，服务器在运行过程中，堆空间不断地&lt;strong&gt;扩容与收缩&lt;/strong&gt;，势必形成不必要的系统压力，所以在线上生产环境中，&lt;code&gt;JVM&lt;/code&gt;的&lt;code&gt;Xms&lt;/code&gt;和&lt;code&gt;Xmx&lt;/code&gt;设置成一样大小，避免在&lt;code&gt;GC&lt;/code&gt;后调整堆大小时带来的额外压力&lt;/p&gt; &lt;p&gt;堆分成两大块：新生代和老年代，需要注意的是：&lt;strong&gt;Perm Gen 不是 Heap 的一部分&lt;/strong&gt;(java8以上已经没有Perm，元空间代替，元空间在本地内存)，如下图：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;|&amp;lt;--Minor GC-&amp;gt;|     |&amp;lt;--------Major GC--------&amp;gt;| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||            |  |  |                          |                       || ||     Eden   |s0|s1|         Old Memory       |         Perm          || ||            |  |  |                          |                       || ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| |&amp;lt;--------------JVM Heap(-Xms -Xmx)-----------&amp;gt;|   -XX:PermSize |&amp;lt;-Young Gen(-Xmn)-&amp;gt;|                              -XX:MaxPermSize &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;对象产生之初在新生代，步入暮年时进入老年代，但是老年代也接纳在新生代无法容纳的超大对象&lt;/p&gt; &lt;pre&gt;&lt;code&gt;新生代= 1个Eden区+ 2个Survivor区 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;绝大部分对象在 &lt;code&gt;Eden&lt;/code&gt; 区生成，当&lt;code&gt;Eden&lt;/code&gt;区装填满的时候，会触发 &lt;code&gt;Young GC&lt;/code&gt;。垃圾回收的时候，在&lt;code&gt;Eden&lt;/code&gt;区实现清除策略，没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区，这个区真是名副其实的存在。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Survivor&lt;/code&gt; 区分为&lt;code&gt;S0&lt;/code&gt; 和 &lt;code&gt;S1&lt;/code&gt; 两块内存空间，送到哪块空间呢?每次 &lt;code&gt;Young GC&lt;/code&gt;的时候，将存活的对象复制到未使用的那块空间，然后将当前正在使用的空间完全清除，交换两块空间的使用状态。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;有没有思考过，为什么需要Survivor区？并且是2个Survivor区，s0，s1呢？&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Survivor的存在意义，就是减少被送到老年代的对象，进而减少Full GC的发生，Survivor的预筛选保证，只有经历16次Minor GC还能在新生代中存活的对象，才会被送到老年代。&lt;/li&gt; &lt;li&gt;一个Survivor区会导致内存使用的碎片化问题，碎片化带来的风险是极大的，严重影响 Java 程序的性能。堆空间被散布的对象占据不连续的内存，最直接的结果就是，堆中没有足够大的连续内存空间，接下去如果程序需要给一个内存需求很大的对象分配内存。&lt;/li&gt; &lt;li&gt;那么，顺理成章的，应该建立两块Survivor区，刚刚新建的对象在Eden中，经历一次Minor GC，Eden中的存活对象就会被移动到第一块survivor space S0，Eden被清空；等Eden区再满了，就再触发一次Minor GC，Eden和S0中的存活对象又会被复制送入第二块survivor space S1（这个过程非常重要，因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间，避免了碎片化的发生**）。S0和Eden被清空，然后下一**轮S0与S1交换角色，如此循环往复。如果对象的复制次数达到16次，该对象就会被送到老年代中。&lt;/li&gt; &lt;li&gt;上述机制最大的好处就是，整个过程中，&lt;strong&gt;永远有一个survivor space是空的，另一个非空的survivor space无碎片&lt;/strong&gt;。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;如果 &lt;code&gt;YGC&lt;/code&gt; 要移送的对象大于&lt;code&gt;Survivor&lt;/code&gt;区容量上限，则直接移交给老年代，假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去，那就错了。每个对象都有一个计数器，每次YGC都会加1。&lt;/p&gt; &lt;p&gt;&lt;code&gt;-XX:MaxTenuringThreshold&lt;/code&gt;参数能配置计数器的值到达某个阈值的时候，对象从新生代晋升至老年代。如果该参数配置为&lt;code&gt;1&lt;/code&gt;,那么从新生代的&lt;code&gt;Eden&lt;/code&gt;区直接移至老年代。默认值是&lt;code&gt;15&lt;/code&gt;，可以在&lt;code&gt;Survivor&lt;/code&gt; 区交换14次之后，晋升至老年代&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020319232859-gc-process.png" alt="2020319232859-gc-process" /&gt;&lt;/p&gt; &lt;p&gt;若 &lt;code&gt;Survivor&lt;/code&gt;区无法放下，或者超大对象的阈值超过上限，则尝试在老年代中进行分配;&lt;/p&gt; &lt;p&gt;如果老年代也无法放下，则会触发Full Garbage Collection(Full GC);&lt;/p&gt; &lt;p&gt;如果依然无法放下，则抛OOM。堆出现OOM的概率是所有内存耗尽异常中最高的，出错时的堆内信息对解决问题非常有帮助，所以给JVM设置运行参数 &lt;code&gt;XX:+HeapDumpOnOutOfMemoryError&lt;/code&gt; 让JVM遇到OOM异常时能输出堆内信息。&lt;/p&gt; &lt;p&gt;在不同的 JVM 实现及不同的回收机制中，堆内存的划分方式是不一样的&lt;/p&gt; &lt;p&gt;除了实例数据，还保存了对象的其他信息，如Mark Word（存储对象哈希码，GC标志，GC年龄，同步锁等信息），Klass Pointy(指向存储类型元数据的指针）及一些字节对齐补白的填充数据（若实例数据刚好满足8字节对齐，则可不存在补白）&lt;/p&gt; &lt;h3&gt;特点&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Heap 是 Java 虚拟机所需要管理的内存中最大的一块。&lt;/li&gt; &lt;li&gt;堆内存物理上不一定要连续，只需要逻辑上连续即可，就像磁盘空间一样。&lt;/li&gt; &lt;li&gt;堆是垃圾回收的主要区域，所以也被称为GC堆。&lt;/li&gt; &lt;li&gt;堆的大小既可以固定也可以扩展，但主流的虚拟机堆的大小是可扩展的(通过-Xmx和-Xms控制),因此当线程请求分配内存，但堆已满,且内存已满无法再扩展时，就抛出OutOfMemoryError。&lt;/li&gt; &lt;li&gt;线程共享 &lt;ul&gt; &lt;li&gt;整个Java虚拟机只有一个堆，所有的线程都访问同一个堆。&lt;/li&gt; &lt;li&gt;它是被所有线程共享的一块内存区域，在虚拟机启动时创建。&lt;/li&gt; &lt;li&gt;而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;方法区&lt;/h2&gt; &lt;h3&gt;定义&lt;/h3&gt; &lt;p&gt;Java虚拟机规范中定义方法区是堆的一个逻辑部分，但是别名Non-Heap(非堆)，以与Java堆区分。方法区中存放已经被虚拟机加载的&lt;strong&gt;类信息、常量、静态变量、即时编译器编译后的代码等数据&lt;/strong&gt;。&lt;/p&gt; &lt;h3&gt;特点&lt;/h3&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;线程共享&lt;/p&gt; &lt;p&gt;方法区是堆的一个逻辑部分，因此和堆一样，都是线程共享的。整个虚拟机中只有一个方法区。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;永久代&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;方法区中的信息一般需要长期存在，而且它又是堆的逻辑分区，因此用堆的划分方法，我们把方法区称为永久代。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;内存回收效率低&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Java 虚拟机规范对方法区的要求比较宽松，可以不实现垃圾收集。&lt;/li&gt; &lt;li&gt;方法区中的信息一般需要长期存在，回收一遍内存之后可能只有少量信息无效。&lt;/li&gt; &lt;li&gt;对方法区的内存回收的主要目标是:&lt;strong&gt;对常量池的回收和对类型的卸载&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;和堆一样，允许固定大小，也允许可扩展的大小，还允许不实现垃圾回收。当方法区内存空间无法满足内存分配需求时，将抛出&lt;code&gt;OutOfMemoryError&lt;/code&gt;异常。&lt;/p&gt; &lt;h3&gt;运行时常量池(Runtime Constant Pool)&lt;/h3&gt; &lt;h4&gt;定义&lt;/h4&gt; &lt;p&gt;运行时常量池是方法区的一部分，方法区中存放三种数据：&lt;strong&gt;类信息、常量、静态变量、即时编译器编译后的代码&lt;/strong&gt;，其中常量存储在运行时常量池中。&lt;/p&gt; &lt;p&gt;我们知道，&lt;code&gt;.java&lt;/code&gt; 文件被编译之后生成的&lt;code&gt;.class&lt;/code&gt;文件中除了包含：类的版本、字段、方法、接口等信息外，还有一项就是常量池。&lt;/p&gt; &lt;p&gt;常量池中存放编译时期产生的各种字面量和符号引用，&lt;code&gt;.class&lt;/code&gt;文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。例如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//age是一个变量，可以被赋值；21就是一个字面值常量，不能被赋值； int age = 21; //pai就是一个符号常量，一旦被赋值之后就不能被修改。 int final pai = 3.14; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Class文件中除了有类的版本、字段、方法、接口等描述信息外，还有一项信息是常量池( Constant pool table)，用于存放编译期生成的各种字面量和符号引用，这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性，java语言并不要求常量一定只有编译器才产生，也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池，运行期间也可能将新的常量放入池中。&lt;/p&gt; &lt;p&gt;在近三个&lt;code&gt;JDK&lt;/code&gt;版本（6、7、8）中， 运行时常量池的所处区域一直在不断的变化：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在JDK6时它是方法区的一部分&lt;/li&gt; &lt;li&gt;7又把他放到了堆内存中&lt;/li&gt; &lt;li&gt;8之后出现了元空间，它又回到了方法区。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;其实，这也说明了官方对“永久代”的优化从7就已经开始了&lt;/p&gt; &lt;h4&gt;特性&lt;/h4&gt; &lt;p&gt;class文件中的常量池具有动态性，Java并不要求常量只能在编译时候产生，Java允许在运行期间将新的常量放入方法区的运行时常量池中。&lt;code&gt;String&lt;/code&gt;类中的&lt;code&gt;intern()&lt;/code&gt;方法就是采用了运行时常量池的动态性。当调用 &lt;code&gt;intern&lt;/code&gt; 方法时，如果池已经包含一个等于此 &lt;code&gt;String&lt;/code&gt; 对象的字符串，则返回池中的字符串。否则，将此 String 对象添加到池中，并返回此 String 对象的引用。&lt;/p&gt; &lt;h4&gt;可能抛出的异常&lt;/h4&gt; &lt;p&gt;运行时常量池是方法区的一部分，所以会受到方法区内存的限制，因此当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。&lt;/p&gt; &lt;p&gt;我们一般在一个类中通过 public static final 来声明一个常量。这个类被编译后便生成Class文件，这个类的所有信息都存储在这个class文件中。&lt;/p&gt; &lt;p&gt;当这个类被Java虚拟机加载后，class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间，可以向常量池中添加新的常量。如：String类的intern()方法就能在运行期间向常量池中添加字符串常量。&lt;/p&gt; &lt;p&gt;当运行时常量池中的某些常量没有被对象引用，同时也没有被变量引用，那么就需要垃圾收集器回收。&lt;/p&gt; &lt;h2&gt;直接内存(Direct Memory)&lt;/h2&gt; &lt;p&gt;直接内存不是虚拟机运行时数据区的一部分，也不是JVM规范中定义的内存区域，但在JVM的实际运行过程中会频繁地使用这块区域。而且也会抛OOM。&lt;/p&gt; &lt;p&gt;在JDK 1.4中加入了NIO(New Input／Output)类,引入了一种基于管道和缓冲区的&lt;code&gt;IO&lt;/code&gt;方式,它可以使用&lt;code&gt;Native&lt;/code&gt;函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据。&lt;/p&gt; &lt;p&gt;这样能在一些场景中显著提升性能，因为避免了在 &lt;code&gt;Java&lt;/code&gt;堆和&lt;code&gt;Native&lt;/code&gt;堆中来回复制数据。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;综上看来：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;程序计数器、Java虚拟机栈、本地方法栈是线程私有的，即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。&lt;/li&gt; &lt;li&gt;而堆、方法区是线程共享的，在Java虚拟机中只有一个堆、一个方法栈。并在&lt;code&gt;JVM&lt;/code&gt;启动的时候就创建，&lt;code&gt;JVM&lt;/code&gt;停止才销毁。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Metaspace (元空间)&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;JDK8&lt;/code&gt;，元空间的前身&lt;code&gt;Perm&lt;/code&gt;区已经被淘汰，在&lt;code&gt;JDK7&lt;/code&gt;及之前的版本中，只有&lt;code&gt;Hotspot&lt;/code&gt;才有&lt;code&gt;Perm&lt;/code&gt;区(永久代)，它在启动时固定大小，很难进行调优，并且&lt;code&gt;Full GC&lt;/code&gt;时会移动类元信息&lt;/p&gt; &lt;p&gt;在某些场景下，如果动态加载类过多，容易产生&lt;code&gt;Perm&lt;/code&gt;区的&lt;code&gt;OOM&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;比如某个实际 &lt;code&gt;Web&lt;/code&gt;工程中，因为功能点比较多，在运行过程中，要不断动态加载很多的类，经常出现致命错误:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;为解决该问题，需要设定运行参数:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;-XX:MaxPermSize=l280m &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果部署到新机器上，往往会因为 &lt;code&gt;JVM&lt;/code&gt;参数没有修改导致故障再现。不熟悉此应用的人排查问题时往往苦不堪言，除此之外，永久代在&lt;code&gt;GC&lt;/code&gt;过程中还存在诸多问题。&lt;/p&gt; &lt;p&gt;所以，&lt;code&gt;JDK8&lt;/code&gt;使用&lt;strong&gt;元空间替换永久代&lt;/strong&gt;，区别于永久代，&lt;strong&gt;元空间在本地内存中分配&lt;/strong&gt;，也就是说，只要本地内存足够，它不会出现像永久代中 &lt;code&gt;java.lang.OutOfMemoryError: PermGen space&lt;/code&gt;。同样的，对永久代的设置参数&lt;code&gt;PermSize&lt;/code&gt;和&lt;code&gt;MaxPermSize&lt;/code&gt;也会失效，在 &lt;code&gt;JDK8&lt;/code&gt;及以上版本中，设定&lt;code&gt;MaxPermSize&lt;/code&gt;参数，&lt;code&gt;JVM&lt;/code&gt;在启动时并不会报错，但是会提示:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;默认情况下，“元空间”的大小可以动态调整，或者使用新参数 &lt;code&gt;MaxMetaspaceSize&lt;/code&gt;来限制本地内存分配给类元数据的大小。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;JDK8&lt;/code&gt;里，&lt;code&gt;Perm&lt;/code&gt; 区所有内容中&lt;/p&gt; &lt;ul&gt; &lt;li&gt;字符串常量移至堆内存&lt;/li&gt; &lt;li&gt;其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020320141113-constant-pool.png" alt="2020320141113-constant-pool" /&gt;&lt;/p&gt; &lt;p&gt;比如上图中的&lt;code&gt;Object&lt;/code&gt;类元信息、静态属性&lt;code&gt;System.out&lt;/code&gt;、整型常量&lt;code&gt;000000&lt;/code&gt;等，图中显示在常量池中的&lt;code&gt;String&lt;/code&gt;，其实际对象是被保存在堆内存中的。&lt;/p&gt; &lt;h3&gt;元空间特色&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;充分利用了Java语言规范：类及相关的元数据的生命周期与类加载器的一致&lt;/li&gt; &lt;li&gt;每个类加载器都有它的内存区域-元空间&lt;/li&gt; &lt;li&gt;只进行线性分配&lt;/li&gt; &lt;li&gt;不会单独回收某个类（除了重定义类 RedefineClasses 或类加载失败）&lt;/li&gt; &lt;li&gt;没有GC扫描或压缩&lt;/li&gt; &lt;li&gt;元空间里的对象不会被转移&lt;/li&gt; &lt;li&gt;如果GC发现某个类加载器不再存活，会对整个元空间进行集体回收&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;GC&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Full GC&lt;/code&gt;时，指向元数据指针都不用再扫描，减少了 &lt;code&gt;Full GC&lt;/code&gt;的时间&lt;/li&gt; &lt;li&gt;很多复杂的元数据扫描的代码（尤其是CMS里面的那些）都删除了&lt;/li&gt; &lt;li&gt;元空间只有少量的指针指向&lt;code&gt;Java&lt;/code&gt;堆，这包括：类的元数据中指向&lt;code&gt;java.lang.Class&lt;/code&gt;实例的指针数组类的元数据中，指向&lt;code&gt;java.lang.Class&lt;/code&gt;集合的指针。&lt;/li&gt; &lt;li&gt;没有元数据压缩的开销&lt;/li&gt; &lt;li&gt;减少了&lt;code&gt;GC Root&lt;/code&gt;的扫描（不再扫描虚拟机里面的已加载类的目录和其它的内部哈希表）&lt;/li&gt; &lt;li&gt;&lt;code&gt;G1&lt;/code&gt;回收器中，并发标记阶段完成后就可以进行类的卸载&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;元空间内存分配模型&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;绝大多数的类元数据的空间都在本地内存中分配&lt;/li&gt; &lt;li&gt;用来描述类元数据的对象也被移除&lt;/li&gt; &lt;li&gt;为元数据分配了多个映射的虚拟内存空间&lt;/li&gt; &lt;li&gt;为每个类加载器分配一个内存块列表 &lt;ul&gt; &lt;li&gt;块的大小取决于类加载器的类型&lt;/li&gt; &lt;li&gt;Java反射的字节码存取器（sun.reflect.DelegatingClassLoader ）占用内存更小&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;空闲块内存返还给块内存列表&lt;/li&gt; &lt;li&gt;当元空间为空，虚拟内存空间会被回收&lt;/li&gt; &lt;li&gt;减少了内存碎片&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;最后从线程共享的角度来看：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;堆和元空间是所有线程共享的&lt;/li&gt; &lt;li&gt;虚拟机栈、本地方法栈、程序计数器是线程内部私有的&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;从这个角度看一下Java内存结构：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020320141816-memory-model.png" alt="2020320141816-memory-model" /&gt;&lt;/p&gt; &lt;h2&gt;从GC角度看Java堆&lt;/h2&gt; &lt;p&gt;堆和方法区都是线程共享的区域，主要用来存放对象的相关信息。我们知道，一个接口中的多个实现类需要的内存可能不一样，一个方法中的多个分支需要的内存也可能不一样，我们只有在程序运行期间才能知道会创建哪些对象，因此， 这部分的内存和回收都是动态的，垃圾收集器所关注的就是这部分内存（本节后续所说的“内存”分配与回收也仅指这部分内存）而在JDK1.7和1.8对这部分内存的分配也有所不同，下面我们来详细看一下：&lt;/p&gt; &lt;p&gt;&lt;code&gt;Java8&lt;/code&gt; 中堆内存分配如下图：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020320142055-java8-memory.png" alt="2020320142055-java8-memory" /&gt;&lt;/p&gt; &lt;h3&gt;配置打印GC日志&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;nohup java -Xms400m -Xmx400m -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Dfile.encoding=UTF-8 -jar tale-latest.jar --app.env=prod &amp;gt; tale.log 2&amp;gt;&amp;amp;1 &amp;amp; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;分析GC日志&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;2019-05-13T10:17:10.902+0800: 37622.895: [GC (Allocation Failure) 2019-05-13T10:17:10.902+0800: 37622.895: [DefNew: 109585K-&amp;gt;282K(122880K), 0.0020507 secs] 117507K-&amp;gt;8205K(395968K), 0.0021385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2019-05-13T10:21:43.112+0800: 37895.104: [GC (Allocation Failure) 2019-05-13T10:21:43.112+0800: 37895.104: [DefNew: 109530K-&amp;gt;268K(122880K), 0.0018163 secs] 117453K-&amp;gt;8191K(395968K), 0.0018836 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;首先每次请求都会有一两条如上的 &lt;code&gt;GC&lt;/code&gt; 日志，&lt;code&gt;Allocation Failure&lt;/code&gt; 表示向 &lt;code&gt;Young generation(eden)&lt;/code&gt;给新对象申请空间。简单来讲不是 &lt;code&gt;Full GC&lt;/code&gt; 并且 &lt;code&gt;GC&lt;/code&gt; 的时间很快，对系统性能影响有限。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;下面对整个第一条 &lt;code&gt;GC&lt;/code&gt; 日志做个详细的分析：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;2019-05-13T10:17:10.902+0800&lt;/code&gt; 代表 &lt;code&gt;GC&lt;/code&gt; 日志开始的时间点&lt;/li&gt; &lt;li&gt;&lt;code&gt;37622.895&lt;/code&gt; &lt;code&gt;GC&lt;/code&gt; 事件的开始时间，相对于 &lt;code&gt;JVM&lt;/code&gt; 的启动时间，单位是秒&lt;/li&gt; &lt;li&gt;&lt;code&gt;GC&lt;/code&gt; 用来区分(distinguish)是&lt;code&gt;Minor GC&lt;/code&gt; 还是 &lt;code&gt;Full GC&lt;/code&gt; 的标志(Flag)， 这里的 &lt;code&gt;GC&lt;/code&gt; 表明本次发生的是 &lt;code&gt;Minor GC&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;Allocation Failure&lt;/code&gt; 引起垃圾回收的原因. 本次 &lt;code&gt;GC&lt;/code&gt; 是因为年轻代中（&lt;code&gt;Young Generation&lt;/code&gt;）没有任何合适的区域能够存放需要分配的数据结构而触发的&lt;/li&gt; &lt;li&gt;&lt;code&gt;DefNew&lt;/code&gt; 使用的垃圾收集器的名字，&lt;code&gt;DefNew&lt;/code&gt; 这个名字代表的是：单线程(&lt;code&gt;single-threaded&lt;/code&gt;)，采用标记复制(&lt;code&gt;mark-copy&lt;/code&gt;)算法的，使整个 &lt;code&gt;JVM&lt;/code&gt; 暂停运行(&lt;code&gt;stop-the-world&lt;/code&gt;)的年轻代(&lt;code&gt;Young generation&lt;/code&gt;) 垃圾收集器(&lt;code&gt;garbage collector&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;109585K-&amp;gt;282K&lt;/code&gt; 在本次垃圾收集之前和之后的年轻代内存使用情况(&lt;code&gt;Usage&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;122880K&lt;/code&gt; 年轻代的总的大小(&lt;code&gt;Total size&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;117507K-&amp;gt;8205K&lt;/code&gt; 本次垃圾收集之前和之后整个堆内存的使用情况(&lt;code&gt;Total used heap&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;395968K&lt;/code&gt; 总的可用的堆内存(Total available heap)&lt;/li&gt; &lt;li&gt;&lt;code&gt;0.0021385 secs&lt;/code&gt; GC事件的持续时间(Duration)，单位是秒&lt;/li&gt; &lt;li&gt;&lt;code&gt;Times user&lt;/code&gt; 此次垃圾回收，垃圾收集线程消耗的所有CPU时间(Total CPU time)&lt;/li&gt; &lt;li&gt;&lt;code&gt;sys&lt;/code&gt; 操作系统调用(OS call) 以及等待系统事件的时间(waiting for system event)&lt;/li&gt; &lt;li&gt;&lt;code&gt;real&lt;/code&gt; 应用程序暂停的时间(Clock time)。由于串行垃圾收集器(Serial Garbage Collector)只会使用单个线程，所以 real time 等于 user 以及 system time 的总和&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;JVM关闭&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;正常关闭：当最后一个非守护线程结束或调用了&lt;code&gt;System.exit&lt;/code&gt;或通过其他特定于平台的方式，比如&lt;code&gt;ctrl+c&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;强制关闭：调用 &lt;code&gt;Runtime.halt&lt;/code&gt;方法，或在操作系统中直接&lt;code&gt;kill&lt;/code&gt;（发送&lt;code&gt;single&lt;/code&gt;信号）掉&lt;code&gt;JVM&lt;/code&gt;进程。&lt;/li&gt; &lt;li&gt;异常关闭：运行中遇到 &lt;code&gt;RuntimeException&lt;/code&gt; 异常等&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;在某些情况下，我们需要在 &lt;code&gt;JVM&lt;/code&gt;关闭时做一些扫尾的工作，比如删除临时文件、停止日志服务。为此 &lt;code&gt;JVM&lt;/code&gt;提供了关闭钩子（&lt;code&gt;shutdown hocks&lt;/code&gt;）来做这些事件。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Runtime&lt;/code&gt; 类封装 &lt;code&gt;java&lt;/code&gt;应用运行时的环境，每个&lt;code&gt;java&lt;/code&gt;应用程序都有一个&lt;code&gt;Runtime&lt;/code&gt;类实例，使用程序能与其运行环境相连。&lt;/p&gt; &lt;p&gt;关闭钩子本质上是一个线程（也称为&lt;code&gt;hock&lt;/code&gt;线程），可以通过&lt;code&gt;Runtime&lt;/code&gt;的&lt;code&gt;addshutdownhock （Thread hock）&lt;/code&gt;向主&lt;code&gt;jvm&lt;/code&gt;注册一个关闭钩子。&lt;code&gt;hock&lt;/code&gt;线程在&lt;code&gt;jvm&lt;/code&gt;正常关闭时执行，强制关闭不执行。&lt;/p&gt; &lt;p&gt;对于在&lt;code&gt;jvm&lt;/code&gt;中注册的多个关闭钩子，他们会并发执行，&lt;code&gt;jvm&lt;/code&gt;并不能保证他们的执行顺序。&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 20 Mar 2020 06:44:00 GMT</pubDate>
    </item>
    <item>
      <title>volatile、synchronized和lock解析</title>
      <link>https://www.zhangaoo.com/article/volatile-synchronized-lock</link>
      <content:encoded>&lt;h1&gt;volatile、synchronized和lock解析&lt;/h1&gt; &lt;p&gt;首先了解下 java 的内存模型：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020318185925-java-memory-model.png" alt="2020318185925-java-memory-model" style="zoom:75%;" /&gt; &lt;ul&gt; &lt;li&gt;每个线程都有自己的本地内存空间（java栈中的帧）。线程执行时，先把变量从内存读到线程自己的本地内存空间，然后对变量进行操作。&lt;/li&gt; &lt;li&gt;对该变量操作完成后，在某个时间再把变量刷新回主内存。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;那么我们再了解下锁提供的两种特性：&lt;strong&gt;互斥&lt;/strong&gt;（mutual exclusion） 和&lt;strong&gt;可见性&lt;/strong&gt;（visibility）：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;互斥（mutual exclusion）：互斥即一次只允许一个线程持有某个特定的锁，因此可使用该特性实现对共享数据的协调访问协议，这样，一次就只有一个线程能够使用该共享数据；&lt;/li&gt; &lt;li&gt;可见性（visibility）：简单来说就是一个线程修改了变量，其他线程可以立即知道。保证可见性的方法：volatile,synchronized,final(一旦初始化完成其他线程就可见)。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;volatile&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;volatile是一个类型修饰符（type specifier）。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略，且要求每次直接读值。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;上面的话有些拗口，简单概括 &lt;code&gt;volatile&lt;/code&gt;,它能够使变量在值发生改变时能尽快地让其他线程知道。&lt;/p&gt; &lt;h3&gt;问题来源&lt;/h3&gt; &lt;p&gt;首先我们要先意识到有这样的现象，编译器为了加快程序运行的速度，对一些变量的写操作会先在寄存器或者是CPU缓存上进行，最后才写入内存。而在这个过程中，变量的新值对其他线程是不可见的。&lt;/p&gt; &lt;p&gt;一个例子如下，这里定义了 isRunning 成员变量，来控制子线程结束：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class RunThread extends Thread {     private boolean isRunning = true;      public boolean isRunning() {         return isRunning;     }      public void setRunning(boolean isRunning) {         this.isRunning = isRunning;     }      @Override     public void run() {         System.out.println(&amp;quot;进入到run方法中了&amp;quot;);         while (isRunning == true) {         }         System.out.println(&amp;quot;线程执行完成了&amp;quot;);     }      public static void main(String[] args) {         try {             RunThread thread = new RunThread();             thread.start();             Thread.sleep(1000);             thread.setRunning(false);         } catch (InterruptedException e) {             e.printStackTrace();         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在主线程中设置了&lt;code&gt;thread.setRunning(false);&lt;/code&gt; 但是子线程并不会结束而是一直在循环，&lt;/p&gt; &lt;h3&gt;解决方法&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;volatile private boolean isRunning = true; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;原理&lt;/h3&gt; &lt;p&gt;当对 &lt;code&gt;volatil&lt;/code&gt;e 标记的变量进行修改时，会将其他缓存中存储的修改前的变量清除，然后重新读取。一般来说应该是先在进行修改的缓存&lt;code&gt;A&lt;/code&gt; 中修改为新值，然后通知其他缓存清除掉此变量，当其他缓存 &lt;code&gt;B&lt;/code&gt; 中的线程读取此变量时，会向总线发送消息，这时存储新值的缓存 &lt;code&gt;A&lt;/code&gt;获取到消息，将新值穿给&lt;code&gt;B&lt;/code&gt;。最后将新值写入内存。当变量需要更新时都是此步骤，&lt;code&gt;volatile&lt;/code&gt; 的作用是被其修饰的变量，每次更新时，都会刷新上述步骤。&lt;/p&gt; &lt;h2&gt;synchronized&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;Java 语言的关键字，可用来给&lt;strong&gt;对象&lt;/strong&gt;和&lt;strong&gt;方法&lt;/strong&gt;或者&lt;strong&gt;代码块&lt;/strong&gt;加锁，当它锁定一个方法或者一个代码块的时候，同一时刻最多只有一个线程执行这段代码。&lt;/p&gt; &lt;p&gt;当两个并发线程访问同一个对象 &lt;code&gt;object&lt;/code&gt; 中的这个加锁同步代码块时，一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而，当一个线程访问 &lt;code&gt;object&lt;/code&gt;的一个加锁代码块时，另一个线程仍然可以访问该 &lt;code&gt;object&lt;/code&gt;中的非加锁代码块。&lt;/p&gt; &lt;/blockquote&gt; &lt;h3&gt;synchronized 方法&lt;/h3&gt; &lt;p&gt;方法声明时使用,放在范围操作符 (&lt;code&gt;public&lt;/code&gt;等)之后,返回类型声明(&lt;code&gt;void&lt;/code&gt;等)之前.这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在&lt;code&gt;synchronized&lt;/code&gt;方法内部的线程)执行完该方法后,别的线程才能进入。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public synchronized void synMethod(){     //方法体 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如在线程 &lt;code&gt;t1&lt;/code&gt; 中有语句&lt;code&gt;obj.synMethod();&lt;/code&gt; 那么由于&lt;code&gt;synMethod&lt;/code&gt;被&lt;code&gt;synchronized&lt;/code&gt;修饰,在执行该语句前, 需要先获得调用者&lt;code&gt;obj&lt;/code&gt;的对象锁, 如果其他线程(如&lt;code&gt;t2&lt;/code&gt;)已经锁定了&lt;code&gt;obj&lt;/code&gt;(可能是通过&lt;code&gt;obj.synMethod&lt;/code&gt;,也可能是通过其他被&lt;code&gt;synchronized&lt;/code&gt;修饰的方法&lt;code&gt;obj.otherSynMethod&lt;/code&gt;锁定的&lt;code&gt;obj&lt;/code&gt;), &lt;code&gt;t1&lt;/code&gt;需要等待直到其他线程(&lt;code&gt;t2&lt;/code&gt;)释放&lt;code&gt;obj&lt;/code&gt;, 然后&lt;code&gt;t1&lt;/code&gt;锁定&lt;code&gt;obj&lt;/code&gt;, 执行&lt;code&gt;synMethod&lt;/code&gt;方法. 返回之前之前释放&lt;code&gt;obj&lt;/code&gt;锁。&lt;/p&gt; &lt;p&gt;简单来说就是如果对象中有方法是使用 &lt;code&gt;synchronized&lt;/code&gt; 来同步，必须先获得该方法对象的锁，才能调用该方法，否则只能等待锁释放。&lt;/p&gt; &lt;h3&gt;synchronized 块&lt;/h3&gt; &lt;p&gt;对某一代码块使用 &lt;code&gt;synchronized&lt;/code&gt; 后跟括号，括号里是变量,这样,一次只有一个线程进入该代码块。此时线程获得的是成员锁。&lt;/p&gt; &lt;h3&gt;synchronized (this)&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;当两个并发线程访问同一个对象 &lt;code&gt;object&lt;/code&gt; 中的这个 &lt;code&gt;synchronized(this)&lt;/code&gt; 同步代码块时，一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完&lt;strong&gt;这个代码块&lt;/strong&gt;以后才能执行该代码块。&lt;/li&gt; &lt;li&gt;当一个线程访问 &lt;code&gt;object&lt;/code&gt;的一个&lt;code&gt;synchronized(this)&lt;/code&gt;同步代码块时，其他线程对 &lt;code&gt;object&lt;/code&gt; 中所有&lt;strong&gt;其它&lt;/strong&gt;&lt;code&gt;synchronized(this)&lt;/code&gt; 同步代码块的访问将被阻塞。&lt;/li&gt; &lt;li&gt;然而，当一个线程访问 &lt;code&gt;object&lt;/code&gt; 的一个 &lt;code&gt;synchronized(this)&lt;/code&gt; 同步代码块时，另一个线程仍然可以访问该&lt;code&gt;object 中的除&lt;/code&gt;synchronized(this)` 同步代码块以外的部分。　&lt;/li&gt; &lt;li&gt;第三个例子同样适用其它同步代码块。也就是说，当一个线程访问 &lt;code&gt;object&lt;/code&gt; 的一个 &lt;code&gt;synchronized(this)&lt;/code&gt; 同步代码块时，它就获得了这个&lt;code&gt;object&lt;/code&gt;的对象锁。结果，其它线程对该 &lt;code&gt;object&lt;/code&gt;对象所有同步代码部分的访问都被暂时阻塞。&lt;/li&gt; &lt;li&gt;以上规则对其它对象锁同样适用。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;第三点举例说明：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Thread2 {        public void m4t1() {             synchronized(this) {                  int i = 5;                  while( i-- &amp;gt; 0) {                       System.out.println(Thread.currentThread().getName() + &amp;quot; : &amp;quot; + i);                       try {                            Thread.sleep(500);                       } catch (InterruptedException ie) {                       }                  }             }        }        public void m4t2() {             int i = 5;             while( i-- &amp;gt; 0) {                  System.out.println(Thread.currentThread().getName() + &amp;quot; : &amp;quot; + i);                  try {                       Thread.sleep(500);                  } catch (InterruptedException ie) {                  }             }        }        public static void main(String[] args) {             final Thread2 myt2 = new Thread2();             Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, &amp;quot;t1&amp;quot;  );             Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, &amp;quot;t2&amp;quot;  );             t1.start();             t2.start();        }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;含有 &lt;code&gt;synchronized&lt;/code&gt; 同步块的方法 &lt;code&gt;m4t1&lt;/code&gt; 被访问时，线程中 &lt;code&gt;m4t2()&lt;/code&gt;依然可以被访问。&lt;/p&gt; &lt;h3&gt;wait() 与notify()/notifyAll()&lt;/h3&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;wait():&lt;/p&gt; &lt;p&gt;释放占有的对象锁，线程进入等待池，释放&lt;code&gt;cpu&lt;/code&gt;,而其他正在等待的线程即可抢占此锁，获得锁的线程即可运行程序。而 &lt;code&gt;sleep()&lt;/code&gt;不同的是，线程调用此方法后，会休眠一段时间，休眠期间，会暂时释放&lt;code&gt;cpu&lt;/code&gt;，但并不释放对象锁。也就是说，在休眠期间，其他线程依然无法进入此代码内部。休眠结束，线程重新获得&lt;code&gt;cpu&lt;/code&gt;,执行代码。&lt;code&gt;wait()&lt;/code&gt;和&lt;code&gt;sleep()&lt;/code&gt;最大的不同在于&lt;code&gt;wait()&lt;/code&gt;会释放对象锁，而&lt;code&gt;sleep()&lt;/code&gt;不会！&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;notify():&lt;/p&gt; &lt;p&gt;该方法会唤醒因为调用对象的wait()而等待的线程，其实就是对对象锁的唤醒，从而使得wait()的线程可以有机会获取对象锁。调用notify()后，并不会立即释放锁，而是继续执行当前代码，直到synchronized中的代码全部执行完毕，才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁，执行代码。需要注意的是，wait()和notify()必须在synchronized代码块中调用。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;notifyAll():&lt;/p&gt; &lt;p&gt;则是唤醒所有等待的线程。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;lock&lt;/h2&gt; &lt;h3&gt;synchronized 的缺陷&lt;/h3&gt; &lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt; 是 &lt;code&gt;java&lt;/code&gt; 中的一个关键字，也就是说是 &lt;code&gt;Java&lt;/code&gt; 语言内置的特性。那么为什么会出现 &lt;code&gt;Lock&lt;/code&gt; 呢？&lt;/p&gt; &lt;p&gt;如果一个代码块被 &lt;code&gt;synchronized&lt;/code&gt; 修饰了，当一个线程获取了对应的锁，并执行该代码块时，其他线程便只能一直等待，等待获取锁的线程释放锁，而这里获取锁的线程释放锁只会有两种情况：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;获取锁的线程执行完了该代码块，然后线程释放对锁的占有；&lt;/li&gt; &lt;li&gt;线程执行发生异常，此时JVM会让线程自动释放锁。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;那么如果这个获取锁的线程由于要等待&lt;code&gt;IO&lt;/code&gt; 或者其他原因（比如调用&lt;code&gt;sleep&lt;/code&gt;方法）被阻塞了，但是又没有释放锁，其他线程便只能等待，试想一下，这多么影响程序执行效率。&lt;/p&gt; &lt;p&gt;因此就需要有一种机制可以不让等待的线程一直无期限地等待下去（比如只等待一定的时间或者能够响应中断），通过 &lt;code&gt;Lock&lt;/code&gt; 就可以办到。&lt;/p&gt; &lt;p&gt;再举个例子：&lt;/p&gt; &lt;p&gt;当有多个线程读写文件时，读操作和写操作会发生冲突现象，写操作和写操作会发生冲突现象，但是读操作和读操作不会发生冲突现象。&lt;/p&gt; &lt;p&gt;但是采用 &lt;code&gt;synchronized&lt;/code&gt; 关键字来实现同步的话，就会导致一个问题：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;如果多个线程都只是进行读操作，所以当一个线程在进行读操作时，其他线程只能等待无法进行读操作。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;因此就需要一种机制来使得多个线程都只是进行读操作时，线程之间不会发生冲突，通过 &lt;code&gt;Lock&lt;/code&gt;就可以办到。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;另外，通过 &lt;code&gt;Lock&lt;/code&gt; 可以知道线程有没有成功获取到锁。这个是 &lt;code&gt;synchronized&lt;/code&gt;无法办到的。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;总结一下，也就是说 &lt;code&gt;Lock&lt;/code&gt; 提供了比&lt;code&gt;synchronized&lt;/code&gt; 更多的功能。但是要注意以下几点：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Lock&lt;/code&gt; 不是 &lt;code&gt;Java&lt;/code&gt; 语言内置的，&lt;code&gt;synchronized&lt;/code&gt;是&lt;code&gt;Java&lt;/code&gt;语言的关键字，因此是内置特性。&lt;code&gt;Lock&lt;/code&gt;是一个类，通过这个类可以实现同步访问；&lt;/li&gt; &lt;li&gt;&lt;code&gt;Lock&lt;/code&gt;和 &lt;code&gt;synchronized&lt;/code&gt; 有一点非常大的不同，采用&lt;code&gt;synchronized&lt;/code&gt;不需要用户去手动释放锁，当&lt;code&gt;synchronized&lt;/code&gt;方法或者&lt;code&gt;synchronized&lt;/code&gt;代码块执行完之后，系统会自动让线程释放对锁的占用；而&lt;code&gt;Lock&lt;/code&gt;则必须要用户去手动释放锁，如果没有主动释放锁，就有可能导致出现死锁现象。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;java.util.concurrent.locks包下常用的类&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Lock {     //获取锁，如果锁被其他线程获取，则进行等待     void lock();       //当通过这个方法去获取锁时，如果线程正在等待获取锁，则这个线程能够响应中断，即中断线程的等待状态。也就使说，当两个线程同时通过lock.lockInterruptibly()想获取某个锁时，假若此时线程A获取到了锁，而线程B只有在等待，那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。     void lockInterruptibly() throws InterruptedException;      /**tryLock()方法是有返回值的，它表示用来尝试获取锁，如果获取成     *功，则返回true，如果获取失败（即锁已被其他线程获取），则返回     *false，也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。*/     boolean tryLock();      //tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的，只不过区别在于这个方法在拿不到锁时会等待一定的时间，在时间期限之内如果还拿不到锁，就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁，则返回true。     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;     void unlock(); //释放锁     Condition newCondition(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;通常使用lock进行同步：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Lock lock = ...; lock.lock(); try{     //处理任务 }catch(Exception ex){  }finally{     lock.unlock();   //释放锁 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;trylock使用方法：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Lock lock = ...; if(lock.tryLock()) {      try{          //处理任务      }catch(Exception ex){       }finally{          lock.unlock();   //释放锁      }  }else {     //如果不能获取锁，则直接做其他事情 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;lockInterruptibly()一般的使用形式如下：&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void method() throws InterruptedException {     lock.lockInterruptibly();     try {        //.....     }     finally {         lock.unlock();     }   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意:&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;当一个线程获取了锁之后，是不会被 &lt;code&gt;interrupt()&lt;/code&gt;方法中断的。因为本身在前面的文章中讲过单独调用&lt;code&gt;interrupt()&lt;/code&gt;方法不能中断正在运行过程中的线程，只能中断阻塞过程中的线程。&lt;/p&gt; &lt;p&gt;而用 &lt;code&gt;synchronized&lt;/code&gt;修饰的话，当一个线程处于等待某个锁的状态，是无法被中断的，只有一直等待下去。&lt;/p&gt; &lt;h4&gt;ReentrantLock&lt;/h4&gt; &lt;p&gt;&lt;code&gt;ReentrantLock&lt;/code&gt;，意思是“可重入锁”,是唯一实现了&lt;code&gt;Lock&lt;/code&gt;接口的类，并且&lt;code&gt;ReentrantLock&lt;/code&gt;提供了更多的方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Test {     private ArrayList&amp;lt;Integer&amp;gt; arrayList = new ArrayList&amp;lt;Integer&amp;gt;();     private Lock lock = new ReentrantLock();    //注意这个地方     public static void main(String[] args)  {         final Test test = new Test();          new Thread(){             public void run() {                 test.insert(Thread.currentThread());             };         }.start();          new Thread(){             public void run() {                 test.insert(Thread.currentThread());             };         }.start();     }        public void insert(Thread thread) {         lock.lock();         try {             System.out.println(thread.getName()+&amp;quot;得到了锁&amp;quot;);             for(int i=0;i&amp;lt;5;i++) {                 arrayList.add(i);             }         } catch (Exception e) {             // TODO: handle exception         }finally {             System.out.println(thread.getName()+&amp;quot;释放了锁&amp;quot;);             lock.unlock();         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果锁具备可重入性，则称作为可重入锁。像 &lt;code&gt;synchronized&lt;/code&gt;和 &lt;code&gt;ReentrantLock&lt;/code&gt; 都是可重入锁，可重入性在我看来实际上表明了锁的分配机制：基于线程的分配，而不是基于方法调用的分配。举个简单的例子，当一个线程执行到某个&lt;code&gt;synchronized&lt;/code&gt;方法时，比如说&lt;code&gt;method1&lt;/code&gt;，而在&lt;code&gt;method1&lt;/code&gt;中会调用另外一个&lt;code&gt;synchronized&lt;/code&gt;方法&lt;code&gt;method2&lt;/code&gt;，此时线程不必重新去申请锁，而是可以直接执行方法&lt;code&gt;method2&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;实例代码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;class MyClass {     public synchronized void method1() {         method2();     }     public synchronized void method2() {      } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述代码中的两个方法 &lt;code&gt;method&lt;/code&gt;1和&lt;code&gt;method2&lt;/code&gt;都用&lt;code&gt;synchronized&lt;/code&gt;修饰了，假如某一时刻，线程&lt;code&gt;A&lt;/code&gt;执行到了&lt;code&gt;method1&lt;/code&gt;，此时线程&lt;code&gt;A&lt;/code&gt;获取了这个对象的锁，而由于&lt;code&gt;method2&lt;/code&gt;也是&lt;code&gt;synchronized&lt;/code&gt;方法，假如&lt;code&gt;synchronized&lt;/code&gt;不具备可重入性，此时线程&lt;code&gt;A&lt;/code&gt;需要重新申请锁。但是这就会造成一个问题，因为线程A已经持有了该对象的锁，而又在申请获取该对象的锁，这样就会线程&lt;code&gt;A&lt;/code&gt;一直等待永远不会获取到的锁。&lt;/p&gt; &lt;p&gt;而由于 &lt;code&gt;synchronized&lt;/code&gt; 和 &lt;code&gt;Lock&lt;/code&gt;都具备可重入性，所以不会发生上述现象。&lt;/p&gt; &lt;h3&gt;volatile和synchronized区别&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;volatile&lt;/code&gt; 本质是在告诉 &lt;code&gt;jvm&lt;/code&gt;当前变量在寄存器中的值是不确定的，需要从主存中读取，&lt;code&gt;synchronized&lt;/code&gt;则是锁定当前变量，只有当前线程可以访问该变量，其他线程被阻塞住。&lt;/li&gt; &lt;li&gt;&lt;code&gt;volatile&lt;/code&gt;仅能使用在变量级别，&lt;code&gt;synchronized&lt;/code&gt;则可以使用在变量、方法。&lt;/li&gt; &lt;li&gt;&lt;code&gt;volatile&lt;/code&gt; 仅能实现变量的修改可见性，而 &lt;code&gt;synchronized&lt;/code&gt;则可以保证变量的修改可见性和原子性。《Java编程思想》上说，定义long或double变量时，如果使用volatile关键字，就会获得（简单的赋值与返回操作）原子性。&lt;/li&gt; &lt;li&gt;&lt;code&gt;volatile&lt;/code&gt; 不会造成线程的阻塞，而&lt;code&gt;synchronized&lt;/code&gt;可能会造成线程的阻塞。&lt;/li&gt; &lt;li&gt;当一个域的值依赖于它之前的值时，&lt;code&gt;volatile&lt;/code&gt; 就无法工作了，如&lt;code&gt;n=n+1,n++&lt;/code&gt;等。如果某个域的值受到其他域的值的限制，那么&lt;code&gt;volatile&lt;/code&gt;也无法工作，如&lt;code&gt;Range&lt;/code&gt;类的&lt;code&gt;lower&lt;/code&gt;和&lt;code&gt;upper&lt;/code&gt;边界，必须遵循&lt;code&gt;lower&amp;lt;=upper&lt;/code&gt;的限制。&lt;/li&gt; &lt;li&gt;使用 &lt;code&gt;volatile&lt;/code&gt;而不是 &lt;code&gt;synchronized&lt;/code&gt;的唯一安全的情况是类中只有一个可变的域。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;synchronized和lock区别&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;Lock&lt;/code&gt; 是一个接口，而 &lt;code&gt;synchronized&lt;/code&gt;是 &lt;code&gt;Java&lt;/code&gt;中的关键字，&lt;code&gt;synchronized&lt;/code&gt;是内置的语言实现；&lt;/li&gt; &lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt; 在发生异常时，会自动释放线程占有的锁，因此不会导致死锁现象发生；而Lock在发生异常时，如果没有主动通过&lt;code&gt;unLock()&lt;/code&gt;去释放锁，则很可能造成死锁现象，因此使用&lt;code&gt;Lock&lt;/code&gt;时需要在&lt;code&gt;finally&lt;/code&gt;块中释放锁；&lt;/li&gt; &lt;li&gt;&lt;code&gt;Lock&lt;/code&gt;可以让等待锁的线程响应中断，而 &lt;code&gt;synchronized&lt;/code&gt;却不行，使用&lt;code&gt;synchronized&lt;/code&gt;时，等待的线程会一直等待下去，不能够响应中断；&lt;/li&gt; &lt;li&gt;通过 &lt;code&gt;Lock&lt;/code&gt;可以知道有没有成功获取锁，而 &lt;code&gt;synchronized&lt;/code&gt;却无法办到。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Lock&lt;/code&gt;可以提高多个线程进行读操作的效率。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;在性能上来说，如果竞争资源不激烈，两者的性能是差不多的，而当竞争资源非常激烈时（即有大量线程同时竞争），此时 &lt;code&gt;Lock&lt;/code&gt;的性能要远远优于&lt;code&gt;synchronized&lt;/code&gt;。所以说，在具体使用时要根据适当情况选择。&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 18 Mar 2020 14:47:00 GMT</pubDate>
    </item>
    <item>
      <title>Java 类的加载机制</title>
      <link>https://www.zhangaoo.com/article/java-class-load-machanism</link>
      <content:encoded>&lt;h1&gt;Java 类的加载机制&lt;/h1&gt; &lt;h3&gt;类的生命周期&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;加载（Loading）&lt;/li&gt; &lt;li&gt;验证（Verification）&lt;/li&gt; &lt;li&gt;准备（Preparation)&lt;/li&gt; &lt;li&gt;解析（Resolution）&lt;/li&gt; &lt;li&gt;初始化（Initialization）&lt;/li&gt; &lt;li&gt;使用（Using）&lt;/li&gt; &lt;li&gt;卸载（Unloading）&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;类从被加载到虚拟机内存中开始，直到卸载出内存为止，它的整个生命周期包括了：加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中，验证、准备和解析这三个部分统称为连接（linking）。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2020318111629-class-load.png" alt="2020318111629-class-load" /&gt;&lt;/p&gt; &lt;p&gt;其中，加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的，类的加载过程必须按照这种顺序按部就班的“开始”（&lt;strong&gt;仅仅指的是开始，而非执行或者结束&lt;/strong&gt;，因为这些阶段通常都是互相交叉的混合进行，通常会在一个阶段执行的过程中调用或者激活另一个阶段）。&lt;/p&gt; &lt;p&gt;其中解析过程在某些情况下可以在初始化阶段之后再开始，这是为了支持 Java 的动态绑定。&lt;/p&gt; &lt;h3&gt;何时开始类的初始化&lt;/h3&gt; &lt;p&gt;什么情况下需要开始类加载过程的第一个阶段:&lt;strong&gt;加载&lt;/strong&gt;。虚拟机规范中并没强行约束，这点可以交给虚拟机的的具体实现自由把握，但是对于初始化阶段虚拟机规范是严格规定了如下几种情况，如果类未初始化会对类进行初始化。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;遇到 &lt;code&gt;new、getstatic、putstatic、invokestatic&lt;/code&gt; 这四条字节码指令时，如果类没有进行过初始化，则必须先触发其初始化。最常见的生成这 4 条指令的场景是：使用 &lt;code&gt;new&lt;/code&gt; 关键字实例化对象的时候；读取或设置一个类的静态字段（被 &lt;code&gt;fina&lt;/code&gt;l 修饰、已在编译期把结果放入常量池的静态字段除外）的时候；&lt;/li&gt; &lt;li&gt;调用一个类的静态方法的时候。&lt;/li&gt; &lt;li&gt;使用&lt;code&gt;java.lang.reflect&lt;/code&gt; 包的方法对类进行反射调用的时候，如果类没有进行初始化，则需要先触发其初始化。&lt;/li&gt; &lt;li&gt;当初始化一个类的时候，如果发现其父类还没有进行过初始化，则需要先触发其父类的初始化。&lt;/li&gt; &lt;li&gt;当虚拟机启动时，用户需要指定一个要执行的主类（包含 main() 方法的那个类），虚拟机会先初始化这个主类；&lt;/li&gt; &lt;li&gt;当使用 JDK 1.7 的动态语言支持时，如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic,REF_putStatic, REF_invokeStatic 的方法句柄，并且这个方法句柄所对应的类没有进行过初始化，则需要先触发其初始化；&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;以上 6 种场景中的行为称为对一个类进行主动引用。除此之外，所有引用类的方式都&lt;strong&gt;不会触发初始化&lt;/strong&gt;，称为&lt;strong&gt;被动引用&lt;/strong&gt;。被动引用的常见例子包括：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;通过子类引用父类的静态字段，不会导致子类初始化。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;System.out.println(SubClass.value); // value 字段在 SuperClass 中定义 &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;通过数组定义来引用类，不会触发此类的初始化。该过程会对数组类进行初始化，数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类，其中包含了数组的属性和方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;SuperClass[] sca = new SuperClass[10]; &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;常量在编译阶段会存入调用类的常量池中，本质上并没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;System.out.println(ConstClass.HELLOWORLD); &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;加载&lt;/h4&gt; &lt;p&gt;加载是类加载的一个阶段，注意不要混淆。&lt;/p&gt; &lt;p&gt;加载过程完成以下三件事：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通过一个类的全限定名来获取定义此类的二进制字节流。&lt;/li&gt; &lt;li&gt;将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。&lt;/li&gt; &lt;li&gt;在内存中生成一个代表这个类的 Class 对象，作为方法区这个类的各种数据的访问入口。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;其中二进制字节流可以从以下方式中获取：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;从 ZIP 包读取，这很常见，最终成为日后 JAR、EAR、WAR 格式的基础。&lt;/li&gt; &lt;li&gt;从网络中获取，这种场景最典型的应用是 Applet。&lt;/li&gt; &lt;li&gt;运行时计算生成，这种场景使用得最多得就是动态代理技术，在 java.lang.reflect.Proxy 中，就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。&lt;/li&gt; &lt;li&gt;由其他文件生成，典型场景是 JSP 应用，即由 JSP 文件生成对应的 Class 类。&lt;/li&gt; &lt;li&gt;从数据库读取，这种场景相对少见，例如有些中间件服务器（如 SAP Netweaver）可以选择把程序安装到数据库中来完成程序代码在集群间的分发。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;验证&lt;/h4&gt; &lt;p&gt;确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;文件格式验证：验证字节流是否符合 Class 文件格式的规范，并且能被当前版本的虚拟机处理。&lt;/li&gt; &lt;li&gt;元数据验证：对字节码描述的信息进行语义分析，以保证其描述的信息符合 Java 语言规范的要求。&lt;/li&gt; &lt;li&gt;字节码验证：通过数据流和控制流分析，确保程序语义是合法、符合逻辑的。&lt;/li&gt; &lt;li&gt;符号引用验证：发生在虚拟机将符号引用转换为直接引用的时候，对类自身以外（常量池中的各种符号引用）的信息进行匹配性校验。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;准备&lt;/h4&gt; &lt;p&gt;类变量是被 static 修饰的变量，准备阶段为类变量分配内存并设置&lt;strong&gt;初始值(默认值)&lt;/strong&gt;，使用的是方法区的内存。&lt;/p&gt; &lt;p&gt;实例变量不会在这阶段分配内存，它将会在对象实例化时随着对象一起分配在 Java 堆中。（实例化不是类加载的一个过程，类加载发生在所有实例化操作之前，并且类加载只进行一次，实例化可以进行多次）&lt;/p&gt; &lt;p&gt;初始值一般为 0 值，例如下面的类变量 value 被初始化为 0 而不是 123。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static int value = 123; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果类变量是常量，那么会按照表达式来进行初始化，而不是赋值为 0。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static final int value = 123; &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;解析&lt;/h4&gt; &lt;p&gt;将常量池的符号引用替换为直接引用的过程。&lt;/p&gt; &lt;h4&gt;初始化&lt;/h4&gt; &lt;p&gt;初始化阶段才真正开始执行类中的定义的 &lt;code&gt;Java&lt;/code&gt; 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit&gt;() 方法的过程。&lt;/p&gt; &lt;p&gt;在准备阶段，类变量已经赋过一次系统要求的初始值，而在初始化阶段，根据程序员通过程序制定的主观计划去初始化类变量和其它资源。&lt;/p&gt; &lt;p&gt;&lt;clinit&gt;() 方法具有以下特点：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;是由编译器自动收集类中**所有类变量的赋值动作和静态语句块（static{} 块）**中的语句合并产生的，编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是，&lt;strong&gt;静态语句块只能访问到定义在它之前的类变量，定义在它之后的类变量只能赋值，不能访问&lt;/strong&gt;。例如以下代码：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Test {     static {         i = 0;                // 给变量赋值可以正常编译通过(赋值)         System.out.print(i);  // 这句编译器会提示“非法向前引用”（访问）     }     static int i = 1; } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;与类的构造函数（或者说实例构造器 &lt;init&gt;()）不同，不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit&gt;() 方法运行之前，父类的 &lt;clinit&gt;() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit&gt;() 方法的类肯定为&lt;code&gt;java.lang.Object&lt;/code&gt;。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;由于父类的 &lt;clinit&gt;() 方法先执行，也就意味着&lt;strong&gt;父类中定义的静态语句块要优于子类的变量赋值操作&lt;/strong&gt;。例如以下代码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;static class Parent {     public static int A = 1;     static {         A = 2;     } } static class Sub extends Parent {     public static int B = A; } public static void main(String[] args) {      System.out.println(Sub.B);  // 输出结果是父类中的静态变量 A 的值，也就是 2。 } &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;clinit&gt;() 方法对于类或接口不是必须的，如果一个类中不包含静态语句块，也没有对类变量的赋值操作，编译器可以不为该类生成 &lt;clinit&gt;() 方法。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;接口中不可以使用静态语句块，但仍然有类变量初始化的赋值操作，因此接口与类一样都会生成 &lt;clinit&gt;() 方法。但接口与类不同的是，执行接口的 &lt;clinit&gt;() 方法不需要先执行父接口的 &lt;clinit&gt;() 方法。只有当父接口中定义的变量使用时，父接口才会初始化。另外，接口的实现类在初始化时也一样不会执行接口的 &lt;clinit&gt;() 方法。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;虚拟机会保证一个类的 &lt;clinit&gt;() 方法在多线程环境下被正确的加锁和同步，如果多个线程同时初始化一个类，只会有一个线程执行这个类的 &lt;clinit&gt;() 方法，其它线程都会阻塞等待，直到活动线程执行 &lt;clinit&gt;() 方法完毕。如果在一个类的 &lt;clinit&gt;() 方法中有耗时的操作，就可能造成多个线程阻塞，在实际过程中此种阻塞很隐蔽。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;类加载器&lt;/h3&gt; &lt;p&gt;实现类的加载动作。在 Java 虚拟机外部实现，以便让应用程序自己决定如何去获取所需要的类。&lt;/p&gt; &lt;h4&gt;类与类加载器&lt;/h4&gt; &lt;p&gt;两个类相等：类本身相等，并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。&lt;/p&gt; &lt;p&gt;这里的相等，包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true，也包括使用 instanceof 关键字做对象所属关系判定结果为 true。&lt;/p&gt; &lt;h4&gt;类加载器分类&lt;/h4&gt; &lt;p&gt;从 &lt;code&gt;Java&lt;/code&gt; 虚拟机的角度来讲，只存在以下两种不同的类加载器：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;启动类加载器（&lt;code&gt;Bootstrap ClassLoader&lt;/code&gt;），这个类加载器用 &lt;code&gt;C++&lt;/code&gt; 实现，是虚拟机自身的一部分；&lt;/li&gt; &lt;li&gt;所有其他类的加载器，这些类由 &lt;code&gt;Java&lt;/code&gt; 实现，独立于虚拟机外部，并且全都继承自抽象类 &lt;code&gt;java.lang.ClassLoader&lt;/code&gt;。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;从&lt;code&gt;Java&lt;/code&gt; 开发人员的角度看，类加载器可以划分得更细致一些：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;启动类加载器（Bootstrap ClassLoader）此类加载器负责将存放在 &amp;lt;JAVA_HOME&amp;gt;\lib 目录中的，或者被 -Xbootclasspath 参数所指定的路径中的，并且是虚拟机识别的（仅按照文件名识别，如 rt.jar，名字不符合的类库即使放在 lib 目录中也不会被加载）类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用，用户在编写自定义类加载器时，如果需要把加载请求委派给启动类加载器，直接使用 null 代替即可。&lt;/li&gt; &lt;li&gt;扩展类加载器（Extension ClassLoader）这个类加载器是由 ExtClassLoader（sun.misc.Launcher$ExtClassLoader）实现的。它负责将 &amp;lt;JAVA_HOME&amp;gt;/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中，开发者可以直接使用扩展类加载器。&lt;/li&gt; &lt;li&gt;应用程序类加载器（Application ClassLoader）这个类加载器是由 AppClassLoader（sun.misc.Launcher$AppClassLoader）实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值，因此一般称为系统类加载器。它负责加载用户类路径（ClassPath）上所指定的类库，开发者可以直接使用这个类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;双亲委派模型&lt;/h4&gt; &lt;p&gt;应用程序都是由三种类加载器相互配合进行加载的，如果有必要，还可以加入自己定义的类加载器。&lt;/p&gt; &lt;p&gt;下图展示的类加载器之间的层次关系，称为类加载器的双亲委派模型（Parents Delegation Model）。该模型要求除了顶层的启动类加载器外，其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合（Composition）关系来实现，而不是通过继承（Inheritance）的关系实现。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020318151114-parent-delegate-model.jpg" alt="2020318151114-parent-delegate-model" style="zoom:80%;" /&gt; &lt;h5&gt;工作过程&lt;/h5&gt; &lt;p&gt;一个类加载器首先将类加载请求传送到父类加载器，只有当父类加载器无法完成类加载请求时才尝试加载。&lt;/p&gt; &lt;h5&gt;好处&lt;/h5&gt; &lt;p&gt;使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系，从而是的基础类得到统一。&lt;/p&gt; &lt;p&gt;例如 &lt;code&gt;java.lang.Object&lt;/code&gt; 存放在 &lt;code&gt;rt.jar&lt;/code&gt; 中，如果编写另外一个&lt;code&gt;java.lang.Object&lt;/code&gt; 的类并放到 &lt;code&gt;ClassPath&lt;/code&gt; 中，程序可以编译通过。因为双亲委派模型的存在，所以在 &lt;code&gt;rt.jar&lt;/code&gt; 中的 &lt;code&gt;Object&lt;/code&gt; 比在 &lt;code&gt;ClassPath&lt;/code&gt; 中的 &lt;code&gt;Object&lt;/code&gt; 优先级更高，因为 &lt;code&gt;rt.jar&lt;/code&gt; 中的 &lt;code&gt;Object&lt;/code&gt; 使用的是启动类加载器，而 &lt;code&gt;ClassPath&lt;/code&gt; 中的 &lt;code&gt;Object&lt;/code&gt; 使用的是应用程序类加载器。正因为&lt;code&gt;rt.jar&lt;/code&gt; 中的 &lt;code&gt;Object&lt;/code&gt; 优先级更高，因为程序中所有的 &lt;code&gt;Object&lt;/code&gt; 都是这个 &lt;code&gt;Object&lt;/code&gt;。&lt;/p&gt; &lt;h5&gt;实现&lt;/h5&gt; &lt;p&gt;以下是抽象类 &lt;code&gt;java.lang.ClassLoader&lt;/code&gt; 的代码片段，其中的 &lt;code&gt;loadClass()&lt;/code&gt; 方法运行过程如下：先检查类是否已经加载过，如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 &lt;code&gt;ClassNotFoundException&lt;/code&gt;，此时尝试自己去加载。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public abstract class ClassLoader {     // The parent class loader for delegation     private final ClassLoader parent;       public Class&amp;lt;?&amp;gt; loadClass(String name) throws ClassNotFoundException {         return loadClass(name, false);     }       protected Class&amp;lt;?&amp;gt; loadClass(String name, boolean resolve) throws ClassNotFoundException {         synchronized (getClassLoadingLock(name)) {             // First, check if the class has already been loaded             Class&amp;lt;?&amp;gt; c = findLoadedClass(name);             if (c == null) {                 try {                     if (parent != null) {                         c = parent.loadClass(name, false);                     } else {                         c = findBootstrapClassOrNull(name);                     }                 } catch (ClassNotFoundException e) {                     // ClassNotFoundException thrown if class not found                     // from the non-null parent class loader                 }                 if (c == null) {                     // If still not found, then invoke findClass in order                     // to find the class.                     c = findClass(name);                 }             }             if (resolve) {                 resolveClass(c);             }             return c;         }     }       protected Class&amp;lt;?&amp;gt; findClass(String name) throws ClassNotFoundException {         throw new ClassNotFoundException(name);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;自定义类加载器实现&lt;/h4&gt; &lt;p&gt;&lt;code&gt;FileSystemClassLoader&lt;/code&gt; 是自定义类加载器，继承自&lt;code&gt;java.lang.ClassLoader&lt;/code&gt;，用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件（&lt;code&gt;.class&lt;/code&gt; 文件），然后读取该文件内容，最后通过 &lt;code&gt;defineClass()&lt;/code&gt; 方法来把这些字节代码转换成 &lt;code&gt;java.lang.Class&lt;/code&gt; 类的实例。&lt;/p&gt; &lt;p&gt;&lt;code&gt;java.lang.ClassLoader&lt;/code&gt; 类的方法&lt;code&gt;loadClass()&lt;/code&gt; 实现了双亲委派模型的逻辑，因此自定义类加载器一般不去重写它，而是通过重写 &lt;code&gt;findClass()&lt;/code&gt; 方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class FileSystemClassLoader extends ClassLoader {       private String rootDir;       public FileSystemClassLoader(String rootDir) {         this.rootDir = rootDir;     }       protected Class&amp;lt;?&amp;gt; findClass(String name) throws ClassNotFoundException {         byte[] classData = getClassData(name);         if (classData == null) {             throw new ClassNotFoundException();         } else {             return defineClass(name, classData, 0, classData.length);         }     }       private byte[] getClassData(String className) {         String path = classNameToPath(className);         try {             InputStream ins = new FileInputStream(path);             ByteArrayOutputStream baos = new ByteArrayOutputStream();             int bufferSize = 4096;             byte[] buffer = new byte[bufferSize];             int bytesNumRead;             while ((bytesNumRead = ins.read(buffer)) != -1) {                 baos.write(buffer, 0, bytesNumRead);             }             return baos.toByteArray();         } catch (IOException e) {             e.printStackTrace();         }         return null;     }       private String classNameToPath(String className) {         return rootDir + File.separatorChar                 + className.replace('.', File.separatorChar) + &amp;quot;.class&amp;quot;;     } } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;例题（判断输出）&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class B {     //静态变量     static int i = 1;     //静态语句块     static {         System.out.println(&amp;quot;Class B1:static blocks,i=&amp;quot; + i);     }     //非静态变量     int j = 1;      //静态语句块     static {         i++;         System.out.println(&amp;quot;Class B2:static blocks,i=&amp;quot; + i);     }     //构造函数     public B() {         i++;         j++;         System.out.println(&amp;quot;constructor B: &amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);     }     //非静态语句块     {         i++;         j++;         System.out.println(&amp;quot;Class B:common blocks,&amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);     }     //非静态方法     public void bDisplay() {         i++;         System.out.println(&amp;quot;Class B:static void bDisplay(): &amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);         return;     }     //静态方法     public static void bTest() {         i++;         System.out.println(&amp;quot;Class B:static void bTest():    &amp;quot; + &amp;quot;i=&amp;quot; + i);         return;     } }  &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class A extends B {     //静态变量     static int i = 1;      //静态语句块     static {         System.out.println(&amp;quot;Class A1:static blocks&amp;quot; + i);     }      //非静态变量     int j = 1;      //静态语句块     static {         i++;         System.out.println(&amp;quot;Class A2:static blocks&amp;quot; + i);     }      //构造函数     public A() {         super();         i++;         j++;         System.out.println(&amp;quot;constructor A: &amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);     }      //非静态语句块     {         i++;         j++;         System.out.println(&amp;quot;Class A:common blocks&amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);     }      //非静态方法     public void aDisplay() {         i++;         System.out.println(&amp;quot;Class A:static void aDisplay(): &amp;quot; + &amp;quot;i=&amp;quot; + i + &amp;quot;,j=&amp;quot; + j);         return;     }      //静态方法     public static void aTest() {         i++;         System.out.println(&amp;quot;Class A:static void aTest():    &amp;quot; + &amp;quot;i=&amp;quot; + i);         return;     }      public static void main(String[] args) {         A a = new A();         a.aDisplay();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上两个类，B是A的父类，输出结果如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;1.Class B1:static blocks,i=1 2.Class B2:static blocks,i=2 3.Class A1:static blocks1 4.Class A2:static blocks2 5.Class B:common blocks,i=3,j=2 6.constructor B: i=4,j=3 7.Class A:common blocksi=3,j=2 8.constructor A: i=4,j=3 9.Class A:static void aDisplay(): i=5,j=3 &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;分析&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;首先按定义顺序加载&lt;strong&gt;父类&lt;/strong&gt;的静态变量和静态代码块，所以输出了1、2两行&lt;/li&gt; &lt;li&gt;然后按定义顺序加载&lt;strong&gt;子类&lt;/strong&gt;的静态变量和静态代码块，所以输出了3、4两行&lt;/li&gt; &lt;li&gt;然后加载&lt;strong&gt;父类&lt;/strong&gt;的非静态代码块，输出了第5行&lt;/li&gt; &lt;li&gt;然后加载&lt;strong&gt;父类的构造函数&lt;/strong&gt;，输出第6行&lt;/li&gt; &lt;li&gt;接着加载子类的非静态代码块，输出了第7行&lt;/li&gt; &lt;li&gt;然后加载子类的构造函数，输出第8行&lt;/li&gt; &lt;li&gt;最后执行子类的非静态方法&lt;/li&gt; &lt;/ol&gt; &lt;h4&gt;总结顺序如下&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;父类静态变量或静态代码块（按先后定义顺序）&lt;/li&gt; &lt;li&gt;子类静态变量或静态代码块（按先后定义顺序）&lt;/li&gt; &lt;li&gt;父类非静态代码块&lt;/li&gt; &lt;li&gt;父类构造函数&lt;/li&gt; &lt;li&gt;子类非静态代码块&lt;/li&gt; &lt;li&gt;子类的构造函数&lt;/li&gt; &lt;/ol&gt;</content:encoded>
      <pubDate>Wed, 18 Mar 2020 09:52:00 GMT</pubDate>
    </item>
    <item>
      <title>NoSQL HBase、Cassandra、MongoDB 对比</title>
      <link>https://www.zhangaoo.com/article/hbase-cassandra-mongodb</link>
      <content:encoded>&lt;h1&gt;NoSQL HBase、Cassandra、MongoDB 对比&lt;/h1&gt; &lt;p&gt;主要从生态、性能、服务化难易程度、技术栈方面来探索、对比 &lt;code&gt;HBase、Cassandra、MongoDB&lt;/code&gt; 三个数据库。&lt;/p&gt; &lt;h2&gt;排名（Ranking Trend）&lt;/h2&gt; &lt;p&gt;简单看一下当前的&lt;a href="https://db-engines.com/en/ranking_trend" target="_blank"&gt;排名&lt;/a&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;全量排名&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201982215282-all-db-rank.jpg" alt="201982215282-all-db-rank" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;宽表数据库排名&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019822153039-wide-column-store.jpg" alt="2019822153039-wide-column-store" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;时序数据库排名&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019822153219-TSDB.jpg" alt="2019822153219-TSDB" /&gt;&lt;/p&gt; &lt;h2&gt;HBase&lt;/h2&gt; &lt;p&gt;宽列式数据库，基于 &lt;code&gt;Apache Hadoop&lt;/code&gt; 和 &lt;code&gt;BigTable&lt;/code&gt; 的概念。&lt;code&gt;Hbase&lt;/code&gt; 有集中式架构， &lt;code&gt;Master&lt;/code&gt; 服务器负责监控集群中的所有 &lt;code&gt;RegionServer&lt;/code&gt;（负责服务和管理区域）实例，它也是查看所有元数据变化的界面。它提供了 &lt;code&gt;CAP&lt;/code&gt; 原理中的 &lt;code&gt;CP&lt;/code&gt;（一致性和可用性）。&lt;/p&gt; &lt;h3&gt;主要特点(HBase Features)&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;分布式和可扩展的庞大数据存储系统&lt;/li&gt; &lt;li&gt;强一致性&lt;/li&gt; &lt;li&gt;建立在Hadoop HDFS的基础上&lt;/li&gt; &lt;li&gt;&lt;code&gt;CAP&lt;/code&gt; 中的 &lt;code&gt;CP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;高可用，线性伸缩&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;使用场景（HBase Scenario）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Random, real-time read/write access to Big Data&lt;/li&gt; &lt;li&gt;快速读写的场景&lt;/li&gt; &lt;li&gt;基于 &lt;code&gt;Row Key&lt;/code&gt; 的范围扫描&lt;/li&gt; &lt;li&gt;对一致性有严格要求的场景&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;不擅长的场景（HBase Weakness）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;典型的事务型应用程序或甚至关系分析&lt;/li&gt; &lt;li&gt;应用程序需要全表扫描/模糊匹配&lt;/li&gt; &lt;li&gt;数据需要跨聚合、累积以及跨行分析&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Cassandra&lt;/h2&gt; &lt;p&gt;宽列式数据库，基于 &lt;code&gt;BigTable&lt;/code&gt; 和 &lt;code&gt;DynamoDB&lt;/code&gt; 的概念。&lt;code&gt;Cassandra&lt;/code&gt; 拥有分散式架构。任何节点都能执行任何操作。它提供了 &lt;code&gt;CAP&lt;/code&gt; 原理中的 &lt;code&gt;AP&lt;/code&gt;（可用性和分区可容忍性）。&lt;/p&gt; &lt;h3&gt;主要特点(Cassandra Features)&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;高可用性，逐步可扩展性&lt;/li&gt; &lt;li&gt;最终一致性，平衡一致性和延时&lt;/li&gt; &lt;li&gt;最小化管理&lt;/li&gt; &lt;li&gt;没有单一故障点 ―― &lt;code&gt;Cassandra&lt;/code&gt; 中所有节点都一样&lt;/li&gt; &lt;li&gt;&lt;code&gt;CAP&lt;/code&gt; 中的 &lt;code&gt;AP&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;使用场景（Cassandra Scenario）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;简单的安装，维护节点&lt;/li&gt; &lt;li&gt;快速随机性读取/写入&lt;/li&gt; &lt;li&gt;灵活的解析/宽列需求&lt;/li&gt; &lt;li&gt;不需要多个辅助索引&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;不擅长的场景（Cassandra Weakness）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;辅助索引&lt;/li&gt; &lt;li&gt;关系数据&lt;/li&gt; &lt;li&gt;事务型操作（回滚和提交）&lt;/li&gt; &lt;li&gt;主记录/财务记录&lt;/li&gt; &lt;li&gt;对数据需要严格的授权&lt;/li&gt; &lt;li&gt;针对列数据的动态查询/搜索&lt;/li&gt; &lt;li&gt;低延迟&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;MongoDB&lt;/h2&gt; &lt;p&gt;它是一种面向文档的数据库。&lt;code&gt;Mongodb&lt;/code&gt; 中的所有数据以 &lt;code&gt;JSON/BSON&lt;/code&gt; 格式来处理。它是一种无模式数据库，数据库中的数据量超过 &lt;code&gt;TB&lt;/code&gt; 级。它还支持主从复制方法，以便在服务器上复制数据的多个副本，从而使得某些应用系统中的数据整合来得更容易、更快速。&lt;/p&gt; &lt;p&gt;&lt;code&gt;MongoDB&lt;/code&gt; 保留了关系数据库最宝贵的功能特性：强一致性、表达式查询语言和辅助索引。因而，开发人员能够以比 &lt;code&gt;NoSQL&lt;/code&gt; 数据库更快的速度来构建高度实用的应用程序。&lt;/p&gt; &lt;h3&gt;主要特点(MongoDB Features)&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;应用程序完善后，模式随之变化（无模式）&lt;/li&gt; &lt;li&gt;支持全索引，实现高性能&lt;/li&gt; &lt;li&gt;复制和故障切换，实现高可用性&lt;/li&gt; &lt;li&gt;自动分片，易于扩展&lt;/li&gt; &lt;li&gt;基于丰富文档的查询，易于读取&lt;/li&gt; &lt;li&gt;主从模式&lt;/li&gt; &lt;li&gt;&lt;code&gt;CAP&lt;/code&gt; 中的 &lt;code&gt;CP&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;使用场景（MongoDB Scenario）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;取代Web应用的RDBMS&lt;/li&gt; &lt;li&gt;半结构化内容管理&lt;/li&gt; &lt;li&gt;实时分析和高速日志、缓存和高扩展性&lt;/li&gt; &lt;li&gt;Web 2.0、媒体、SaaS和游戏&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;不擅长的场景（MongoDB Weakness）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;高度事务型系统&lt;/li&gt; &lt;li&gt;存在传统数据库需求的应用程序，比如外键约束。&lt;/li&gt; &lt;li&gt;内存占用较高&lt;/li&gt; &lt;li&gt;Limited Data Size，not more than 16MB&lt;/li&gt; &lt;li&gt;Limited Nesting，cannot perform nesting of documents for more than 100 levels&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;NoSQL Performance Benchmarks&lt;/h2&gt; &lt;h3&gt;第三方测试结果(Third party test results)&lt;/h3&gt; &lt;p&gt;&lt;a href="https://www.datastax.com/nosql-databases/benchmarks-cassandra-vs-mongodb-vs-hbase" target="_blank"&gt;benchmarks-cassandra-vs-mongodb-vs-hbase&lt;/a&gt;&lt;/p&gt; &lt;h4&gt;Throughput by Workload&lt;/h4&gt; &lt;p&gt;Each workload appears below with the throughput/operations-per-second (more is better) graphed vertically, the number of nodes used for the workload displayed horizontally, and a table with the result numbers following each graph.&lt;/p&gt; &lt;h4&gt;Load process&lt;/h4&gt; &lt;p&gt;For load, Couchbase, HBase, and MongoDB all had to be configured for non-durable writes to complete in a reasonable amount of time, with Cassandra being the only database performing durable write operations. Therefore, the numbers below for Couchbase, HBase, and MongoDB represent non-durable write metrics&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019823111135-nosql-load-test.jpg" alt="2019823111135-nosql-load-test" /&gt;&lt;/p&gt; &lt;h4&gt;Mixed Operational and Analytical Workload&lt;/h4&gt; &lt;p&gt;Note that Couchbase was eliminated from this test because it does not support scan operations (producing the error: “Range scan is not supported”).&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019823111331-mix-test.jpg" alt="2019823111331-mix-test" /&gt;&lt;/p&gt; &lt;h3&gt;实测(Testing)&lt;/h3&gt; &lt;h4&gt;测试方法(Testing Method)&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;Yahoo&lt;/code&gt; 开源的测试套件 &lt;a href="https://github.com/brianfrankcooper/YCSB" target="_blank"&gt;YCSB&lt;/a&gt;，验证测试 Workload A 用例&lt;/li&gt; &lt;li&gt;每次测试前都重启机器，重新启动相应的服务，然后进行测试&lt;/li&gt; &lt;li&gt;在单机单节点默认配置进行测试，硬件信息如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# CPU 信息 $ lscpu Architecture:          x86_64 CPU op-mode(s):        32-bit, 64-bit Byte Order:            Little Endian CPU(s):                8 On-line CPU(s) list:   0-7 Thread(s) per core:    2 Core(s) per socket:    4 Socket(s):             1 NUMA node(s):          1 Vendor ID:             GenuineIntel CPU family:            6 Model:                 142 Model name:            Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz Stepping:              10 CPU MHz:               701.941 CPU max MHz:           3400.0000 CPU min MHz:           400.0000 BogoMIPS:              3600.00 Virtualization:        VT-x L1d cache:             32K L1i cache:             32K L2 cache:              256K L3 cache:              6144K NUMA node0 CPU(s):     0-7 # 内存信息 $ free -m               total        used        free      shared  buff/cache   available Mem:           7837        2914        2079           4        2843        4526 Swap:          8051  # 硬盘信息 $ lsblk NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT sda      8:0    0 238.5G  0 disk  ├─sda2   8:2    0 230.1G  0 part / ├─sda3   8:3    0   7.9G  0 part [SWAP] └─sda1   8:1    0   512M  0 part /boot/efi &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;Testing Result&lt;/h4&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th align="left"&gt;序号&lt;/th&gt;&lt;th align="left"&gt;组件&lt;/th&gt;&lt;th align="center"&gt;版本&lt;/th&gt;&lt;th align="left"&gt;测试类型&lt;/th&gt;&lt;th align="left"&gt;Throughput(ops/sec)&lt;/th&gt;&lt;th align="left"&gt;95thPercentileLatency(us)&lt;/th&gt;&lt;th align="left"&gt;99thPercentileLatency(us)&lt;/th&gt;&lt;th align="left"&gt;测试命令&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td align="left"&gt;1&lt;/td&gt;&lt;td align="left"&gt;Cassandra&lt;/td&gt;&lt;td align="center"&gt;apache-cassandra-3.11.4&lt;/td&gt;&lt;td align="left"&gt;load&lt;/td&gt;&lt;td align="left"&gt;6122.199&lt;/td&gt;&lt;td align="left"&gt;223&lt;/td&gt;&lt;td align="left"&gt;263&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb load cassandra-cql -P workloads/workloada -p hosts=&amp;quot;127.0.0.1&amp;quot; -p recordcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;2&lt;/td&gt;&lt;td align="left"&gt;Cassandra&lt;/td&gt;&lt;td align="center"&gt;apache-cassandra-3.11.4&lt;/td&gt;&lt;td align="left"&gt;run&lt;/td&gt;&lt;td align="left"&gt;6473.875&lt;/td&gt;&lt;td align="left"&gt;211(READ),170(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;262(READ),225(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb run cassandra-cql -P workloads/workloada -p hosts=&amp;quot;127.0.0.1&amp;quot; -p operationcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;3&lt;/td&gt;&lt;td align="left"&gt;hbase1.2&lt;/td&gt;&lt;td align="center"&gt;hbase-1.2.6.1/hadoop-3.0.3/zookeeper-3.4.13&lt;/td&gt;&lt;td align="left"&gt;load&lt;/td&gt;&lt;td align="left"&gt;2984.723&lt;/td&gt;&lt;td align="left"&gt;431&lt;/td&gt;&lt;td align="left"&gt;734&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb load hbase10 -P workloads/workloada -p table=usertable -p columnfamily=family -p recordcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;4&lt;/td&gt;&lt;td align="left"&gt;hbase1.2&lt;/td&gt;&lt;td align="center"&gt;hbase-1.2.6.1/hadoop-3.0.3/zookeeper-3.4.13&lt;/td&gt;&lt;td align="left"&gt;run&lt;/td&gt;&lt;td align="left"&gt;3164.362&lt;/td&gt;&lt;td align="left"&gt;349(READ),535(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;777(READ),1444(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb run hbase10 -P workloads/workloada -p table=usertable -p columnfamily=family -p operationcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;5&lt;/td&gt;&lt;td align="left"&gt;hbase2.2&lt;/td&gt;&lt;td align="center"&gt;hbase-2.2.0/hadoop-3.0.3/zookeeper-3.4.13&lt;/td&gt;&lt;td align="left"&gt;load&lt;/td&gt;&lt;td align="left"&gt;4538.811&lt;/td&gt;&lt;td align="left"&gt;281&lt;/td&gt;&lt;td align="left"&gt;405&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb load hbase20 -P workloads/workloada -p table=usertable -p columnfamily=family -p recordcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;6&lt;/td&gt;&lt;td align="left"&gt;hbase2.2&lt;/td&gt;&lt;td align="center"&gt;hbase-2.2.0/hadoop-3.0.3/zookeeper-3.4.13&lt;/td&gt;&lt;td align="left"&gt;run&lt;/td&gt;&lt;td align="left"&gt;4200.772&lt;/td&gt;&lt;td align="left"&gt;292(READ),335(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;360(READ),433(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb run hbase20 -P workloads/workloada -p table=usertable -p columnfamily=family -p operationcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;7&lt;/td&gt;&lt;td align="left"&gt;MongoDB&lt;/td&gt;&lt;td align="center"&gt;MongoDB shell version: 2.6.10&lt;/td&gt;&lt;td align="left"&gt;load&lt;/td&gt;&lt;td align="left"&gt;9834.437&lt;/td&gt;&lt;td align="left"&gt;98&lt;/td&gt;&lt;td align="left"&gt;341&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb load mongodb -P workloads/workloada -p recordcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;8&lt;/td&gt;&lt;td align="left"&gt;MongoDB&lt;/td&gt;&lt;td align="center"&gt;MongoDB shell version: 2.6.10&lt;/td&gt;&lt;td align="left"&gt;run&lt;/td&gt;&lt;td align="left"&gt;19780.046&lt;/td&gt;&lt;td align="left"&gt;31(READ),67(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;59(READ),92(UPDATE)&lt;/td&gt;&lt;td align="left"&gt;&lt;code&gt;bin/ycsb run mongodb -P workloads/workloada -p operationcount=2000000 -p threads=10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;以上是自己的测试结果，和第三方给出的测试结果不一致，主要是 &lt;code&gt;MongoDB&lt;/code&gt; 性能相比第三方好很多；简单分析原因：第一，本次为验证性试验只测试了单节点小数据量的场景；第二此时场景较少，只测试了数据写入和数据读取更新两种场景；第三，&lt;code&gt;MongoDB&lt;/code&gt; 虽然也是 &lt;code&gt;NoSQL&lt;/code&gt; 数据库，其实现原理和 &lt;code&gt;HBase&lt;/code&gt; 和 &lt;code&gt;Cassandra&lt;/code&gt; 还是有本质区别。简单测试了一下当把写入数据量增加到 &lt;code&gt;20000000&lt;/code&gt; 写入性能有明显下降，当数据规模达到一定程度测试结果应该与第三方结果一致。&lt;/p&gt; &lt;h2&gt;对比(Comprasion)&lt;/h2&gt; &lt;p&gt;从架构、性能、生态、服务化难易程度，以及团队的技术栈等方面做一个简单的分析对比，为选型提供数据支撑。这里忽略了一个比较重要的因素，就是业务，因为当前业务场景待定，暂时忽略这个因素。实际情况可能会根据不同的业务来选取不同类型的数据库。&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th align="left"&gt;Name&lt;/th&gt;&lt;th align="left"&gt;HBase&lt;/th&gt;&lt;th align="left"&gt;Cassandra&lt;/th&gt;&lt;th align="left"&gt;MongoDB&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td align="left"&gt;架构&lt;/td&gt;&lt;td align="left"&gt;Wide Column Store&lt;/td&gt;&lt;td align="left"&gt;Wide Column Store&lt;/td&gt;&lt;td align="left"&gt;Document Store&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;Replication&lt;/td&gt;&lt;td align="left"&gt;Master-Slave Replication&lt;/td&gt;&lt;td align="left"&gt;Masterless Ring&lt;/td&gt;&lt;td align="left"&gt;Master-Slave Replication&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;Programming Language (Base Code)&lt;/td&gt;&lt;td align="left"&gt;Java&lt;/td&gt;&lt;td align="left"&gt;Java&lt;/td&gt;&lt;td align="left"&gt;C++&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;服务化&lt;/td&gt;&lt;td align="left"&gt;Difficulty&lt;/td&gt;&lt;td align="left"&gt;Normal&lt;/td&gt;&lt;td align="left"&gt;Normal&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;性能&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;(参考以上数据)&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;大数据生态圈&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️⭐️&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;使用场景&lt;/td&gt;&lt;td align="left"&gt;Online Log Analytics, Hadoop, Write Heavy Applications, MapReduc&lt;/td&gt;&lt;td align="left"&gt;Sensor Data, Messaging Systems, E-commerce Websites, Always-On Applications, Fraud Detection for Banks&lt;/td&gt;&lt;td align="left"&gt;Operational Intelligence, Product Data Management, Content Management Systems, IoT, Real-Time Analytics&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;团队技术栈&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️&lt;/td&gt;&lt;td align="left"&gt;⭐️⭐️⭐️&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;※ 以上 ⭐ ️评价目前是以我掌握的信息主观评价，并不一定客观&lt;/p&gt; &lt;h2&gt;结论(Summary)&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;三种 &lt;code&gt;NoSQL&lt;/code&gt; 数据库并没有一种在各方面都完全性压倒优势的，不同的场景个有优缺点&lt;/li&gt; &lt;li&gt;&lt;code&gt;HBase&lt;/code&gt; 大数据生态最为完整和丰富，案列解决方案较多，但是部署、维护、调优、服务化复杂，依赖组件较多。&lt;/li&gt; &lt;li&gt;&lt;code&gt;MongoDB&lt;/code&gt; 提供了NoSQL数据库的数据模型灵活性、弹性可扩展性以及高性能，大数据生态不如 HBase，数据的存储、压缩能力远不如 &lt;code&gt;HBase&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Cassandra&lt;/code&gt; 技术实现和 &lt;code&gt;HBase&lt;/code&gt; 类似，但架构相比 &lt;code&gt;HBase&lt;/code&gt; 更简单，服务化比 HBase 更容易，大数据生态不如  &lt;code&gt;HBase&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;中短期考虑 &lt;code&gt;DBaaS&lt;/code&gt; 探索研究可以先使用 &lt;code&gt;Cassandra&lt;/code&gt; 尝试，先掌握服务化尤其是在 &lt;code&gt;K8s&lt;/code&gt; 架构中的服务化。&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;参考(Reference)&lt;/h1&gt; &lt;p&gt;&lt;a href="https://www.datastax.com/wp-content/themes/datastax-2014-08/files/NoSQL_Benchmarks_EndPoint.pdf" target="_blank"&gt;NoSQL_Benchmarks_EndPoint&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://db-engines.com/en/system/HBase%3BMongoDB" target="_blank"&gt;System Properties Comparison HBase vs. MongoDB&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.datastax.com/apache-cassandra-leads-nosql-benchmark" target="_blank"&gt;apache-cassandra-leads-nosql-benchmark&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.mongodb.com/compare/mongodb-hbase" target="_blank"&gt;mongodb-hbase&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://info.couchbase.com/rs/302-GJY-034/images/2016_NoSQL_Database_Performance_Report.pdf" target="_blank"&gt;2016_NoSQL_Database_Performance_Report&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://logz.io/blog/nosql-database-comparison/" target="_blank"&gt;nosql-database-comparison&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://db-engines.com/en/ranking_trend" target="_blank"&gt;DB-Engines Ranking&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 15 Mar 2020 14:05:00 GMT</pubDate>
    </item>
    <item>
      <title>阿里、Oracle、IBM DBaaS 调研学习学习</title>
      <link>https://www.zhangaoo.com/article/study-dbaas</link>
      <content:encoded>&lt;h1&gt;数据库服务化规范调研学习&lt;/h1&gt; &lt;h2&gt;维基百科的定义&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;面向服务的体系结构（英语：&lt;a href="https://en.wikipedia.org/wiki/Service-oriented_architecture" target="_blank"&gt;service-oriented architecture&lt;/a&gt;）并不特指一种技术，而是一种分布式运算的软件设计方法。软件的部分组件（调用者），可以透过网络上的通用协议调用另一个应用软件组件运行、运作，让调用者获得服务。SOA原则上采用开放标准、与软件资源进行交互并采用表示的标准方式。因此应能跨越厂商、产品与技术。一项服务应视为一个独立的功能单元，可以远程访问并独立运行与更新，例如在线查询信用卡账单。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;四个特性如下：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;针对某特定要求的输出，该服务就是运作一项商业逻辑&lt;/li&gt; &lt;li&gt;具有完备的特性（&lt;code&gt;self-contained&lt;/code&gt;）&lt;/li&gt; &lt;li&gt;消费者并不需要了解此服务的运作过程&lt;/li&gt; &lt;li&gt;可能由底层其他服务组成&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;指导开发的基本原则：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;可重复使用、粒度、模块性、可组合型、对象化原件、构件化以及具交互操作性&lt;/li&gt; &lt;li&gt;符合开放标准（通用的或行业的）&lt;/li&gt; &lt;li&gt;服务的识别和分类，提供和发布，监控和跟踪。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;背景&lt;/h2&gt; &lt;p&gt;为什么要服务化？&lt;/p&gt; &lt;h3&gt;技术角度&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;架构分层复用&lt;/li&gt; &lt;li&gt;平台服务、同类服务的抽象与整合&lt;/li&gt; &lt;li&gt;可靠、可扩展、容易维护&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;商业角度&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Reduced Costs&lt;/li&gt; &lt;li&gt;Business user benefit&lt;/li&gt; &lt;li&gt;Contribution to strategic business goals and objectives&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;用户角度&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;高质量服务&lt;/li&gt; &lt;li&gt;简单易用，摒弃繁杂的运维&lt;/li&gt; &lt;li&gt;按需使用，按量付费&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;案列调查&lt;/h2&gt; &lt;h3&gt;阿里云&lt;/h3&gt; &lt;h4&gt;DBaaS&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019820151549-slide-10.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019820151549-slide-10" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019820152010-slide-11.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019820152010-slide-11" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019820152024-slide-12.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019820152024-slide-12" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019820152036-slide-13.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019820152036-slide-13" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019820152059-slide-14.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019820152059-slide-14" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201982015219-slide-15.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="201982015219-slide-15" /&gt;&lt;/p&gt; &lt;h4&gt;阿里云HBase架构&lt;/h4&gt; &lt;p&gt;&lt;img src="https://yqfile.alicdn.com/0195348c6728849eee81d6dca80b5520500de4b0.png" alt="阿里云HBase架构" /&gt;&lt;/p&gt; &lt;h4&gt;云数据库HBase2.0产品架构&lt;/h4&gt; &lt;p&gt;&lt;img src="https://img.alicdn.com/tfs/TB1yDwguMmTBuNjy1XbXXaMrVXa-1200-792.jpg" alt="云数据库HBase2.0产品架构" /&gt;&lt;/p&gt; &lt;h4&gt;理解&lt;/h4&gt; &lt;p&gt;阿里云的数据库服务化应该是基于基础设施的虚拟化进行的，基础设施比如 &lt;code&gt;VM&lt;/code&gt; 为最底层的资源池，在资源池的基础上定义服务单元。比如以 &lt;code&gt;HBase&lt;/code&gt; 为例，在物理层会定义多种类型的物理实例，比如 &lt;code&gt;2 Master 1 RegionServer&lt;/code&gt;、&lt;code&gt;2 Master&lt;/code&gt; 多 &lt;code&gt;RegionServer&lt;/code&gt; 就是这些最基本的物理实例构成了上层的服务支持。阿里云购买 &lt;code&gt;HBase&lt;/code&gt; 服务界面如下：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201982015497-hbase-buy.png" alt="201982015497-hbase-buy" /&gt;&lt;/p&gt; &lt;h3&gt;Oracle&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Oracle&lt;/code&gt; 的 &lt;code&gt;DBaaS&lt;/code&gt; 主要是由 &lt;code&gt;OEM12C&lt;/code&gt; 实现的&lt;/p&gt; &lt;p&gt;当前，大量使用Oracle数据库的客户面临以下一些问题：&lt;/p&gt; &lt;p&gt;&lt;img src="http://dbaplus.cn/uploadfile/2016/0803/20160803031609571.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://dbaplus.cn/uploadfile/2016/0803/20160803031617453.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h4&gt;DBaaS的优势是&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;自助服务式的数据库部署和管理；&lt;/li&gt; &lt;li&gt;预打包、预配置数据库配置；&lt;/li&gt; &lt;li&gt;一键式数据库部署；&lt;/li&gt; &lt;li&gt;底层平台的按需可伸缩性；&lt;/li&gt; &lt;li&gt;高效利用硬件和资源；&lt;/li&gt; &lt;li&gt;明确的计量和收费；&lt;/li&gt; &lt;li&gt;实现开发人员的极致“敏捷”，IT的“企业级”控制。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;DBaaS Conceptual Model&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821142052-oracle-DBaaS-Conceptual-Model.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821142052-oracle-DBaaS-Conceptual-Model" /&gt;&lt;/p&gt; &lt;h4&gt;DBaaS Service Catalog&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821143144-DBaaS-Service-Catalog.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821143144-DBaaS-Service-Catalog" /&gt;&lt;/p&gt; &lt;h4&gt;Oracle DBaaS 的架构&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821142647-oracle-DBaaS-Infrastructure-Model.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821142647-oracle-DBaaS-Infrastructure-Model" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://dbaplus.cn/uploadfile/2016/0803/20160803031646834.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="alt" /&gt;&lt;/p&gt; &lt;h4&gt;实施OEM12C的流程&lt;/h4&gt; &lt;p&gt;&lt;img src="http://dbaplus.cn/uploadfile/2016/0803/20160803031707193.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://dbaplus.cn/uploadfile/2016/0803/20160803031719999.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="alt" /&gt;&lt;/p&gt; &lt;h4&gt;理解&lt;/h4&gt; &lt;p&gt;从 &lt;code&gt;Oracle&lt;/code&gt; &lt;a href="https://www.oracle.com/technetwork/topics/entarch/oes-refarch-dbaas-508111.pdf" target="_blank"&gt;白皮书&lt;/a&gt;中从各个角度阐述了 &lt;code&gt;DBaaS&lt;/code&gt; 带来的益处。技术架构理解和阿里云类似，应该还是底层的基础设施的虚拟化，在此基础上建成相应的服务资源池，然后以 &lt;code&gt;Service Catalog&lt;/code&gt; 的形式给服务消费者提供服务。&lt;/p&gt; &lt;h2&gt;IBM&lt;/h2&gt; &lt;h3&gt;Open Platform for DBaaS on Power Systems solution&lt;/h3&gt; &lt;p&gt;The Open Platform for DBaaS on Power Systems solution is a solution that integrates several components, including software and hardware, and implements a complete environment that is easy to use and fast for deploying open source databases such as MariaDB, MongoDB, MySQL, PostgreSQL, and Redis. This solution provides all the necessary components to create quickly a database instance within minutes, providing you with an interface to connect to such a database and start developing your application.&lt;/p&gt; &lt;h4&gt;The Open Platform for DBaaS on Power Systems components&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821171548-The-Open-Platform-for-DBaaS-on-Power-Systems-components.jpg" alt="2019821171548-The-Open-Platform-for-DBaaS-on-Power-Systems-components" /&gt;&lt;/p&gt; &lt;h4&gt;Why use the Open Platform for DBaaS on Power Systems solution&lt;/h4&gt; &lt;p&gt;Several sources of data and how an environment with multiple database engines, specially open source databases, can benefit and take advantage of a mix of structured and unstructured data.&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821191656-mixed-and-optimized-database.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821191656-mixed-and-optimized-database" /&gt;&lt;/p&gt; &lt;h4&gt;The Open Platform for DBaaS on Power Systems solution architecture&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821191930-DBaaS-on-power-system.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821191930-DBaaS-on-power-system" /&gt;&lt;/p&gt; &lt;h4&gt;DBaaS elastic cloud infrastructure&lt;/h4&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201982119232-DBaaS-integration-on-existing-cloud-environments.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="201982119232-DBaaS-integration-on-existing-cloud-environments" /&gt;&lt;/p&gt; &lt;h4&gt;Kernel-based Virtual Machine&lt;/h4&gt; &lt;p&gt;The KVM virtualization feature runs on Linux, and transforms the Linux OS into a hypervisor, enabling it to run multiple VMs. KVM provides an open source virtualization choice for scale-out Power Systems servers, taking advantage of the performance, scalability, and security features of Power Systems servers.&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019821193643-Kernel-based-Virtual-Machine.jpg?imageView2/2/w/800/h/800/q/75%7Cimageslim" alt="2019821193643-Kernel-based-Virtual-Machine" /&gt;&lt;/p&gt; &lt;h1&gt;小结&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;对 &lt;code&gt;DBaaS&lt;/code&gt; 理解：主要从技术角度理解，&lt;code&gt;DBaaS&lt;/code&gt; 是对 &lt;code&gt;Database&lt;/code&gt; 资源的更高层次的抽象和整合，包含 &lt;code&gt;Relational DB&lt;/code&gt;，&lt;code&gt;NoSQL DB&lt;/code&gt;，其目的是为了高效的维护、开发、扩展和使用这一类服务。服务化的技术实现有多种方式，传统的在基础设施资源池的技术实现，以及现在容器化技术兴起的技术实现方式。&lt;/li&gt; &lt;li&gt;目前学习了解到的资料基本都是在硬件资源虚拟化话的的基础上形成一个资源池，然后再资源池的基础上构建应用池，以应用池为单位搭配组合出不同规格的数据库服务。&lt;/li&gt; &lt;li&gt;目前获得的已知的资料中，应用于生产基于容器化实现的 &lt;code&gt;NoSQL DBaaS&lt;/code&gt; 不是太多，不同架构的的 &lt;code&gt;（NoSQL）DB&lt;/code&gt; 其实现难度也是不同的，因为对于数据库这种有状态的服务其本身比无状态服务更复杂，不少系统因为本身的架构特点，比如 &lt;code&gt;Cassandra&lt;/code&gt; 相比 &lt;code&gt;HBase&lt;/code&gt; 移植起来就更容易。一些案例如下： &lt;ul&gt; &lt;li&gt;&lt;a href="https://github.com/IBM/Scalable-Cassandra-deployment-on-Kubernetes/blob/master/README-cn.md" target="_blank"&gt;Cassandra&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://diamanti.com/resource/do-you-have-what-it-takes-to-run-database-as-a-service-on-kubernetes/" target="_blank"&gt;Run Database-as-a-Service On Kubernetes&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;部分参考资料&lt;/h1&gt; &lt;p&gt;&lt;a href="https://myslide.cn/slides/3392?vertical=1" target="_blank"&gt;阿里云数据库平台架构演进之路&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://yq.aliyun.com/articles/346438?utm_content=m_39940" target="_blank"&gt;阿里云HBase产品体系架构及特性解析&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://img.alicdn.com/tfs/TB1yDwguMmTBuNjy1XbXXaMrVXa-1200-792.jpg" target="_blank"&gt;云数据库HBase2.0产品架构&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.slideshare.net/objectrocket/database-as-a-service-dbaas-on-kubernetes" target="_blank"&gt;Database as a Service (DBaaS) on Kubernetes&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://diamanti.com/resource/do-you-have-what-it-takes-to-run-database-as-a-service-on-kubernetes/" target="_blank"&gt;Run Database-as-a-Service On Kubernetes&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://yq.aliyun.com/articles/78877" target="_blank"&gt;通过数据云解决方案DBaaS&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://yq.aliyun.com/articles/531035?spm=5176.10695662.1996646101.searchclickresult.75a74b3dXhjbXF" target="_blank"&gt;NoSQL数据库 Cassandra&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.oracle.com/technetwork/topics/entarch/oes-refarch-dbaas-508111.pdf" target="_blank"&gt;An Architect’s Guide to the Oracle Private Database Cloud&lt;/a&gt;&lt;/p&gt; &lt;p&gt;[IBM Open Platform for DBaaS on IBM Power Systems&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 15 Mar 2020 13:26:00 GMT</pubDate>
    </item>
    <item>
      <title>Facebook Gorilla 时序数据压缩原理</title>
      <link>https://www.zhangaoo.com/article/gorilla-tsdb-compression</link>
      <content:encoded>&lt;img src="http://img.zhangaoo.com/202031512839-golrilla-facebook.jpg" alt="202031512839-golrilla-facebook" style="zoom:50%;" /&gt; &lt;h3&gt;Facebook Gorilla 时序数据压缩原理&lt;/h3&gt; &lt;p&gt;Gorilla源自Facebook对于内部基础设施的近乎”变态”的监控需求，我们先来看看这些数据：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;20亿个不同的Time Series&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;每分钟产生7亿个Data Points，即每秒钟产生1200万Data Points&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;数据需要存储26个小时&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;高峰期的查询高达40000次每秒&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;查询时延需要小于1ms&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;每个 Time Series每分钟可产生4个Data Points&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;每年的数据增长率为200%&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;对于监控数据，本身还具有如下几个典型特点：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;重写轻读&lt;/strong&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;以&lt;strong&gt;聚合分析&lt;/strong&gt;为主，几乎不存在针对单Data Point的点查场景。因此，即使丢失个别的Data Points，一般不会影响到整体的分析结果。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;越老的数据价值越低，而应用通常只读取最近发生的数据。&lt;strong&gt;Facebook的统计表明，85%的查询与最近26个小时的数据写入有关。&lt;/strong&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Timestamp压缩(Delta-Of-Delta)&lt;/h3&gt; &lt;p&gt;在大多数情形下，可以将一个Time Series中的连续的Data Points的Timestamp列表视作一个&lt;strong&gt;等差数列&lt;/strong&gt;，这是Delta-Of-Delta编码算法的最适用场景。编码原理如下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;在Block Header中，存放起始Timestamp T(-1)..一个Block对应2个小时的时间窗口。假设第一个Data Point的Timestamp为T(0)，那么，实际存放时，我们只需要存T(0)与T(-1)的差值。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;对于接下来的Data Point的Timestamp T(N), 按如下算法计算Delta Of Delta值：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;D = (T(N) – T(N-1)) – (T(N-1) – T(N-2)) &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;按照D的值所处的区间范围，分别有5种不同情形的处理：&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;如果D为0，那么，存储一个bit ‘0’&lt;/li&gt; &lt;li&gt;如果D位于区间[-63, 64]，存储2个bits ’10’，后面跟着用7个bits表示的D值&lt;/li&gt; &lt;li&gt;如果D位于区间[-255, 256]，存储3个bits ‘110’，后面跟着9个bits表示的D值&lt;/li&gt; &lt;li&gt;如果D位于区间[-2047, 2048]，存储4个bits ‘1110’，后面跟着12个bits表示的D值&lt;/li&gt; &lt;li&gt;如果D位于其它区间则存储4个bits ‘1111’，后面跟着32个bits表示的D值&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;关于这些Range的选取，是出于个别Data Points可能会缺失的考虑。例如：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;假设正常的Interval为每60秒产生一个Data Point，如果缺失一个Data Point，那么，相邻的两个Data Points之间的Delta值为：60, 60, 121 and 59，此时，Delta Of Delta值将变为： 0, 61, -62。&lt;/p&gt; &lt;p&gt;这恰好落在区间[-63, 64]之间。&lt;/p&gt; &lt;p&gt;如果缺失4个Data Point，那么，Delta Of Delta值将落在区间[-255, 256]之间。&lt;/p&gt; &lt;p&gt;下图针对Gorilla中的440,000条真实的Data Points采样数据，对Timestamp数据应用了Delta-Of-Delta编码之后的效果：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020315114519-DeltaOfDelta-Mini.png" alt="2020315114519-DeltaOfDelta-Mini" style="zoom:60%;" /&gt; &lt;p&gt;&lt;strong&gt;96.39%的Timestamps只需要占用一个Bit的空间&lt;/strong&gt;，这样看来，压缩的效果非常明显。&lt;/p&gt; &lt;h3&gt;Point Value压缩（XOR）&lt;/h3&gt; &lt;p&gt;Gorilla中限制Point Value的类型为&lt;strong&gt;双精度浮点数&lt;/strong&gt;，在未启用任何压缩编码的前提下，每个Point Value理应占用64个Bits。&lt;/p&gt; &lt;p&gt;同样，Facebook在认真调研了ODS中的数据特点以后也有了这样一个&lt;strong&gt;关键发现&lt;/strong&gt;：&lt;strong&gt;在大多数Time Series中，相邻的Data Points的Value变化比较轻微&lt;/strong&gt;。这一点比较好理解，假设某一个Time Series关联某个仪器温度指标的监控，那么，温度的变化应该是渐进式的而非跳跃式的。&lt;strong&gt;XOR编码&lt;/strong&gt;就是用来描述相邻两条Point Value的变化部分，下图直观描述了Point Value “24.0”与”12.0″的变化部分：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/202031511477-XOR-EFFECT.png" alt="202031511477-XOR-EFFECT" style="zoom:60%;" /&gt; &lt;p&gt;&lt;strong&gt;XOR编码&lt;/strong&gt;详细原理如下：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;第一个 &lt;code&gt;Value&lt;/code&gt;存储时不做任何压缩。&lt;/li&gt; &lt;li&gt;后面产生的每一个&lt;code&gt;Value&lt;/code&gt;与前一个&lt;code&gt;Value&lt;/code&gt;计算&lt;code&gt;XOR&lt;/code&gt;值： &lt;ul&gt; &lt;li&gt;如果XOR值为0，即两个Value相同，那么存为’0’，只占用一个bit。&lt;/li&gt; &lt;li&gt;如果XOR为非0，首先计算XOR中位于&lt;strong&gt;前端&lt;/strong&gt;的和&lt;strong&gt;后端&lt;/strong&gt;的0的个数，即Leading Zeros与Trailing Zeros。&lt;/li&gt; &lt;li&gt;第一个bit值存为’1’。&lt;/li&gt; &lt;li&gt;如果Leading Zeros与Trailing Zeros与前一个XOR值相同，则第2个bit值存为’0’，而后，紧跟着去掉Leading Zeros与Trailing Zeros以后的&lt;strong&gt;有效XOR值&lt;/strong&gt;部分。&lt;/li&gt; &lt;li&gt;如果Leading Zeros与Trailing Zeros与前一个XOR值不同，则第2个bit值存为’1’，而后，紧跟着5个bits用来描述Leading Zeros的值，再用6个bits来描述&lt;strong&gt;有效XOR值&lt;/strong&gt;的长度，最后再存储&lt;strong&gt;有效XOR值&lt;/strong&gt;部分（这种情形下，至少产生了13个bits的冗余信息）&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;如下是针对Gorilla中1,600,000个Point Value采样数据采用了XOR压缩编码后的效果：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020315115236-XOR-Mini.png" alt="2020315115236-XOR-Mini" style="zoom:60%;" /&gt; &lt;p&gt;从结果来看：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;只占用1个bit的Value比例高达&lt;strong&gt;59.06%&lt;/strong&gt;，这说明约一半以上的Point Value较之上一个Value并未发生变化。&lt;/li&gt; &lt;li&gt;30%比例的Value平均占用26.6 bits，即上面的情形2.1。&lt;/li&gt; &lt;li&gt;余下的12.64%的Value平均占用39.6 bits，即上面的情形2.2。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;另外，XOR压缩编码机制，对于Integer类型的Point Value效果尤为显著。&lt;/p&gt; &lt;h3&gt;合理的Block大小&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;压缩算法通常都是基于Block进行的，Block越大，效果越明显&lt;/strong&gt;。对于时序数据的压缩，则需要选择一个合理的时间窗大小的数据进行压缩。Facebook测试了不同的时间窗大小的压缩效果：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020315115512-Compression-Timewindow.png" alt="2020315115512-Compression-Timewindow" style="zoom:60%;" /&gt; &lt;p&gt;可以看出来，随着时间窗的逐步变大，压缩效果的确越显著。但超过2个小时(120 minutes)的时间窗大小以后，随着时间窗口的逐步变大，压缩效果的改善并不明显。&lt;strong&gt;时间窗口为2小时的每个Data Point平均只占用1.37 bytes&lt;/strong&gt;。&lt;/p&gt; &lt;h3&gt;参考&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://www.vldb.org/pvldb/vol8/p1816-teller.pdf" target="_blank"&gt;Gorilla: A Fast, Scalable, In-Memory Time Series Database&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://bbs.huaweicloud.com/blogs/103626" target="_blank"&gt;Facebook的时序数据库技术&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sun, 15 Mar 2020 04:04:00 GMT</pubDate>
    </item>
    <item>
      <title>时序数据库OpenTSDB介绍</title>
      <link>https://www.zhangaoo.com/article/opentsdb-introduce</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202031512132-opentsdb-img.jpg" alt="202031512132-opentsdb-img" /&gt;&lt;/p&gt; &lt;h2&gt;OpenTSDB&lt;/h2&gt; &lt;h3&gt;Hbase 背景&lt;/h3&gt; &lt;img src="http://img.zhangaoo.com/202031415232-opentsdb-hbase.jpg" alt="202031415232-opentsdb-hbase" style="zoom:50%;" /&gt; &lt;p&gt;HBase或表格存储这类底层采用 LSM-tree (The Log-Structured Merge-Tree (LSM-Tree))的数据库中，表数据会按列存储。每行中的每一列在存储文件中都会以Key-value的形式存在于文件中。其中Key的结构为：行主键 + 列名，Value为列的值。该种存储结构的特点是：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;每行主键会重复存储，取决于列的个数&lt;/li&gt; &lt;li&gt;列名会重复存储，每一列的存储都会携带列名&lt;/li&gt; &lt;li&gt;存储数据按row-key排序，相邻的row-key会存储在相邻的块中&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;OpenTSDB的基本概念&lt;/h3&gt; &lt;p&gt;OpenTSDB定义每个时间序列数据需要包含以下属性：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;指标名称（metric name）&lt;/li&gt; &lt;li&gt;时间戳（UNIX timestamp，毫秒或者秒精度）&lt;/li&gt; &lt;li&gt;值（64位整数或者单精度浮点数）&lt;/li&gt; &lt;li&gt;一组标签（tags，用于描述数据属性，至少包含一个或多个标签，每个标签由tagKey和tagValue组成，tagKey和tagValue均为字符串）&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;举个例子，在监控场景中，我们可以这样定义一个监控指标：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;指标名称：     sys.cpu.user 标签：     host = 10.101.168.111     cpu = 0 指标值：     0.5 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;指标名称代表这个监控指标是对用户态CPU的使用监控，引入了两个标签，分别标识该监控位于哪台机器的哪个核。&lt;/p&gt; &lt;p&gt;OpenTSDB支持的查询场景为：指定指标名称和时间范围，给定一个或多个标签名称和标签的值作为条件，查询出所有的数据。&lt;/p&gt; &lt;p&gt;以上面那个例子举例，我们可以查询：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;sys.cpu.user (host=&lt;em&gt;,cpu=&lt;/em&gt;)(1465920000 &amp;lt;= timestamp &amp;lt; 1465923600)：查询凌晨0点到1点之间，所有机器的所有CPU核上的用户态CPU消耗。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;sys.cpu.user (host=10.101.168.111,cpu=*)(1465920000 &amp;lt;= timestamp &amp;lt; 1465923600)：查询凌晨0点到1点之间，某台机器的所有CPU核上的用户态CPU消耗。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;sys.cpu.user (host=10.101.168.111,cpu=0)(1465920000 &amp;lt;= timestamp &amp;lt; 1465923600)：查询凌晨0点到1点之间，某台机器的第0个CPU核上的用户态CPU消耗。&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;OpenTSDB的存储优化&lt;/h3&gt; &lt;p&gt;了解了OpenTSDB的基本概念后，我们来尝试设计一下表结构。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/20203141572-opentsdb-save-struct.jpg" alt="20203141572-opentsdb-save-struct" style="zoom:50%;" /&gt; &lt;p&gt;如上图是一个简单的表结构设计，rowkey采用metric name + timestamp + tags的组合，因为这几个元素才能唯一确定一个指标值。&lt;/p&gt; &lt;p&gt;这张表已经能满足我们的写入和查询的业务需求，但是OpenTSDB采用的表结构设计远没有这么简单，我们接下来一项一项看它对表结构做的一些优化。&lt;/p&gt; &lt;h3&gt;优化一：缩短row key&lt;/h3&gt; &lt;p&gt;观察这张表内存储的数据，在rowkey的组成部分内，其实有很大一部分的重复数据，重复的指标名称，重复的标签。以上图为例，如果每秒采集一次监控指标，cpu为2核，host规模为100台，则一天时间内sys.cpu.user这个监控指标就会产生17280000行数据，而这些行中，监控指标名称均是重复的。如果能将这部分重复数据的长度尽可能的缩短，则能带来非常大的存储空间的节省。&lt;/p&gt; &lt;p&gt;OpenTSDB采用的策略是，为每个metric、tag key和tag value都分配一个UID，UID为固定长度三个字节。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020314151128-opentsdb-row-ket-tunning.jpg" alt="2020314151128-opentsdb-row-ket-tunning" style="zoom:50%;" /&gt; &lt;p&gt;上图为优化后的存储结构，可以看出，rowkey的长度大大的缩短了。rowkey的缩短，带来了很多好处：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;节省存储空间&lt;/li&gt; &lt;li&gt;提高查询效率：减少key匹配查找的时间&lt;/li&gt; &lt;li&gt;提高传输效率：不光节省了从文件系统读取的带宽，也节省了数据返回占用的带宽，提高了数据写入和读取的速度。&lt;/li&gt; &lt;li&gt;缓解Java程序内存压力：Java程序，GC是老大难的问题，能节省内存的地方尽量节省。原先用String存储的metric name、tag key或tag value，现在均可以用3个字节的byte array替换，大大节省了内存占用。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;优化二：减少Key-Value数&lt;/h3&gt; &lt;p&gt;优化一是 &lt;code&gt;OpenTSDB&lt;/code&gt; 做的最核心的一个优化，很直观的可以看到存储的数据量被大大的节省了。原理也很简单，将长的变短。但是是否还可以进一步优化呢？&lt;/p&gt; &lt;p&gt;在上面的存储模型章节中，我们了解到。HBase在底层存储结构中，每一列都会以Key-Value的形式存储，每一列都会包含一个rowkey。如果要进一步缩短存储量，那就得想办法减少Key-Value的个数。&lt;/p&gt; &lt;p&gt;OpenTSDB分了几个步骤来减少Key-Value的个数：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;将多行合并为一行，多行单列变为单行多列。&lt;/li&gt; &lt;li&gt;将多列合并为一列，单行多列变为单行单列。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;多行单列合并为单行单列&lt;/strong&gt;&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020314151649-multiple-to-one-row.jpg" alt="2020314151649-multiple-to-one-row" style="zoom:50%;" /&gt; &lt;p&gt;&lt;code&gt;OpenTSDB&lt;/code&gt; 将同属于一个时间周期内的具有相同&lt;code&gt;TSUID&lt;/code&gt;（相同的&lt;code&gt;metric name&lt;/code&gt;，以及相同的&lt;code&gt;tags&lt;/code&gt;）的数据合并为一行存储。&lt;code&gt;OpenTSDB&lt;/code&gt;内默认的时间周期是一个小时，也就是说同属于这一个小时的所有数据点，会合并到一行内存储，如图上所示。合并为一行后，该行的&lt;code&gt;rowkey&lt;/code&gt;中的&lt;code&gt;timestamp&lt;/code&gt;会指定为该小时的起始时间（所属时间周期的base时间），而每一列的列名，则记录真实数据点的时间戳与该时间周期起始时间（base）的差值。&lt;/p&gt; &lt;p&gt;这里列名采用差值而不是真实值也是一个有特殊考虑的设计，如存储模型章节所述，列名也是会存在于每个Key-Value中，占用一定的存储空间。如果是秒精度的时间戳，需要4个字节，如果是毫秒精度的时间戳，则需要8个字节。但是如果列名只存差值且时间周期为一个小时的话，则如果是秒精度，则差值取值范围是0-3600，只需要2个字节；如果是毫秒精度，则差值取值范围是0-360000，只需要4个字节；所以相比存真实时间戳，这个设计是能节省不少空间的。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;单行多列合并为单行单列&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;多行合并为单行后，并不能真实的减少Key-Value个数，因为总的列数并没有减少。所以要达到真实的节省存储的目的，还需要将一行的列变少，才能真正的将Key-Value数变少。&lt;/p&gt; &lt;p&gt;OpenTSDB采取的做法是，会在后台定期的将一行的多列合并为一列，称之为『compaction』，合并完之后效果如下。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/202031415236-tunning-one-kv.jpg" alt="202031415236-tunning-one-kv" style="zoom:50%;" /&gt; &lt;p&gt;同一行中的所有列被合并为一列，如果是秒精度的数据，则一行中的3600列会合并为1列，Key-Value数从3600个降低到只有1个。&lt;/p&gt; &lt;h3&gt;优化三：并发写优化&lt;/h3&gt; &lt;p&gt;上面两个优化主要是 &lt;code&gt;OpenTSDB&lt;/code&gt; 对存储的优化，存储量下降以及 &lt;code&gt;Key-Value&lt;/code&gt;个数下降后，除了直观的存储量上的缩减，对读和写的效率都是有一定提升的。&lt;/p&gt; &lt;p&gt;时间序列数据的写入，有一个不可规避的问题是写热点问题，当某一个metric下数据点很多时，则该metric很容易造成写入热点。OpenTSDB采取了和这篇&lt;a href="https://yq.aliyun.com/articles/54644" target="_blank"&gt;文章&lt;/a&gt;中介绍的一样的方法，允许将metric预分桶，可通过『&lt;strong&gt;tsd.storage.salt.buckets&lt;/strong&gt;』配置项来配置。&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020314152811-tunning-bucket.jpg" alt="2020314152811-tunning-bucket" style="zoom:50%;" /&gt; &lt;p&gt;如上图所示，预分桶后的变化就是在rowkey前会拼上一个桶编号（bucket index）。预分桶后，可将某个热点metric的写压力分散到多个桶中，避免了写热点的产生。&lt;/p&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;p&gt;&lt;code&gt;OpenTSDB&lt;/code&gt; 作为一个应用广泛的时间序列数据库，在存储上做了大量的优化，优化的选择也是完全契合其底层依赖的HBase数据库的存储模型。表格存储拥有和HBase一样的存储模型，这部分优化经验可以直接借鉴使用到表格存储的应用场景中，值得我们好好学习。有问题欢迎大家一起探讨。&lt;/p&gt; &lt;p&gt;同时 &lt;code&gt;OpenTSDB&lt;/code&gt; 在使用的时候又有如下缺点：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;不完整的分布式架构，需要自己搭建网关节点&lt;/li&gt; &lt;li&gt;未进行数据的压缩&lt;/li&gt; &lt;/ol&gt;</content:encoded>
      <pubDate>Sun, 15 Mar 2020 04:03:56 GMT</pubDate>
    </item>
    <item>
      <title>心跳的感觉</title>
      <link>https://www.zhangaoo.com/article/heart-rate</link>
      <content:encoded>&lt;img src="http://img.zhangaoo.com/202013122378-heart-rate.jpg" style="zoom:50%;" /&gt; &lt;h2&gt;百无聊赖&lt;/h2&gt; &lt;p&gt;过年期间肺炎疫情，从大年初一就呆在家里，实在是闲得慌。玩手机的时间占了一大半，终于玩手游、刷微信、刷新闻把手给刷残了，肩膀一直隐隐作痛。&lt;/p&gt; &lt;p&gt;对着镜子怀念一下我那腹肌，已经缩水一大半了，于是决定捡起之前的减脂训练。打开某运动软件发现当前最适合的室内有氧运动就是跳绳了。&lt;/p&gt; &lt;p&gt;本着胆小怕死的精神没敢出去买跳绳，于是就就地取材，用大概 2 平方毫米的电线制作了跳绳如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020131225240-rope-skipping.jpg" style="zoom:40%;" /&gt; &lt;p&gt;用报纸做的手柄，丑是丑了点，至少能跳，可是一组还没跳完绳子就断了。原因是绳子不能转动，于是改进版本如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020131225914-rope-skipping-2.jpg" style="zoom:40%;" /&gt; &lt;p&gt;使用胶带加固易摩擦部位后，屡试不爽，看到这你可能要开骂了“这跟心跳有毛关系，我要看美女！！”，别急啊，请听我慢慢道来。&lt;/p&gt; &lt;h2&gt;机缘巧合&lt;/h2&gt; &lt;p&gt;就这样跳了 4 天左右，突然有一天我瞄了一眼手机，一个页面引起了我的注意，猜对了就是你们想看的美女，如下：&lt;/p&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;p&gt;再往下拉一点。。。&lt;/p&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;/br&gt; &lt;p&gt;😆😆😆~逗你玩~😆😆😆，木有美女，只有数据，全是心跳的数据。都看到这了，不继续往下看吗。说明如下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;这是我2020年1月14日 至 2020年1月30日之间的心率数据统计&lt;/li&gt; &lt;li&gt;数据包括最高、最低、平均、静息心率数据&lt;/li&gt; &lt;li&gt;2020年1月14日 至 2020年1月22日 还在工作城市&lt;/li&gt; &lt;li&gt;2020年1月23日 至 2020年1月30日 已经回到家&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;多瞟了几眼数据，感觉整体心率变高了&lt;/strong&gt;，这一下就勾起了我的好奇心，究竟是否变高了？可能是什么原因导致的呢？所以决定简单分析一下这组数据&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th&gt;日期&lt;/th&gt;&lt;th&gt;最高&lt;/th&gt;&lt;th&gt;最低&lt;/th&gt;&lt;th&gt;平均&lt;/th&gt;&lt;th&gt;静息&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;2020年1月14日&lt;/td&gt;&lt;td&gt;122&lt;/td&gt;&lt;td&gt;46&lt;/td&gt;&lt;td&gt;68&lt;/td&gt;&lt;td&gt;57&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月15日&lt;/td&gt;&lt;td&gt;118&lt;/td&gt;&lt;td&gt;49&lt;/td&gt;&lt;td&gt;67&lt;/td&gt;&lt;td&gt;56&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月16日&lt;/td&gt;&lt;td&gt;119&lt;/td&gt;&lt;td&gt;47&lt;/td&gt;&lt;td&gt;65&lt;/td&gt;&lt;td&gt;62&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月17日&lt;/td&gt;&lt;td&gt;115&lt;/td&gt;&lt;td&gt;46&lt;/td&gt;&lt;td&gt;69&lt;/td&gt;&lt;td&gt;57&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月18日&lt;/td&gt;&lt;td&gt;110&lt;/td&gt;&lt;td&gt;48&lt;/td&gt;&lt;td&gt;70&lt;/td&gt;&lt;td&gt;65&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月19日&lt;/td&gt;&lt;td&gt;132&lt;/td&gt;&lt;td&gt;47&lt;/td&gt;&lt;td&gt;68&lt;/td&gt;&lt;td&gt;59&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月20日&lt;/td&gt;&lt;td&gt;123&lt;/td&gt;&lt;td&gt;36&lt;/td&gt;&lt;td&gt;66&lt;/td&gt;&lt;td&gt;61&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月21日&lt;/td&gt;&lt;td&gt;124&lt;/td&gt;&lt;td&gt;48&lt;/td&gt;&lt;td&gt;69&lt;/td&gt;&lt;td&gt;63&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月22日&lt;/td&gt;&lt;td&gt;110&lt;/td&gt;&lt;td&gt;48&lt;/td&gt;&lt;td&gt;69&lt;/td&gt;&lt;td&gt;54&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月23日&lt;/td&gt;&lt;td&gt;110&lt;/td&gt;&lt;td&gt;49&lt;/td&gt;&lt;td&gt;74&lt;/td&gt;&lt;td&gt;63&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月24日&lt;/td&gt;&lt;td&gt;111&lt;/td&gt;&lt;td&gt;48&lt;/td&gt;&lt;td&gt;71&lt;/td&gt;&lt;td&gt;57&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月25日&lt;/td&gt;&lt;td&gt;109&lt;/td&gt;&lt;td&gt;49&lt;/td&gt;&lt;td&gt;76&lt;/td&gt;&lt;td&gt;68&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月26日&lt;/td&gt;&lt;td&gt;109&lt;/td&gt;&lt;td&gt;48&lt;/td&gt;&lt;td&gt;72&lt;/td&gt;&lt;td&gt;68&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月27日&lt;/td&gt;&lt;td&gt;206&lt;/td&gt;&lt;td&gt;43&lt;/td&gt;&lt;td&gt;73&lt;/td&gt;&lt;td&gt;71&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月28日&lt;/td&gt;&lt;td&gt;147&lt;/td&gt;&lt;td&gt;47&lt;/td&gt;&lt;td&gt;74&lt;/td&gt;&lt;td&gt;56&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月29日&lt;/td&gt;&lt;td&gt;118&lt;/td&gt;&lt;td&gt;47&lt;/td&gt;&lt;td&gt;75&lt;/td&gt;&lt;td&gt;73&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2020年1月30日&lt;/td&gt;&lt;td&gt;169&lt;/td&gt;&lt;td&gt;36&lt;/td&gt;&lt;td&gt;75&lt;/td&gt;&lt;td&gt;68&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;简单查了一下以上心率数据的意义，最高、最低、平均大家都理解，其实最有参考意义的还是静息心率:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;&lt;a href="[https://baike.baidu.com/item/%E9%9D%99%E6%81%AF%E5%BF%83%E7%8E%87/10803094?fr=aladdin](https://baike.baidu.com/item/静息心率/10803094?fr=aladdin)" target="_blank"&gt;静息心率&lt;/a&gt;，又称为安静&lt;a href="https://baike.baidu.com/item/心率" target="_blank"&gt;心率&lt;/a&gt;，是指在清醒、不活动的安静状态下，每分钟心跳的次数。&lt;a href="[https://baike.baidu.com/item/%E9%9D%99%E6%81%AF%E5%BF%83%E7%8E%87/10803094?fr=aladdin](https://baike.baidu.com/item/静息心率/10803094?fr=aladdin)" target="_blank"&gt;静息心率&lt;/a&gt;能在一定程度上，反应身体的健康水平。性别、年龄及身体的健康状况的差异，静息心率的表现也会有所不同。&lt;/p&gt; &lt;/blockquote&gt; &lt;h3&gt;静息心率表现说明&lt;/h3&gt; &lt;p&gt;身体在相对健康的情况下，每日静息心率的值，以下两个表为不同性别的人，随年龄的变化的静息心率表现评价。&lt;strong&gt;本数据出自某运动软件，仅供参考，身体如有异常请尽快前往正规医院就医。&lt;/strong&gt;&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020210125-male.jpg" style="zoom:50%;" /&gt; &lt;img src="http://img.zhangaoo.com/20202101214-female.jpg" style="zoom:52%;" /&gt; &lt;h2&gt;分析说明&lt;/h2&gt; &lt;p&gt;简单粗暴求个算术平均值，分析一下趋势，小学的数学终于派上用场了。计算结果如下：&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th&gt;项目&lt;/th&gt;&lt;th&gt;最高均值&lt;/th&gt;&lt;th&gt;最低均值&lt;/th&gt;&lt;th&gt;平均均值&lt;/th&gt;&lt;th&gt;静息均值&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;14日-22日（工作地）&lt;/td&gt;&lt;td&gt;119.22&lt;/td&gt;&lt;td&gt;46.11&lt;/td&gt;&lt;td&gt;67.89&lt;/td&gt;&lt;td&gt;59.33&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;23日-30日（家）&lt;/td&gt;&lt;td&gt;134.88&lt;/td&gt;&lt;td&gt;45.88&lt;/td&gt;&lt;td&gt;73.75&lt;/td&gt;&lt;td&gt;65.50&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;差值&lt;/td&gt;&lt;td&gt;15.65&lt;/td&gt;&lt;td&gt;0.24&lt;/td&gt;&lt;td&gt;5.86&lt;/td&gt;&lt;td&gt;6.17&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;增加幅度&lt;/td&gt;&lt;td&gt;13.13%&lt;/td&gt;&lt;td&gt;-0.51%&lt;/td&gt;&lt;td&gt;8.63%&lt;/td&gt;&lt;td&gt;10.39%&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;h3&gt;均值分析&lt;/h3&gt; &lt;p&gt;在家期间有跳绳等剧烈有氧运动，因此对最高均值影响较大，比较有参考意义的数据是 &lt;strong&gt;静息均值&lt;/strong&gt; 和 &lt;strong&gt;平均均值&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;总体来看在家期间的心率最高值的均值为在工作地多出了&lt;code&gt;15.65&lt;/code&gt;，最低值的均值基本相同，平均值的均值在期间比在工作地高出 &lt;code&gt;5.86&lt;/code&gt;；静息心率均值在家比在工作地高出 &lt;code&gt;6.17&lt;/code&gt;；整体在家区间除了最低心率均值其他指标都有升高，增加幅度为 &lt;strong&gt;8%-14%&lt;/strong&gt;。&lt;/p&gt; &lt;h3&gt;折线图分析&lt;/h3&gt; &lt;img src="http://img.zhangaoo.com/20202101918-2020年1月8日至一月30日心率折线图.jpg" alt="20202101918-2020年1月8日至一月30日心率折线图" style="zoom:48%;" /&gt; &lt;p&gt;重点关注静息心率和平均心率两个指标，随着时间的推移总体有向上的趋势，虽然变化比较小，这种微小的上升趋势可以从上面表格统计的静息均值和平均均值得到反应。&lt;/p&gt; &lt;h3&gt;初步结论&lt;/h3&gt; &lt;p&gt;回家后总体的心率有上升的趋势，上升幅度在 &lt;strong&gt;8%-14%&lt;/strong&gt; 之间。那么问题来了：&lt;strong&gt;是什么原因导致的心率升上呢？&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;原因猜测&lt;/h2&gt; &lt;p&gt;得出以上结论后立马找了一个学医的朋友咨询了一下，她给了我一个重要的提示：&lt;strong&gt;海拔&lt;/strong&gt;。的确工作的城市海拔20-30米，回家后海拔1630-1740米。立马 &lt;code&gt;Google&lt;/code&gt; 了一下，这的确是一个重要的影响因素，现在把我觉得可能影响心率的因素都列出来：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;海拔变化&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;身体状况、睡眠、疾病、体温、药物等，比如生病啥的&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;情绪，比如回家情绪激动&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;气温、湿度、空气质量，简单统计了一下数据如下&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th&gt;项目&lt;/th&gt;&lt;th&gt;最高气温均值&lt;/th&gt;&lt;th&gt;最低气温均值&lt;/th&gt;&lt;th&gt;空气污染指数均值&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;14日-22日（工作地）&lt;/td&gt;&lt;td&gt;6.22&lt;/td&gt;&lt;td&gt;1.67&lt;/td&gt;&lt;td&gt;106.67（轻微污染）&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;23日-30日（家）&lt;/td&gt;&lt;td&gt;13.38&lt;/td&gt;&lt;td&gt;2.88&lt;/td&gt;&lt;td&gt;33.25（优）&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;h3&gt;&lt;/h3&gt; &lt;h2&gt;猜测分析&lt;/h2&gt; &lt;h3&gt;海拔变化&lt;/h3&gt; &lt;p&gt;上面已经提到了从工作地的海拔 20-30米 到了家里1630-1740米，找到一篇1987年的论文，年龄比我还大。结论如下：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;The mean day- time heart rate in the alpinists was significantly lower com- pared with that of the nonalpinists. At high altitude mean, maximal, and minimal heart rates were significantly high both awake and asleep. The circadian rhythm of heart rate disappeared at extremely high altitude in several alpinists.&lt;/p&gt; &lt;p&gt;We have also observed that the mean nighttime heart rate at high altitude (&lt;strong&gt;74.6beats/min&lt;/strong&gt;) was higher than that of sea level (&lt;strong&gt;61.6 beats/min&lt;/strong&gt;).&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;部分图表如下：&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/20202117856-f1.jpg" alt="20202117856-f1" style="zoom:50%;" /&gt; &lt;img src="http://img.zhangaoo.com/20202117913-f2.jpg" alt="20202117913-f2" style="zoom:50%;" /&gt; &lt;img src="http://img.zhangaoo.com/20202117919-f3.jpg" alt="20202117919-f3" style="zoom:50%;" /&gt; &lt;p&gt;以上论文研究了运动员和非运动员在海平面以及高海拔（7800m）在睡眠期间和非睡眠期间的心率变化，得出结论在高海拔下平均、最大、最小心率都会变高。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;以上结论支持了海拔对心率有明显影响，并且高海拔下心率会变高，具体影响因子有多大暂无数据支持&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;身体状况&lt;/h3&gt; &lt;p&gt;通过查阅资料可知，身体状况对心率的影响也是很明显的。&lt;/p&gt; &lt;h4&gt;体重&lt;/h4&gt; &lt;p&gt;由于涉及个人隐私就不公布详细数据了，在整个数据采集区间我的体重是有变化的，大致增加了 &lt;code&gt;1kg&lt;/code&gt; 左右，主要是由于冬天影响户外有氧运动导致的，因此这个能也是一个影响因素，查询资料可知肥胖可能导致心率增加。&lt;/p&gt; &lt;h4&gt;睡眠&lt;/h4&gt; &lt;p&gt;在家的整体睡眠不如工作时规律，睡眠总时间和深度睡眠时间大致相同，在家基本上在都是晚睡晚起。这个可能也是影响的因素之一，但影响因子未知。&lt;/p&gt; &lt;h4&gt;疾病&lt;/h4&gt; &lt;p&gt;这个数据记录期间未曾发生过我自己可感知的疾病，也没吃过任何中西药。&lt;/p&gt; &lt;h4&gt;体温&lt;/h4&gt; &lt;p&gt;未曾感觉异常过。&lt;/p&gt; &lt;h4&gt;饮食&lt;/h4&gt; &lt;p&gt;恰逢过年，在家摄入的热量比回家之前更多，估算可能翻倍，这也是每逢佳节胖三斤的原因。&lt;/p&gt; &lt;h4&gt;运动&lt;/h4&gt; &lt;p&gt;晚饭后会进行大概 16分钟 的剧烈有氧运动，大概消耗 180千卡，应该对静息心率影响不大，因为在工作地也坚持了长达数月的有氧运动。&lt;/p&gt; &lt;h3&gt;情绪&lt;/h3&gt; &lt;p&gt;回家之前情绪还有些激动，到家之后情绪一直很稳定，无太大波动。不知道打游戏会不会影响心率。。。&lt;/p&gt; &lt;h3&gt;气温、湿度、空气质量&lt;/h3&gt; &lt;p&gt;从上表的数据可以看到家里的气温更高；但更干燥，没有找到相关数据，日常生活就能明显的感觉到，每天喝2L+ 的水，面部、手脚、嘴唇还是很干燥；空气质量倒是好不少。找到相关资料如下：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;&lt;a href="https://health.clevelandclinic.org/5-best-warm-weather-workouts-help-stay-fit-safely/" target="_blank"&gt;Summer heat&lt;/a&gt; — especially during exercise — can be hard on the heart. That’s because your heart plays a big role in keeping you cool. The added workload of &lt;a href="https://health.clevelandclinic.org/level-exercise-safe-will-benefit-heart/" target="_blank"&gt;exercise&lt;/a&gt; only increases the demands on your cardiovascular system. The load on the heart increases with activity and exercise, especially in hot weather. For every degree the body’s internal temperature rises, the &lt;a href="https://health.clevelandclinic.org/is-a-slow-heart-rate-good-or-bad-for-you/" target="_blank"&gt;heart beats&lt;/a&gt; about 10 beats per minute faster. The result is a dramatic increase of stress on your heart.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;以上资料说明了在运动是体温对心率的影响，但不能说明对静息心率的影响，当前暂没有对体温的记录的数据，但推测可能有影响，影响程度暂不可知。&lt;/p&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;由此可以看出过年期间我真的很无聊 -_-&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;以此方式来了解自己身体的运作规律感觉很有趣，就是打字的时候很无聊&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;跟据以上分析，主观判断以下因素对 &lt;strong&gt;静息心率&lt;/strong&gt; 影响的大小，从高到底&lt;/p&gt; &lt;pre&gt;&lt;code&gt;身体状况 &amp;gt; 海拔变化 &amp;gt; 情绪 &amp;gt; 气温、湿度、空气质量 &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;本博客非专业人员书写，缺乏足够的数据样本和参照，分析方法也有待商榷，纯属个人好奇，欢迎各位拍砖&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;参考资料&lt;/h2&gt; &lt;p&gt;&lt;a href="https://activesalem.com/factors-affecting-heart-rate/" target="_blank"&gt;Factors Affecting Your Heart Rate&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.heart.org/en/health-topics/high-blood-pressure/the-facts-about-high-blood-pressure/all-about-heart-rate-pulse" target="_blank"&gt;All About Heart Rate (Pulse)&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://www.medicinenet.com/script/main/art.asp?articlekey=190894" target="_blank"&gt;Health Tip: Things That Affect Your Heart Rate&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Heart_rate#Factors_influencing_heart_rate" target="_blank"&gt;Heart rate&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://onlinelibrary.wiley.com/doi/pdf/10.1002/clc.4960100406" target="_blank"&gt;Changes of Heart Rate and QT Interval at High Altitude in Alpinists: Analysis by Holter Ambulatory Electrocardiogram &lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://health.clevelandclinic.org/how-hot-weather-can-affect-your-heart-when-you-exercise/" target="_blank"&gt;How Hot Weather Can Affect Your Heart When You Exercise&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 01 Feb 2020 10:51:00 GMT</pubDate>
    </item>
    <item>
      <title>时间是把猪饲料-健身篇一</title>
      <link>https://www.zhangaoo.com/article/abdominal-muscle-1</link>
      <content:encoded>&lt;h2&gt;背景&lt;/h2&gt; &lt;p&gt;本篇博客其实在 2019年8月末 就写好了，一直没发，趁着过年在家闲的慌发出来。文末有上半身肌肉照，介意者请自行打马赛克&amp;gt;_&amp;lt;!&lt;/p&gt; &lt;h2&gt;时间是把猪饲料&lt;/h2&gt; &lt;p&gt;时间是把猪饲料，说的一点没错。高中、大学同学，当你在胖友圈看到他们的近照时，你会有一中莫名的恐惧。刚好最近重映了《千与千寻》：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;不能吃太胖喔，会被杀掉的&lt;/p&gt; &lt;/blockquote&gt; &lt;img src="http://img.zhangaoo.com/201983172621-pangyou.jpg" style="zoom:60%;" /&gt; &lt;p&gt;身边同事，看了刚毕业的照片，视觉冲击之强烈。翻倍了有木有！！从 &lt;code&gt;S&lt;/code&gt; 号直接到 &lt;code&gt;XXXL&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;虽然我们 90 后已经不可避免的的走上油腻大叔的道路，你可能要问了怎么只有大叔没有大妈？一看你就思想觉悟就不够高，现在哪里还有大妈啊，一律小姐姐，18 岁的是小姐姐，28、48、88 的都是小姐姐！！~_~&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201983174851-xiaojiejie.jpg" alt="201983174851-xiaojiejie" /&gt;&lt;/p&gt; &lt;h2&gt;是谁喂胖了我&lt;/h2&gt; &lt;p&gt;对于大部分普通人来说，答案很明显，是你自己。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;时间 + 剩余卡路里 = 胖友 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在刚好是夏天，吃完午饭冰镇饮料来一瓶吧，写了一天代码小饿，嘎嘣脆的鸡排来一份，吃鸡排没奶茶怎么吃啊。&lt;/p&gt; &lt;p&gt;吃饱了写了两行代码，下班时间差不多到了，今天要加班，加班餐来一份啊，最爱的还是鸡排套餐。加完班，肚子好像又有点饿了，烧烤走起！&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20198317494-code-one-day.png" alt="20198317494-code-one-day" /&gt;&lt;/p&gt; &lt;p&gt;据不完全统计，我特么也不知道有多少卡路里啊，反正是超了。&lt;/p&gt; &lt;h2&gt;我是谁我在干什么&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;Talk is cheap show me the muscle&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;这就是我这半年多的成果&lt;/p&gt; &lt;img src="http://img.zhangaoo.com/2020130172016-my-muscle.jpg" style="zoom:60%;" /&gt; &lt;p&gt;事情还要从盘古开天辟地说起，......转眼我毕业工作了。刚成为社会人那会，感觉很棒啊，随便吃，随便宅，这一吃一宅，体重蹭蹭往上飙。&lt;/p&gt; &lt;p&gt;差点上80kg ，老东家福利还不错啊，五楼有健身房，还有各种俱乐部，爬山，游泳，足球，亲子应有尽有。二话没说报了个游泳俱乐部，免费游泳啊，多爽，从那时候就养成了一周至少锻炼一次的习惯，加上平时骑车上班，体重也就保持在 75kg 上下浮动，没太大变化。&lt;/p&gt; &lt;p&gt;每逢过节胖个两三斤，就这样过了三四年，直到到了现在，遇到另一位胖友，其实他身材很好，在他的建议下我下载某运动软件。这么一用发现相见恨晚，完全停不下来的节奏，记录每天的骑行，接着又新开辟的户外跑、徒手练胸肌、腹肌撕裂者、跳绳等项目。&lt;/p&gt; &lt;p&gt;大概是从 2019年3月 份开始有规律的运动，有氧运动主要是跑步，跳绳，偶尔游泳，一般一周 2-4 次剧烈运动，加上晚饭严格控制，体重蹭蹭往下掉。最轻的时候已经到 64 kg 左右，那感觉真好，身轻如燕。&lt;/p&gt; &lt;p&gt;记忆最深刻的是去菜市场买菜，每次卖菜的阿姨都要说“又瘦了！”，我从阿姨的眼神里看到了同情和怜悯。每次都要解释一遍，我在健身，健身 -_-!!!&lt;/p&gt; &lt;p&gt;明显感觉到身体和已经有了明显的变化，最明显的是肚子上的两个游泳圈不见了，腹肌隐约展露，胸肌也稍微有点形状了。&lt;/p&gt; &lt;h2&gt;小结&lt;/h2&gt; &lt;p&gt;成年人的世界也是有容易二字，容易胖、容易老、容易病、容易秃。对我来说身体和灵魂必须同时在路上！&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;You can either travel or read, but either your body or soul must be on the way&lt;/p&gt; &lt;/blockquote&gt;</content:encoded>
      <pubDate>Thu, 30 Jan 2020 10:37:00 GMT</pubDate>
    </item>
    <item>
      <title>快速入门 Kubernetes&amp;CRD&amp;Operator 篇五</title>
      <link>https://www.zhangaoo.com/article/quick-start-crd-operator</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202016202759-k8s.jpg" alt="202016202759-k8s" /&gt;&lt;/p&gt; &lt;h1&gt;快速入门 Kubernetes&amp;amp;CRD&amp;amp;Operator&lt;/h1&gt; &lt;p&gt;&lt;code&gt;Kubernetes&lt;/code&gt; 是一个容器管理系统。 具体功能：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;基于容器的应用部署、维护和滚动升级&lt;/li&gt; &lt;li&gt;负载均衡和服务发现&lt;/li&gt; &lt;li&gt;跨机器和跨地区的集群调度&lt;/li&gt; &lt;li&gt;自动伸缩&lt;/li&gt; &lt;li&gt;无状态服务和有状态服务&lt;/li&gt; &lt;li&gt;广泛的 &lt;code&gt;Volume&lt;/code&gt; 支持&lt;/li&gt; &lt;li&gt;插件机制保证扩展性&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;通过阅读&lt;a href="https://kubernetes.feisky.xyz/" target="_blank"&gt;Kubernetes指南&lt;/a&gt;和&lt;a href="https://jimmysong.io/kubernetes-handbook/" target="_blank"&gt;Kubernetes HandBook&lt;/a&gt;以及&lt;a href="https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/" target="_blank"&gt;官方文档&lt;/a&gt; 或者 阅读&lt;a href="https://book.douban.com/subject/27112874/" target="_blank"&gt; Kubernetes权威指南&lt;/a&gt;可以获得更好的学习体验。&lt;/p&gt; &lt;p&gt;在开始安装 &lt;code&gt;Kubernetes&lt;/code&gt; 之前，我们需要知道：&lt;/p&gt; &lt;p&gt;&lt;strong&gt;1、Docker与Kubernetes&lt;/strong&gt; &lt;code&gt;Docker&lt;/code&gt; 是一个容器运行时的实现，&lt;code&gt;Kubernetes&lt;/code&gt; 依赖于某种容器运行时的实现。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;2、Pod&lt;/strong&gt; &lt;code&gt;Kubernetes&lt;/code&gt; 中最基本的调度单位是 &lt;code&gt;Pod&lt;/code&gt;，&lt;code&gt;Pod&lt;/code&gt; 从属于 &lt;code&gt;Node&lt;/code&gt;（物理机或虚拟机），&lt;code&gt;Pod&lt;/code&gt; 中可以运行多个 &lt;code&gt;Docker&lt;/code&gt; 容器，会共享 &lt;code&gt;PID、IPC、Network&lt;/code&gt; 和 &lt;code&gt;UTS namespace&lt;/code&gt;。&lt;code&gt;Pod&lt;/code&gt; 在创建时会被分配一个 &lt;code&gt;IP&lt;/code&gt; 地址，&lt;code&gt;Pod&lt;/code&gt; 间的容器可以互相通信。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;3、Yaml&lt;/strong&gt; &lt;code&gt;Kubernetes&lt;/code&gt; 中有着很多概念，它们都算做是一种对象，如 &lt;code&gt;Pod、Deployment、Service&lt;/code&gt; 等，都可以通过一个 &lt;code&gt;yaml&lt;/code&gt; 文件来进行描述，并可以对这些对象进行 &lt;code&gt;CRUD&lt;/code&gt; 操作（对应 &lt;code&gt;REST&lt;/code&gt; 中的各种 &lt;code&gt;HTTP&lt;/code&gt; 方法）。&lt;/p&gt; &lt;p&gt;下面一个 &lt;code&gt;Pod&lt;/code&gt; 的 &lt;code&gt;yaml&lt;/code&gt; 文件示例：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1 kind: Pod metadata:   name: my-nginx-app   labels:     app: my-nginx-app spec:   containers:   - name: nginx     image: nginx:1.7.9     ports:     - containerPort: 80 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;kind：对象的类别&lt;/li&gt; &lt;li&gt;metadata：元数据，如 &lt;code&gt;Pod&lt;/code&gt; 的名称，以及标签 &lt;code&gt;Label&lt;/code&gt;【用于识别一系列关联的 &lt;code&gt;Pod&lt;/code&gt;，可以使用 &lt;code&gt;Label Selector&lt;/code&gt; 来选择一组相同 &lt;code&gt;label&lt;/code&gt; 的对象】&lt;/li&gt; &lt;li&gt;spec：希望 &lt;code&gt;Pod&lt;/code&gt; 能达到的状态，在此体现了 &lt;code&gt;Kubernetes&lt;/code&gt; 的声明式的思想，我们只需要定义出期望达到的状态，而不需要关心如何达到这个状态，这部分工作由 &lt;code&gt;Kubernetes&lt;/code&gt; 来完成。这里我们定义了Pod中运行的容器列表，包括一个 &lt;code&gt;nginx&lt;/code&gt; 容器，该容器对外暴露了 &lt;code&gt;80&lt;/code&gt; 端口。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;4、Node&lt;/strong&gt; &lt;code&gt;Node&lt;/code&gt; 是 &lt;code&gt;Pod&lt;/code&gt; 真正运行的主机，可以是物理机，也可以是虚拟机。为了管理 &lt;code&gt;Pod&lt;/code&gt;，每个 &lt;code&gt;Node&lt;/code&gt; 节点上至少要运行 &lt;code&gt;container runtime、kubelet&lt;/code&gt; 和 &lt;code&gt;kube-proxy&lt;/code&gt; 服务。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;5、Deployment&lt;/strong&gt; &lt;code&gt;Deployment&lt;/code&gt; 用于管理一个无状态应用，对应一个 &lt;code&gt;Pod&lt;/code&gt; 的集群，每个 &lt;code&gt;Pod&lt;/code&gt; 的地位是对等的，对 &lt;code&gt;Deployment&lt;/code&gt; 来说只是用于维护一定数量的 &lt;code&gt;Pod&lt;/code&gt;，这些 &lt;code&gt;Pod&lt;/code&gt; 有着相同的 &lt;code&gt;Pod&lt;/code&gt; 模板。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: apps/v1 kind: Deployment metadata:   name: my-nginx-app spec:   replicas: 3   selector:     matchLabels:       app: my-nginx-app   template:     metadata:       labels:         app: my-nginx-app     spec:       containers:       - name: nginx         image: nginx:1.7.9         ports:         - containerPort: 80 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以对 &lt;code&gt;Deployment&lt;/code&gt; 进行部署、升级、扩缩容等操作。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;6、Service&lt;/strong&gt; &lt;code&gt;Service&lt;/code&gt; 用于将一组 &lt;code&gt;Pod&lt;/code&gt; 暴露为一个服务。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;kubernetes&lt;/code&gt; 中，&lt;code&gt;Pod&lt;/code&gt; 的 &lt;code&gt;IP&lt;/code&gt; 地址会随着 &lt;code&gt;Pod&lt;/code&gt; 的重启而变化，并不建议直接拿 &lt;code&gt;Pod&lt;/code&gt; 的 &lt;code&gt;IP&lt;/code&gt; 来交互。那如何来访问这些 &lt;code&gt;Pod&lt;/code&gt; 提供的服务呢？使用 &lt;code&gt;Service&lt;/code&gt;。&lt;code&gt;Service&lt;/code&gt; 为一组 &lt;code&gt;Pod&lt;/code&gt;（通过 &lt;code&gt;labels&lt;/code&gt; 来选择）提供一个统一的入口，并为它们提供负载均衡和自动服务发现。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1 kind: Service metadata:   name: my-nginx-app   labels:     name: my-nginx-app spec:   type: NodePort      #这里代表是NodePort类型的   ports:   - port: 80          # 这里的端口和clusterIP(10.97.114.36)对应，即10.97.114.36:80,供内部访问。     targetPort: 80    # 端口一定要和container暴露出来的端口对应     protocol: TCP     nodePort: 32143   # 每个Node会开启，此端口供外部调用。   selector:     app: my-nginx-app &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;7、Kubernetes组件&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;etcd 保存了整个集群的状态；&lt;/li&gt; &lt;li&gt;apiserver 提供了资源操作的唯一入口，并提供认证、授权、访问控制、API 注册和发现等机制；&lt;/li&gt; &lt;li&gt;controller manager 负责维护集群的状态，比如故障检测、自动扩展、滚动更新等；&lt;/li&gt; &lt;li&gt;scheduler 负责资源的调度，按照预定的调度策略将 Pod 调度到相应的机器上；&lt;/li&gt; &lt;li&gt;kubelet 负责维护容器的生命周期，同时也负责 Volume（CVI）和网络（CNI）的管理；&lt;/li&gt; &lt;li&gt;Container runtime 负责镜像管理以及 Pod 和容器的真正运行（CRI）；&lt;/li&gt; &lt;li&gt;kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;安装 Kubernetes【Minikube】&lt;/h2&gt; &lt;p&gt;&lt;code&gt;minikube&lt;/code&gt; 为开发或者测试在本地启动一个节点的 &lt;code&gt;kubernetes&lt;/code&gt; 集群，&lt;code&gt;minikube&lt;/code&gt; 打包了和配置一个 &lt;code&gt;linux&lt;/code&gt; 虚拟机、&lt;code&gt;docker&lt;/code&gt; 与&lt;code&gt;kubernetes&lt;/code&gt; 组件。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Kubernetes&lt;/code&gt; 集群是由 &lt;code&gt;Master&lt;/code&gt; 和 &lt;code&gt;Node&lt;/code&gt; 组成的，&lt;code&gt;Master&lt;/code&gt; 用于进行集群管理，&lt;code&gt;Node&lt;/code&gt; 用于运行 &lt;code&gt;Pod&lt;/code&gt; 等 &lt;code&gt;workload&lt;/code&gt;。而&lt;code&gt;minikube&lt;/code&gt; 是一个 &lt;code&gt;Kubernetes&lt;/code&gt; 集群的最小集。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;1、安装virtualbox&lt;/strong&gt; &lt;a href="https://www.virtualbox.org/wiki/Downloads" target="_blank"&gt;virtualbox&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;2、安装minikube&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 &amp;amp;&amp;amp; chmod +x minikube &amp;amp;&amp;amp; sudo mv minikube /usr/local/bin/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;3、启用dashboard（web console）【可选】&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube addons enable dashboard &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;4、启动minikube&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube start &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;start&lt;/code&gt; 之后可以通过 &lt;code&gt;minikube status&lt;/code&gt; 来查看状态，如果 &lt;code&gt;minikube&lt;/code&gt; 和 &lt;code&gt;cluster&lt;/code&gt; 都是 &lt;code&gt;Running&lt;/code&gt;，则说明启动成功。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;5、查看启动状态&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl get pods &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;kubectl体验【以一个Deployment为例】&lt;/h2&gt; &lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; 是一个命令行工具，用于向 &lt;code&gt;API Server&lt;/code&gt; 发送指令。我们以部署、升级、扩缩容一个 &lt;code&gt;Deployment&lt;/code&gt;、发布一个 &lt;code&gt;Service&lt;/code&gt; 为例体验一下 &lt;code&gt;Kubernetes&lt;/code&gt;。 命令的通常格式为：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;kubectl $operation $object_type(单数or复数) $object_name other params &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;operation 如 get,replace,create,expose,delete 等。&lt;/li&gt; &lt;li&gt;object_type 是操作的对象类型，如 pods,deployments,services&lt;/li&gt; &lt;li&gt;object_name 是对象的 name&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;kubectl命令表：&lt;/p&gt; &lt;p&gt;&lt;a href="http://docs.kubernetes.org.cn/490.html" target="_blank"&gt;Kubernetes kubectl create 命令详解&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;1、创建一个Deployment&lt;/strong&gt; 可以使用 &lt;code&gt;kubectl run&lt;/code&gt; 来运行，也可以基于现有的 &lt;code&gt;yaml&lt;/code&gt; 文件来 &lt;code&gt;create&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl run –image=nginx:1.7.9 nginx-app –port=80 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;或者&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl create -f my-nginx-deployment.yaml &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;# my-nginx-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata:   name: my-nginx-app spec:   replicas: 3   selector:     matchLabels:       app: my-nginx-app   template:     metadata:       labels:         app: my-nginx-app     spec:       containers:       - name: nginx         image: nginx:1.7.9         ports:         - containerPort: 80 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后可以通过 &lt;code&gt;kubectl get pods&lt;/code&gt;来查看创建好了的 &lt;code&gt;3&lt;/code&gt; 个&lt;code&gt;Pod&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ kubectl get pods NAME                            READY   STATUS              RESTARTS   AGE my-nginx-app-6f647db65c-9w8kx   0/1     ContainerCreating   0          8s my-nginx-app-6f647db65c-dmhrx   0/1     ContainerCreating   0          8s my-nginx-app-6f647db65c-rbp9s   0/1     ContainerCreating   0          8s &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;再通过 &lt;code&gt;kubectl get deployments&lt;/code&gt; 来查看创建好了的 &lt;code&gt;deployment&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ kubectl get deployments NAME           READY   UP-TO-DATE   AVAILABLE   AGE my-nginx-app   3/3     3            3           51s &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里有4列，分别是：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;DESIRED：Pod副本数量的期望值，即Deployment里面定义的replicas&lt;/li&gt; &lt;li&gt;CURRENT：当前Replicas的值&lt;/li&gt; &lt;li&gt;UP-TO-DATE：最新版本的Pod的副本梳理，用于指示在滚动升级的过程中，有多少个Pod副本已经成功升级&lt;/li&gt; &lt;li&gt;AVAILABLE：集群中当前存活的Pod数量&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;2、删除掉任意一个Pod&lt;/strong&gt; &lt;code&gt;Deployment&lt;/code&gt; 自身拥有副本保持机制，会始终将其所管理的 &lt;code&gt;Pod&lt;/code&gt; 数量保持为 &lt;code&gt;spec&lt;/code&gt; 中定义的 &lt;code&gt;replicas&lt;/code&gt; 数量。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 打开一个新终端 $ kubectl get pods -w -l app=my-nginx-app # 删除指定pod $ kubectl delete pods $pod_name &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看出被删掉的 &lt;code&gt;Pod&lt;/code&gt; 的关闭与代替它的 &lt;code&gt;Pod&lt;/code&gt; 的启动过程。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;3、缩扩容&lt;/strong&gt; 缩扩容有两种实现方式:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;一种是修改 &lt;code&gt;yaml&lt;/code&gt; 文件，将 &lt;code&gt;replicas&lt;/code&gt; 修改为新的值，然后 &lt;code&gt;kubectl replace -f my-nginx-deployment.yaml&lt;/code&gt;；&lt;/li&gt; &lt;li&gt;另一种是使用 &lt;code&gt;scale&lt;/code&gt; 命令：&lt;code&gt;kubectl scale deployment my-nginx-app --replicas=2&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;4、更新&lt;/strong&gt; 更新也是有两种实现方式，&lt;code&gt;Kubernetes&lt;/code&gt; 的升级可以实现无缝升级，即不需要进行停机。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;一种是 rolling-update 方式，重建 Pod，&lt;code&gt;edit/replace/set image&lt;/code&gt; 等均可以实现。比如说我们可以修改 &lt;code&gt;yaml&lt;/code&gt; 文件&lt;/li&gt; &lt;li&gt;另一种是 &lt;code&gt;patch&lt;/code&gt; 方式，&lt;code&gt;patch&lt;/code&gt; 不会去重建 Pod，Pod 的 &lt;code&gt;IP&lt;/code&gt; 可以保持。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;#方法一 kubectl replace -f my-nginx-deployment.yaml #方法二 kubectl set image $resource_type/$resource_name $container_name=nginx:1.9.1 # 查看 kubectl get pods -o yaml &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;5、暴露服务&lt;/strong&gt; 暴露服务也有两种实现方式，一种是通过 &lt;code&gt;kubectl create -f my-nginx-service.yaml&lt;/code&gt; 可以创建一个服务：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;# my-nginx-service.yaml apiVersion: v1 kind: Service metadata:   name: my-nginx-app   labels:     name: my-nginx-app spec:   type: NodePort      #这里代表是NodePort类型的   ports:   - port: 80          # 这里的端口和clusterIP，供内部访问。     targetPort: 80    # 端口一定要和 pod container 暴露出来的端口对应（上面配置的端口）     protocol: TCP     nodePort: 32143   # 每个 Node 会开启，此端口供外部调用。（每个Pod对外暴露的端口）   selector:     app: my-nginx-app &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;ports&lt;/code&gt; 中有三个端口，第一个 &lt;code&gt;port&lt;/code&gt; 是 &lt;code&gt;Pod&lt;/code&gt; 供内部访问暴露的端口，第二个 &lt;code&gt;targetPort&lt;/code&gt; 是 &lt;code&gt;Pod&lt;/code&gt; 的 &lt;code&gt;Container&lt;/code&gt; 中配置的 另一种是通过 &lt;code&gt;kubectl expose&lt;/code&gt; 命令实现。 &lt;code&gt;minikube ip&lt;/code&gt; 返回的就是 &lt;code&gt;minikube&lt;/code&gt; 所管理的 &lt;code&gt;Kubernetes&lt;/code&gt; 集群所在的虚拟机 &lt;code&gt;ip&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube service my-nginx-app --url -p zzz-cluster http://192.168.39.121:32143 &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;CRD【CustomResourceDefinition】&lt;/h2&gt; &lt;p&gt;&lt;code&gt;CRD&lt;/code&gt; 是 &lt;code&gt;Kubernetes&lt;/code&gt; 为提高可扩展性，让开发者去自定义资源（如 &lt;code&gt;Deployment，StatefulSet&lt;/code&gt; 等）的一种方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;Operator = CRD + Controller &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;CRD&lt;/code&gt; 仅仅是资源的定义，而 &lt;code&gt;Controller&lt;/code&gt; 可以去监听 &lt;code&gt;CRD&lt;/code&gt; 的 &lt;code&gt;CRUD&lt;/code&gt; 事件来添加自定义业务逻辑。&lt;/p&gt; &lt;p&gt;关于CRD有一些链接先贴出来： &lt;a href="https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/" target="_blank"&gt;Extend the Kubernetes API with CustomResourceDefinitions&lt;/a&gt;&lt;/p&gt; &lt;p&gt;如果说只是对 &lt;code&gt;CRD&lt;/code&gt; 实例进行 &lt;code&gt;CRUD&lt;/code&gt; 的话，不需要 &lt;code&gt;Controller&lt;/code&gt; 也是可以实现的，只是只有数据，没有针对数据的操作。&lt;/p&gt; &lt;p&gt;一个 &lt;code&gt;CRD&lt;/code&gt; 的 &lt;code&gt;yaml&lt;/code&gt; 文件示例：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata:   # name must match the spec fields below, and be in the form: &amp;lt;plural&amp;gt;.&amp;lt;group&amp;gt;   name: crontabs.stable.example.com spec:   # group name to use for REST API: /apis/&amp;lt;group&amp;gt;/&amp;lt;version&amp;gt;   group: stable.example.com   # list of versions supported by this CustomResourceDefinition   version: v1beta1   # either Namespaced or Cluster   scope: Namespaced   names:     # plural name to be used in the URL: /apis/&amp;lt;group&amp;gt;/&amp;lt;version&amp;gt;/&amp;lt;plural&amp;gt;     plural: crontabs     # singular name to be used as an alias on the CLI and for display     singular: crontab     # kind is normally the CamelCased singular type. Your resource manifests use this.     kind: CronTab     # shortNames allow shorter string to match your resource on the CLI     shortNames:     - ct &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;通过 &lt;code&gt;kubectl create -f crd.yaml&lt;/code&gt; 可以创建一个 &lt;code&gt;CRD&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;Operator&lt;/h2&gt; &lt;p&gt;我们平时在部署一个简单的 Webserver 到 Kubernetes 集群中的时候，都需要先编写一个 Deployment 的控制器，然后创建一个 Service 对象，通过 Pod 的 label 标签进行关联，最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务，每次都需要这样操作，是不是略显麻烦，我们就可以创建一个自定义的资源对象，通过我们的 CRD 来描述我们要部署的应用信息，比如镜像、服务端口、环境变量等等，然后创建我们的自定义类型的资源对象的时候，通过控制器去创建对应的 Deployment 和 Service，是不是就方便很多了，相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。&lt;/p&gt; &lt;h3&gt;Operator-SDK&lt;/h3&gt; &lt;p&gt;Operator 是由 CoreOS 开发的，用来扩展 Kubernetes API，特定的应用程序控制器，它用来创建、配置和管理复杂的有状态应用，如数据库、缓存和监控系统。Operator 基于 Kubernetes 的资源和控制器概念之上构建，但同时又包含了应用程序特定的领域知识。创建Operator 的关键是CRD（自定义资源）的设计。&lt;/p&gt; &lt;h3&gt;Workflow&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Operator SDK&lt;/code&gt; 提供以下工作流来开发一个新的 &lt;code&gt;Operator&lt;/code&gt;：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;使用 SDK 创建一个新的 Operator 项目&lt;/li&gt; &lt;li&gt;通过添加自定义资源（CRD）定义新的资源 API&lt;/li&gt; &lt;li&gt;指定使用 SDK API 来 watch 的资源&lt;/li&gt; &lt;li&gt;定义 Operator 的协调（reconcile）逻辑&lt;/li&gt; &lt;li&gt;使用 Operator SDK 构建并生成 Operator 部署清单文件&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;创建新项目&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ operator-sdk new cassandra-operator --repo=github.com/zealzhangz/cassandra-operator $ tree . ├── build │   ├── Dockerfile │   └── bin │       ├── entrypoint │       └── user_setup ├── cassandra-operator.iml ├── cmd │   └── manager │       └── main.go ├── deploy │   ├── operator.yaml │   ├── role.yaml │   ├── role_binding.yaml │   └── service_account.yaml ├── go.mod ├── go.sum ├── pkg │   ├── apis │   │   └── apis.go │   └── controller │       └── controller.go ├── tools.go └── version     └── version.go &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;添加 API&lt;/h3&gt; &lt;p&gt;接下来为我们的自定义资源添加一个新的 &lt;code&gt;API&lt;/code&gt;，按照上面我们预定义的资源清单文件，在 &lt;code&gt;Operator&lt;/code&gt; 相关根目录下面执行如下命令：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 等个半分钟就创建好了，大概新增了10个文件左右 $ operator-sdk add api --api-version=cassandra.zhangaoo.com/v1alpha1 --kind=CassandraService &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;添加控制器&lt;/h3&gt; &lt;p&gt;上面我们添加自定义的 &lt;code&gt;API&lt;/code&gt;，接下来可以添加对应的自定义 &lt;code&gt;API&lt;/code&gt; 的具体实现 &lt;code&gt;Controller&lt;/code&gt;，同样在项目根目录下面执行如下命令&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ operator-sdk add controller --api-version=cassandra.zhangaoo.com/v1alpha1 --kind=CassandraService &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;自定义 API&lt;/h3&gt; &lt;p&gt;打开源文件 &lt;code&gt;pkg/apis/cassandra/v1alpha1/cassandraservice_types.go&lt;/code&gt; ，需要我们根据我们的需求去自定义结构体 &lt;code&gt;CassandraServiceSpec&lt;/code&gt;，我们最上面预定义的资源清单中就有 &lt;code&gt;size、image、ports&lt;/code&gt; 这些属性，所有我们需要用到的属性都需要在这个结构体中进行定义&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;type CassandraServiceSpec struct {  // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster  // Important: Run &amp;quot;operator-sdk generate k8s&amp;quot; to regenerate code after modifying this file  // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html  Size *int32  `json:&amp;quot;size&amp;quot;`  Image   string   `json:&amp;quot;image&amp;quot;`  Resources corev1.ResourceRequirements `json:&amp;quot;resources,omitempty&amp;quot;`  Envs      []corev1.EnvVar             `json:&amp;quot;envs,omitempty&amp;quot;`  Ports     []corev1.ServicePort        `json:&amp;quot;ports,omitempty&amp;quot;` } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-go"&gt;type CassandraServiceStatus struct {  // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster  // Important: Run &amp;quot;operator-sdk generate k8s&amp;quot; to regenerate code after modifying this file  // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html  appsv1.StatefulSetStatus `json:&amp;quot;,inline&amp;quot;` } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;定义完成后，在项目根目录下面执行如下命令：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ operator-sdk generate k8s &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;实现业务逻辑&lt;/h3&gt; &lt;p&gt;上面 &lt;code&gt;API&lt;/code&gt; 描述声明完成了，接下来就需要我们来进行具体的业务逻辑实现了，编写具体的 &lt;code&gt;controller&lt;/code&gt; 实现，打开源文件 &lt;code&gt;pkg/controller/cassandraservice/cassandraservice_controller.go&lt;/code&gt; ，需要我们去更改的地方也不是很多，核心的就是 &lt;code&gt;Reconcile&lt;/code&gt; 方法，该方法就是去不断的 watch 资源的状态，然后根据状态的不同去实现各种操作逻辑&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;// Reconcile reads that state of the cluster for a CassandraService object and makes changes based on the state read // and what is in the CassandraService.Spec // TODO(user): Modify this Reconcile function to implement your Controller logic.  This example creates // a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. func (r *ReconcileCassandraService) Reconcile(request reconcile.Request) (reconcile.Result, error) {  reqLogger := log.WithValues(&amp;quot;Request.Namespace&amp;quot;, request.Namespace, &amp;quot;Request.Name&amp;quot;, request.Name)  reqLogger.Info(&amp;quot;Reconciling CassandraService&amp;quot;)   // Fetch the CassandraService instance  instance := &amp;amp;cassandrav1alpha1.CassandraService{}  err := r.client.Get(context.TODO(), request.NamespacedName, instance)  if err != nil {   if errors.IsNotFound(err) {    // Request object not found, could have been deleted after reconcile request.    // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.    // Return and don't requeue    return reconcile.Result{}, nil   }   // Error reading the object - requeue the request.   return reconcile.Result{}, err  }  //Service  // Check if this Service already exists  serviceFound := &amp;amp;corev1.Service{}  err = r.client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, serviceFound)  if err != nil &amp;amp;&amp;amp; errors.IsNotFound(err) {   reqLogger.Info(&amp;quot;Creating a new Service&amp;quot;, &amp;quot;Service.Namespace&amp;quot;, instance.Namespace, &amp;quot;Service.Name&amp;quot;, instance.Name)    // Define a new Service object   service := r.newServiceForCr(instance)    // Set CassandraService instance as the owner and controller   if err := controllerutil.SetControllerReference(instance, service, r.scheme); err != nil {    return reconcile.Result{}, err   }    err = r.client.Create(context.TODO(), service)   if err != nil {    return reconcile.Result{}, err   }   // Service created successfully - don't requeue   //return reconcile.Result{}, nil //todo  } else if err != nil {   return reconcile.Result{}, err  }  // Service already exists - don't requeue  reqLogger.Info(&amp;quot;Skip reconcile: Service already exists&amp;quot;, &amp;quot;Service.Namespace&amp;quot;, instance.Namespace, &amp;quot;Service.Name&amp;quot;, instance.Name)   //StatefulSet  // Check if this Service already exists  statefulSetFound := &amp;amp;appsv1.StatefulSet{}  err = r.client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, statefulSetFound)  if err != nil &amp;amp;&amp;amp; errors.IsNotFound(err) {   reqLogger.Info(&amp;quot;Creating a new StatefulSet&amp;quot;, &amp;quot;StatefulSet.Namespace&amp;quot;, instance.Namespace, &amp;quot;StatefulSet.Name&amp;quot;, instance.Name)    // Define a new StatefulSet object   statefulSet := r.newStatefulSetForCr(instance)    // Set CassandraService instance as the owner and controller   if err := controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err != nil {    return reconcile.Result{}, err   }    err = r.client.Create(context.TODO(), statefulSet)   if err != nil {    return reconcile.Result{}, err   }   // StatefulSet created successfully - don't requeue   return reconcile.Result{}, nil  } else if err != nil {   return reconcile.Result{}, err  }   // Ensure the statefulset size is the same as the spec  reqLogger.Info(&amp;quot;Matching size in spec&amp;quot;)  size := instance.Spec.Size  if *statefulSetFound.Spec.Replicas != *size {   statefulSetFound.Spec.Replicas = size   err = r.client.Update(context.TODO(),statefulSetFound)   if err != nil{    reqLogger.Info(&amp;quot;Failed to update StatefulSet: %v\n&amp;quot;, err)    return reconcile.Result{}, err   }   reqLogger.Info(&amp;quot;Spec was updated, so request is getting re-queued&amp;quot;)   // Spec updated - return and requeue   return reconcile.Result{Requeue: true}, nil  }   // StatefulSet already exists - don't requeue  reqLogger.Info(&amp;quot;Skip reconcile: CanssandraService already exists&amp;quot;, &amp;quot;CanssandraService.Namespace&amp;quot;, instance.Namespace, &amp;quot;CanssandraService.Name&amp;quot;, instance.Name)  return reconcile.Result{}, nil } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;调试&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ operator-sdk up local &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: cassandra.zhangaoo.com/v1alpha1 kind: CassandraService metadata:   name: cassandra   labels:     app: cassandra spec:   # Add fields here   size: 3   image: gcr.io/google-samples/cassandra:v13   ports:   - containerPort: 7000     name: intra-node   - containerPort: 7001     name: tls-intra-node   - containerPort: 7199     name: jmx   - containerPort: 9042     name: cql   - port: 9042   resources:     limits:       cpu: &amp;quot;500m&amp;quot;       memory: 1Gi     requests:       cpu: &amp;quot;500m&amp;quot;       memory: 1Gi   env:   - name: MAX_HEAP_SIZE     value: 512M   - name: HEAP_NEWSIZE     value: 100M   - name: CASSANDRA_SEEDS     value: &amp;quot;cassandra-0.cassandra.default.svc.cluster.local&amp;quot;   - name: CASSANDRA_CLUSTER_NAME     value: &amp;quot;K8Demo&amp;quot;   - name: CASSANDRA_DC     value: &amp;quot;DC1-K8Demo&amp;quot;   - name: CASSANDRA_RACK     value: &amp;quot;Rack1-K8Demo&amp;quot;   - name: POD_IP     valueFrom:         ieldRef:           fieldPath: status.podIP &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;参考资料&lt;/h3&gt; &lt;p&gt;&lt;a href="https://www.qikqiak.com/post/k8s-operator-101/" target="_blank"&gt;Kubernetes Operator 101&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 06 Jan 2020 12:51:00 GMT</pubDate>
    </item>
    <item>
      <title>学习 Kubernetes StatefulSets 篇四</title>
      <link>https://www.zhangaoo.com/article/k8s-sts</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202016202759-k8s.jpg" alt="202016202759-k8s" /&gt;&lt;/p&gt; &lt;h1&gt;Kubernetes StatefulSets&lt;/h1&gt; &lt;p&gt;&lt;code&gt;StatefulSets&lt;/code&gt;（有状态系统服务设计）在 &lt;code&gt;Kubernetes 1.7&lt;/code&gt; 中还是beta特性，同时StatefulSets是1.4 版本中PetSets的替代品。PetSets的用户参考1.5 &lt;a href="https://kubernetes.io/docs/tasks/manage-stateful-set/upgrade-pet-set-to-stateful-set/" target="_blank"&gt;升级指南&lt;/a&gt; 。&lt;/p&gt; &lt;h2&gt;使用StatefulSets&lt;/h2&gt; &lt;p&gt;在具有以下特点时使用 &lt;code&gt;StatefulSets&lt;/code&gt;：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;稳定性，唯一的网络标识符。&lt;/li&gt; &lt;li&gt;稳定性，持久化存储。&lt;/li&gt; &lt;li&gt;有序的部署和扩展。&lt;/li&gt; &lt;li&gt;有序的删除和终止。&lt;/li&gt; &lt;li&gt;有序的自动滚动更新。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;code&gt;Pod&lt;/code&gt; 调度运行时，如果应用不需要任何稳定的标示、有序的部署、删除和扩展，则应该使用一组无状态副本的控制器来部署应用，例如 &lt;a href="http://docs.kubernetes.org.cn/317.html" target="_blank"&gt;Deployment&lt;/a&gt; 或 &lt;a href="http://docs.kubernetes.org.cn/314.html" target="_blank"&gt;ReplicaSet&lt;/a&gt;更适合无状态服务需求。&lt;/p&gt; &lt;h2&gt;限制&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;StatefulSet&lt;/code&gt; 还是 &lt;code&gt;beta&lt;/code&gt; 特性，在 &lt;code&gt;Kubernetes 1.5&lt;/code&gt; 版本之前任何版本都不可以使用。&lt;/li&gt; &lt;li&gt;与所有 &lt;code&gt;alpha/beta&lt;/code&gt; 特性的资源一样，可以通过 &lt;code&gt;apiserver&lt;/code&gt; 配置 &lt;code&gt;-runtime-config&lt;/code&gt; 来禁用 &lt;code&gt;StatefulSet&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Pod&lt;/code&gt; 的存储，必须基于请求 &lt;code&gt;storage class&lt;/code&gt; 的 &lt;code&gt;PersistentVolume Provisioner&lt;/code&gt; 或由管理员预先配置来提供&lt;/li&gt; &lt;li&gt;基于数据安全性设计，删除或缩放 &lt;code&gt;StatefulSet&lt;/code&gt; 将不会删除与 &lt;code&gt;StatefulSet&lt;/code&gt; 关联的 &lt;code&gt;Volume&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;StatefulSets&lt;/code&gt; 需要&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#headless-services" target="_blank"&gt;Headless Service&lt;/a&gt;负责 &lt;code&gt;Pods&lt;/code&gt; 的网络的一致性（必须创建此服务）。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;组件&lt;/h2&gt; &lt;p&gt;示例：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;nginx&lt;/code&gt; 的 &lt;code&gt;Headless Service&lt;/code&gt; 用于控制网络域。&lt;/li&gt; &lt;li&gt;&lt;code&gt;StatefulSet&lt;/code&gt;（name为web）有一个 &lt;code&gt;Spec&lt;/code&gt;，在一个 &lt;code&gt;Pod&lt;/code&gt; 中启动具有 &lt;code&gt;3&lt;/code&gt; 个副本的 &lt;code&gt;nginx&lt;/code&gt; 容器。&lt;/li&gt; &lt;li&gt;&lt;code&gt;volumeClaimTemplates&lt;/code&gt; 使用 &lt;code&gt;PersistentVolumes&lt;/code&gt; 供应商的 &lt;code&gt;PersistentVolume&lt;/code&gt; 来提供稳定的存储。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1 kind: Service metadata:   name: nginx   labels:     app: nginx spec:   ports:   - port: 80     name: web   clusterIP: None   selector:     app: nginx --- apiVersion: apps/v1beta1 kind: StatefulSet metadata:   name: web spec:   serviceName: &amp;quot;nginx&amp;quot;   replicas: 3   template:     metadata:       labels:         app: nginx     spec:       terminationGracePeriodSeconds: 10       containers:       - name: nginx         image: gcr.io/google_containers/nginx-slim:0.8         ports:         - containerPort: 80           name: web         volumeMounts:         - name: www           mountPath: /usr/share/nginx/html   volumeClaimTemplates:   - metadata:       name: www     spec:       accessModes: [ &amp;quot;ReadWriteOnce&amp;quot; ]       storageClassName: my-storage-class       resources:         requests:           storage: 1Gi &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;部署和扩展&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;对于具有 &lt;code&gt;N&lt;/code&gt; 个副本的 &lt;code&gt;StatefulSet&lt;/code&gt;，当部署 &lt;code&gt;Pod&lt;/code&gt; 时，将会顺序从 &lt;code&gt;{0..N-1}&lt;/code&gt; 开始创建。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Pods&lt;/code&gt; 被删除时，会从 &lt;code&gt;{N-1..0}&lt;/code&gt; 的相反顺序终止。&lt;/li&gt; &lt;li&gt;在将缩放操作应用于 &lt;code&gt;Pod&lt;/code&gt; 之前，它的所有前辈必须运行和就绪。&lt;/li&gt; &lt;li&gt;对 &lt;code&gt;Pod&lt;/code&gt; 执行扩展操作时，前面的 &lt;code&gt;Pod&lt;/code&gt; 必须都处于 &lt;code&gt;Running&lt;/code&gt; 和 &lt;code&gt;Ready&lt;/code&gt; 状态。&lt;/li&gt; &lt;li&gt;在 &lt;code&gt;Pod&lt;/code&gt; 终止之前，所有 &lt;code&gt;successors&lt;/code&gt; 都须完全关闭。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;不要将 &lt;code&gt;StatefulSet&lt;/code&gt; 的 &lt;code&gt;pod.Spec.TerminationGracePeriodSeconds&lt;/code&gt; 值设置为&lt;code&gt;0&lt;/code&gt;，这样设置不安全，建议不要这么使用。更多说明，请参考&lt;a href="https://kubernetes.io/docs/tasks/run-application/force-delete-stateful-set-pod/" target="_blank"&gt;force deleting StatefulSet Pods&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;在上面示例中，会按顺序部署三个 &lt;code&gt;pod&lt;/code&gt;（&lt;code&gt;name: web-0、web-1、web-2&lt;/code&gt;）。&lt;code&gt;web-0&lt;/code&gt; 在&lt;a href="https://kubernetes.io/docs/user-guide/pod-states" target="_blank"&gt;Running and Ready&lt;/a&gt;状态后开始部署&lt;code&gt;web-1，web-1&lt;/code&gt; 在 &lt;code&gt;Running and Ready&lt;/code&gt; 状态后部署 &lt;code&gt;web-2&lt;/code&gt;，期间如果 &lt;code&gt;web-0&lt;/code&gt; 运行失败，&lt;code&gt;web-2&lt;/code&gt;是不会被运行，直到 &lt;code&gt;web-0&lt;/code&gt; 重新运行，&lt;code&gt;web-1、web-2&lt;/code&gt;才会按顺序进行运行。&lt;/p&gt; &lt;p&gt;如果用户通过 &lt;code&gt;StatefulSet&lt;/code&gt; 来扩展修改部署 &lt;code&gt;pod&lt;/code&gt; 副本数，比如修改 &lt;code&gt;replicas=1&lt;/code&gt;，那么 &lt;code&gt;web-2&lt;/code&gt; 首先被终止。在 &lt;code&gt;web-2&lt;/code&gt; 完全关闭和删除之前，&lt;code&gt;web-1&lt;/code&gt; 是不会被终止。如果在 &lt;code&gt;web-2&lt;/code&gt;被终止和完全关闭后，但 &lt;code&gt;web-1&lt;/code&gt; 还没有被终止之前，此时 &lt;code&gt;web-0&lt;/code&gt; 运行出错了，那么直到 &lt;code&gt;web-0&lt;/code&gt; 再次变为 &lt;code&gt;Running and Ready&lt;/code&gt; 状态之后，&lt;code&gt;web-1&lt;/code&gt; 才会被终止。&lt;/p&gt; &lt;h2&gt;Pod管理&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;Kubernetes 1.7&lt;/code&gt; 及更高版本中，&lt;code&gt;StatefulSet&lt;/code&gt; 放宽了排序规则，同时通过 &lt;code&gt;.spec.podManagementPolicy&lt;/code&gt; 字段保留其&lt;code&gt;uniqueness&lt;/code&gt; 和 &lt;code&gt;identity guarantees&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;OrderedReady Pod Management&lt;/h3&gt; &lt;p&gt;&lt;code&gt;OrderedReady Pod Management&lt;/code&gt; 是 &lt;code&gt;StatefulSets&lt;/code&gt; 的默认行为。它实现了上述 “部署/扩展” 行为。&lt;/p&gt; &lt;h3&gt;Parallel Pod Management&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Parallel Pod Management&lt;/code&gt; 告诉 &lt;code&gt;StatefulSet&lt;/code&gt; 控制器同时启动或终止所有 &lt;code&gt;Pod&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;Update Strategies&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;Kubernetes 1.7&lt;/code&gt; 及更高版本中，&lt;code&gt;StatefulSet&lt;/code&gt; 的 &lt;code&gt;.spec.updateStrategy&lt;/code&gt; 字段允许配置和禁用 &lt;code&gt;StatefulSet&lt;/code&gt; 中 &lt;code&gt;Pods&lt;/code&gt; 的 &lt;code&gt;containers&lt;/code&gt;、&lt;a href="http://docs.kubernetes.org.cn/247.html" target="_blank"&gt;labels&lt;/a&gt;、&lt;code&gt;resource request/limits&lt;/code&gt; 和&lt;a href="http://docs.kubernetes.org.cn/255.html" target="_blank"&gt;annotations&lt;/a&gt; 的滚动更新。&lt;/p&gt; &lt;h2&gt;删除&lt;/h2&gt; &lt;p&gt;当 &lt;code&gt;spec.updateStrategy&lt;/code&gt; 未指定时的默认策略，&lt;code&gt;OnDelete&lt;/code&gt; 更新策略实现了传统（1.6和以前）的行为。当 &lt;code&gt;StatefulSet .spec.updateStrategy.type&lt;/code&gt; 设置为 &lt;code&gt;OnDelete，StatefulSet&lt;/code&gt; 控制器将不会自动更新 &lt;code&gt;StatefulSet&lt;/code&gt; 中的 &lt;code&gt;Pod&lt;/code&gt;，用户必须手动删除 &lt;code&gt;Pods&lt;/code&gt; 以使控制器创建新的 &lt;code&gt;Pod&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;滚动更新&lt;/h2&gt; &lt;p&gt;&lt;code&gt;RollingUpdate&lt;/code&gt; 更新策略实现了自动化，使 &lt;code&gt;StatefulSet&lt;/code&gt; 中的 &lt;code&gt;Pod&lt;/code&gt; 滚动更新。当 &lt;code&gt;StatefulSet&lt;/code&gt; &lt;code&gt;.spec.updateStrategy.type&lt;/code&gt; 设置为 &lt;code&gt;RollingUpdate&lt;/code&gt;，&lt;code&gt;StatefulSet&lt;/code&gt;控制器将删除并重新创建 &lt;code&gt;StatefulSet&lt;/code&gt; 中的每个 &lt;code&gt;Pod&lt;/code&gt;。它将以与 &lt;code&gt;Pod&lt;/code&gt; 终止相同的顺序进行（从最大的序数到最小的顺序）来更新每个 &lt;code&gt;Pod&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;Partitions&lt;/h2&gt; &lt;p&gt;通过指定 &lt;code&gt;.spec.updateStrategy.rollingUpdate.partition&lt;/code&gt; 来分割 &lt;code&gt;RollingUpdate&lt;/code&gt; 更新策略。如果指定了 &lt;code&gt;partition&lt;/code&gt;，则当更新 &lt;code&gt;StatefulSet&lt;/code&gt; 时，将更新具有大于或等于 &lt;code&gt;partition&lt;/code&gt; 的序数的所有 &lt;code&gt;Pods .spec.template&lt;/code&gt;，小于 &lt;code&gt;partition&lt;/code&gt; 的序数的所有 &lt;code&gt;Pod&lt;/code&gt; 将不会被更新。如果一个 &lt;code&gt;StatefulSet&lt;/code&gt; 的 &lt;code&gt;.spec.updateStrategy.rollingUpdate.partition&lt;/code&gt; 大于它&lt;code&gt;.spec.replicas&lt;/code&gt;，它的更新 &lt;code&gt;.spec.template&lt;/code&gt; 将不会被传 &lt;code&gt;Pods&lt;/code&gt;。在通常数情况下，不需要使用 &lt;code&gt;partition&lt;/code&gt;，但如果需要进行更新，推出金丝雀或执行分阶段推出，可以使用 &lt;code&gt;partition&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;了解更多&lt;/strong&gt; &lt;a href="https://www.kubernetes.org.cn/1130.html" target="_blank"&gt;StatefulSet: Kubernetes 中对有状态应用的运行和伸缩&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 06 Jan 2020 12:38:00 GMT</pubDate>
    </item>
    <item>
      <title>使用 kubectl 创建 Deployment 篇三</title>
      <link>https://www.zhangaoo.com/article/kubectl-deployment</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202016202759-k8s.jpg" alt="202016202759-k8s" /&gt;&lt;/p&gt; &lt;h1&gt;使用 kubectl 创建 Deployment&lt;/h1&gt; &lt;p&gt;&lt;a href="https://www.jianshu.com/p/0e3311bf94d5" target="_blank"&gt;更多kubectl命令用法&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;目标&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;了解 &lt;code&gt;Deployments&lt;/code&gt; 请求。&lt;/li&gt; &lt;li&gt;使用 &lt;code&gt;kubectl&lt;/code&gt; 在 &lt;code&gt;Kubernetes&lt;/code&gt; 上部署应用。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Kubernetes Deployments&lt;/h2&gt; &lt;p&gt;为了实现在 &lt;code&gt;Kubernetes&lt;/code&gt; 集群上部署容器化应用程序。需要创建一个 &lt;code&gt;Kubernetes Deployment，Deployment&lt;/code&gt; 负责创建和更新应用。创建 &lt;code&gt;Deployment&lt;/code&gt; 后，&lt;code&gt;Kubernetes master&lt;/code&gt; 会将 &lt;code&gt;Deployment&lt;/code&gt; 创建好的应用实例调度到集群中的各个节点。&lt;/p&gt; &lt;p&gt;应用实例创建完成后，&lt;code&gt;Kubernetes Deployment Controller&lt;/code&gt;会持续监视这些实例。如果管理实例的节点被关闭或删除，那么 &lt;code&gt;Deployment Controller&lt;/code&gt; 将会替换它们，实现自我修复能力。&lt;/p&gt; &lt;p&gt;“在旧的世界中” ，一般通常安装脚本来启动应用，但是便不会在机器故障后自动恢复。通过在 &lt;a href="http://docs.kubernetes.org.cn/304.html" target="_blank"&gt;Node&lt;/a&gt; 节点上运行创建好的应用实例，使 &lt;code&gt;Kubernetes Deployment&lt;/code&gt; 对应用管理提供了截然不同的方法。&lt;/p&gt; &lt;h2&gt;在Kubernetes上部署第一个应用程序&lt;/h2&gt; &lt;p&gt;&lt;img src="https://d33wubrfki0l68.cloudfront.net/3854a4db66ad3dd4ede078865eff41510eeba7c0/33ac5/docs/tutorials/kubernetes-basics/public/images/module_02_first_app.svg" alt="Kubernetes Cluster" /&gt;&lt;/p&gt; &lt;p&gt;使用 Kubernetes Kubectl（命令管理工具）创建和管理 Deployment。Kubectl使用Kubernetes API与集群进行交互。在本学习模块中，学会在Kubernetes集群上运行应用所需Deployment的Kubectl常见命令。&lt;/p&gt; &lt;p&gt;创建Deployment时，需要为应用程序指定容器镜像以及要运行的副本数，后续可以通过Deployment更新来更改该这些信息; bootcamp的第5和第6部分讨论了如何扩展和更新Deployment。&lt;/p&gt; &lt;p&gt;知道Deployment是什么，&lt;a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/deploy-app/deploy-intro/" target="_blank"&gt;来看看在线教程并部署你的第一个应用&lt;/a&gt;！&lt;/p&gt; &lt;h2&gt;Hello Minikube&lt;/h2&gt; &lt;p&gt;&lt;a href="https://kubernetes.io/docs/tutorials/hello-minikube/" target="_blank"&gt;官方参考&lt;/a&gt;&lt;/p&gt; &lt;h3&gt;目标&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Deploy a hello world application to Minikube.&lt;/li&gt; &lt;li&gt;Run the app&lt;/li&gt; &lt;li&gt;View application logs.&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;准备&lt;/h3&gt; &lt;p&gt;创建一个 &lt;code&gt;NodeJs&lt;/code&gt; 服务端代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;minikube/server.js   var http = require('http');  var handleRequest = function(request, response) {   console.log('Received request for URL: ' + request.url);   response.writeHead(200);   response.end('Hello World!'); }; var www = http.createServer(handleRequest); www.listen(8980); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;配置一个 &lt;code&gt;Dockerfile&lt;/code&gt; ，用于构建 &lt;code&gt;docker&lt;/code&gt; 镜像&lt;/p&gt; &lt;pre&gt;&lt;code&gt;FROM node:6.14.2 EXPOSE 8980 COPY server.js . CMD node server.js &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;For more information on the docker build command, read the &lt;a href="https://docs.docker.com/engine/reference/commandline/build/" target="_blank"&gt;Docker documentation&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;通过 &lt;code&gt;Dockerfile&lt;/code&gt; 构建一个 &lt;code&gt;docker&lt;/code&gt; 镜像&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 注意当前路径下需要有 Dockerfile 文件，注意后面的点 docker build -t node-app:v0.0.1 . # 构建成功后输出如下 ➜ docker build -t my-node-app:v0.0.1 . Sending build context to Docker daemon  3.072kB Step 1/4 : FROM node:6.14.2  ---&amp;gt; 00165cd5d0c0 Step 2/4 : EXPOSE 8980  ---&amp;gt; Using cache  ---&amp;gt; a78665dc1c60 Step 3/4 : COPY server.js .  ---&amp;gt; 8a7a68b5f3a6 Step 4/4 : CMD node server.js  ---&amp;gt; Running in bae95f61e18e Removing intermediate container bae95f61e18e  ---&amp;gt; 3ad203eefe49 Successfully built 3ad203eefe49 Successfully tagged my-node-app:v0.0.1 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Create a Deployment&lt;/h3&gt; &lt;p&gt;使用 &lt;code&gt;kubectl create&lt;/code&gt; 创建一个 &lt;code&gt;Deployment&lt;/code&gt;，&lt;code&gt;Deployment&lt;/code&gt; 管理一个 Pod，Pod 里面运行一个 Docker 镜像的容器&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 默认会去外网拉镜像，如果不加参数 --image-pull-policy=Never kubectl create deployment hello-node --image=my-node-app:v0.0.1 --image-pull-policy=Never # 返回值 # deployment.apps/hello-node created # 查看 deployment kubectl get deployments  NAME         READY   UP-TO-DATE   AVAILABLE   AGE hello-node   0/1     1            0           62s # 查看Pod，发现状态不对，没有拉到镜像，因为镜像在本地 kubectl get pods NAME                         READY   STATUS         RESTARTS   AGE hello-node-554b87dc5-5w6wt   0/1     ErrImagePull   0          2m11s  # 删除重建 kubectl delete deployment hello-node # 重新运行一下 kubectl run hello-node --image=my-node-app:v0.0.1 --image-pull-policy=Never --port=8080 --expose=true # 发现还是没有成功，就直接把镜像上传到 docker hub 了 kubectl create deployment hello-node --image=zealzhangz/my-node-app:v0.0.1 # 显示容器创建中 ➜  ~ kubectl get pods NAME                         READY   STATUS              RESTARTS   AGE hello-node-66f488898-mhnfm   0/1     ContainerCreating   0          19s # 过了一会就好了 ➜  ~ kubectl get pods NAME                         READY   STATUS    RESTARTS   AGE hello-node-66f488898-mhnfm   1/1     Running   0          2m23s &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;重新查看一遍 deployment&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# View the Deployment: $ kubectl get deployments NAME         READY   UP-TO-DATE   AVAILABLE   AGE hello-node   1/1     1            1           4m6s # View the Pod: $ kubectl get pods NAME                         READY   STATUS    RESTARTS   AGE hello-node-66f488898-mhnfm   1/1     Running   0          5m43s # View cluster events: kubectl get events # View the kubectl configuration: kubectl config view &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1 clusters: - cluster:     certificate-authority: /home/aozhang/.minikube/ca.crt     server: https://192.168.39.14:8443   name: test contexts: - context:     cluster: test     user: test   name: test current-context: test kind: Config preferences: {} users: - name: test   user:     client-certificate: /home/aozhang/.minikube/client.crt     client-key: /home/aozhang/.minikube/client.key &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Create a Service&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Pod&lt;/code&gt; 启动起来后，并没有对外暴露服务，因为我们一 Kubenetes Service 的形式来对外暴露服务; service是一组相同逻辑的pods和一个访问它们的策略。一组pods通常由label选择器确定。可以在ServiceSpec 中指定类型以不同的方式暴露服务。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Cluster IP(默认)： 只有集群内部访问。&lt;/li&gt; &lt;li&gt;NodePort: 使用NAT方式，在集群中每个选定的节点的同一端口上暴露服务。可以在集群外部访问服务。&lt;/li&gt; &lt;li&gt;LoadBalancer：创建外部负载均衡。&lt;/li&gt; &lt;li&gt;ExternalName：使用任意名称显示服务。&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;使用  &lt;code&gt;kubectl expose&lt;/code&gt; 对外暴露服务&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl expose deployment hello-node --type=LoadBalancer --port=8980 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;--type=LoadBalancer&lt;/code&gt; 参数表明你想对外网暴露你的集群服务&lt;/p&gt; &lt;ol start="2"&gt; &lt;li&gt;查看当前暴露的服务:&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl get services &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;输出&lt;/p&gt; &lt;pre&gt;&lt;code&gt;NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE hello-node   LoadBalancer   10.104.209.147   &amp;lt;pending&amp;gt;     8980:32740/TCP   3m1s kubernetes   ClusterIP      10.96.0.1        &amp;lt;none&amp;gt;        443/TCP          18h &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这样就 &lt;code&gt;expose&lt;/code&gt; 了一个服务，就可以外部访问了。可以通过一下指令得到 &lt;code&gt;url&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 这里需要加上集群参数，-p zzz-cluster 否则会访问默认的集群 minikube，然后不存在该集群，一直报错 $ minikube service hello-node --url -p zzz-cluster &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;扩容和更新&lt;/h3&gt; &lt;p&gt;根据线上需求，扩容和缩容是常会遇到的问题。&lt;code&gt;Scaling&lt;/code&gt; 是通过更改 &lt;code&gt;Deployment&lt;/code&gt; 中的副本数量实现的。一旦您有应用程序的多个实例，您将能够滚动更新，而不会停止服务。通过 &lt;code&gt;kubectl scale&lt;/code&gt; 指令来扩容和缩容&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl get pods -L app --show-labels # 输出如下 NAME                         READY   STATUS    RESTARTS   AGE     APP          LABELS hello-node-66f488898-mbwmm   1/1     Running   2          3h22m   hello-node   app=hello-node,pod-template-hash=66f488898 &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 调查可知，扩充pods数一般就扩充 deployment kubectl scale --replicas=2 deployment/hello-node &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;再查看当前pods：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;NAME                         READY   STATUS    RESTARTS   AGE hello-node-66f488898-22nrh   1/1     Running   0          68s hello-node-66f488898-mbwmm   1/1     Running   2          3h43m &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;官方扩展的资源示例如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# Scale a replicaset named 'foo' to 3. kubectl scale --replicas=3 rs/foo  # Scale a resource identified by type and name specified in &amp;quot;foo.yaml&amp;quot; to 3. kubectl scale --replicas=3 -f foo.yaml  # If the deployment named mysql's current size is 2, scale mysql to 3. kubectl scale --current-replicas=2 --replicas=3 deployment/mysql  # Scale multiple replication controllers. kubectl scale --replicas=5 rc/foo rc/bar rc/baz  # Scale job named 'cron' to 3. kubectl scale --replicas=3 job/cron &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们都是期望应用 &lt;code&gt;24/7&lt;/code&gt; 运行，但又需要频繁部署。这我们就需要 &lt;code&gt;rolling update&lt;/code&gt; (滚动更新)。&lt;code&gt;Rolling updates&lt;/code&gt; 允许通过使用新的 &lt;code&gt;Pods&lt;/code&gt; 实例逐个更新来实现零停机的部署更新。新的 &lt;code&gt;Pods&lt;/code&gt; 会被调度到可用资源的 &lt;code&gt;Node&lt;/code&gt; 节点上。可以通过 &lt;code&gt;set image&lt;/code&gt; 修改镜像。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ kubectl set image deployments/&amp;lt;部署名&amp;gt; &amp;lt;部署名&amp;gt;=镜像名：tag &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如我们的第二版镜像。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;kubectl set image deployments/docker-demo docker-demo=dennisge/docker_demo:v2 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;重新设置镜像之后，就会执行滚动更新。 以上我们就成功部署了自己的应用。但是都是通过指令来进行的，下面我们将介绍一下部署 kubernetes dashboard，并通过 web ui的方式部署、删除和修改相关应用等。&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 06 Jan 2020 12:36:00 GMT</pubDate>
    </item>
    <item>
      <title>使用 Minikube 部署 Kubernetes 集群 篇二</title>
      <link>https://www.zhangaoo.com/article/deploy-minikube</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202016202759-k8s.jpg" alt="202016202759-k8s" /&gt;&lt;/p&gt; &lt;h1&gt;使用 Minikube 部署 Kubernetes 集群&lt;/h1&gt; &lt;h2&gt;环境确认&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 确认宿主机是否支持虚拟化，支持的话下面命令有内容输出 egrep --color 'vmx|svm' /proc/cpuinfo &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;单机部署&lt;/h2&gt; &lt;h3&gt;Installing minikube&lt;/h3&gt; &lt;h4&gt;Install kubectl&lt;/h4&gt; &lt;p&gt;直接二进制安装最新版本&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl # 赋予可执行权限 chmod +x ./kubectl # Move the binary in to your PATH. sudo mv ./kubectl /usr/local/bin/kubectl # Test to ensure the version you installed is up-to-date: kubectl version # 输出信息如下 Client Version: version.Info{Major:&amp;quot;1&amp;quot;, Minor:&amp;quot;15&amp;quot;, GitVersion:&amp;quot;v1.15.1&amp;quot;, GitCommit:&amp;quot;4485c6f18cee9a5d3c3b4e523bd27972b1b53892&amp;quot;, GitTreeState:&amp;quot;clean&amp;quot;, BuildDate:&amp;quot;2019-07-18T09:18:22Z&amp;quot;, GoVersion:&amp;quot;go1.12.5&amp;quot;, Compiler:&amp;quot;gc&amp;quot;, Platform:&amp;quot;linux/amd64&amp;quot;} &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;Install a Hypervisor&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 安装KVM，参考链接：https://www.cyberciti.biz/faq/installing-kvm-on-ubuntu-16-04-lts-server/ sudo apt-get install qemu-kvm libvirt-bin virtinst bridge-utils cpu-checker # 确认安装 ➜  kvm-ok INFO: /dev/kvm exists KVM acceleration can be used &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;二进制安装 minikube&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# download curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \   &amp;amp;&amp;amp; chmod +x minikube # install sudo install minikube /usr/local/bin   # confirm ➜  minikube version minikube version: v1.2.0 &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;进入 minikube 虚拟机&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube ssh -p zzz-cluster # 打开浏览器载入 dashboard minikube dashboard &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;minikube 创建集群&lt;/h4&gt; &lt;pre&gt;&lt;code&gt;$ minikube start -p zzz-cluster --vm-driver kvm2 😄  minikube v1.2.0 on linux (amd64) 🔥  Creating kvm2 VM (CPUs=2, Memory=2048MB, Disk=20000MB) ... 🐳  Configuring environment for Kubernetes v1.15.0 on Docker 18.09.6 💾  Downloading kubelet v1.15.0 💾  Downloading kubeadm v1.15.0 🚜  Pulling images ... 🚀  Launching Kubernetes ...  ⌛  Verifying: apiserver proxy etcd scheduler controller dns 🏄  Done! kubectl is now configured to use &amp;quot;zzz-cluster&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;启动集群&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 启动 cluster 且输出详细日志 minikube start -p zzz-cluster --vm-driver kvm2 –-v=9 &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;关闭集群&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube stop -p zzz-cluster   &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;删除集群&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;minikube delete -p zzz-cluster  &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Mon, 06 Jan 2020 12:34:00 GMT</pubDate>
    </item>
    <item>
      <title>K8s Kubernetes 基础概述 篇一</title>
      <link>https://www.zhangaoo.com/article/basic-summary</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/202016202759-k8s.jpg" alt="Kubernetes 基础概述" /&gt;&lt;/p&gt; &lt;h1&gt;Kubernetes 基础概述&lt;/h1&gt; &lt;h2&gt;Kubernetes基础&lt;/h2&gt; &lt;p&gt;本互动教程介绍了Kubernetes群集编排系统的基础知识。每个模块都包含Kubernetes的主要功能、概念的一些背景介绍。使用本教程，你可以了解：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在集群上部署容器化应用&lt;/li&gt; &lt;li&gt;集群规模化部署&lt;/li&gt; &lt;li&gt;更新容器化应用的版本&lt;/li&gt; &lt;li&gt;调试容器化应用&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这些教程使用 &lt;code&gt;Katacoda&lt;/code&gt; 在浏览器中运行虚拟终端，虚拟终端运行 &lt;code&gt;Minikube&lt;/code&gt;，它可在任何环境任何地方小规模的部署 &lt;code&gt;Kubernetes&lt;/code&gt;，且不需要安装任何软件或配置任何东西，每个互动教程都在自己浏览器中运行。&lt;/p&gt; &lt;h2&gt;Kubernetes 特点&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;可移植: 支持公有云，私有云，混合云，多重云（multi-cloud）&lt;/li&gt; &lt;li&gt;可扩展: 模块化, 插件化, 可挂载, 可组合&lt;/li&gt; &lt;li&gt;自动化: 自动部署，自动重启，自动复制，自动伸缩/扩展&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Why containers?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201985113244-why_containers.jpg" alt="201985113244-why_containers" /&gt;&lt;/p&gt; &lt;p&gt;传统的应用部署方式是通过插件或脚本来安装应用。这样做的缺点是应用的运行、配置、管理、所有生存周期将与当前操作系统绑定，这样做并不利于应用的升级更新/回滚等操作，当然也可以通过创建虚机的方式来实现某些功能，但是虚拟机非常重，并不利于可移植性。&lt;/p&gt; &lt;p&gt;新的方式是通过部署容器方式实现，每个容器之间互相隔离，每个容器有自己的文件系统 ，容器之间进程不会相互影响，能区分计算资源。相对于虚拟机，容器能快速部署，由于容器与底层设施、机器文件系统解耦的，所以它能在不同云、不同版本操作系统间进行迁移。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;容器优势总结：&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;快速创建/部署应用：与VM虚拟机相比，容器镜像的创建更加容易。&lt;/li&gt; &lt;li&gt;持续开发、集成和部署：提供可靠且频繁的容器镜像构建/部署，并使用快速和简单的回滚(由于镜像不可变性)。&lt;/li&gt; &lt;li&gt;开发和运行相分离：在 &lt;code&gt;build&lt;/code&gt; 或者 &lt;code&gt;release&lt;/code&gt; 阶段创建容器镜像，使得应用和基础设施解耦。&lt;/li&gt; &lt;li&gt;开发，测试和生产环境一致性：在本地或外网（生产环境）运行的一致性。&lt;/li&gt; &lt;li&gt;云平台或其他操作系统：可以在 &lt;code&gt;Ubuntu、RHEL、 CoreOS、on-prem、Google Container Engine&lt;/code&gt; 或其它任何环境中运行。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Loosely coupled&lt;/code&gt;，分布式，弹性，微服务化：应用程序分为更小的、独立的部件，可以动态部署和管理。&lt;/li&gt; &lt;li&gt;资源隔离&lt;/li&gt; &lt;li&gt;资源利用：更高效&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;使用Kubernetes能做什么？&lt;/h2&gt; &lt;p&gt;可以在物理或虚拟机的 &lt;code&gt;Kubernetes&lt;/code&gt; 集群上运行容器化应用，&lt;code&gt;Kubernetes&lt;/code&gt; 能提供一个以“容器为中心的基础架构”，满足在生产环境中运行应用的一些常见需求，如：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;多个进程（作为容器运行）协同工作。（Pod）&lt;/li&gt; &lt;li&gt;存储系统挂载&lt;/li&gt; &lt;li&gt;Distributing secrets&lt;/li&gt; &lt;li&gt;应用健康检测&lt;/li&gt; &lt;li&gt;应用实例的复制&lt;/li&gt; &lt;li&gt;Pod自动伸缩/扩展&lt;/li&gt; &lt;li&gt;Naming and discovering&lt;/li&gt; &lt;li&gt;负载均衡&lt;/li&gt; &lt;li&gt;滚动更新&lt;/li&gt; &lt;li&gt;资源监控&lt;/li&gt; &lt;li&gt;日志访问&lt;/li&gt; &lt;li&gt;调试应用程序&lt;/li&gt; &lt;li&gt;提供认证和授权&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Kubernetes不是什么？&lt;/h2&gt; &lt;p&gt;Kubernetes并不是传统的PaaS（平台即服务）系统。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Kubernetes不限制支持应用的类型，不限制应用框架。不限制受支持的语言runtimes (例如, Java, Python, Ruby)，满足12-factor applications 。不区分 “apps” 或者“services”。 Kubernetes支持不同负载应用，包括有状态、无状态、数据处理类型的应用。只要这个应用可以在容器里运行，那么就能很好的运行在Kubernetes上。&lt;/li&gt; &lt;li&gt;Kubernetes不提供中间件（如message buses）、数据处理框架（如Spark）、数据库(如Mysql)或者集群存储系统(如Ceph)作为内置服务。但这些应用都可以运行在Kubernetes上面。&lt;/li&gt; &lt;li&gt;Kubernetes不部署源码不编译应用。持续集成的 (CI)工作流方面，不同的用户有不同的需求和偏好的区域，因此，我们提供分层的 CI工作流，但并不定义它应该如何工作。&lt;/li&gt; &lt;li&gt;Kubernetes允许用户选择自己的日志、监控和报警系统。&lt;/li&gt; &lt;li&gt;Kubernetes不提供或授权一个全面的应用程序配置 语言/系统（例如，jsonnet）。&lt;/li&gt; &lt;li&gt;Kubernetes不提供任何机器配置、维护、管理或者自修复系统。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;另一方面，大量的Paas系统都可以运行在Kubernetes上，比如Openshift、Deis、Gondor。可以构建自己的Paas平台，与自己选择的CI系统集成。&lt;/p&gt; &lt;p&gt;由于Kubernetes运行在应用级别而不是硬件级，因此提供了普通的Paas平台提供的一些通用功能，比如部署，扩展，负载均衡，日志，监控等。这些默认功能是可选的。&lt;/p&gt; &lt;p&gt;另外，Kubernetes不仅仅是一个“编排系统”；它消除了编排的需要。“编排”的定义是指执行一个预定的工作流：先执行A，之B，然C。相反，Kubernetes由一组独立的可组合控制进程组成。怎么样从A到C并不重要，达到目的就好。当然集中控制也是必不可少，方法更像排舞的过程。这使得系统更加易用、强大、弹性和可扩展。&lt;/p&gt; &lt;h2&gt;Kubernetes基础模块&lt;/h2&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_01.svg" alt="创建一个KUBERNETES集群" /&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/create-cluster/cluster-intro/" target="_blank"&gt;上图是创建一个 &lt;code&gt;KUBERNETES&lt;/code&gt; 集群的模型&lt;/a&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_02.svg?v=1469803628347" alt="部署一个应用程序导集群" /&gt;&lt;/p&gt; &lt;ol start="2"&gt; &lt;li&gt;&lt;a href="http://docs.kubernetes.org.cn/113.html" target="_blank"&gt;部署一个应用程序导集群&lt;/a&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_03.svg?v=1469803628347" alt="查看应用程序" /&gt; 3. &lt;a href="http://docs.kubernetes.org.cn/115.html" target="_blank"&gt;查看应用程序&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_04.svg?v=1469803628347" alt="发布应用程序" /&gt; 4. &lt;a href="http://docs.kubernetes.org.cn/117.html" target="_blank"&gt;发布应用程序&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_05.svg?v=1469803628347" alt="扩展应用程序" /&gt; 5. &lt;a href="http://docs.kubernetes.org.cn/122.html" target="_blank"&gt;扩展应用程序&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="https://kubernetes.io/docs/tutorials/kubernetes-basics/public/images/module_06.svg?v=1469803628347" alt="更新应用程序" /&gt; 6. &lt;a href="http://docs.kubernetes.org.cn/124.html" target="_blank"&gt;更新应用程序&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 06 Jan 2020 12:29:00 GMT</pubDate>
    </item>
    <item>
      <title>3 分钟学会 Makefile</title>
      <link>https://www.zhangaoo.com/article/makefile-start</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20191212195522-makefile-img.png" alt="20191212195522-makefile-img" /&gt;&lt;/p&gt; &lt;h1&gt;Makefile 介绍&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;make&lt;/code&gt; 是一个命令工具，它解释 &lt;code&gt;Makefile&lt;/code&gt; 中的指令（应该说是规则）。&lt;/li&gt; &lt;li&gt;在 &lt;code&gt;Makefile&lt;/code&gt; 文件中描述了整个工程所有文件的编译顺序、编译规则。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Makefile&lt;/code&gt; 有自己的书写格式、关键字、函数。像 C 语言有自己的格式、关键字和函数一样。&lt;/li&gt; &lt;li&gt;而且在 &lt;code&gt;Makefile&lt;/code&gt; 中可以使用系统&lt;code&gt;shell&lt;/code&gt; 所提供的任何命令来完成想要的工作。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Makefile 规则&lt;/h2&gt; &lt;h3&gt;Makefile 语法&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-makefile"&gt;target: prerequisites         commands  目标文件： 依赖项          命令1          命令2          ... &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;目标&lt;/h3&gt; &lt;p&gt;目标即要生成的文件。如果目标文件的更新时间晚于依赖文件的更新时间，则说明依赖文件没有改动，目标文件不需要重新编译。否则重新编译并更新目标。&lt;/p&gt; &lt;h3&gt;依赖&lt;/h3&gt; &lt;p&gt;即目标文件由哪些文件生成。如果依赖条件中存在不存在的依赖条件，则会寻找其它规则是否可以产生依赖条件。例如：规则一是生成目标 hello.out 需要使用到依赖条件 hello.o，但是 hello.o 不存在。则 Makefile 会寻找到一个生成 hello.o 的规则二并执行。&lt;/p&gt; &lt;h3&gt;命令&lt;/h3&gt; &lt;p&gt;即通过执行该命令，由依赖文件生成目标文件。注意每条命令前必须有且仅有一个 &lt;code&gt;tab&lt;/code&gt; 保持缩进，这是语法要求。&lt;/p&gt; &lt;h3&gt;示例&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-c"&gt;#include &amp;lt;stdio.h&amp;gt;   int main() {     printf(&amp;quot;Hello World !\n&amp;quot;);     return 0; &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-Makefile"&gt;ALL: hello.out   hello.out: hello.c     # 必须是 tab 不能是四个空格     gcc hello.c -o hello.out &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;编译运行&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ make gcc hello.c -o hello.out $ ./hello.out Hello World ! &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;ALL&lt;/h3&gt; &lt;p&gt;由于 &lt;code&gt;Makefile&lt;/code&gt; 只能有一个目标，所以可以构造一个没有规则的终极目标 &lt;code&gt;ALL&lt;/code&gt;；&lt;code&gt;Makefile&lt;/code&gt; 文件默认只生成第一个目标文件即完成编译，但是我们可以通过 &lt;code&gt;ALL&lt;/code&gt; 指定需要生成的目标文件。直接 &lt;code&gt;make&lt;/code&gt; 或 &lt;code&gt;make all&lt;/code&gt; 的话会用 &lt;code&gt;gcc&lt;/code&gt; 编译 &lt;code&gt;hello.c&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;注意这里 &lt;code&gt;ALL&lt;/code&gt; 并不是一个关键词，随便写什么 &lt;code&gt;ABC&lt;/code&gt; 都行，只是大家习惯写 &lt;code&gt;ALL&lt;/code&gt; 来代表整个目标。&lt;/p&gt; &lt;h2&gt;.PHONY&lt;/h2&gt; &lt;p&gt;phony ['fəuni] adj. 假的&lt;/p&gt; &lt;p&gt;&lt;code&gt;PHONY&lt;/code&gt; 目标并非实际的文件名：只是在显式请求时执行命令的名字。有两种理由需要使用 &lt;code&gt;PHONY&lt;/code&gt; 目标：避免和同名文件冲突，改善性能。&lt;/p&gt; &lt;p&gt;继续上面的例子，假设我们需要清理 &lt;code&gt;hello.out&lt;/code&gt; 这个产物，重新编译，那么 Makefile 会这么写&lt;/p&gt; &lt;pre&gt;&lt;code class="language-makefile"&gt;ALL: hello.out   hello.out: hello.c     # 必须是 tab 不能是四个空格     gcc hello.c -o hello.out clean:     rm hello.out &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当执行 &lt;code&gt;make clean&lt;/code&gt; 的时候，可以正常清理文件，但是假设有一种情况，我们有一个叫 &lt;code&gt;clean&lt;/code&gt; 的文件在同级目录，那么执行 &lt;code&gt;make clean&lt;/code&gt; 的时候就会报下面的错误：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ make clean  make: `clean' is up to date. &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;报错的原因是文件名冲突了，因为 &lt;code&gt;make&lt;/code&gt; 认为默认的依赖是文件，为了避免这种情况，我们加上 &lt;code&gt;.PHONY&lt;/code&gt; 告诉 &lt;code&gt;make&lt;/code&gt; 这不是一个依赖的文件。更新后 &lt;code&gt;Makefile&lt;/code&gt; 如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-makefile"&gt;ALL: hello.out   hello.out: hello.c     # 必须是 tab 不能是四个空格     gcc hello.c -o hello.out  .PHONY: clean     clean:          rm hello.out &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 12 Dec 2019 11:57:00 GMT</pubDate>
    </item>
    <item>
      <title>删库跑路未遂</title>
      <link>https://www.zhangaoo.com/article/rm-rf-all</link>
      <content:encoded>&lt;h1&gt;删库跑路未遂&lt;/h1&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20191029175312-tiger.jpg" alt="20191029175312-tiger" /&gt;&lt;/p&gt; &lt;h2&gt;惊魂 rm -rf /*&lt;/h2&gt; &lt;p&gt;话不多说，先上图。胖友你是否也有过这么脑残的行为！！&lt;code&gt;root&lt;/code&gt; 猛如虎！！&lt;code&gt;root&lt;/code&gt; 下的 &lt;code&gt;rm&lt;/code&gt; 猛如虎！！&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019102917494-rm.jpg" alt="2019102917494-rm" /&gt;&lt;/p&gt; &lt;h2&gt;冷静思考&lt;/h2&gt; &lt;p&gt;心里暗想，这难道要删库跑路吗！搬着指头算了一下，上面装了多少个环境：&lt;code&gt;K8s&lt;/code&gt; 集群 + &lt;code&gt;TSDB&lt;/code&gt; + &lt;code&gt;CDH&lt;/code&gt;；虽然不是生产环境，但这么多系统，多多少少还是有点懵逼加心慌。&lt;/p&gt; &lt;p&gt;简单检查了一下 &lt;code&gt;K8s&lt;/code&gt; 集群、&lt;code&gt;TSDB&lt;/code&gt;、 &lt;code&gt;CDH&lt;/code&gt;发现这些应用都还是正常的，祈求上天保佑没删什么重要的文件！🙏🙏🙏&lt;/p&gt; &lt;h2&gt;为什么连不上SSH&lt;/h2&gt; &lt;p&gt;仔细一对比发现 &lt;code&gt;/bin&lt;/code&gt; 目录不见的，这下搞的有点大，难怪 &lt;code&gt;ssh&lt;/code&gt; 连不上了，应该是找不到相关的二进制文件了。也有可能是 &lt;code&gt;.ssh&lt;/code&gt; 被删除了，必须 &lt;code&gt;ssh&lt;/code&gt; 上去验证一下才能确定&lt;/p&gt; &lt;p&gt;现在的问题是怎么恢复 &lt;code&gt;/bin&lt;/code&gt; 这个目录，关键是 ssh 不上去；开始思考怎么拿到 &lt;code&gt;shell&lt;/code&gt;；以前研究过一点渗透测试，于是开始思考怎么利用上面的应用软件来搞事。&lt;/p&gt; &lt;h2&gt;想办法拿shell权限&lt;/h2&gt; &lt;p&gt;还必须是 &lt;code&gt;root&lt;/code&gt; 权限；&lt;/p&gt; &lt;h3&gt;CDH&lt;/h3&gt; &lt;p&gt;之前在安装 &lt;code&gt;CDH&lt;/code&gt; 的时候已经做过 &lt;code&gt;SSH&lt;/code&gt; 互信，当然我本地不能连上去，做了 &lt;code&gt;SSH&lt;/code&gt; 互信也没用。然后把方向调转到了 &lt;code&gt;CDH Web&lt;/code&gt; 管理界面，到 &lt;code&gt;web&lt;/code&gt; 界面逛了一圈没发现有现成的入口可以利用，先暂时放弃这条路。&lt;/p&gt; &lt;h3&gt;K8s&lt;/h3&gt; &lt;p&gt;比较下来感觉还是 &lt;code&gt;K8s&lt;/code&gt; 希望大一点，我简单试了几次，部署了个测试的 &lt;code&gt;pod&lt;/code&gt; 到该节点上去，可以部署成功，但是 &lt;code&gt;ssh&lt;/code&gt; 上去连接到的是 &lt;code&gt;Container&lt;/code&gt; 内的 &lt;code&gt;ssh&lt;/code&gt;。也操作不了 宿主机。突然一个灵光闪现，把宿主机的根路径挂我我容器里面来，感觉可行，于是就创建了如下的 &lt;code&gt;pod&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;由于一开始我没指定 &lt;code&gt;pod&lt;/code&gt; 部署的 &lt;code&gt;node&lt;/code&gt;，老是会把这个 &lt;code&gt;pod&lt;/code&gt; 部署到其他节点上去，通过下面的方法查询 &lt;code&gt;node&lt;/code&gt; 的 &lt;code&gt;label&lt;/code&gt;，然后进行指定：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 查询 node 标签 $ kubectl describe node &amp;quot;dbnode2.localdomain&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从结果中选取一个能标识 &lt;code&gt;node&lt;/code&gt; 的 &lt;code&gt;label&lt;/code&gt;，我们就选取&lt;code&gt;kubernetes.io/hostname=dbnode2.localdomain&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;Labels: beta.kubernetes.io/arch=amd64         beta.kubernetes.io/os=linux         kubernetes.io/arch=amd64         kubernetes.io/hostname=dbnode2.localdomain         kubernetes.io/os=linux &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;最终 &lt;code&gt;pod&lt;/code&gt; 的配置如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1 kind: Pod metadata:   name: privileged-pod   namespace: default spec:   containers:   - name: busybox     image: busybox     resources:       limits:         cpu: 200m         memory: 100Mi       requests:         cpu: 100m         memory: 50Mi     stdin: true     securityContext:       privileged: true     volumeMounts:     - name: host-root-volume       mountPath: /host       readOnly: true   volumes:   - name: host-root-volume     hostPath:       path: /   hostNetwork: true   hostPID: true   restartPolicy: Never   nodeSelector:     kubernetes.io/hostname: dbnode2.localdomain &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 部署pod kubectl create -f privileged-pod.yaml # 查看，pod 已经在运行了 kubectl get pods -o=wide NAME                                      READY   STATUS    RESTARTS   AGE   IP             NODE                  NOMINATED NODE   READINESS GATES privileged-pod                            1/1     Running   0          8s    10.115.0.223   dbnode2.localdomain   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt; # ssh 连接 kubectl exec -ti privileged-pod sh # 查看挂载目录，果然挂上来了 / # ls /host bin  boot  data1  data2  data3  data4  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分析到底是缺了什么导致 &lt;code&gt;ssh&lt;/code&gt; 不能连接，进入 &lt;code&gt;.ssh&lt;/code&gt; 目录发现文件还在，先还原 &lt;code&gt;/bin&lt;/code&gt; 看看，发现该目录上只读的。刚燃起的信心又凉了半截，仔细一看发现 &lt;code&gt;readOnly: true&lt;/code&gt; 果断改成  &lt;code&gt;readOnly: false&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 发现没有 w 权限，执行创建还是没权限 / # ls -na dr-xr-xr-x   21 0        0             4096 Oct 29 09:29 host # 添加权限然后再创建然后成功了 / # chmod u+w /host &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;幸运的是发现 &lt;code&gt;/bin&lt;/code&gt; 原来是 &lt;code&gt;/usr/bin&lt;/code&gt; 的一个软连接，搞半天删了个软件链接，看来机会大大的有，于是新建一个软连接&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;ln -s bin usr/bin &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后在 &lt;code&gt;ssh&lt;/code&gt; 试一下，发现成功的了！！卧槽感动的想哭！！再次提醒自己一边 &lt;code&gt;root&lt;/code&gt; 猛如虎！&lt;code&gt;rm&lt;/code&gt; 猛如虎！这次真的不用跑了。&lt;/p&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;最小权限原则无论是什么时候都适用，就算因为 0day 被攻破了，黑客拿到的是非 root 权限，那么黑客能做的事业有限。&lt;/li&gt; &lt;li&gt;rm 必须谨慎使用，特别是加上 -rf 两个参数，特别是在 root 权限下执行。&lt;/li&gt; &lt;li&gt;不要心存侥幸，墨菲定律，凡事有几率的事情都一定会发生。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Tue, 29 Oct 2019 13:06:00 GMT</pubDate>
    </item>
    <item>
      <title>《墨菲定律》读书笔记</title>
      <link>https://www.zhangaoo.com/article/murphy-law</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/201991165540-Murphy.jpg" alt="201991165540-Murphy" /&gt;&lt;/p&gt; &lt;h1&gt;墨菲定律读书笔记&lt;/h1&gt; &lt;p&gt;通常我们所做的工作80%都是无用功，只有20%是产生收效的。如何避免这种情况的发生?二八法则告 诉我们，要把主要精力放在20%的工作上，让其产生80%的收效。&lt;/p&gt; &lt;p&gt;奥卡姆剃刀定律认为，在我们做过的事情中，可能绝大部分是毫无意义的，真正有效的活动只是其中的一小部分，而它们通常隐含于繁杂的事物中。找到关键的部分，去掉多余的活动，成功就由复杂变得简单了。&lt;/p&gt; &lt;p&gt;马蝇效应认为，没有马蝇叮咬，马就会慢慢腾腾，走走停停;如果有马蝇叮咬，马就不敢怠慢，跑得飞快。也就是说，人是需要一根鞭子的，只有被不停地抽打，才不会松懈，才会努力拼搏，不断进步。这根鞭子是压力，是挫折和困难，是危机意识。这一解释不仅适用于个人，同样也适用于企业管理。&lt;/p&gt; &lt;p&gt;凡勃伦效应认为，一件商品的价格定得越高，就越能受到消费者的注意与青睐。其实，消费者购买这类商品的目的，并不仅仅是为了获得直接的物质满足和享受，更大程度上是为了获得心理上的满足。凡勃伦效应同时告诉我们:不要被事务的外表所蒙蔽，要警惕事务的华而不实，防止出现花费与收益出现严重偏差。&lt;/p&gt; &lt;p&gt;人常常迷失在自我当中，很容易受到周围信息的暗示，并把他人的言行作为自己行动的参照，常常认为一种笼统的、一般性的人格描述十分准确地揭示了自己的特点。也就是说，算命先生所说的话一般是共性的，即这些话对谁说都能有一定的准确性。人在那种特殊的情况下，就会在无形中把被说中的部分扩大了，所以会感觉很准。对此，巴纳姆效应告诉我们:要认识你自己，要相信你自己，树立科学的人生观，才不会被一些骗子所迷惑。&lt;/p&gt; &lt;p&gt;除非你清楚自己要到哪里去，否则你永远也到不了自己想去的地方。&lt;/p&gt; &lt;p&gt;在这个世界上有这样一种现象，那就是“没有目标的人在为有目标的人达成目标”。&lt;/p&gt; &lt;p&gt;瓦拉赫效应:成功，要懂得经营自己的长处。&lt;/p&gt; &lt;p&gt;人生成功的诀窍在于经营自己的个性长处，经营长处能使自己的人生增值，否则，必将使自己的人生贬值。&lt;/p&gt; &lt;p&gt;缺憾应当成为一种促使自己向上的激励机制，而不是一种自甘沉沦的理由，它暗示你在它上面应当作一点努力。&lt;/p&gt; &lt;p&gt;木桶定律:抓最“长”的，不如抓最“短”&lt;/p&gt; &lt;p&gt;与“阿喀琉斯之踵”类似，任何事情或组织都有它的最薄弱之处，而问题又往往由这里产生。那么，如果我们把这个最薄弱处解决，问题往往就迎刃而解了。&lt;/p&gt; &lt;p&gt;孩子成绩不好，解决的方法不是帮他们做题、写作业，也不是用训斥来打击他们幼小的心灵，而是要找到孩子在学习上的薄弱之处，从这里着手，才能从根本上提高孩子的成绩......&lt;/p&gt; &lt;p&gt;艾森豪威尔法则:分清主次，高效成事&lt;/p&gt; &lt;p&gt;我们常常会看到这样的现象，一个人忙得团团转，可是当你问他忙些什么时，他却说不出个具体来，只说自己忙死了。&lt;/p&gt; &lt;p&gt;这一原则将工作区分为5个类别: A:必须做的事情; B:应该做的事情; C:量力而为的事情; D:可 委托他人去做的事情; E:应该删除的工作。&lt;/p&gt; &lt;p&gt;记住苏格拉底的话:“任何问题最可能的解决办法是步骤最少的办法。”&lt;/p&gt; &lt;p&gt;失败并不意味着你是一位失败者——失败只是表明你尚未成功。&lt;/p&gt; &lt;p&gt;酝酿效应:灵感来自偶然，有时不期而至&lt;/p&gt; &lt;p&gt;韦特莱法则:先有超人之想，才有超人之举&lt;/p&gt; &lt;p&gt;孟子曰:“故天将降大任于斯人也，必先苦其心志，劳其筋骨，饿其体肤，空乏其身，行拂乱其所为，所以动心忍性，曾益其所不能。”就是说，能成就大业的人，都要经历一个痛苦的过程，出乎意外，才能得乎意外。&lt;/p&gt; &lt;p&gt;老子云:“天下难事，必作于易。”&lt;/p&gt; &lt;p&gt;布利斯定理:凡事预则立，不预则废。&lt;/p&gt; &lt;p&gt;成功=明确目标+详细计划+马上行动+检查修正+坚持到底&lt;/p&gt; &lt;p&gt;古语中“欲求其上上，而得其上;欲求其上，而得其中;欲求其中，而得其下”，说的就是“起点高才能至高”的道理。&lt;/p&gt; &lt;p&gt;无论你天资如何，无论你有多大的缺陷，决定你输赢的都不是这些，而是你是否能永远清醒地认识自己，是否能做到戒骄戒躁。在跑步时，跑得快的不一定赢;在打架时，实力弱的不一定输。没到最后一刻，都无法定输赢。只有笑到最后的人，才是真正的赢家。&lt;/p&gt; &lt;p&gt;怨天尤人，一点益处也没有。对你的工作不会有任何帮助，还会让别人看低你。所以，潜伏办公室，就要把自己消极的情绪锁起来，永远呈现出积极阳光、精明能干的一面，这才会赢得别人的尊重，领导的器重，工作的顺利。&lt;/p&gt; &lt;p&gt;冷语伤人。同事只是你的工作伙伴，而不是你的兄弟姐妹，就算你句句有理，谁愿意洗耳恭听你的指责?每个人都有貌似坚强实则脆弱的自尊心，凭什么对你的冷言冷语一再宽容?很多人会介意你的态度“你以为你是谁?”何况很多人不会把你的好放在心上，一件事造成的摩擦就可能使你一无是处。&lt;/p&gt; &lt;p&gt;1927年，鲁迅先生作了篇名为《无声的中国》的文章，其中有段话写道:“中国人的性情，总是喜欢调 和、折中的，譬如你说，这屋子太暗，说在这里开一个天窗，大家一定是不允许的，但如果你主张拆掉 屋顶，他们就会来调和，愿意开天窗了。”&lt;/p&gt; &lt;p&gt;只有你意识到危险了，才会更加集中精力，那样反而会更安全。这儿发生过好几起坠谷事件，都是迷路的游客在毫无压力的情况下一不小心摔下去的。我们每天都挑着东西来来去去，却从来没人出事。&lt;/p&gt; &lt;p&gt;两根沉木条在危险面前竟成了人们的“护身符”。其实，许多时候，如果我们学会在肩上压上两根“沉木条”，给自己一些压力，确实会让我们走得更好。&lt;/p&gt; &lt;p&gt;没错，人生需要一定的“激发”，就好比著名的钱塘大潮，至柔至弱的水，一经激发，便能产生“白马千群浪涌，银山万迭天高”的蔚为壮观的景象。&lt;/p&gt; &lt;p&gt;无论何时，无论何种情形之下，抓紧到手的利益才是上策。&lt;/p&gt; &lt;p&gt;要珍惜自己拥有的，不要轻易地为了看似更美好的东西就放弃了手里的东西。最愚蠢的人莫过于还没有拿到新东西，就放弃已到手的宝贝。&lt;/p&gt; &lt;p&gt;天下熙熙皆为利来，天下攘攘皆为利往，没有永远的敌人，只有永远的利益。&lt;/p&gt; &lt;p&gt;人生有很多个选择，不要以为放掉了一个就失去了所有，有时候只有放弃了才能获得。&lt;/p&gt; &lt;p&gt;当你紧握双手，里面什么也没有;当你打开双手，世界就在你手中。&lt;/p&gt; &lt;p&gt;人生是一场大火，我们每个人唯一可做的，就是从这场大火中多抢一点东西出来。”在火中抢东西，一定要注意主次，没有多少时间供我们考虑，尽可能挑最重要的拿，而放弃那些相比之下次要的东西。&lt;/p&gt; &lt;p&gt;竞争中，我们不可能每个机会都去尝试，也不可能每个领域都获得成功，放弃自己不擅长的，放弃没有结果的尝试，放弃过多的欲望，放弃错误的坚持。&lt;/p&gt; &lt;p&gt;给人留下一个好印象，牢记以下5点：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;显露自信和朝气蓬勃的精神&lt;/li&gt; &lt;li&gt;讲信用，守时间&lt;/li&gt; &lt;li&gt;仪表、举止得体&lt;/li&gt; &lt;li&gt;微笑待人，不卑不亢&lt;/li&gt; &lt;li&gt;言行举止讲究文明礼貌&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;人生最美丽的补偿之一，就是人们真诚地帮助别人之后，同时也帮助了自己。帮助别人也就是帮助自己。&lt;/p&gt; &lt;p&gt;把自己当成别人，把别人当成自己，把别人当成别人，把自己当成自己。&lt;/p&gt; &lt;p&gt;实际上，并非目前的预测与未来的事件吻合，而是目前的预测造就了未来的事件。&lt;/p&gt; &lt;p&gt;“口红效应”，经济不景气的时候，生活压力会增加，人们的收入和对未来的预期都会降低，这时候首先削减的是那些大宗商品的消费，如买房、买车、出国旅游等，这样一来，反而可能会比正常时期有更多“闲钱”&lt;/p&gt; &lt;p&gt;小时或者更长时间的持续满足感。危机时期令人绝望的境况，让人们黯然神伤，信心与快乐成为最稀缺的商品。而此时，文化娱乐产业将成为“口红效应”中的获益者。&lt;/p&gt; &lt;p&gt;口红效应获益的产业主要有以下几个：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;第一，化妆品行业&lt;/li&gt; &lt;li&gt;电影产业&lt;/li&gt; &lt;li&gt;动漫游戏行业&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;凯恩斯的最大笨蛋理论，又叫博傻理论。你之所以完全不管某个东西的真实价值，即使它一文不值，你也愿意花高价买下，是因为你预期有一个更大的笨蛋，会花更高的价格，从你那儿把它买走。投机行为关键是判断有无比自己更大的笨蛋。&lt;/p&gt; &lt;p&gt;博傻理论告诉人们最重要的一个道理是在这个世界上，傻不可怕，可怕的是做最后一个笨蛋。&lt;/p&gt; &lt;p&gt;消费者剩余是指消费者购买某种商品时，所愿支付的价格与实际支付的价格之间的差额。&lt;/p&gt; &lt;p&gt;棘轮效应:由俭入奢易，由奢入俭难。&lt;/p&gt; &lt;p&gt;配套效应给人们一种启示:对于那些非必需的东西尽量不要购买。因为如果你接受了了一件，那么外界的和心里的压力会使你不断地接受更多的非必须的东西。&lt;/p&gt; &lt;p&gt;简单地说，所谓长尾理论是指，当商品存储流通展示的场地、渠道足够宽广，商品生产成本急剧下降以至于数人就可以进行生产，并且商品的销售成本急剧降低时，几乎以前类似需求极低的产品，只要有人卖，就会有人买，我们只要抓住了这个长尾，便可以将自己的成功最大化。&lt;/p&gt; &lt;p&gt;劣币驱逐良币背后的信息不对称。&lt;/p&gt; &lt;p&gt;二八法则:抓住起主宰作用的“关键”&lt;/p&gt; &lt;p&gt;鲇鱼效应:让外来“鲇鱼”助你越游越快&lt;/p&gt; &lt;p&gt;环境具有强烈的暗示性和诱导性，不要轻易去打破任何一扇窗户，一旦一个缺口被打开，即使看上去微不足道，如果不及时制止，其恶劣影响就会滋生、蔓延，这就是所谓的破窗效应。&lt;/p&gt; &lt;p&gt;要想解决“帕金森定律”的症结，就必须要建造一个公平、公正、公开的用人机制，不受人为因素的干扰，不要将用人权放在一个被招聘者的直接上司手里。&lt;/p&gt; &lt;p&gt;詹森效应是人的一种浅层的心理疾病，就是将现有的困境无限放大的心理异常现象。&lt;/p&gt; &lt;p&gt;世事往往就是这样，幸福总喜欢披着一件不幸的外套走进我们的生活。&lt;/p&gt; &lt;p&gt;这种幸福的递减告诉我们，幸福随着追求而来，随着希望而来，随着需要而来，但随着这些条件的变化，它又像过客一样，不会永远停留在某时、某处。既然如此，那不断追求和企盼幸福的我们，又该怎么办呢？&lt;/p&gt; &lt;p&gt;知足与感恩，飞往幸福的一对翅膀&lt;/p&gt; &lt;p&gt;克制自我是一种智慧&lt;/p&gt; &lt;p&gt;愤怒是一种带有破坏性的负面情感。长期被这些心理情绪困扰就会导致身心疾病的发生。有道是“要活好，心别小;善治怒，寿无数”&lt;/p&gt; &lt;p&gt;遇到困难时，首先问自己，可能发生的最坏情况是什么;其次，接受这个最坏的情况;最后，镇定地想办法改善最坏的情况。&lt;/p&gt; &lt;p&gt;心理上的平静能顶住最坏的境遇，能让你焕发新的活力。&lt;/p&gt; &lt;p&gt;最坏的结果，不过是回到原处，又有什么可恐惧的呢？&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 01 Sep 2019 08:52:00 GMT</pubDate>
    </item>
    <item>
      <title>Docker 之 Dockerfile 指令详解</title>
      <link>https://www.zhangaoo.com/article/docker-command</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/201981717716-timg.jpg" alt="201981717716-timg" /&gt;&lt;/p&gt; &lt;h1&gt;Dockerfile 指令详解&lt;/h1&gt; &lt;p&gt;我们已经介绍了 &lt;code&gt;FROM&lt;/code&gt;，&lt;code&gt;RUN&lt;/code&gt;，还提及了 &lt;code&gt;COPY&lt;/code&gt;, &lt;code&gt;ADD&lt;/code&gt;，其实 &lt;code&gt;Dockerfile&lt;/code&gt; 功能很强大，它提供了十多个指令。下面我们继续讲解其他的指令。&lt;/p&gt; &lt;h2&gt;COPY 复制文件&lt;/h2&gt; &lt;p&gt;格式：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;COPY [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] &amp;lt;源路径&amp;gt;... &amp;lt;目标路径&amp;gt; COPY [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] [&amp;quot;&amp;lt;源路径1&amp;gt;&amp;quot;,... &amp;quot;&amp;lt;目标路径&amp;gt;&amp;quot;] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和 &lt;code&gt;RUN&lt;/code&gt; 指令一样，也有两种格式，一种类似于命令行，一种类似于函数调用。&lt;/p&gt; &lt;p&gt;&lt;code&gt;COPY&lt;/code&gt; 指令将从构建上下文目录中 &amp;lt;源路径&amp;gt; 的文件/目录复制到新的一层的镜像内的 &amp;lt;目标路径&amp;gt; 位置。比如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;COPY package.json /usr/src/app/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&amp;lt;源路径&amp;gt; 可以是多个，甚至可以是通配符，其通配符规则要满足 &lt;code&gt;Go&lt;/code&gt; 的 &lt;a href="https://golang.org/pkg/path/filepath/#Match" target="_blank"&gt;filepath.Match&lt;/a&gt; 规则，如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;COPY hom* /mydir/ COPY hom?.txt /mydir/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&amp;lt;目标路径&amp;gt; 可以是容器内的绝对路径，也可以是相对于工作目录的相对路径（工作目录可以用 &lt;code&gt;WORKDIR&lt;/code&gt; 指令来指定）。目标路径不需要事先创建，如果目录不存在会在复制文件前先行创建缺失目录。&lt;/p&gt; &lt;p&gt;此外，还需要注意一点，使用 &lt;code&gt;COPY&lt;/code&gt; 指令，源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 &lt;code&gt;Git&lt;/code&gt; 进行管理的时候。&lt;/p&gt; &lt;p&gt;在使用该指令的时候还可以加上 &lt;code&gt;--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;&lt;/code&gt; 选项来改变文件的所属用户及所属组。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;COPY --chown=55:mygroup files* /mydir/ COPY --chown=bin files* /mydir/ COPY --chown=1 files* /mydir/ COPY --chown=10:11 files* /mydir/ &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;ADD 更高级的复制文件&lt;/h2&gt; &lt;p&gt;&lt;code&gt;ADD&lt;/code&gt; 指令和 &lt;code&gt;COPY&lt;/code&gt; 的格式和性质基本一致。但是在 &lt;code&gt;COPY&lt;/code&gt; 基础上增加了一些功能。&lt;/p&gt; &lt;p&gt;比如 &amp;lt;源路径&amp;gt; 可以是一个 &lt;code&gt;URL&lt;/code&gt;，这种情况下，&lt;code&gt;Docker&lt;/code&gt; 引擎会试图去下载这个链接的文件放到 &amp;lt;目标路径&amp;gt; 去。下载后的文件权限自动设置为 &lt;code&gt;600&lt;/code&gt;，如果这并不是想要的权限，那么还需要增加额外的一层 &lt;code&gt;RUN&lt;/code&gt; 进行权限调整，另外，如果下载的是个压缩包，需要解压缩，也一样还需要额外的一层 &lt;code&gt;RUN&lt;/code&gt; 指令进行解压缩。所以不如直接使用 &lt;code&gt;RUN&lt;/code&gt; 指令，然后使用 &lt;code&gt;wget&lt;/code&gt; 或者 &lt;code&gt;curl&lt;/code&gt; 工具下载，处理权限、解压缩、然后清理无用文件更合理。因此，这个功能其实并不实用，而且不推荐使用。&lt;/p&gt; &lt;p&gt;如果 &amp;lt;源路径&amp;gt; 为一个 &lt;code&gt;tar&lt;/code&gt; 压缩文件的话，压缩格式为 &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;bzip2&lt;/code&gt; 以及 &lt;code&gt;xz&lt;/code&gt; 的情况下，&lt;code&gt;ADD&lt;/code&gt; 指令将会自动解压缩这个压缩文件到 &amp;lt;目标路径&amp;gt; 去。&lt;/p&gt; &lt;p&gt;在某些情况下，这个自动解压缩的功能非常有用，比如官方镜像 &lt;code&gt;ubuntu&lt;/code&gt; 中：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但在某些情况下，如果我们真的是希望复制个压缩文件进去，而不解压缩，这时就不可以使用 &lt;code&gt;ADD&lt;/code&gt; 命令了。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;Docker&lt;/code&gt; 官方的 &lt;code&gt;Dockerfile&lt;/code&gt; 最佳实践文档 中要求，尽可能的使用 &lt;code&gt;COPY&lt;/code&gt;，因为 &lt;code&gt;COPY&lt;/code&gt; 的语义很明确，就是复制文件而已，而 &lt;code&gt;ADD&lt;/code&gt; 则包含了更复杂的功能，其行为也不一定很清晰。&lt;strong&gt;最适合使用 &lt;code&gt;ADD&lt;/code&gt; 的场合，就是所提及的需要自动解压缩的场合。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;另外需要注意的是，&lt;code&gt;ADD&lt;/code&gt; 指令会令镜像构建缓存失效，从而可能会令镜像构建变得比较缓慢。&lt;/p&gt; &lt;p&gt;因此在 &lt;code&gt;COPY&lt;/code&gt; 和 &lt;code&gt;ADD&lt;/code&gt; 指令中选择的时候，可以遵循这样的原则，所有的文件复制均使用 &lt;code&gt;COPY&lt;/code&gt; 指令，仅在需要自动解压缩的场合使用 &lt;code&gt;ADD&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;在使用该指令的时候还可以加上 &lt;code&gt;--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;&lt;/code&gt; 选项来改变文件的所属用户及所属组。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;ADD --chown=55:mygroup files* /mydir/ ADD --chown=bin files* /mydir/ ADD --chown=1 files* /mydir/ ADD --chown=10:11 files* /mydir/ &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;CMD 容器启动命令&lt;/h2&gt; &lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; 指令的格式和 &lt;code&gt;RUN&lt;/code&gt; 相似，也是两种格式：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;shell&lt;/code&gt; 格式：&lt;code&gt;CMD&lt;/code&gt; &amp;lt;命令&amp;gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;exec&lt;/code&gt; 格式：&lt;code&gt;CMD&lt;/code&gt; [&amp;quot;可执行文件&amp;quot;, &amp;quot;参数1&amp;quot;, &amp;quot;参数2&amp;quot;...]&lt;/li&gt; &lt;li&gt;参数列表格式：&lt;code&gt;CMD&lt;/code&gt; [&amp;quot;参数1&amp;quot;, &amp;quot;参数2&amp;quot;...]。在指定了 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 指令后，用 &lt;code&gt;CMD&lt;/code&gt; 指定具体的参数。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;之前介绍容器的时候曾经说过，&lt;code&gt;Docker&lt;/code&gt; 不是虚拟机，容器就是进程。既然是进程，那么在启动容器的时候，需要指定所运行的程序及参数。&lt;code&gt;CMD&lt;/code&gt; 指令就是用于指定默认的容器主进程的启动命令的。&lt;/p&gt; &lt;p&gt;在运行时可以指定新的命令来替代镜像设置中的这个默认命令，比如，&lt;code&gt;ubuntu&lt;/code&gt; 镜像默认的 &lt;code&gt;CMD&lt;/code&gt; 是 &lt;code&gt;/bin/bash&lt;/code&gt;，如果我们直接 &lt;code&gt;docker run -it ubuntu&lt;/code&gt; 的话，会直接进入 &lt;code&gt;bash&lt;/code&gt;。我们也可以在运行时指定运行别的命令，如&lt;code&gt;docker run -it ubuntu cat /etc/os-release&lt;/code&gt;。这就是用 &lt;code&gt;cat /etc/os-release&lt;/code&gt; 命令替换了默认的 &lt;code&gt;/bin/bash&lt;/code&gt; 命令了，输出了系统版本信息。&lt;/p&gt; &lt;p&gt;在指令格式上，一般推荐使用 &lt;code&gt;exec&lt;/code&gt; 格式，这类格式在解析时会被解析为 &lt;code&gt;JSON&lt;/code&gt; 数组，因此一定要使用双引号 &amp;quot;，而不要使用单引号。&lt;/p&gt; &lt;p&gt;如果使用 &lt;code&gt;shell&lt;/code&gt; 格式的话，实际的命令会被包装为 &lt;code&gt;sh -c&lt;/code&gt; 的参数的形式进行执行。比如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;CMD echo $HOME &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在实际执行中，会将其变更为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;CMD [ &amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;echo $HOME&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这就是为什么我们可以使用环境变量的原因，因为这些环境变量会被 &lt;code&gt;shell&lt;/code&gt; 进行解析处理。&lt;/p&gt; &lt;p&gt;提到 &lt;code&gt;CMD&lt;/code&gt; 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Docker&lt;/code&gt; 不是虚拟机，容器中的应用都应该以前台执行，而不是像虚拟机、物理机里面那样，用 &lt;code&gt;systemd&lt;/code&gt; 去启动后台服务，容器内没有后台服务的概念。&lt;/p&gt; &lt;p&gt;一些初学者将 CMD 写为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;CMD service nginx start &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后发现容器执行后就立即退出了。甚至在容器内去使用 &lt;code&gt;systemctl&lt;/code&gt; 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念，没有区分容器和虚拟机的差异，依旧在以传统虚拟机的角度去理解容器。&lt;/p&gt; &lt;p&gt;对于容器而言，其启动程序就是容器应用进程，容器就是为了主进程而存在的，主进程退出，容器就失去了存在的意义，从而退出，其它辅助进程不是它需要关心的东西。&lt;/p&gt; &lt;p&gt;而使用 &lt;code&gt;service nginx start&lt;/code&gt; 命令，则是希望 &lt;code&gt;upstart&lt;/code&gt; 来以后台守护进程形式启动 &lt;code&gt;nginx&lt;/code&gt; 服务。而刚才说了 &lt;code&gt;CMD service nginx start&lt;/code&gt; 会被理解为 &lt;code&gt;CMD [ &amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;service nginx start&amp;quot;]&lt;/code&gt;，因此主进程实际上是 &lt;code&gt;sh&lt;/code&gt;。那么当 &lt;code&gt;service nginx start&lt;/code&gt; 命令结束后，&lt;code&gt;sh&lt;/code&gt; 也就结束了，&lt;code&gt;sh&lt;/code&gt; 作为主进程退出了，自然就会令容器退出。&lt;/p&gt; &lt;p&gt;正确的做法是直接执行 &lt;code&gt;nginx&lt;/code&gt; 可执行文件，并且要求以前台形式运行。比如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;CMD [&amp;quot;nginx&amp;quot;, &amp;quot;-g&amp;quot;, &amp;quot;daemon off;&amp;quot;] &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;ENTRYPOINT 入口点&lt;/h2&gt; &lt;p&gt;&lt;code&gt;ENTRYPOINT&lt;/code&gt; 的格式和 &lt;code&gt;RUN&lt;/code&gt; 指令格式一样，分为 &lt;code&gt;exec&lt;/code&gt; 格式和 &lt;code&gt;shell&lt;/code&gt; 格式。&lt;/p&gt; &lt;p&gt;&lt;code&gt;ENTRYPOINT&lt;/code&gt; 的目的和 &lt;code&gt;CMD&lt;/code&gt; 一样，都是在指定容器启动程序及参数。&lt;code&gt;ENTRYPOINT&lt;/code&gt; 在运行时也可以替代，不过比 &lt;code&gt;CMD&lt;/code&gt; 要略显繁琐，需要通过 &lt;code&gt;docker run&lt;/code&gt; 的参数 &lt;code&gt;--entrypoint&lt;/code&gt; 来指定。&lt;/p&gt; &lt;p&gt;指定了 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 后，&lt;code&gt;CMD&lt;/code&gt; 的含义就发生了改变，不再是直接的运行其命令，而是将 &lt;code&gt;CMD&lt;/code&gt; 的内容作为参数传给 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 指令，换句话说实际执行时，将变为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;&amp;lt;ENTRYPOINT&amp;gt; &amp;quot;&amp;lt;CMD&amp;gt;&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;那么有了 &lt;code&gt;CMD&lt;/code&gt; 后，为什么还要有 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 呢？这种&lt;code&gt;&amp;lt;ENTRYPOINT&amp;gt; &amp;quot;&amp;lt;CMD&amp;gt;&amp;quot;&lt;/code&gt; 有什么好处么？让我们来看几个场景。&lt;/p&gt; &lt;h3&gt;场景一：让镜像变成像命令一样使用&lt;/h3&gt; &lt;p&gt;假设我们需要一个得知自己当前公网 &lt;code&gt;IP&lt;/code&gt; 的镜像，那么可以先用 &lt;code&gt;CMD&lt;/code&gt; 来实现：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM ubuntu:18.04 RUN apt-get update \     &amp;amp;&amp;amp; apt-get install -y curl \     &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* CMD [ &amp;quot;curl&amp;quot;, &amp;quot;-s&amp;quot;, &amp;quot;https://ip.cn&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;假如我们使用 &lt;code&gt;docker build -t myip .&lt;/code&gt; 来构建镜像的话，如果我们需要查询当前公网 &lt;code&gt;IP&lt;/code&gt;，只需要执行：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run myip 当前 IP：61.148.226.66 来自：北京市 联通 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;嗯，这么看起来好像可以直接把镜像当做命令使用了，不过命令总有参数，如果我们希望加参数呢？比如从上面的 &lt;code&gt;CMD&lt;/code&gt; 中可以看到实质的命令是 &lt;code&gt;curl&lt;/code&gt;，那么如果我们希望显示 &lt;code&gt;HTTP&lt;/code&gt; 头信息，就需要加上 &lt;code&gt;-i&lt;/code&gt; 参数。那么我们可以直接加 &lt;code&gt;-i&lt;/code&gt; 参数给 &lt;code&gt;docker run myip&lt;/code&gt; 么？&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run myip -i docker: Error response from daemon: invalid header field value &amp;quot;oci runtime error: container_linux.go:247: starting container process caused \&amp;quot;exec: \\\&amp;quot;-i\\\&amp;quot;: executable file not found in $PATH\&amp;quot;\n&amp;quot;. &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们可以看到可执行文件找不到的报错，&lt;code&gt;executable file not found&lt;/code&gt;。之前我们说过，跟在镜像名后面的是 command，运行时会替换 &lt;code&gt;CMD&lt;/code&gt; 的默认值。因此这里的 &lt;code&gt;-i&lt;/code&gt; 替换了原来的 &lt;code&gt;CMD&lt;/code&gt;，而不是添加在原来的 &lt;code&gt;curl -s https://ip.cn&lt;/code&gt; 后面。而 &lt;code&gt;-i&lt;/code&gt; 根本不是命令，所以自然找不到。&lt;/p&gt; &lt;p&gt;那么如果我们希望加入 &lt;code&gt;-i&lt;/code&gt; 这参数，我们就必须重新完整的输入这个命令：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run myip curl -s https://ip.cn -i &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这显然不是很好的解决方案，而使用 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 就可以解决这个问题。现在我们重新用 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 来实现这个镜像：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM ubuntu:18.04 RUN apt-get update \     &amp;amp;&amp;amp; apt-get install -y curl \     &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* ENTRYPOINT [ &amp;quot;curl&amp;quot;, &amp;quot;-s&amp;quot;, &amp;quot;https://ip.cn&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这次我们再来尝试直接使用 &lt;code&gt;docker run myip -i&lt;/code&gt;：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run myip 当前 IP：61.148.226.66 来自：北京市 联通  $ docker run myip -i HTTP/1.1 200 OK Server: nginx/1.8.0 Date: Tue, 22 Nov 2016 05:12:40 GMT Content-Type: text/html; charset=UTF-8 Vary: Accept-Encoding X-Powered-By: PHP/5.6.24-1~dotdeb+7.1 X-Cache: MISS from cache-2 X-Cache-Lookup: MISS from cache-2:80 X-Cache: MISS from proxy-2_6 Transfer-Encoding: chunked Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006 Connection: keep-alive  当前 IP：61.148.226.66 来自：北京市 联通 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看到，这次成功了。这是因为当存在 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 后，&lt;code&gt;CMD&lt;/code&gt; 的内容将会作为参数传给&lt;code&gt;ENTRYPOINT&lt;/code&gt;，而这里 &lt;code&gt;-i&lt;/code&gt; 就是新的 &lt;code&gt;CMD&lt;/code&gt;，因此会作为参数传给 &lt;code&gt;curl&lt;/code&gt;，从而达到了我们预期的效果。&lt;/p&gt; &lt;h3&gt;场景二：应用运行前的准备工作&lt;/h3&gt; &lt;p&gt;启动容器就是启动主进程，但有些时候，启动主进程前，需要一些准备工作。&lt;/p&gt; &lt;p&gt;比如 &lt;code&gt;mysql&lt;/code&gt; 类的数据库，可能需要一些数据库配置、初始化的工作，这些工作要在最终的 &lt;code&gt;mysql&lt;/code&gt; 服务器运行之前解决。&lt;/p&gt; &lt;p&gt;此外，可能希望避免使用 &lt;code&gt;root&lt;/code&gt; 用户去启动服务，从而提高安全性，而在启动服务前还需要以 &lt;code&gt;root&lt;/code&gt; 身份执行一些必要的准备工作，最后切换到服务用户身份启动服务。或者除了服务外，其它命令依旧可以使用 &lt;code&gt;root&lt;/code&gt; 身份执行，方便调试等。&lt;/p&gt; &lt;p&gt;这些准备工作是和容器 &lt;code&gt;CMD&lt;/code&gt; 无关的，无论 &lt;code&gt;CMD&lt;/code&gt; 为什么，都需要事先进行一个预处理的工作。这种情况下，可以写一个脚本，然后放入 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 中去执行，而这个脚本会将接到的参数（也就是 &lt;CMD&gt;）作为命令，在脚本最后执行。比如官方镜像 &lt;code&gt;redis&lt;/code&gt; 中就是这么做的：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM alpine:3.4 ... RUN addgroup -S redis &amp;amp;&amp;amp; adduser -S -G redis redis ... ENTRYPOINT [&amp;quot;docker-entrypoint.sh&amp;quot;]  EXPOSE 6379 CMD [ &amp;quot;redis-server&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看到其中为了 &lt;code&gt;redis&lt;/code&gt; 服务创建了 &lt;code&gt;redis&lt;/code&gt; 用户，并在最后指定了 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 为&lt;code&gt;docker-entrypoint.sh&lt;/code&gt; 脚本。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;#!/bin/sh ... # allow the container to be started with `--user` if [ &amp;quot;$1&amp;quot; = 'redis-server' -a &amp;quot;$(id -u)&amp;quot; = '0' ]; then     chown -R redis .     exec su-exec redis &amp;quot;$0&amp;quot; &amp;quot;$@&amp;quot; fi  exec &amp;quot;$@&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;该脚本的内容就是根据 CMD 的内容来判断，如果是 redis-server 的话，则切换到 redis 用户身份启动服务器，否则依旧使用 root 身份执行。比如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run -it redis id uid=0(root) gid=0(root) groups=0(root) &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;ENV 设置环境变量&lt;/h2&gt; &lt;p&gt;格式有两种：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;ENV &amp;lt;key&amp;gt; &amp;lt;value&amp;gt; ENV &amp;lt;key1&amp;gt;=&amp;lt;value1&amp;gt; &amp;lt;key2&amp;gt;=&amp;lt;value2&amp;gt;... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个指令很简单，就是设置环境变量而已，无论是后面的其它指令，如 &lt;code&gt;RUN&lt;/code&gt;，还是运行时的应用，都可以直接使用这里定义的环境变量。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;ENV VERSION=1.0 DEBUG=on \     NAME=&amp;quot;Happy Feet&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个例子中演示了如何换行，以及对含有空格的值用双引号括起来的办法，这和 &lt;code&gt;Shell&lt;/code&gt; 下的行为是一致的。&lt;/p&gt; &lt;p&gt;定义了环境变量，那么在后续的指令中，就可以使用这个环境变量。比如在官方 &lt;code&gt;node&lt;/code&gt; 镜像 &lt;code&gt;Dockerfile&lt;/code&gt; 中，就有类似这样的代码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;ENV NODE_VERSION 7.2.0  RUN curl -SLO &amp;quot;https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz&amp;quot; \   &amp;amp;&amp;amp; curl -SLO &amp;quot;https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc&amp;quot; \   &amp;amp;&amp;amp; gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \   &amp;amp;&amp;amp; grep &amp;quot; node-v$NODE_VERSION-linux-x64.tar.xz\$&amp;quot; SHASUMS256.txt | sha256sum -c - \   &amp;amp;&amp;amp; tar -xJf &amp;quot;node-v$NODE_VERSION-linux-x64.tar.xz&amp;quot; -C /usr/local --strip-components=1 \   &amp;amp;&amp;amp; rm &amp;quot;node-v$NODE_VERSION-linux-x64.tar.xz&amp;quot; SHASUMS256.txt.asc SHASUMS256.txt \   &amp;amp;&amp;amp; ln -s /usr/local/bin/node /usr/local/bin/nodejs &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在这里先定义了环境变量 &lt;code&gt;NODE_VERSION&lt;/code&gt;，其后的 &lt;code&gt;RUN&lt;/code&gt; 这层里，多次使用 &lt;code&gt;$NODE_VERSION&lt;/code&gt; 来进行操作定制。可以看到，将来升级镜像构建版本的时候，只需要更新 &lt;code&gt;7.2.0&lt;/code&gt; 即可，&lt;code&gt;Dockerfile&lt;/code&gt; 构建维护变得更轻松了。&lt;/p&gt; &lt;p&gt;下列指令可以支持环境变量展开：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以从这个指令列表里感觉到，环境变量可以使用的地方很多，很强大。通过环境变量，我们可以让一份 &lt;code&gt;Dockerfile&lt;/code&gt; 制作更多的镜像，只需使用不同的环境变量即可。&lt;/p&gt; &lt;h3&gt;ARG 构建参数&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;格式：ARG &amp;lt;参数名&amp;gt;[=&amp;lt;默认值&amp;gt;] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;构建参数和 &lt;code&gt;ENV&lt;/code&gt; 的效果一样，都是设置环境变量。所不同的是，&lt;code&gt;ARG&lt;/code&gt; 所设置的构建环境的环境变量，在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 &lt;code&gt;ARG&lt;/code&gt; 保存密码之类的信息，因为 &lt;code&gt;docker history&lt;/code&gt; 还是可以看到所有值的。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; 中的 &lt;code&gt;ARG&lt;/code&gt; 指令是定义参数名称，以及定义其默认值。该默认值可以在构建命令 &lt;code&gt;docker build&lt;/code&gt; 中用 &lt;code&gt;--build-arg&lt;/code&gt; &amp;lt;参数名&amp;gt;=&amp;lt;值&amp;gt; 来覆盖。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;1.13&lt;/code&gt; 之前的版本，要求 &lt;code&gt;--build-arg&lt;/code&gt; 中的参数名，必须在 &lt;code&gt;Dockerfile&lt;/code&gt; 中用 &lt;code&gt;ARG&lt;/code&gt; 定义过了，换句话说，就是 &lt;code&gt;--build-arg&lt;/code&gt; 指定的参数，必须在 &lt;code&gt;Dockerfile&lt;/code&gt; 中使用了。如果对应参数没有被使用，则会报错退出构建。从 &lt;code&gt;1.13&lt;/code&gt; 开始，这种严格的限制被放开，不再报错退出，而是显示警告信息，并继续构建。这对于使用 &lt;code&gt;CI&lt;/code&gt; 系统，用同样的构建流程构建不同的 &lt;code&gt;Dockerfile&lt;/code&gt; 的时候比较有帮助，避免构建命令必须根据每个 &lt;code&gt;Dockerfile&lt;/code&gt; 的内容修改。&lt;/p&gt; &lt;h3&gt;VOLUME 定义匿名卷&lt;/h3&gt; &lt;p&gt;格式为：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;VOLUME [&amp;quot;&amp;lt;路径1&amp;gt;&amp;quot;, &amp;quot;&amp;lt;路径2&amp;gt;&amp;quot;...] VOLUME &amp;lt;路径&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之前我们说过，容器运行时应该尽量保持容器存储层不发生写操作，对于数据库类需要保存动态数据的应用，其数据库文件应该保存于卷(&lt;code&gt;volume&lt;/code&gt;)中，后面的章节我们会进一步介绍 &lt;code&gt;Docker&lt;/code&gt; 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷，在 &lt;code&gt;Dockerfile&lt;/code&gt; 中，我们可以事先指定某些目录挂载为匿名卷，这样在运行时如果用户不指定挂载，其应用也可以正常运行，不会向容器存储层写入大量数据。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;VOLUME /data &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里的 &lt;code&gt;/data&lt;/code&gt; 目录就会在运行时自动挂载为匿名卷，任何向 /data 中写入的信息都不会记录进容器存储层，从而保证了容器存储层的无状态化。当然，运行时可以覆盖这个挂载设置。比如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;docker run -d -v mydata:/data xxxx &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在这行命令中，就使用了 &lt;code&gt;mydata&lt;/code&gt; 这个命名卷挂载到了 &lt;code&gt;/data&lt;/code&gt; 这个位置，替代了 &lt;code&gt;Dockerfile&lt;/code&gt; 中定义的匿名卷的挂载配置。&lt;/p&gt; &lt;h3&gt;EXPOSE 声明端口&lt;/h3&gt; &lt;p&gt;格式为:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;EXPOSE &amp;lt;端口1&amp;gt; [&amp;lt;端口2&amp;gt;...] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;EXPOSE&lt;/code&gt; 指令是声明运行时容器提供服务端口，这只是一个声明，在运行时并不会因为这个声明应用就会开启这个端口的服务。在 &lt;code&gt;Dockerfile&lt;/code&gt; 中写入这样的声明有两个好处，一个是帮助镜像使用者理解这个镜像服务的守护端口，以方便配置映射；另一个用处则是在运行时使用随机端口映射时，也就是 &lt;code&gt;docker run -P&lt;/code&gt; 时，会自动随机映射 &lt;code&gt;EXPOSE&lt;/code&gt; 的端口。&lt;/p&gt; &lt;p&gt;要将 &lt;code&gt;EXPOSE&lt;/code&gt; 和在运行时使用 &lt;code&gt;-p &amp;lt;宿主端口&amp;gt;:&amp;lt;容器端口&amp;gt;&lt;/code&gt; 区分开来。&lt;code&gt;-p&lt;/code&gt;，是映射宿主端口和容器端口，换句话说，就是将容器的对应端口服务公开给外界访问，而 EXPOSE 仅仅是声明容器打算使用什么端口而已，并不会自动在宿主进行端口映射。&lt;/p&gt; &lt;h3&gt;WORKDIR 指定工作目录&lt;/h3&gt; &lt;p&gt;格式为:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;WORKDIR &amp;lt;工作目录路径&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用 &lt;code&gt;WORKDIR&lt;/code&gt; 指令可以来指定工作目录（或者称为当前目录），以后各层的当前目录就被改为指定的目录，如该目录不存在，&lt;code&gt;WORKDIR&lt;/code&gt; 会帮你建立目录。&lt;/p&gt; &lt;p&gt;之前提到一些初学者常犯的错误是把 &lt;code&gt;Dockerfile&lt;/code&gt; 等同于 &lt;code&gt;Shell&lt;/code&gt; 脚本来书写，这种错误的理解还可能会导致出现下面这样的错误：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;RUN cd /app RUN echo &amp;quot;hello&amp;quot; &amp;gt; world.txt &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果将这个 &lt;code&gt;Dockerfile&lt;/code&gt; 进行构建镜像运行后，会发现找不到 &lt;code&gt;/app/world.txt&lt;/code&gt; 文件，或者其内容不是 &lt;code&gt;hello&lt;/code&gt;。原因其实很简单，在 &lt;code&gt;Shell&lt;/code&gt; 中，连续两行是同一个进程执行环境，因此前一个命令修改的内存状态，会直接影响后一个命令；而在 &lt;code&gt;Dockerfile&lt;/code&gt; 中，这两行 &lt;code&gt;RUN&lt;/code&gt; 命令的执行环境根本不同，是两个完全不同的容器。这就是对 &lt;code&gt;Dockerfile&lt;/code&gt; 构建分层存储的概念不了解所导致的错误。&lt;/p&gt; &lt;p&gt;之前说过每一个 &lt;code&gt;RUN&lt;/code&gt; 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层&lt;code&gt;RUN cd /app&lt;/code&gt; 的执行仅仅是当前进程的工作目录变更，一个内存上的变化而已，其结果不会造成任何文件变更。而到第二层的时候，启动的是一个全新的容器，跟第一层的容器更完全没关系，自然不可能继承前一层构建过程中的内存变化。&lt;/p&gt; &lt;p&gt;因此如果需要改变以后各层的工作目录的位置，那么应该使用 &lt;code&gt;WORKDIR&lt;/code&gt; 指令。&lt;/p&gt; &lt;h3&gt;USER 指定当前用户&lt;/h3&gt; &lt;p&gt;格式:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;USER &amp;lt;用户名&amp;gt;[:&amp;lt;用户组&amp;gt;] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;USER&lt;/code&gt; 指令和 &lt;code&gt;WORKDIR&lt;/code&gt; 相似，都是改变环境状态并影响以后的层。&lt;code&gt;WORKDIR&lt;/code&gt; 是改变工作目录，&lt;code&gt;USER&lt;/code&gt; 则是改变之后层的执行 &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;CMD&lt;/code&gt; 以及 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 这类命令的身份。&lt;/p&gt; &lt;p&gt;当然，和 &lt;code&gt;WORKDIR&lt;/code&gt; 一样，&lt;code&gt;USER&lt;/code&gt; 只是帮助你切换到指定用户而已，这个用户必须是事先建立好的，否则无法切换。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;RUN groupadd -r redis &amp;amp;&amp;amp; useradd -r -g redis redis USER redis RUN [ &amp;quot;redis-server&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果以 &lt;code&gt;root&lt;/code&gt; 执行的脚本，在执行期间希望改变身份，比如希望以某个已经建立好的用户来运行某个服务进程，不要使用 &lt;code&gt;su&lt;/code&gt; 或者 &lt;code&gt;sudo&lt;/code&gt;，这些都需要比较麻烦的配置，而且在 &lt;code&gt;TTY&lt;/code&gt; 缺失的环境下经常出错。建议使用 &lt;a href="https://github.com/tianon/gosu" target="_blank"&gt;&lt;code&gt;gosu&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;# 建立 redis 用户，并使用 gosu 换另一个用户执行命令 RUN groupadd -r redis &amp;amp;&amp;amp; useradd -r -g redis redis # 下载 gosu RUN wget -O /usr/local/bin/gosu &amp;quot;https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64&amp;quot; \     &amp;amp;&amp;amp; chmod +x /usr/local/bin/gosu \     &amp;amp;&amp;amp; gosu nobody true # 设置 CMD，并以另外的用户执行 CMD [ &amp;quot;exec&amp;quot;, &amp;quot;gosu&amp;quot;, &amp;quot;redis&amp;quot;, &amp;quot;redis-server&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HEALTHCHECK 健康检查&lt;/h3&gt; &lt;p&gt;格式：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;HEALTHCHECK [选项] CMD &amp;lt;命令&amp;gt;：设置检查容器健康状况的命令 HEALTHCHECK NONE：如果基础镜像有健康检查指令，使用这行可以屏蔽掉其健康检查指令 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;HEALTHCHECK&lt;/code&gt; 指令是告诉 &lt;code&gt;Docker&lt;/code&gt; 应该如何进行判断容器的状态是否正常，这是 &lt;code&gt;Docker 1.12&lt;/code&gt; 引入的新指令。&lt;/p&gt; &lt;p&gt;在没有 &lt;code&gt;HEALTHCHECK&lt;/code&gt; 指令前，&lt;code&gt;Docker&lt;/code&gt; 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题，但是如果程序进入死锁状态，或者死循环状态，应用进程并不退出，但是该容器已经无法提供服务了。在 &lt;code&gt;1.12&lt;/code&gt; 以前，&lt;code&gt;Docker&lt;/code&gt; 不会检测到容器的这种状态，从而不会重新调度，导致可能会有部分容器已经无法提供服务了却还在接受用户请求。&lt;/p&gt; &lt;p&gt;而自 &lt;code&gt;1.12&lt;/code&gt; 之后，&lt;code&gt;Docker&lt;/code&gt; 提供了 &lt;code&gt;HEALTHCHECK&lt;/code&gt; 指令，通过该指令指定一行命令，用这行命令来判断容器主进程的服务状态是否还正常，从而比较真实的反应容器实际状态。&lt;/p&gt; &lt;p&gt;当在一个镜像指定了 &lt;code&gt;HEALTHCHECK&lt;/code&gt; 指令后，用其启动容器，初始状态会为 &lt;code&gt;starting&lt;/code&gt;，在 &lt;code&gt;HEALTHCHECK&lt;/code&gt; 指令检查成功后变为 &lt;code&gt;healthy&lt;/code&gt;，如果连续一定次数失败，则会变为 &lt;code&gt;unhealthy&lt;/code&gt;。 HEALTHCHECK 支持下列选项：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;--interval=&amp;lt;间隔&amp;gt;：两次健康检查的间隔，默认为 `30` 秒； --timeout=&amp;lt;时长&amp;gt;：健康检查命令运行超时时间，如果超过这个时间，本次健康检查就被视为失败，默认 `30` 秒； --retries=&amp;lt;次数&amp;gt;：当连续失败指定次数后，则将容器状态视为 `unhealthy`，默认 `3` 次。 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和 &lt;code&gt;CMD&lt;/code&gt;, &lt;code&gt;ENTRYPOINT&lt;/code&gt; 一样，&lt;code&gt;HEALTHCHECK&lt;/code&gt; 只可以出现一次，如果写了多个，只有最后一个生效。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;HEALTHCHECK [选项] CMD&lt;/code&gt; 后面的命令，格式和 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 一样，分为 &lt;code&gt;shell&lt;/code&gt; 格式，和 &lt;code&gt;exec&lt;/code&gt; 格式。命令的返回值决定了该次健康检查的成功与否：0：成功；1：失败；2：保留，不要使用这个值。&lt;/p&gt; &lt;p&gt;假设我们有个镜像是个最简单的 &lt;code&gt;Web&lt;/code&gt; 服务，我们希望增加健康检查来判断其 &lt;code&gt;Web&lt;/code&gt; 服务是否在正常工作，我们可以用 &lt;code&gt;curl&lt;/code&gt; 来帮助判断，其 &lt;code&gt;Dockerfile&lt;/code&gt; 的 &lt;code&gt;HEALTHCHECK&lt;/code&gt; 可以这么写：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM nginx RUN apt-get update &amp;amp;&amp;amp; apt-get install -y curl &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=5s --timeout=3s \   CMD curl -fs http://localhost/ || exit 1 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里我们设置了每 &lt;code&gt;5&lt;/code&gt; 秒检查一次（这里为了试验所以间隔非常短，实际应该相对较长），如果健康检查命令超过 &lt;code&gt;3&lt;/code&gt; 秒没响应就视为失败，并且使用&lt;code&gt;curl -fs http://localhost/ || exit 1&lt;/code&gt; 作为健康检查命令。 使用 &lt;code&gt;docker build&lt;/code&gt; 来构建这个镜像：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker build -t myweb:v1 . &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;构建好了后，我们启动一个容器：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker run -d --name web -p 80:80 myweb:v1 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当运行该镜像后，可以通过 &lt;code&gt;docker container ls&lt;/code&gt; 看到最初的状态为 (&lt;code&gt;health: starting&lt;/code&gt;)：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker container ls CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES 03e28eb00bd0        myweb:v1            &amp;quot;nginx -g 'daemon off&amp;quot;   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在等待几秒钟后，再次 &lt;code&gt;docker container ls&lt;/code&gt;，就会看到健康状态变化为了 (&lt;code&gt;healthy&lt;/code&gt;)：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker container ls CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES 03e28eb00bd0        myweb:v1            &amp;quot;nginx -g 'daemon off&amp;quot;   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果健康检查连续失败超过了重试次数，状态就会变为 (&lt;code&gt;unhealthy&lt;/code&gt;)。 为了帮助排障，健康检查命令的输出（包括 &lt;code&gt;stdout&lt;/code&gt; 以及 &lt;code&gt;stderr&lt;/code&gt;）都会被存储于健康状态里，可以用 &lt;code&gt;docker inspect&lt;/code&gt; 来查看。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool {     &amp;quot;FailingStreak&amp;quot;: 0,     &amp;quot;Log&amp;quot;: [         {             &amp;quot;End&amp;quot;: &amp;quot;2016-11-25T14:35:37.940957051Z&amp;quot;,             &amp;quot;ExitCode&amp;quot;: 0,             &amp;quot;Output&amp;quot;: &amp;quot;&amp;lt;!DOCTYPE html&amp;gt;\n&amp;lt;html&amp;gt;\n&amp;lt;head&amp;gt;\n&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;\n&amp;lt;style&amp;gt;\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n&amp;lt;/style&amp;gt;\n&amp;lt;/head&amp;gt;\n&amp;lt;body&amp;gt;\n&amp;lt;h1&amp;gt;Welcome to nginx!&amp;lt;/h1&amp;gt;\n&amp;lt;p&amp;gt;If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.&amp;lt;/p&amp;gt;\n\n&amp;lt;p&amp;gt;For online documentation and support please refer to\n&amp;lt;a href=\&amp;quot;http://nginx.org/\&amp;quot;&amp;gt;nginx.org&amp;lt;/a&amp;gt;.&amp;lt;br/&amp;gt;\nCommercial support is available at\n&amp;lt;a href=\&amp;quot;http://nginx.com/\&amp;quot;&amp;gt;nginx.com&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;\n\n&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Thank you for using nginx.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;\n&amp;lt;/body&amp;gt;\n&amp;lt;/html&amp;gt;\n&amp;quot;,             &amp;quot;Start&amp;quot;: &amp;quot;2016-11-25T14:35:37.780192565Z&amp;quot;         }     ],     &amp;quot;Status&amp;quot;: &amp;quot;healthy&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;ONBUILD 为他人做嫁衣裳&lt;/h3&gt; &lt;p&gt;格式：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;ONBUILD &amp;lt;其它指令&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;ONBUILD&lt;/code&gt; 是一个特殊的指令，它后面跟的是其它指令，比如 &lt;code&gt;RUN, COPY&lt;/code&gt; 等，而这些指令，在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像，去构建下一级镜像的时候才会被执行。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; 中的其它指令都是为了定制当前镜像而准备的，唯有 &lt;code&gt;ONBUILD&lt;/code&gt; 是为了帮助别人定制自己而准备的。&lt;/p&gt; &lt;p&gt;假设我们要制作 &lt;code&gt;Node.js&lt;/code&gt; 所写的应用的镜像。我们都知道 &lt;code&gt;Node.js&lt;/code&gt; 使用 &lt;code&gt;npm&lt;/code&gt; 进行包管理，所有依赖、配置、启动信息等会放到 &lt;code&gt;package.json&lt;/code&gt; 文件里。在拿到程序代码后，需要先进行 &lt;code&gt;npm install&lt;/code&gt; 才可以获得所有需要的依赖。然后就可以通过 &lt;code&gt;npm start&lt;/code&gt; 来启动应用。因此，一般来说会这样写 &lt;code&gt;Dockerfile&lt;/code&gt;：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM node:slim RUN mkdir /app WORKDIR /app COPY ./package.json /app RUN [ &amp;quot;npm&amp;quot;, &amp;quot;install&amp;quot; ] COPY . /app/ CMD [ &amp;quot;npm&amp;quot;, &amp;quot;start&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;把这个 &lt;code&gt;Dockerfile&lt;/code&gt; 放到 &lt;code&gt;Node.js&lt;/code&gt; 项目的根目录，构建好镜像后，就可以直接拿来启动容器运行。但是如果我们还有第二个 &lt;code&gt;Node.js&lt;/code&gt; 项目也差不多呢？好吧，那就再把这个 &lt;code&gt;Dockerfile&lt;/code&gt; 复制到第二个项目里。那如果有第三个项目呢？再复制么？文件的副本越多，版本控制就越困难，让我们继续看这样的场景维护的问题。&lt;/p&gt; &lt;p&gt;如果第一个 &lt;code&gt;Node.js&lt;/code&gt; 项目在开发过程中，发现这个 &lt;code&gt;Dockerfile&lt;/code&gt; 里存在问题，比如敲错字了、或者需要安装额外的包，然后开发人员修复了这个 &lt;code&gt;Dockerfile&lt;/code&gt;，再次构建，问题解决。 第一个项目没问题了，但是第二个项目呢？虽然最初 &lt;code&gt;Dockerfile&lt;/code&gt; 是复制、粘贴自第一个项目的，但是并不会因为第一个项目修复了他们的 &lt;code&gt;Dockerfile&lt;/code&gt;，而第二个项目的 &lt;code&gt;Dockerfile&lt;/code&gt; 就会被自动修复。&lt;/p&gt; &lt;p&gt;那么我们可不可以做一个基础镜像，然后各个项目使用这个基础镜像呢？这样基础镜像更新，各个项目不用同步 &lt;code&gt;Dockerfile&lt;/code&gt; 的变化，重新构建后就继承了基础镜像的更新？好吧，可以，让我们看看这样的结果。那么上面的这个 &lt;code&gt;Dockerfile&lt;/code&gt; 就会变为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM node:slim RUN mkdir /app WORKDIR /app CMD [ &amp;quot;npm&amp;quot;, &amp;quot;start&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里我们把项目相关的构建指令拿出来，放到子项目里去。假设这个基础镜像的名字为 &lt;code&gt;my-node&lt;/code&gt; 的话，各个项目内的自己的 &lt;code&gt;Dockerfile&lt;/code&gt; 就变为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM my-node COPY ./package.json /app RUN [ &amp;quot;npm&amp;quot;, &amp;quot;install&amp;quot; ] COPY . /app/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;基础镜像变化后，各个项目都用这个 Dockerfile 重新构建镜像，会继承基础镜像的更新。&lt;/p&gt; &lt;p&gt;那么，问题解决了么？没有。准确说，只解决了一半。如果这个 &lt;code&gt;Dockerfile&lt;/code&gt; 里面有些东西需要调整呢？比如 &lt;code&gt;npm install&lt;/code&gt; 都需要加一些参数，那怎么办？这一行 &lt;code&gt;RUN&lt;/code&gt; 是不可能放入基础镜像的，因为涉及到了当前项目的 &lt;code&gt;./package.json&lt;/code&gt;，难道又要一个个修改么？所以说，这样制作基础镜像，只解决了原来的 &lt;code&gt;Dockerfile&lt;/code&gt; 的前4条指令的变化问题，而后面三条指令的变化则完全没办法处理。&lt;/p&gt; &lt;p&gt;&lt;code&gt;ONBUILD&lt;/code&gt; 可以解决这个问题。让我们用 &lt;code&gt;ONBUILD&lt;/code&gt; 重新写一下基础镜像的 Dockerfile:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM node:slim RUN mkdir /app WORKDIR /app ONBUILD COPY ./package.json /app ONBUILD RUN [ &amp;quot;npm&amp;quot;, &amp;quot;install&amp;quot; ] ONBUILD COPY . /app/ CMD [ &amp;quot;npm&amp;quot;, &amp;quot;start&amp;quot; ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这次我们回到原始的 &lt;code&gt;Dockerfile&lt;/code&gt;，但是这次将项目相关的指令加上 &lt;code&gt;ONBUILD&lt;/code&gt;，这样在构建基础镜像的时候，这三行并不会被执行。然后各个项目的 &lt;code&gt;Dockerfile&lt;/code&gt; 就变成了简单地：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM my-node &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;是的，只有这么一行。当在各个项目目录中，用这个只有一行的 &lt;code&gt;Dockerfile&lt;/code&gt; 构建镜像时，之前基础镜像的那三行 &lt;code&gt;ONBUILD&lt;/code&gt; 就会开始执行，成功的将当前项目的代码复制进镜像、并且针对本项目执行 &lt;code&gt;npm install&lt;/code&gt;，生成应用镜像。&lt;/p&gt; &lt;h2&gt;参考文档&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://docs.docker.com/engine/reference/builder/" target="_blank"&gt;Dockerfie 官方文档&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/" target="_blank"&gt;Dockerfile 最佳实践文档&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://github.com/docker-library/docs" target="_blank"&gt;Docker 官方镜像 Dockerfile&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sat, 17 Aug 2019 09:23:00 GMT</pubDate>
    </item>
    <item>
      <title>Docker之学习制作Dockerfile</title>
      <link>https://www.zhangaoo.com/article/create-dockerfile</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/201981717716-timg.jpg" alt="201981717716-timg" /&gt;&lt;/p&gt; &lt;h1&gt;Dockerfile&lt;/h1&gt; &lt;h2&gt;使用 Dockerfile 定制镜像&lt;/h2&gt; &lt;p&gt;从刚才的 &lt;a href="https://yeasy.gitbooks.io/docker_practice/image/commit.html" target="_blank"&gt;docker commit&lt;/a&gt; 的学习中，我们可以了解到，镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本，用这个脚本来构建、定制镜像，那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; 是一个文本文件，其内包含了一条条的 指令(&lt;code&gt;Instruction&lt;/code&gt;)，每一条指令构建一层，因此每一条指令的内容，就是描述该层应当如何构建。&lt;/p&gt; &lt;p&gt;还以之前定制 &lt;code&gt;nginx&lt;/code&gt; 镜像为例，这次我们使用 &lt;code&gt;Dockerfile&lt;/code&gt; 来定制。&lt;/p&gt; &lt;p&gt;在一个空白目录中，建立一个文本文件，并命名为 &lt;code&gt;Dockerfile&lt;/code&gt;：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ mkdir mynginx $ cd mynginx $ touch Dockerfile &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;其内容为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;FROM nginx RUN echo '&amp;lt;h1&amp;gt;Hello, Docker!&amp;lt;/h1&amp;gt;' &amp;gt; /usr/share/nginx/html/index.html &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个 &lt;code&gt;Dockerfile&lt;/code&gt; 很简单，一共就两行。涉及到了两条指令，&lt;code&gt;FROM&lt;/code&gt; 和 &lt;code&gt;RUN&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;FROM 指定基础镜像&lt;/h2&gt; &lt;p&gt;所谓定制镜像，那一定是以一个镜像为基础，在其上进行定制。就像我们之前运行了一个 &lt;code&gt;nginx&lt;/code&gt; 镜像的容器，再进行修改一样，基础镜像是必须指定的。而 &lt;code&gt;FROM&lt;/code&gt; 就是指定 基础镜像，因此一个 &lt;code&gt;Dockerfile&lt;/code&gt; 中 &lt;code&gt;FROM&lt;/code&gt; 是必备的指令，并且必须是第一条指令。 在 &lt;a href="https://hub.docker.com/search/?q=&amp;amp;type=image&amp;amp;image_filter=official" target="_blank"&gt;Docker Hub&lt;/a&gt; 上有非常多的高质量的官方镜像，有可以直接拿来使用的服务类的镜像，如： &lt;a href="https://hub.docker.com/_/nginx/" target="_blank"&gt;nginx&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/redis/" target="_blank"&gt;redist&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/mongo/" target="_blank"&gt;mongo&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/mysql/" target="_blank"&gt;mysql&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/httpd/" target="_blank"&gt;httpd&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/php/" target="_blank"&gt;php&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/tomcat/" target="_blank"&gt;tomcat&lt;/a&gt; 等；也有一些方便开发、构建、运行各种语言应用的镜像，如 &lt;a href="https://hub.docker.com/_/node" target="_blank"&gt;node&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/openjdk/" target="_blank"&gt;openjdk&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/python/" target="_blank"&gt;python&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/ruby/" target="_blank"&gt;ruby&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/golang/" target="_blank"&gt;golang&lt;/a&gt; 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。&lt;/p&gt; &lt;p&gt;如果没有找到对应服务的镜像，官方镜像中还提供了一些更为基础的操作系统镜像，如 &lt;a href="https://hub.docker.com/_/ubuntu/" target="_blank"&gt;ubuntu&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/debian/" target="_blank"&gt;debian&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/centos/" target="_blank"&gt;centos&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/fedora/" target="_blank"&gt;fedora&lt;/a&gt;、&lt;a href="https://hub.docker.com/_/alpine/" target="_blank"&gt;alpine&lt;/a&gt; 等，这些操作系统的软件库为我们提供了更广阔的扩展空间。&lt;/p&gt; &lt;p&gt;除了选择现有镜像为基础镜像外，&lt;code&gt;Docker&lt;/code&gt; 还存在一个特殊的镜像，名为 &lt;code&gt;scratch&lt;/code&gt;。这个镜像是虚拟的概念，并不实际存在，它表示一个空白的镜像。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM scratch ... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果你以 &lt;code&gt;scratch&lt;/code&gt; 为基础镜像的话，意味着你不以任何镜像为基础，接下来所写的指令将作为镜像第一层开始存在。&lt;/p&gt; &lt;p&gt;不以任何系统为基础，直接将可执行文件复制进镜像的做法并不罕见，比如 &lt;code&gt;swarm、coreos/etcd&lt;/code&gt;。对于 &lt;code&gt;Linux&lt;/code&gt; 下静态编译的程序来说，并不需要有操作系统提供运行时支持，所需的一切库都已经在可执行文件里了，因此直接 &lt;code&gt;FROM scratch&lt;/code&gt; 会让镜像体积更加小巧。使用 &lt;code&gt;Go&lt;/code&gt; 语言开发的应用很多会使用这种方式来制作镜像，这也是为什么有人认为 &lt;code&gt;Go&lt;/code&gt; 是特别适合容器微服务架构的语言的原因之一。&lt;/p&gt; &lt;h2&gt;RUN 执行命令&lt;/h2&gt; &lt;p&gt;&lt;code&gt;RUN&lt;/code&gt; 指令是用来执行命令行命令的。由于命令行的强大能力，&lt;code&gt;RUN&lt;/code&gt; 指令在定制镜像时是最常用的指令之一。其格式有两种：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;shell&lt;/code&gt; 格式：&lt;code&gt;RUN&lt;/code&gt; &amp;lt;命令&amp;gt;，就像直接在命令行中输入的命令一样。刚才写的 &lt;code&gt;Dockerfile&lt;/code&gt; 中的 &lt;code&gt;RUN&lt;/code&gt; 指令就是这种格式。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;RUN echo '&amp;lt;h1&amp;gt;Hello, Docker!&amp;lt;/h1&amp;gt;' &amp;gt; /usr/share/nginx/html/index.html &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;&lt;code&gt;exec&lt;/code&gt; 格式：&lt;code&gt;RUN&lt;/code&gt; [&amp;quot;可执行文件&amp;quot;, &amp;quot;参数1&amp;quot;, &amp;quot;参数2&amp;quot;]，这更像是函数调用中的格式。 既然 &lt;code&gt;RUN&lt;/code&gt; 就像 &lt;code&gt;Shell&lt;/code&gt; 脚本一样可以执行命令，那么我们是否就可以像 &lt;code&gt;Shell&lt;/code&gt; 脚本一样把每个命令对应一个 &lt;code&gt;RUN&lt;/code&gt; 呢？比如这样：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM debian:stretch  RUN apt-get update RUN apt-get install -y gcc libc6-dev make wget RUN wget -O redis.tar.gz &amp;quot;http://download.redis.io/releases/redis-5.0.3.tar.gz&amp;quot; RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之前说过，&lt;code&gt;Dockerfile&lt;/code&gt; 中每一个指令都会建立一层，&lt;code&gt;RUN&lt;/code&gt; 也不例外。每一个 &lt;code&gt;RUN&lt;/code&gt; 的行为，就和刚才我们手工建立镜像的过程一样：新建立一层，在其上执行这些命令，执行结束后，&lt;code&gt;commit&lt;/code&gt; 这一层的修改，构成新的镜像。&lt;/p&gt; &lt;p&gt;而上面的这种写法，创建了 7 层镜像。这是完全没有意义的，而且很多运行时不需要的东西，都被装进了镜像里，比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像，不仅仅增加了构建部署的时间，也很容易出错。 这是很多初学 &lt;code&gt;Docker&lt;/code&gt; 的人常犯的一个错误。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Union FS&lt;/code&gt; 是有最大层数限制的，比如 &lt;code&gt;AUFS&lt;/code&gt;，曾经是最大不得超过 &lt;code&gt;42&lt;/code&gt; 层，现在是不得超过 &lt;code&gt;127&lt;/code&gt; 层。&lt;/p&gt; &lt;p&gt;上面的 Dockerfile 正确的写法应该是这样：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM debian:stretch  RUN buildDeps='gcc libc6-dev make wget' \     &amp;amp;&amp;amp; apt-get update \     &amp;amp;&amp;amp; apt-get install -y $buildDeps \     &amp;amp;&amp;amp; wget -O redis.tar.gz &amp;quot;http://download.redis.io/releases/redis-5.0.3.tar.gz&amp;quot; \     &amp;amp;&amp;amp; mkdir -p /usr/src/redis \     &amp;amp;&amp;amp; tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \     &amp;amp;&amp;amp; make -C /usr/src/redis \     &amp;amp;&amp;amp; make -C /usr/src/redis install \     &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \     &amp;amp;&amp;amp; rm redis.tar.gz \     &amp;amp;&amp;amp; rm -r /usr/src/redis \     &amp;amp;&amp;amp; apt-get purge -y --auto-remove $buildDeps &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;首先，之前所有的命令只有一个目的，就是编译、安装 &lt;code&gt;redis&lt;/code&gt; 可执行文件。因此没有必要建立很多层，这只是一层的事情。因此，这里没有使用很多个 &lt;code&gt;RUN&lt;/code&gt; 对一一对应不同的命令，而是仅仅使用一个 &lt;code&gt;RUN&lt;/code&gt; 指令，并使用 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 将各个所需命令串联起来。将之前的 &lt;code&gt;7&lt;/code&gt; 层，简化为了 &lt;code&gt;1&lt;/code&gt; 层。在撰写 &lt;code&gt;Dockerfile&lt;/code&gt; 的时候，要经常提醒自己，这并不是在写 &lt;code&gt;Shell&lt;/code&gt; 脚本，而是在定义每一层该如何构建。&lt;/p&gt; &lt;p&gt;并且，这里为了格式化还进行了换行。&lt;code&gt;Dockerfile&lt;/code&gt; 支持 &lt;code&gt;Shell&lt;/code&gt; 类的行尾添加 &lt;code&gt;\&lt;/code&gt; 的命令换行方式，以及行首 &lt;code&gt;#&lt;/code&gt; 进行注释的格式。良好的格式，比如换行、缩进、注释等，会让维护、排障更为容易，这是一个比较好的习惯。&lt;/p&gt; &lt;p&gt;此外，还可以看到这一组命令的最后添加了清理工作的命令，删除了为了编译构建所需要的软件，清理了所有下载、展开的文件，并且还清理了 &lt;code&gt;apt&lt;/code&gt; 缓存文件。这是很重要的一步，我们之前说过，镜像是多层存储，每一层的东西并不会在下一层被删除，会一直跟随着镜像。因此镜像构建时，一定要确保每一层只添加真正需要添加的东西，任何无关的东西都应该清理掉。&lt;/p&gt; &lt;p&gt;很多人初学 &lt;code&gt;Docker&lt;/code&gt; 制作出了很臃肿的镜像的原因之一，就是忘记了每一层构建的最后一定要清理掉无关文件。&lt;/p&gt; &lt;h2&gt;构建镜像&lt;/h2&gt; &lt;p&gt;好了，让我们再回到之前定制的 &lt;code&gt;nginx&lt;/code&gt; 镜像的 &lt;code&gt;Dockerfile&lt;/code&gt; 来。现在我们明白了这个 &lt;code&gt;Dockerfile&lt;/code&gt; 的内容，那么让我们来构建这个镜像吧。 在 Dockerfile 文件所在目录执行：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB Step 1 : FROM nginx  ---&amp;gt; e43d811ce2f4 Step 2 : RUN echo '&amp;lt;h1&amp;gt;Hello, Docker!&amp;lt;/h1&amp;gt;' &amp;gt; /usr/share/nginx/html/index.html  ---&amp;gt; Running in 9cdc27646c7b  ---&amp;gt; 44aa4490ce2c Removing intermediate container 9cdc27646c7b Successfully built 44aa4490ce2c &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从命令的输出结果中，我们可以清晰的看到镜像的构建过程。在 &lt;code&gt;Step 2&lt;/code&gt; 中，如同我们之前所说的那样，&lt;code&gt;RUN&lt;/code&gt; 指令启动了一个容器 &lt;code&gt;9cdc27646c7b&lt;/code&gt;，执行了所要求的命令，并最后提交了这一层 &lt;code&gt;44aa4490ce2c&lt;/code&gt;，随后删除了所用到的这个容器 &lt;code&gt;9cdc27646c7b&lt;/code&gt;。 这里我们使用了 &lt;code&gt;docker build&lt;/code&gt; 命令进行镜像构建。其格式为：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;docker build [选项] &amp;lt;上下文路径/URL/-&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在这里我们指定了最终镜像的名称 &lt;code&gt;-t nginx:v3&lt;/code&gt;，构建成功后，我们可以像之前运行 &lt;code&gt;nginx:v2&lt;/code&gt; 那样来运行这个镜像，其结果会和 &lt;code&gt;nginx:v2&lt;/code&gt; 一样。&lt;/p&gt; &lt;h2&gt;镜像构建上下文（Context）&lt;/h2&gt; &lt;p&gt;如果注意，会看到 &lt;code&gt;docker build&lt;/code&gt; 命令最后有一个 &lt;code&gt;.&lt;/code&gt;。&lt;code&gt;.&lt;/code&gt; 表示当前目录，而 &lt;code&gt;Dockerfile&lt;/code&gt; 就在当前目录，因此不少初学者以为这个路径是在指定 &lt;code&gt;Dockerfile&lt;/code&gt; 所在路径，这么理解其实是不准确的。如果对应上面的命令格式，你可能会发现，这是在指定上下文路径。那么什么是上下文呢？&lt;/p&gt; &lt;p&gt;首先我们要理解 &lt;code&gt;docker build&lt;/code&gt; 的工作原理。&lt;code&gt;Docker&lt;/code&gt; 在运行时分为 &lt;code&gt;Docker&lt;/code&gt; 引擎（也就是服务端守护进程）和客户端工具。&lt;code&gt;Docker&lt;/code&gt; 的引擎提供了一组 &lt;code&gt;REST API&lt;/code&gt;，被称为 &lt;a href="https://docs.docker.com/develop/sdk/" target="_blank"&gt;Docker Remote API&lt;/a&gt;，而如 &lt;code&gt;docker&lt;/code&gt; 命令这样的客户端工具，则是通过这组 &lt;code&gt;API&lt;/code&gt; 与 &lt;code&gt;Docker&lt;/code&gt; 引擎交互，从而完成各种功能。因此，虽然表面上我们好像是在本机执行各种 &lt;code&gt;docker&lt;/code&gt; 功能，但实际上，一切都是使用的远程调用形式在服务端（&lt;code&gt;Docker&lt;/code&gt; 引擎）完成。也因为这种 &lt;code&gt;C/S&lt;/code&gt; 设计，让我们操作远程服务器的 &lt;code&gt;Docker&lt;/code&gt; 引擎变得轻而易举。&lt;/p&gt; &lt;p&gt;当我们进行镜像构建的时候，并非所有定制都会通过 &lt;code&gt;RUN&lt;/code&gt; 指令完成，经常会需要将一些本地文件复制进镜像，比如通过 &lt;code&gt;COPY&lt;/code&gt; 指令、&lt;code&gt;ADD&lt;/code&gt; 指令等。而 &lt;code&gt;docker build&lt;/code&gt; 命令构建镜像，其实并非在本地构建，而是在服务端，也就是 &lt;code&gt;Docker&lt;/code&gt; 引擎中构建的。那么在这种客户端/服务端的架构中，如何才能让服务端获得本地文件呢？&lt;/p&gt; &lt;p&gt;这就引入了上下文的概念。当构建的时候，用户会指定构建镜像上下文的路径，&lt;code&gt;docker build&lt;/code&gt; 命令得知这个路径后，会将路径下的所有内容打包，然后上传给 &lt;code&gt;Docker&lt;/code&gt; 引擎。这样 &lt;code&gt;Docker&lt;/code&gt; 引擎收到这个上下文包后，展开就会获得构建镜像所需的一切文件。 如果在 &lt;code&gt;Dockerfile&lt;/code&gt; 中这么写：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;COPY ./package.json /app/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这并不是要复制执行 &lt;code&gt;docker build&lt;/code&gt; 命令所在的目录下的 &lt;code&gt;package.json&lt;/code&gt;，也不是复制 &lt;code&gt;Dockerfile&lt;/code&gt; 所在目录下的 &lt;code&gt;package.json&lt;/code&gt;，而是复制 上下文（&lt;code&gt;context&lt;/code&gt;） 目录下的 &lt;code&gt;package.json&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;因此，&lt;code&gt;COPY&lt;/code&gt; 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 &lt;code&gt;COPY ../package.json /app&lt;/code&gt; 或者 &lt;code&gt;COPY /opt/xxxx /app&lt;/code&gt; 无法工作的原因，因为这些路径已经超出了上下文的范围，&lt;code&gt;Docker&lt;/code&gt; 引擎无法获得这些位置的文件。如果真的需要那些文件，应该将它们复制到上下文目录中去。&lt;/p&gt; &lt;p&gt;现在就可以理解刚才的命令 &lt;code&gt;docker build -t nginx:v3 .&lt;/code&gt; 中的这个 &lt;code&gt;.&lt;/code&gt;，实际上是在指定上下文的目录，&lt;code&gt;docker build&lt;/code&gt; 命令会将该目录下的内容打包交给 &lt;code&gt;Docker&lt;/code&gt; 引擎以帮助构建镜像。 如果观察 docker build 输出，我们其实已经看到了这个发送上下文的过程：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB ... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;理解构建上下文对于镜像构建是很重要的，避免犯一些不应该的错误。比如有些初学者在发现&lt;code&gt;COPY /opt/xxxx/app&lt;/code&gt; 不工作后，于是干脆将 &lt;code&gt;Dockerfile&lt;/code&gt; 放到了硬盘根目录去构建，结果发现 &lt;code&gt;docker build&lt;/code&gt; 执行后，在发送一个几十 &lt;code&gt;GB&lt;/code&gt; 的东西，极为缓慢而且很容易构建失败。那是因为这种做法是在让 &lt;code&gt;docker build&lt;/code&gt; 打包整个硬盘，这显然是使用错误。&lt;/p&gt; &lt;p&gt;一般来说，应该会将 &lt;code&gt;Dockerfile&lt;/code&gt; 置于一个空目录下，或者项目根目录下。如果该目录下没有所需文件，那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 &lt;code&gt;Docker&lt;/code&gt; 引擎，那么可以用 &lt;code&gt;.gitignore&lt;/code&gt; 一样的语法写一个 &lt;code&gt;.dockerignore&lt;/code&gt;，该文件是用于剔除不需要作为上下文传递给 &lt;code&gt;Docker&lt;/code&gt; 引擎的。&lt;/p&gt; &lt;p&gt;那么为什么会有人误以为 . 是指定 &lt;code&gt;Dockerfile&lt;/code&gt; 所在目录呢？这是因为在默认情况下，如果不额外指定 &lt;code&gt;Dockerfile&lt;/code&gt; 的话，会将上下文目录下的名为 &lt;code&gt;Dockerfile&lt;/code&gt; 的文件作为 &lt;code&gt;Dockerfile&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;这只是默认行为，实际上 &lt;code&gt;Dockerfile&lt;/code&gt; 的文件名并不要求必须为 &lt;code&gt;Dockerfile&lt;/code&gt;，而且并不要求必须位于上下文目录中，比如可以用 &lt;code&gt;-f ../Dockerfile.php&lt;/code&gt; 参数指定某个文件作为 &lt;code&gt;Dockerfile&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;当然，一般大家习惯性的会使用默认的文件名 Dockerfile，以及会将其置于镜像构建上下文目录中。&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;其它 docker build 的用法&lt;/h2&gt; &lt;h3&gt;直接用 Git repo 进行构建&lt;/h3&gt; &lt;p&gt;或许你已经注意到了，&lt;code&gt;docker build&lt;/code&gt; 还支持从 &lt;code&gt;URL&lt;/code&gt; 构建，比如可以直接从 &lt;code&gt;Git repo&lt;/code&gt; 中构建：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1  Sending build context to Docker daemon 2.048 kB Step 1 : FROM gitlab/gitlab-ce:11.1.0-ce.0 11.1.0-ce.0: Pulling from gitlab/gitlab-ce aed15891ba52: Already exists 773ae8583d14: Already exists ... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这行命令指定了构建所需的 &lt;code&gt;Git repo&lt;/code&gt;，并且指定默认的 &lt;code&gt;master&lt;/code&gt; 分支，构建目录为 &lt;code&gt;/11.1/&lt;/code&gt;，然后 &lt;code&gt;Docker&lt;/code&gt; 就会自己去 &lt;code&gt;git clone&lt;/code&gt;这个项目、切换到指定分支、并进入到指定目录后开始构建。&lt;/p&gt; &lt;h3&gt;用给定的 tar 压缩包构建&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ docker build http://server/context.tar.gz &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果所给出的 &lt;code&gt;URL&lt;/code&gt; 不是个 &lt;code&gt;Git repo&lt;/code&gt;，而是个 &lt;code&gt;tar&lt;/code&gt; 压缩包，那么 &lt;code&gt;Docker&lt;/code&gt; 引擎会下载这个包，并自动解压缩，以其作为上下文，开始构建。&lt;/p&gt; &lt;h3&gt;从标准输入中读取 Dockerfile 进行构建&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;docker build - &amp;lt; Dockerfile &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;或&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;cat Dockerfile | docker build - &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果标准输入传入的是文本文件，则将其视为 &lt;code&gt;Dockerfile&lt;/code&gt;，并开始构建。这种形式由于直接从标准输入中读取 &lt;code&gt;Dockerfile&lt;/code&gt; 的内容，它没有上下文，因此不可以像其他方法那样可以将本地文件 &lt;code&gt;COPY&lt;/code&gt; 进镜像之类的事情。&lt;/p&gt; &lt;h3&gt;从标准输入中读取上下文压缩包进行构建&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;docker build - &amp;lt; context.tar.gz &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果发现标准输入的文件格式是 &lt;code&gt;gzip、bzip2&lt;/code&gt; 以及 &lt;code&gt;xz&lt;/code&gt; 的话，将会使其为上下文压缩包，直接将其展开，将里面视为上下文，并开始构建。&lt;/p&gt; &lt;p&gt;&lt;a href="https://yeasy.gitbooks.io/docker_practice/image/build.html" target="_blank"&gt;原文链接&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 17 Aug 2019 09:04:00 GMT</pubDate>
    </item>
    <item>
      <title>Redis 4 种模式简单了解</title>
      <link>https://www.zhangaoo.com/article/redis-cluster-sentinel</link>
      <content:encoded>&lt;h1&gt;Redis 模式(Redis Mode)&lt;/h1&gt; &lt;p&gt;Redis 有很多种部署方式，单点（Standalone）、主从（Master-Slave）、哨兵（Sentinel）、集群（Cluster）。&lt;/p&gt; &lt;h2&gt;单点(Standalone)&lt;/h2&gt; &lt;p&gt;最简单的方式，同时也是风险最高的方式，相比其他几种方式做不到HA、Failover、，也不能读写分离，也不能对应高吞吐量。安装方式也很简单这里就不赘述了。&lt;/p&gt; &lt;h2&gt;主从模式(Master Slave)&lt;/h2&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019729172044-Redis-master-slave.png" alt="2019729172044-Redis-master-slave" /&gt;&lt;/p&gt; &lt;p&gt;主从模式同样不具备HA、Failover、等，单可用于读写分离的场景，写操作走Master节点，读操作走Slave节点，能在一定程度提高读写的吞吐量。&lt;/p&gt; &lt;h3&gt;主要配置(Master Slaver Config)&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;vim redis.conf # 以守护进程的方式运行 daemonize yes port 6379 # 绑定的主机地址 bind 0.0.0.0 # 指定当本机为slave服务时，设置master服务的IP地址及端口，在redis启动的时候他会自动跟master进行数据同步 slaveof &amp;lt;masterip&amp;gt; &amp;lt;masterport&amp;gt; # 当master设置了密码保护时，slave服务连接master的密码 masterauth &amp;lt;master-password&amp;gt; # 设置redis连接密码，如果配置了连接密码，客户端在连接redis是需要通过AUTH&amp;lt;password&amp;gt;命令提供密码，默认关闭 requirepass footbared &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意上面的 &lt;code&gt;slaveof &amp;lt;masterip&amp;gt; &amp;lt;masterport&amp;gt;&lt;/code&gt; 填写的是 &lt;code&gt;master&lt;/code&gt; 节点的 IP 和端口，也就是说填写这两个参数的节点是 Slave 节点。 为了简单验证，可以单独拷贝一份配置文件，目录如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜  redis-master-slave tree . ├── 6380 │   ├── redis-master.conf │   ├── redis-master.log │   └── redis-server.pid ├── 6381 │   ├── redis-server.pid │   ├── redis-slave.conf │   └── redis-slave.log └── start.sh # 启动脚本 redis-server ./6380/redis-master.conf redis-server ./6381/redis-slave.conf &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;哨兵模式(Sentinel)&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;哨兵模式集成了主从模式的有点，同时有了哨兵监控节点还能保证HA，当 &lt;code&gt;Master&lt;/code&gt; 节点离线后，哨兵监控节点会把 &lt;code&gt;Slave&lt;/code&gt; 节点切换为 &lt;code&gt;Master&lt;/code&gt; 节点，保证服务可用&lt;/li&gt; &lt;li&gt;哨兵模式是在主从模式的基础上增加了哨兵监控节点，最简单的哨兵模式需要一个 &lt;code&gt;Master&lt;/code&gt;、一个 &lt;code&gt;Slave&lt;/code&gt; 、三个哨兵监控节点。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019729201954-redis-sentinel.png" alt="2019729201954-redis-sentinel" /&gt;&lt;/p&gt; &lt;h3&gt;Ubuntu 安装(Sentinel Install)&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt-get install redis-sentinel &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;配置(Sentinel Config)&lt;/h3&gt; &lt;p&gt;主要配置&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;vim sentinel-26379.conf pidfile /home/xxx/Documents/company/redis-sentinel/26379/redis-sentinel.pid logfile /home/xxx/Documents/company/redis-sentinel/26379/redis-sentinel.log bind 0.0.0.0 port 26379 # 哨兵 sentinel 监控的 redis 主节点的  # sentinel monitor &amp;lt;master-name&amp;gt; &amp;lt;ip&amp;gt; &amp;lt;redis-port&amp;gt; &amp;lt;quorum&amp;gt; # 这个2代表，当集群中有2个sentinel认为master死了时，才能真正认为该master已经不可用了。 sentinel monitor mymaster 10.201.12.66 6380 2 # 当在Redis实例中开启了requirepass &amp;lt;foobared&amp;gt;，所有连接Redis实例的客户端都要提供密码。 sentinel auth-pass mymaster test@123456 # 指定主节点应答哨兵sentinel的最大时间间隔，超过这个时间，哨兵主观上认为主节点下线，默认30秒  # Default is 30 seconds. sentinel down-after-milliseconds mymaster 30000 # 指定了在发生failover主备切换时，最多可以有多少个slave同时对新的master进行同步。这个数字越小，完成failover所需的时间就越长；反之，但是如果这个数字越大，就意味着越多的slave因为replication而不可用。可以通过将这个值设为1，来保证每次只有一个slave，处于不能处理命令请求的状态。 # sentinel parallel-syncs &amp;lt;master-name&amp;gt; &amp;lt;numslaves&amp;gt; sentinel parallel-syncs mymaster 1 # 故障转移的超时时间failover-timeout，默认三分钟，可以用在以下这些方面： ## 1. 同一个sentinel对同一个master两次failover之间的间隔时间。   ## 2. 当一个slave从一个错误的master那里同步数据时开始，直到slave被纠正为从正确的master那里同步数据时结束。   ## 3. 当想要取消一个正在进行的failover时所需要的时间。 ## 4.当进行failover时，配置所有slaves指向新的master所需的最大时间。不过，即使过了这个超时，slaves依然会被正确配置为指向master，但是就不按parallel-syncs所配置的规则来同步数据了 # sentinel failover-timeout &amp;lt;master-name&amp;gt; &amp;lt;milliseconds&amp;gt;   sentinel failover-timeout mymaster 180000 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;启动脚本(Start Script)&lt;/h3&gt; &lt;p&gt;一个简单的启动脚本&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;echo &amp;quot;Starting sentinel-26380&amp;quot; redis-sentinel /home/xxx/Documents/company/redis-sentinel/26380/sentinel-26380.conf echo &amp;quot;Starting sentinel-26381&amp;quot; redis-sentinel /home/xxx/Documents/company/redis-sentinel/26381/sentinel-26381.conf echo &amp;quot;Starting sentinel-26382&amp;quot; redis-sentinel /home/xxx/Documents/company/redis-sentinel/26382/sentinel-26382.conf &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;日志(Sentinel Log)&lt;/h3&gt; &lt;p&gt;部分 &lt;code&gt;sentinel&lt;/code&gt; 节点日志&lt;/p&gt; &lt;pre&gt;&lt;code class="language-log"&gt;6258:X 30 Jul 11:21:01.003 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 6258:X 30 Jul 11:21:01.003 # Sentinel runid is 0984fa31f919e6a66891d7c44623a22cd1bd1718 6258:X 30 Jul 11:21:01.003 # +monitor master mymaster 10.201.12.66 6380 quorum 2 6258:X 30 Jul 11:21:01.004 * +slave slave 10.201.12.66:6381 10.201.12.66 6381 @ mymaster 10.201.12.66 6380 6258:X 30 Jul 11:21:03.028 * +sentinel sentinel 10.201.12.66:26382 10.201.12.66 26382 @ mymaster 10.201.12.66 6380 6258:X 30 Jul 11:21:03.050 * +sentinel sentinel 10.201.12.66:26381 10.201.12.66 26381 @ mymaster 10.201.12.66 6380 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Sentinel 状态(Sentinel State)&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# redis-cli 连接 Sentinel redis-cli -h 10.201.12.66 -p 26380 -a 'test@123456' # 查看当前主节点 10.201.12.66:26380&amp;gt; SENTINEL get-master-addr-by-name mymaster 1) &amp;quot;10.201.12.66&amp;quot; 2) &amp;quot;6380&amp;quot;  #显示被监控的所有 主节点 以及它们的状态。 SENTINEL masters # 显示指定 主节点 的信息和状态。 SENTINEL master &amp;lt;master_name&amp;gt; # 显示指定 主节点 的所有 从节点 以及它们的状态。 SENTINEL slaves &amp;lt;master_name&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;故障切换测试(Failover Test)&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 查看当前主节点 10.201.12.66:26380&amp;gt; SENTINEL get-master-addr-by-name mymaster 1) &amp;quot;10.201.12.66&amp;quot; 2) &amp;quot;6380&amp;quot; # kill master 节点 ➜ kill 27832 # 查看当前主节点 10.201.12.66:26380&amp;gt; SENTINEL get-master-addr-by-name mymaster 1) &amp;quot;10.201.12.66&amp;quot; 2) &amp;quot;6381&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看哨兵节点日志&lt;/p&gt; &lt;pre&gt;&lt;code class="language-log"&gt;6258:X 30 Jul 11:44:43.782 # +sdown master mymaster 10.201.12.66 6380 6258:X 30 Jul 11:44:43.872 # +odown master mymaster 10.201.12.66 6380 #quorum 2/2 6258:X 30 Jul 11:44:43.872 # +new-epoch 1 6258:X 30 Jul 11:44:43.872 # +try-failover master mymaster 10.201.12.66 6380 6258:X 30 Jul 11:44:43.875 # +vote-for-leader 0984fa31f919e6a66891d7c44623a22cd1bd1718 1 6258:X 30 Jul 11:44:43.875 # 10.201.12.66:26381 voted for 014b4a1dca557af06293c1e2e84cfe8233e38c2f 1 6258:X 30 Jul 11:44:43.880 # 10.201.12.66:26382 voted for 014b4a1dca557af06293c1e2e84cfe8233e38c2f 1 6258:X 30 Jul 11:44:44.970 # +config-update-from sentinel 10.201.12.66:26381 10.201.12.66 26381 @ mymaster 10.201.12.66 6380 6258:X 30 Jul 11:44:44.970 # +switch-master mymaster 10.201.12.66 6380 10.201.12.66 6381 6258:X 30 Jul 11:44:44.970 * +slave slave 10.201.12.66:6380 10.201.12.66 6380 @ mymaster 10.201.12.66 6381 6258:X 30 Jul 11:45:15.015 # +sdown slave 10.201.12.66:6380 10.201.12.66 6380 @ mymaster 10.201.12.66 6381 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从以上日志可以清楚的看到故障恢复成功，&lt;code&gt;Slave&lt;/code&gt; 节点切换成了 &lt;code&gt;Master&lt;/code&gt; 节点，并且能正常读写&lt;/p&gt; &lt;h3&gt;重启节点&lt;/h3&gt; &lt;p&gt;重启被 &lt;code&gt;kill&lt;/code&gt; 掉的节点，发现该节点变成了 &lt;code&gt;Slave&lt;/code&gt; 节点，并且是只读的，默认的配置中 &lt;code&gt;Slave&lt;/code&gt; 节点是只读的。&lt;/p&gt; &lt;p&gt;哨兵节点部分日志&lt;/p&gt; &lt;pre&gt;&lt;code class="language-log"&gt;6258:X 30 Jul 14:18:42.387 # -sdown slave 10.201.12.66:6380 10.201.12.66 6380 @ mymaster 10.201.12.66 6381 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Spring Boot 整合&lt;/h3&gt; &lt;p&gt;参考 &lt;a href="https://github.com/zealzhangz/redis-sentinel-demo" target="_blank"&gt;Github&lt;/a&gt; 代码&lt;/p&gt; &lt;h2&gt;Cluster Mode&lt;/h2&gt; &lt;p&gt;集群模式有多种实现方法，比如：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;客户端分区方案&lt;/li&gt; &lt;li&gt;代理分区方案&lt;/li&gt; &lt;li&gt;Twemproxy&lt;/li&gt; &lt;li&gt;Codis&lt;/li&gt; &lt;li&gt;查询路由方案&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这里我们使用查询路由方案：客户端随机地 请求任意一个 Redis 实例，然后由 Redis 将请求 转发 给 正确 的 Redis 节点。Redis Cluster 实现了一种 混合形式 的 查询路由，但并不是 直接 将请求从一个 Redis 节点 转发 到另一个 Redis 节点，而是在 客户端 的帮助下直接 重定向（ redirected）到正确的 Redis 节点。 &lt;code&gt;Cluster&lt;/code&gt; 模式数据是分布在所有节点，为了达到一定程度上的高可用，需要给 &lt;code&gt;Master&lt;/code&gt; 节点备份一个 &lt;code&gt;Slave&lt;/code&gt; 节点；在 Cluster 模式下当 Master 节点 Down 掉的时候，并不会自动切换，需要手动切换。 因此在使用时需要考虑自己的使用场景。&lt;/p&gt; &lt;h3&gt;Deploy Cluster&lt;/h3&gt; &lt;p&gt;这里只是测试就在一个机器上进行部署测试&lt;/p&gt; &lt;p&gt;&lt;strong&gt;新建集群安装目录&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ mkdir {16001,16002,16003} # 拷贝原配置文件到各个目录 sudo cp /etc/redis/redis.conf 16001/redis-01.conf sudo cp /etc/redis/redis.conf 16002/redis-02.conf sudo cp /etc/redis/redis.conf 16003/redis-03.conf  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;配置节点集群(Config Cluster)&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;vim redis-01.conf # 更改以下配置 bind 0.0.0.0 port 16001 daemonize yes cluster-enabled yes cluster-config-file  nodes-16001.conf cluster-node-timeout 15000 appendonly yes logfile /home/aozhang/Documents/company/redis-cluster/16001/redis-server-01.log  # 类似的配置配置其他两个节点，更改对应的 port、logfile、cluster-config-file &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Start Nodes&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;sudo redis-server redis-01.conf sudo redis-server redis-02.conf sudo redis-server redis-03.conf &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;启动完成后，三个节点还是独立的，还需要建立集群 使用 &lt;code&gt;/usr/share/doc/redis-tools/examples/redis-trib.rb&lt;/code&gt; 建立集群（适合&lt;code&gt;Redis3、4&lt;/code&gt;版本，5可以直接使用）下面命令建立&lt;/p&gt; &lt;p&gt;&lt;code&gt;redis5&lt;/code&gt; 建立集群&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;redis3、4&lt;/code&gt; 建立集群&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;/usr/share/doc/redis-tools/examples/redis-trib.rb create 10.201.12.66:16001 10.201.12.66:16002 10.201.12.66:16003 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;输出如下日志：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-log"&gt;➜ /usr/share/doc/redis-tools/examples/redis-trib.rb create 10.201.12.66:16001 10.201.12.66:16002 10.201.12.66:16003 /usr/share/doc/redis-tools/examples/redis-trib.rb:1573: warning: key &amp;quot;threshold&amp;quot; is duplicated and overwritten on line 1573 &amp;gt;&amp;gt;&amp;gt; Creating cluster &amp;gt;&amp;gt;&amp;gt; Performing hash slots allocation on 3 nodes... Using 3 masters: 10.201.12.66:16001 10.201.12.66:16002 10.201.12.66:16003 M: 27f5657223f50af58de671a8f34b7160541c78e2 10.201.12.66:16001    slots:0-5460 (5461 slots) master M: 1fe62e7267f766c02ba956457a4c2473e73e9623 10.201.12.66:16002    slots:5461-10922 (5462 slots) master M: d4846a74d7433afc87168bc4ace9d1de9733b17a 10.201.12.66:16003    slots:10923-16383 (5461 slots) master Can I set the above configuration? (type 'yes' to accept): yes &amp;gt;&amp;gt;&amp;gt; Nodes configuration updated &amp;gt;&amp;gt;&amp;gt; Assign a different config epoch to each node &amp;gt;&amp;gt;&amp;gt; Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join... &amp;gt;&amp;gt;&amp;gt; Performing Cluster Check (using node 10.201.12.66:16001) M: 27f5657223f50af58de671a8f34b7160541c78e2 10.201.12.66:16001    slots:0-5460 (5461 slots) master M: 1fe62e7267f766c02ba956457a4c2473e73e9623 10.201.12.66:16002    slots:5461-10922 (5462 slots) master M: d4846a74d7433afc87168bc4ace9d1de9733b17a 10.201.12.66:16003    slots:10923-16383 (5461 slots) master [OK] All nodes agree about slots configuration. &amp;gt;&amp;gt;&amp;gt; Check for open slots... &amp;gt;&amp;gt;&amp;gt; Check slots coverage... [OK] All 16384 slots covered. &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里没有指定副本拷贝，如果需要副本考本的话，再建3个节点，一个6个节点，启动后执行如下命令&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜ /usr/share/doc/redis-tools/examples/redis-trib.rb create --replicas 10.201.12.66:16001 10.201.12.66:16002 10.201.12.66:16003 10.201.12.66:16004 10.201.12.66:16005 10.201.12.66:16006 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;查看节点状态&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;➜  ~  redis-cli -c -h 10.201.12.66 -p 16001 10.201.12.66:16001&amp;gt; cluster nodes 1fe62e7267f766c02ba956457a4c2473e73e9623 10.201.12.66:16002 master - 0 1562816528369 2 connected 5461-10922 27f5657223f50af58de671a8f34b7160541c78e2 10.201.12.66:16001 myself,master - 0 0 1 connected 0-5460 d4846a74d7433afc87168bc4ace9d1de9733b17a 10.201.12.66:16003 master - 0 1562816527366 3 connected 10923-16383 10.201.12.66:16001&amp;gt;  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Spring Boot 整合&lt;/h3&gt; &lt;p&gt;参考 &lt;a href="https://github.com/zealzhangz/redis-cluster-demo" target="_blank"&gt;Github&lt;/a&gt; 代码&lt;/p&gt; &lt;h1&gt;小结&lt;/h1&gt; &lt;p&gt;以上步骤就完成了 &lt;code&gt;Redis&lt;/code&gt; 几种模式的部署搭建，可具体根据需求选择对应的模式；如果是存储一些敏感信息比如 &lt;code&gt;Web&lt;/code&gt; 应用的 &lt;code&gt;Session&lt;/code&gt; 等需要高可用，但并发量又不是特别大的场景就可以使用哨兵模式。 如果是用于高速缓存，秒杀等场景对性能要求较高，但可用性不是很高的场景则可以使用 &lt;code&gt;Sentinel&lt;/code&gt; 模式。&lt;/p&gt; &lt;h3&gt;参考文档(Refrence)&lt;/h3&gt; &lt;p&gt;&lt;a href="https://redis.io/topics/sentinel" target="_blank"&gt;官方文档&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://juejin.im/post/5b8fc5536fb9a05d2d01fb11" target="_blank"&gt;Redis Cluster&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://fnordig.de/2015/06/01/redis-sentinel-and-redis-cluster/" target="_blank"&gt;Redis Sentinel &amp;amp; Redis Cluster - what?&lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://stackoverflow.com/questions/53060714/redis-sentinel-standalone-or-cluster-which-is-best-for-session" target="_blank"&gt;Redis Sentinel, Standalone or Cluster, which is best for session? &lt;/a&gt;&lt;/p&gt; &lt;p&gt;&lt;a href="https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:sentinel" target="_blank"&gt;Redis Sentinel Support&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 30 Jul 2019 13:07:00 GMT</pubDate>
    </item>
    <item>
      <title>Go语言之并发篇五</title>
      <link>https://www.zhangaoo.com/article/goroutine-channel</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019713151728-gopher.jpg" alt="2019713151728-gopher" /&gt;&lt;/p&gt; &lt;h2&gt;并发&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;作为语言的核心部分，&lt;code&gt;Go&lt;/code&gt; 提供了并发的特性。&lt;/li&gt; &lt;li&gt;这一部分概览了 &lt;code&gt;goroutine&lt;/code&gt; 和 &lt;code&gt;channel&lt;/code&gt;，以及如何使用它们来实现不同的并发模式。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Go&lt;/code&gt; 将并发结构作为核心语言的一部分提供。本节课程通过一些示例介绍并展示了它们的用法。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Go 程&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;em&gt;Go 程&lt;/em&gt;（&lt;code&gt;goroutine&lt;/code&gt;）_ 是由 &lt;code&gt;Go&lt;/code&gt; 运行时管理的轻量级线程。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;go f(x, y, z) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;会启动一个新的 &lt;code&gt;Go&lt;/code&gt; 程并执行&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;f(x, y, z) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;f 、 x 、 y 和 z 的求值发生在当前的 &lt;code&gt;Go&lt;/code&gt; 程中，而 &lt;code&gt;f&lt;/code&gt; 的执行发生在新的 &lt;code&gt;Go&lt;/code&gt; 程中。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Go&lt;/code&gt; 程在相同的地址空间中运行，因此在访问共享的内存时必须进行同步。&lt;code&gt;sync&lt;/code&gt; 包提供了这种能力，不过在 &lt;code&gt;Go&lt;/code&gt; 中并不经常用到，因为还有其它的办法（见下一页）。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;time&amp;quot; )  func say(s string) {  for i := 0; i &amp;lt; 5; i++ {   time.Sleep(100 * time.Millisecond)   fmt.Println(s)  } }  func main() {  go say(&amp;quot;world&amp;quot;)  say(&amp;quot;hello&amp;quot;) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;信道&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;信道是带有类型的管道，你可以通过它用信道操作符 &lt;code&gt;&amp;lt;-&lt;/code&gt; 来发送或者接收值。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;ch &amp;lt;- v //将 v 发送至信道ch v := &amp;lt;- ch //从 ch 接收值并赋予 v。 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;（“箭头”就是数据流的方向。）&lt;/em&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;和映射与切片一样，信道在使用前必须创建&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;ch := make(chan int) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;默认情况下，发送和接收操作在另一端准备好之前都会阻塞。这使得 &lt;code&gt;Go&lt;/code&gt; 程可以在没有显式的锁或竞态变量的情况下进行同步。&lt;/li&gt; &lt;li&gt;以下示例对切片中的数进行求和，将任务分配给两个 &lt;code&gt;Go&lt;/code&gt; 程。 一旦两个 &lt;code&gt;Go&lt;/code&gt; 程完成了它们的计算，它就能算出最终的结果。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func sum(s [] int,c chan int)  {  sum := 0  for _,v := range s {   sum += v  }  c &amp;lt;- sum // 将和送入 c }  func main() {  s := [] int {7,2,-3,2,4,-5}  c := make(chan int)   go sum(s[:len(s)/2],c)  go sum(s[len(s)/2:],c)  x , y := &amp;lt;-c,&amp;lt;-c // 从 c 中接收  fmt.Println(x, y, x+y) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;思考：向同一个信道多次发送值，接受的顺序如何？信道中的值存储的数据结构如何？&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通过后面的练习，发现信道有点类似于一个阻塞队列，先进先出，信道无值阻塞信道输出，信道缓冲区满了阻塞向信道输入值&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;带缓冲的信道&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;信道可以是 带缓冲的 。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道：&lt;/li&gt; &lt;li&gt;不指定缓冲区大小的信道创建&lt;code&gt;make(chan int)&lt;/code&gt;,缓冲区大小为0，如果不在发送值到信道之前创建一个 &lt;code&gt;goroutine&lt;/code&gt; 去读取值那么当前的 &lt;code&gt;goroutine&lt;/code&gt; 会一直阻塞。&lt;a href="https://stackoverflow.com/questions/11943841/golang-what-is-channel-buffer-size" target="_blank"&gt;参考链接&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;ch := make(chan int, 100) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;仅当信道的缓冲区填满后，向其发送数据时才会阻塞。当缓冲区为空时，接受方会阻塞。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;修改示例填满缓冲区，然后看看会发生什么。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;不阻塞代码&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  ch := make(chan int, 2)  ch &amp;lt;- 1  ch &amp;lt;- 2  fmt.Println(&amp;lt;-ch)  fmt.Println(&amp;lt;-ch) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;执行结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;1 2 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;发送数据阻塞代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  ch := make(chan int, 2)  ch &amp;lt;- 1  ch &amp;lt;- 2  ch &amp;lt;- 3  fmt.Println(&amp;lt;-ch)  fmt.Println(&amp;lt;-ch) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;执行结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;fatal error: all goroutines are asleep - deadlock!  goroutine 1 [chan send]: main.main()  /tmp/sandbox937270155/main.go:9 +0xa0 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;只发送不创建goroutine接受&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  c := make(chan int)//此时buffer size默认为0，当前go程将一直阻塞（或者叫死锁）  c &amp;lt;- 1  fmt.Println(&amp;quot;test&amp;quot;) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;fatal error: all goroutines are asleep - deadlock!  goroutine 1 [chan send]: main.main() &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;发送并且创建goroutine接受&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  c := make(chan int)  //注意这里goroutine必须创建在给信道传值之前，否则在`c &amp;lt;- 1`就阻塞了，根本执行不到下面的代码  go func() {   t := &amp;lt;-c   fmt.Println(t)  }()   c &amp;lt;- 1  fmt.Println(&amp;quot;test&amp;quot;) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;1 test &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;range 和 close&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;发送者可通过 &lt;code&gt;close&lt;/code&gt; 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭：若没有值可以接收且信道已被关闭，那么在执行完之后 &lt;code&gt;ok&lt;/code&gt; 会被设置为 &lt;code&gt;false&lt;/code&gt; 。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;v, ok := &amp;lt;-ch &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;循环 &lt;code&gt;for i := range c&lt;/code&gt; 会不断从信道接收值，直到它被关闭。 &lt;strong&gt;注意：&lt;/strong&gt; 只有发送者才能关闭信道，而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌（panic）。 &lt;strong&gt;还要注意：&lt;/strong&gt; 信道与文件不同，通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭，例如终止一个 range 循环。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func fibonacci(n int,c chan int){  x,y := 0,1  for i := 0; i &amp;lt; n; i++{   c &amp;lt;-x   x,y = y,x+y  }  close(c) }  func main(){  c := make(chan int,10)  go fibonacci(cap(c),c)   for i := range c{   fmt.Println(i)  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果&lt;/p&gt; &lt;pre&gt;&lt;code&gt;0 1 1 2 3 5 8 13 21 34 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;select 语句&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;select&lt;/code&gt; 语句使一个 &lt;code&gt;Go&lt;/code&gt; 程可以等待多个通信操作。&lt;/li&gt; &lt;li&gt;&lt;code&gt;select&lt;/code&gt; 会阻塞到某个分支可以继续执行为止，这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。&lt;/li&gt; &lt;li&gt;注意到 &lt;code&gt;select&lt;/code&gt; 的代码形式和 &lt;code&gt;switch&lt;/code&gt; 非常相似， 不过 &lt;code&gt;select&lt;/code&gt; 的 &lt;code&gt;case&lt;/code&gt; 里的操作语句只能是【&lt;code&gt;IO&lt;/code&gt; 操作】 。此示例里面 &lt;code&gt;select&lt;/code&gt; 会一直等待等到某个 &lt;code&gt;case&lt;/code&gt; 语句完成， 也就是等到成功从 &lt;code&gt;ch1&lt;/code&gt; 或者 &lt;code&gt;ch2&lt;/code&gt; 中读到数据。 则 &lt;code&gt;select&lt;/code&gt; 语句结束。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func fibonacci (c, quit chan int){  x, y := 0, 1  for{   select{   case c &amp;lt;- x:    x, y = y, x+y   case &amp;lt;-quit:    fmt.Println(&amp;quot;quit&amp;quot;)    return   }  } }  func main() {  c := make(chan int)  quit := make(chan int)   //goroutine1  go func() {   for i := 0; i &amp;lt; 10; i++ {    fmt.Println(&amp;lt;-c)   }   quit &amp;lt;- 0  }()  //goroutine2  fibonacci(c, quit) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;执行结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;0 1 1 2 3 5 8 13 21 34 quit &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;分析1：这里创建的两个信道buffer size都是0，意味着如果没有新的goroutine来接收值，只发送的话当前goroutine都会被阻塞&lt;/li&gt; &lt;li&gt;分析2：&lt;code&gt;fmt.Println(&amp;lt;-c)&lt;/code&gt;中刚开始信道没有值，因此该&lt;code&gt;goroutine&lt;/code&gt;(&lt;code&gt;goroutine1&lt;/code&gt;)会一直被阻塞&lt;/li&gt; &lt;li&gt;分析3：执行到&lt;code&gt;fibonacci(c, quit)&lt;/code&gt;后，&lt;code&gt;for&lt;/code&gt;循环中一直给信道&lt;code&gt;c&lt;/code&gt;中传值，此时&lt;code&gt;goroutine1&lt;/code&gt;被激活，取完值后再次进入阻塞状态，如此循环&lt;/li&gt; &lt;li&gt;分析4：当&lt;code&gt;goroutine1&lt;/code&gt;中10此for循环执行完毕，信道c一直处于阻塞状态，此时执行&lt;code&gt;quit &amp;lt;- 0&lt;/code&gt;此时信道&lt;code&gt;quit&lt;/code&gt;被激活，执行&lt;code&gt;fibonacci&lt;/code&gt;中的&lt;code&gt;case &amp;lt;-quit&lt;/code&gt;，最终程序退出。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;默认选择&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;当 &lt;code&gt;select&lt;/code&gt; 中的其它分支都没有准备好时，&lt;code&gt;default&lt;/code&gt; 分支就会执行。&lt;/li&gt; &lt;li&gt;为了在尝试发送或者接收时不发生阻塞，可使用 default 分支：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;select { case i := &amp;lt;-c:     // 使用 i default:     // 从 c 中接收会阻塞时执行 } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;time&amp;quot; )  func main() {  tick := time.Tick(100 * time.Millisecond)  boom := time.After(500 * time.Millisecond)   for {   select {   case &amp;lt;-tick:    fmt.Println(&amp;quot;tick.&amp;quot;)   case &amp;lt;-boom:    fmt.Println(&amp;quot;BOOM!&amp;quot;)    return   default:    fmt.Println(&amp;quot; .&amp;quot;)    time.Sleep(50 * time.Millisecond)   }  } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;结果：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt; .  . tick.  .  . tick.  .  . tick.  .  . tick.  .  . BOOM! &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;分析： &lt;code&gt;time.Tick&lt;/code&gt;每隔指定的时间往信道输入值，&lt;code&gt;time.After&lt;/code&gt;指定时间后往把当前时间发送到信道&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;练习：等价二叉查找树&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;不同二叉树的叶节点上可以保存相同的值序列。例如，以下两个二叉树都保存了序列 &lt;code&gt;1，1，2，3，5，8，13&lt;/code&gt; 。&lt;/li&gt; &lt;li&gt;在大多数语言中，检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。&lt;/li&gt; &lt;li&gt;本例使用了 tree 包，它定义了类型：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;type Tree struct {     Left  *Tree     Value int     Right *Tree } &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;实现 &lt;code&gt;Walk&lt;/code&gt; 函数。&lt;/li&gt; &lt;li&gt;测试 &lt;code&gt;Walk&lt;/code&gt; 函数。 函数 &lt;code&gt;tree.New(k)&lt;/code&gt; 用于构造一个随机结构的已排序二叉查找树，它保存了值 &lt;code&gt;k 、 2k 、 3k ... 10k&lt;/code&gt; 。 创建一个新的信道 &lt;code&gt;ch&lt;/code&gt; 并且对其进行步进：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;go Walk(tree.New(1), ch) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后从信道中读取并打印 &lt;code&gt;10&lt;/code&gt; 个值。应当是数字 &lt;code&gt;1, 2, 3, ..., 10&lt;/code&gt; 。 3. 用 &lt;code&gt;Walk&lt;/code&gt; 实现 &lt;code&gt;Same&lt;/code&gt; 函数来检测 &lt;code&gt;t1&lt;/code&gt; 和 &lt;code&gt;t2&lt;/code&gt; 是否存储了相同的值。 4. 测试 &lt;code&gt;Same&lt;/code&gt; 函数。 S&lt;code&gt;ame(tree.New(1), tree.New(1)&lt;/code&gt;) 应当返回 &lt;code&gt;true&lt;/code&gt; ，而 &lt;code&gt;Same(tree.New(1)&lt;/code&gt;, &lt;code&gt;tree.New(2))&lt;/code&gt; 应当返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt; &lt;p&gt;代码&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;golang.org/x/tour/tree&amp;quot;  &amp;quot;fmt&amp;quot; ) /**  二叉树小知识  先序遍历：遍历顺序规则为【根左右】  中序遍历：遍历顺序规则为【左根右】  后序遍历：遍历顺序规则为【左右根】  */  // Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。 // 此处使用中序遍历 func Walk(t *tree.Tree, ch chan int) {  if t.Left != nil{   Walk(t.Left, ch)  }  ch &amp;lt;- t.Value  if t.Right != nil{   Walk(t.Right, ch)  } }  // Same 检测树 t1 和 t2 是否含有相同的值。 func Same(t1, t2 *tree.Tree) bool {  ch1 := make(chan int)  ch2 := make(chan int)  go Walk(t1,ch1)  go Walk(t2,ch2)   for i := 0; i &amp;lt; 10; i++ {   if &amp;lt;-ch1 != &amp;lt;-ch2{    return false   }  }  return true }  func main() {  ch := make(chan int)  go Walk(tree.New(1), ch)  for i := 0; i &amp;lt; 10; i++ {   fmt.Println(&amp;lt;-ch)  }   fmt.Println(Same(tree.New(1),tree.New(1)))  fmt.Println(Same(tree.New(1),tree.New(2))) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;可优化的地方，对于&lt;code&gt;Walk&lt;/code&gt;函数，上面的实现没有 &lt;code&gt;close channle&lt;/code&gt; 因此不能使用&lt;code&gt;range&lt;/code&gt;遍历&lt;code&gt;channel&lt;/code&gt;中的元素改进的方法如下&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;func Walk(t *tree.Tree, ch chan int) {  ReWalk(t,ch)  close(ch) }  func ReWalk(t *tree.Tree, ch chan int) {  if t.Left != nil{   Walk(t.Left, ch)  }  ch &amp;lt;- t.Value  if t.Right != nil{   Walk(t.Right, ch)  } }  func main() {  ch := make(chan int)  go Walk(tree.New(1), ch)  for i := range ch{   fmt.Println(i)  }   fmt.Println(Same(tree.New(1),tree.New(1)))  fmt.Println(Same(tree.New(1),tree.New(2))) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;sync.Mutex&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;我们已经看到信道非常适合在各个 &lt;code&gt;Go&lt;/code&gt; 程间进行通信。&lt;/li&gt; &lt;li&gt;但是如果我们并不需要通信呢？比如说，若我们只是想保证每次只有一个 &lt;code&gt;Go&lt;/code&gt; 程能够访问一个共享的变量，从而避免冲突？&lt;/li&gt; &lt;li&gt;这里涉及的概念叫做 互斥（&lt;code&gt;mutual_exclusion&lt;/code&gt;）_ ，我们通常使用 互斥锁（&lt;code&gt;Mutex&lt;/code&gt;）_ 这一数据结构来提供这种机制。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Go&lt;/code&gt; 标准库中提供了 &lt;code&gt;sync.Mutex&lt;/code&gt; 互斥锁类型及其两个方法：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;Lock Unlock &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;我们可以通过在代码前调用 Lock 方法，在代码后调用 Unlock 方法来保证一段代码的互斥执行。 参见 &lt;code&gt;Inc&lt;/code&gt; 方法。&lt;/li&gt; &lt;li&gt;我们也可以用 &lt;code&gt;defer&lt;/code&gt; 语句来保证互斥锁一定会被解锁。参见 &lt;code&gt;Value&lt;/code&gt; 方法。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;sync&amp;quot;  &amp;quot;time&amp;quot; )  // SafeCounter 的并发使用是安全的。 type SafeCounter struct {  v map[string] int  mux sync.Mutex }  // Inc 增加给定 key 的计数器的值。 func (c * SafeCounter)Inc(key string){  c.mux.Lock()  c.v[key]++  c.mux.Unlock() }  // Value 返回给定 key 的计数器的当前值。 func (c *SafeCounter)Value(key string)int  {  c.mux.Lock()  defer c.mux.Unlock()  return c.v[key] }  func main() {  c := SafeCounter{v:make(map[string] int)}  for i := 0;i &amp;lt; 100;i++{   go c.Inc(&amp;quot;somekey&amp;quot;)  }  time.Sleep(time.Second)  fmt.Println(c.Value(&amp;quot;somekey&amp;quot;)) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;sync.WaitGroup&lt;/h3&gt; &lt;p&gt;我们往往会遇到这样的场景，有多个任务（goroutine）在执行，需要等待最后一个任务执行结束，程序才能推出，针对这种场景我们可以使用 &lt;code&gt;sync.WaitGroup&lt;/code&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;不等待直接退出（有问题程序）&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;func say(s string) {  time.Sleep(1000 * time.Millisecond)  fmt.Println(s) }  func TestScanTheHost(t *testing.T) {  go say(&amp;quot;world&amp;quot;)  fmt.Println(&amp;quot;test&amp;quot;) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;say&lt;/code&gt; 方法执行时间很长需要一秒左右，主 &lt;code&gt;go&lt;/code&gt; 程中没有等待 &lt;code&gt;say&lt;/code&gt;就直接执行退出了，导致 &lt;code&gt;go say(&amp;quot;world&amp;quot;)&lt;/code&gt; 得不到执行&lt;/p&gt; &lt;ul&gt; &lt;li&gt;等待执行&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;func say(s string,wg sync.WaitGroup) {  defer wg.Done()  time.Sleep(1000 * time.Millisecond)  fmt.Println(s) }  func TestScanTheHost(t *testing.T) {  var wg sync.WaitGroup  wg.Add(1)  go say(&amp;quot;world&amp;quot;,wg)  fmt.Println(&amp;quot;test&amp;quot;)  wg.Wait() } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一开始这么写的，发现一直在等待，调查后发现这里有个坑，原来是 &lt;code&gt;golang&lt;/code&gt; 里如果方法传递的不是地址，那么就会做一个拷贝，所以这里调用的 &lt;code&gt;wg&lt;/code&gt; 根本就不是一个对象。简单更改如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;//注意这里是 wg 的引用 func say(s string,wg *sync.WaitGroup) {  defer wg.Done()  time.Sleep(1000 * time.Millisecond)  fmt.Println(s) }  func TestScanTheHost(t *testing.T) {  var wg sync.WaitGroup  wg.Add(1)  go say(&amp;quot;world&amp;quot;,&amp;amp;wg)  fmt.Println(&amp;quot;test&amp;quot;)  wg.Wait() } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;输出结果正常&lt;/p&gt; &lt;pre&gt;&lt;code&gt;test world &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Sat, 13 Jul 2019 07:18:00 GMT</pubDate>
    </item>
    <item>
      <title>《再不开窍就完了》读书笔记</title>
      <link>https://www.zhangaoo.com/article/open-mind</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/201979124732-kaiqiao.jpg" alt="201979124732-kaiqiao" /&gt;&lt;/p&gt; &lt;h2&gt;前言&lt;/h2&gt; &lt;p&gt;一些人，表面上看起来也许很傻，实际上却是为了不让你识破他的心计;还有一些人，是乐意自己吃亏 的傻子，但却占到了常人都没有得到的利益。&lt;/p&gt; &lt;p&gt;我可以装傻，但别以为我真的傻。很多话我没说，别以为我不懂;很多事我没做，别以为我不会。&lt;/p&gt; &lt;p&gt;总之，在社会上混，要有口才，要积人脉，要懂人情，要知低调，要装傻，还要会作秀，有防人之心。 一个混字，可谓一文难尽。&lt;/p&gt; &lt;h2&gt;保守秘密&lt;/h2&gt; &lt;p&gt;世事就是这般奇妙，没有秘密可言的人是悲哀的，保守不住秘密的人，不仅无信，而且悲哀。&lt;/p&gt; &lt;p&gt;英国有句有关秘密的俚语:“三人知，天下知。”&lt;/p&gt; &lt;h2&gt;意气用事&lt;/h2&gt; &lt;p&gt;无论是身处职场，还是过日子，找工作，都须谨记:“静能生慧，忍而超群”这八字原则。意气用事看起来很美、很酷，暗地里却毁掉了你良久的努力。&lt;/p&gt; &lt;p&gt;诗人高适曾写道:“生事应须南亩田，世事尽付东流水。”而如今，人们已经没了古人的深刻，大约成了“一朝意气冲天门，数年辛苦付诸东流。”&lt;/p&gt; &lt;p&gt;直率的人，喜欢以自己的角度和观念去评价别人的行为和言辞，然而不经修饰地，以最原始的论调说出来，自以为是在点明别人，却不知道在得罪别人。&lt;/p&gt; &lt;h2&gt;迁就和原谅&lt;/h2&gt; &lt;p&gt;世界不会迁就你，因为世界根本不在乎你。哪怕是比尔•盖茨、史蒂夫•乔布斯，地球也不会为他们停转 一秒。人要看重自己，但是在这个社会中，请记住，你不过和其他人一样像一只弱小的蚂蚁，不足言道。&lt;/p&gt; &lt;p&gt;以前从书本上学到的知识，都是死知识，一旦踏入社会之后，就必须努力去学习“社会”这本“书”，这本“书”才是现实，才真正告诉了我们何为光明与黑暗，让我们分清什么才是值得付出和相交地。&lt;/p&gt; &lt;h2&gt;笑脸相迎&lt;/h2&gt; &lt;p&gt;所以千万不要因为第一印象或者直觉的好恶，就去决定是否要善面相待。混社会是一个交际的过程，你需要尝试着和各式各样的人做朋友，包括你不喜欢的人，甚至还要尝试和敌人和解、拥抱。这是气度、胸襟，也是混社会的一条法则——没有永远的朋友，只有永远的利益。&lt;/p&gt; &lt;p&gt;混社会就得学会弯腰低头，笑脸迎人。就算是处于弱势的人，也有翻身的可能。现在你够傲、够拽，过几年，也许就该够衰、够霉了。人多有一个特性，恩情记不得多，怨恨永远不忘。形势逼人，你不喜欢的人往往是你最需要交往的人，与其痛苦的应付，不如放开杂念，笑脸相迎，谦和些去对待。&lt;/p&gt; &lt;p&gt;你对人十分好，起码得要求别人对你八分好，不求百分百地收益，但不能毫无底线地付出。&lt;/p&gt; &lt;p&gt;与其自己经常失望，还不如改变一下心态，凡事看淡点，也不要把对方太当一回事，不要掏心掏肺。这样你的期望就小一些，失望也会少一些，或许交的朋友会更多一些、更长久一些。&lt;/p&gt; &lt;h2&gt;不显不露&lt;/h2&gt; &lt;p&gt;善于做生意的商人，总是隐藏其最珍贵的货品，不会让人轻易见到;而品德高尚的君子，从外表看上去显得愚笨。这无疑蕴涵着一种处世智慧，锋芒毕露毫无益处可言，“满招损，谦受益”，自我炫耀者只会招致他人的反感，乃至小人的陷害。&lt;/p&gt; &lt;p&gt;混社会须要懂得“贵而不显，华而不炫”的道理。&lt;/p&gt; &lt;p&gt;言辞谨慎，不露锋芒是一种难得的修养。而通常只有一个浅薄的人才会信口开河。&lt;/p&gt; &lt;h2&gt;战而不屈&lt;/h2&gt; &lt;p&gt;今天天色不早，我愿用一句话纪念先生:许多人是不战而屈，鲁迅先生是战而不屈。&lt;/p&gt; &lt;p&gt;你永远不能断定，下一秒你是否会颠覆自我，成为前一秒否定的那个人。&lt;/p&gt; &lt;h2&gt;为别人着想&lt;/h2&gt; &lt;p&gt;假如有什么成功的秘密的话，就是设身处地替别人着想，了解别人的态度和观点。&lt;/p&gt; &lt;p&gt;一出口就是“我怎么样”“我认为”，以自我为中心。就会在别人心中树立起相当强势的感觉，让别人产生你很不好说话的印象。&lt;/p&gt; &lt;p&gt;年轻人不要用“我”，而用“我们”代替，这样就会让对方明白自己也是参与其中的。不仅能使对方觉得和你的距离接近，而且听起来更加舒服亲切。&lt;/p&gt; &lt;h2&gt;精明人&lt;/h2&gt; &lt;p&gt;“精明人”懂得韬光养晦，大智若愚;“精明人”善于示弱博得同情，巧妙地隐藏自己的实力;“精明 人”知道得意不要忘形，喜怒不形于色;“精明人”大多抓小放大，小事装糊涂，大事才注重。年轻人 要做一个智慧深藏的“精明人”，而不是一个聪明外露的“糊涂蛋”。&lt;/p&gt; &lt;p&gt;做人宁可显得笨拙一些，也不可显得太聪明;宁可收敛一下，也不可锋芒毕露;宁可随和一点，也不可自命清高;宁可退缩一点，也不可太积极前进。&lt;/p&gt; &lt;p&gt;比尔的母亲正是 &lt;code&gt;IBM&lt;/code&gt; 董事会的董事，可以说比尔•盖茨创业之初最大的成果来自母亲的血缘人脉，而并非自己的天才学识。&lt;/p&gt; &lt;h2&gt;雪中送炭&lt;/h2&gt; &lt;p&gt;锦上添花”怎敌“雪中送炭”，你在这一刻给予的恩情，是落难人一辈子都不会忘记的。佛家云:“救人一命胜造七级浮屠。”同样，“救人于危难胜过千言万语”。&lt;/p&gt; &lt;p&gt;朋友就像庙里的“菩萨”，冷庙中才能感受到世态炎凉，才能感受到你的重情重义。而在热庙中就选择众多了，注意力可不会落在你的身上。&lt;/p&gt; &lt;h2&gt;贵人&lt;/h2&gt; &lt;p&gt;不怠慢每一位客户，他们就会成为你的“贵人”&lt;/p&gt; &lt;p&gt;想减肥就别和胖子在一起，想赚钱就跟富翁在一起，要成功就和成功的人在一起。&lt;/p&gt; &lt;p&gt;著名记者阿迪斯曾经说过:“世界上没有陌生人，只有还未认识的朋友。”&lt;/p&gt; &lt;h2&gt;和陌生人说话&lt;/h2&gt; &lt;p&gt;第一，让对方记住你的名字。&lt;/p&gt; &lt;p&gt;第二，始终微笑。&lt;/p&gt; &lt;p&gt;第三，寓庒于谐。&lt;/p&gt; &lt;h2&gt;人脉关系网&lt;/h2&gt; &lt;p&gt;人脉可以帮你缩短 85% 的奋斗历程，剩下的15%你还不能解决吗?年轻人在这个竞争愈加激烈的社会 中，想要有所成，就得建立起自己的关系网，拥有诸多“贵人”的关系网。&lt;/p&gt; &lt;p&gt;俗话说:“压对好牌赢一局，交对朋友赢一生。”成功的捷径就是朋友的提携，上马的时候有人扶，摔倒了有人搀，落水时有人向你抛救生圈。遇见了对的人，就像遇见了终生“保姆”一样，万事不用独自面对，始终有个人站在你背后默默相助。&lt;/p&gt; &lt;p&gt;有一个著名的公式:朋友多多=机会多多=成功多多&lt;/p&gt; &lt;h2&gt;职场&lt;/h2&gt; &lt;p&gt;俗话说:“种瓜得瓜，种豆得豆。”职场却往往“种瓜得豆，种豆没有”，这并不是危言耸听。因此学着让你的老板知道你所做的功劳，才能付出很多，回报不少。&lt;/p&gt; &lt;p&gt;人生如棋，一直做“小卒子”，难免会被人“丢卒保车”，遭到抛弃。&lt;/p&gt; &lt;p&gt;职场有一个标准:“缺人才但不缺人。”你是“人”还是“人才”，你是不可取代还是被取而代之，就看你能否能凸显自己的重要性。&lt;/p&gt; &lt;p&gt;俗话说:“好话不可说尽，力气不可用尽，才华不可露尽。&lt;/p&gt; &lt;p&gt;埋头苦干已经不是一位好下属的标准了，现代职场需要的是“抬头”苦干的下属。&lt;/p&gt; &lt;p&gt;勤恳是一种良好品行，但是在职场中切不可过于发扬光大，什么“垃圾活”都干，长此以往，最后别人都会把“垃圾活”推给你，谁叫你干得好呢？&lt;/p&gt; &lt;p&gt;《高效能人士的气死个习惯》中把时间分4个象限:“第一是重要紧急的，第二是重要不紧急的，第三是紧急不重要的，第四是不紧急不重要的。”&lt;/p&gt; &lt;p&gt;一个优秀的员工应该保持以下几个习惯:一，上司所发送的电子邮件，优先查看，立即回复;二，上司的事情比客户和同事的事情更重要、更紧急;三，习惯性地把上司分派的工作，安排在靠前的日程上处理;四，上司吩咐的工作，不能等到被催促，才去处理。&lt;/p&gt; &lt;p&gt;员工最欠缺的就是老板的高度，想要得到老板的提拔，第一就是了解老板的想法，清楚企业未来发展方向;第二就是掌握市场趋势的变化；第三是善用企业内部的资源和人脉。&lt;/p&gt; &lt;h2&gt;人际交往&lt;/h2&gt; &lt;p&gt;人际交往在本质上是一个社会交换的过程，相互给予彼此所需要的。&lt;/p&gt; &lt;p&gt;若只顾自扫门前雪，不管别人瓦上霜，把帮助别人看作是麻烦事，是不会赢得朋友的真心的，这无异于堵死了人脉圈给自己带来的一切有利途径。&lt;/p&gt; &lt;p&gt;藏锋露拙者更能够得到大家的喜爱。晚清重臣曾国藩说过:“言多招祸，行多有辱;傲者人之殃，慕者退邪兵;为君藏锋，可以及远;为臣藏锋，可以及大;讷于言，慎于行，乃吉凶安危之关，成败存亡之键也!”&lt;/p&gt; &lt;p&gt;不懂装懂装懂的人很多，而假装不懂的人却少。前者实属愚昧，后者才称得上是睿智。&lt;/p&gt; &lt;p&gt;把无谓的胜利让给对方，懂得认输的人很懂说话。&lt;/p&gt; &lt;p&gt;假意认输不会损失什么，反而会得到更多，得到人们的欢心，得到感情的增进，得到气氛的融洽。它是一种混社会的策略，以退为进的明智之举。使你摆脱不健康的心理羁绊，适时地调整好心态，以利于重整旗鼓。假意认输不是认怂，而是莫大的勇气在支撑着你，它也将支撑你走得更远、更坚定。&lt;/p&gt; &lt;h2&gt;居安思危&lt;/h2&gt; &lt;p&gt;世间没有肯定，只有可能，不要对任何事情下定论，相信但也要有所保留。居安而思危，局势再怎么好，也要给自己留下后路。&lt;/p&gt; &lt;p&gt;《左传》上有句话说得好:“居安思危，思则有备，有备无患”&lt;/p&gt; &lt;p&gt;生活就像温水煮青蛙，不断消磨着你的意志，别被甜腻的日子消耗掉一身的本事，待到滚水临身已无再战之力。&lt;/p&gt; &lt;p&gt;见人只说三分话，不可全抛一片心。&lt;/p&gt; &lt;p&gt;每个人或许都有些趋炎附势的心态，但是这并不代表你就可以恶劣地对待那些小人物。给予他们同样的尊重，给予他们力所能及的帮助，他们就是你未来的福星、救星、智星。&lt;/p&gt; &lt;p&gt;人的一切不过是时间赐予的，而这一切都是有时段性的，它并非永恒不变。强大的人只能强大一时，弱小的人也不会弱小一世。给予比你弱小的人一些帮助，得到的是感激，得到的是铭记的恩情，这是帮助一个比你强大的人无法得到的。&lt;/p&gt; &lt;p&gt;不要总把目光放在那些所谓的贵人身上，其实有价值的人际关系中，普通人、小人物占的比例并不小。&lt;/p&gt; &lt;p&gt;“人在屋檐下，一定要低头”，而不是“不得不低头”。“一定要低头”是带有预先性的，不用别人来提 醒，也不会等撞到屋檐了才低头。这是一种对客观环境的理性认知，是审时度势后一种明智的选择。&lt;/p&gt; &lt;p&gt;盛名之下难久居”，所以明智地选择了功成身退。于是，遣人致书好友文种，谓:“飞鸟尽，良弓藏;狡兔死，走狗烹。越王为人长颈鸟喙，可与共患难，不可与共乐，子何不去？”&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 12 Jul 2019 12:13:28 GMT</pubDate>
    </item>
    <item>
      <title>Maven 打包自定义 Property</title>
      <link>https://www.zhangaoo.com/article/maven-property</link>
      <content:encoded>&lt;h1&gt;Maven 打包自定义 Property&lt;/h1&gt; &lt;h2&gt;背景&lt;/h2&gt; &lt;p&gt;继上次的需求，&lt;code&gt;JWT signingKey&lt;/code&gt; 需要每次动态生成指定长度的随机数，使用了&lt;code&gt;Spring Boot&lt;/code&gt; 自带的 &lt;code&gt;${random.int}&lt;/code&gt;，发现不能满足要求，即便在 &lt;code&gt;Spring Config Server&lt;/code&gt; 统一配置 &lt;code&gt;${random.int}&lt;/code&gt; 但是其他模块也只是对 &lt;code&gt;${random.int}&lt;/code&gt; 的引用，然后在各自模块调用各自的 &lt;code&gt;${random.int}&lt;/code&gt; 表达式生成随机数。 这样各个模块生成不一致的随机数，不满足业务需求。想了想退而求其次，我在 &lt;code&gt;maven&lt;/code&gt; 打包的时候生成指定长度的随机字符串，应该能满足我的要求。&lt;/p&gt; &lt;h2&gt;调查编码&lt;/h2&gt; &lt;p&gt;Google 了半天，的确有相关的实现方案：第一种方案是是自己自定义一个 &lt;code&gt;Maven&lt;/code&gt; 插件，在运行插件的目标阶段插入一个 Property 供其他地方使用，相关&lt;a href="https://stackoverflow.com/questions/3984794/generating-uuid-through-maven" target="_blank"&gt;参考资料&lt;/a&gt;;第二种方案，直接找一个现成的插件，&lt;a href="https://stackoverflow.com/questions/3984794/generating-uuid-through-maven" target="_blank"&gt;参考资料&lt;/a&gt; 这里就就直接采用第二种方案，来的比较快，主要配置：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;plugins&amp;gt;     &amp;lt;plugin&amp;gt;         &amp;lt;artifactId&amp;gt;maven-resources-plugin&amp;lt;/artifactId&amp;gt;         &amp;lt;executions&amp;gt;             &amp;lt;execution&amp;gt; &amp;lt;!-- 复制配置文件 --&amp;gt;                 &amp;lt;id&amp;gt;copy-resources&amp;lt;/id&amp;gt;                 &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;                 &amp;lt;goals&amp;gt;                     &amp;lt;goal&amp;gt;copy-resources&amp;lt;/goal&amp;gt;                 &amp;lt;/goals&amp;gt;                 &amp;lt;configuration&amp;gt;                     &amp;lt;outputDirectory&amp;gt;target/classes&amp;lt;/outputDirectory&amp;gt;                     &amp;lt;useDefaultDelimiters&amp;gt;false&amp;lt;/useDefaultDelimiters&amp;gt;                     &amp;lt;!--设置分隔符--&amp;gt;                     &amp;lt;delimiters&amp;gt;                         &amp;lt;delimiter&amp;gt;@&amp;lt;/delimiter&amp;gt;                     &amp;lt;/delimiters&amp;gt;                     &amp;lt;resources&amp;gt;                         &amp;lt;resource&amp;gt;                             &amp;lt;directory&amp;gt;src/main/resources/&amp;lt;/directory&amp;gt;                             &amp;lt;filtering&amp;gt;true&amp;lt;/filtering&amp;gt;                             &amp;lt;!--指定扫描替换哪些文件，当然也可以用exclude来排除文件--&amp;gt;                             &amp;lt;includes&amp;gt;                                 &amp;lt;include&amp;gt;**/*.yml&amp;lt;/include&amp;gt;                                 &amp;lt;include&amp;gt;**/*.properties&amp;lt;/include&amp;gt;                             &amp;lt;/includes&amp;gt;                         &amp;lt;/resource&amp;gt;                     &amp;lt;/resources&amp;gt;                     &amp;lt;outputDirectory&amp;gt;${project.build.directory}&amp;lt;/outputDirectory&amp;gt;                 &amp;lt;/configuration&amp;gt;             &amp;lt;/execution&amp;gt;         &amp;lt;/executions&amp;gt;     &amp;lt;/plugin&amp;gt; &amp;lt;plugin&amp;gt;     &amp;lt;groupId&amp;gt;org.codehaus.gmaven&amp;lt;/groupId&amp;gt;     &amp;lt;artifactId&amp;gt;gmaven-plugin&amp;lt;/artifactId&amp;gt;     &amp;lt;version&amp;gt;1.3&amp;lt;/version&amp;gt;     &amp;lt;executions&amp;gt;         &amp;lt;execution&amp;gt;             &amp;lt;id&amp;gt;set-signKey&amp;lt;/id&amp;gt;             &amp;lt;phase&amp;gt;initialize&amp;lt;/phase&amp;gt;             &amp;lt;goals&amp;gt;                 &amp;lt;goal&amp;gt;execute&amp;lt;/goal&amp;gt;             &amp;lt;/goals&amp;gt;             &amp;lt;configuration&amp;gt;                 &amp;lt;classpath&amp;gt;                 &amp;lt;!--引用依赖--&amp;gt;                     &amp;lt;element&amp;gt;                         &amp;lt;groupId&amp;gt;commons-lang&amp;lt;/groupId&amp;gt;                         &amp;lt;artifactId&amp;gt;commons-lang&amp;lt;/artifactId&amp;gt;                         &amp;lt;version&amp;gt;2.6&amp;lt;/version&amp;gt;                     &amp;lt;/element&amp;gt;                 &amp;lt;/classpath&amp;gt;                 &amp;lt;source&amp;gt;                 &amp;lt;!--重点代码，生成名叫signingKey的property，value为128位的随机字符串--&amp;gt;                     import org.apache.commons.lang.RandomStringUtils                     project.properties.setProperty('signingKey', RandomStringUtils.random(128,true,true))                 &amp;lt;/source&amp;gt;             &amp;lt;/configuration&amp;gt;         &amp;lt;/execution&amp;gt;     &amp;lt;/executions&amp;gt; &amp;lt;/plugin&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 &lt;code&gt;application.yml&lt;/code&gt; 中引用&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;# *.yml配置文件这里要加上双引号或单引号否则 Spring Boot 解析 yml 时会报错 # *.properties 可不加双引号或单引号 signingKey: &amp;quot;@signingKey@&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;运行效果&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;signingKey&amp;quot;: &amp;quot;gvx8NET6pLauCLglerpLexLKtHc8HzNisLAe8g9nZCsoNuOlIpkkAKBIsKK62s1nLg18kPm61msJ8QihENvwE3NWCV3xGQamdUJNVX7RVMkPhJEPwFzEgIC2Z0qN56YH&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看编译打包后的配置文件 &lt;code&gt;@signingKey@&lt;/code&gt; 已经被替换成了 &lt;code&gt;128&lt;/code&gt; 位的随机字符串&lt;/p&gt; &lt;p&gt;&lt;a href="https://github.com/zealzhangz/custom-maven-proverty" target="_blank"&gt;Github 源码&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 05 Jul 2019 12:15:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Boot 自定义自己的 ${random.int} 表达式</title>
      <link>https://www.zhangaoo.com/article/spring-boot-random-int</link>
      <content:encoded>&lt;h1&gt;Spring Boot 定义自己的 ${random.int} 表达式&lt;/h1&gt; &lt;h2&gt;背景&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;Springboot&lt;/code&gt; 的配置中大家肯定见过以下代码吧，生成指定类型的随机值，感觉使用很简洁；于是萌生了想自己实现类似的功能。恰好手边有个类似的需求，需要产生指定长度的随机字符串。 虽然 &lt;code&gt;${random.uuid}&lt;/code&gt; 可以产生一个 &lt;code&gt;32&lt;/code&gt; 位的小写字母和数字的字符串，但是还想扩展含有大写字母且长度可自定义。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;value1: &amp;quot;${random.int}&amp;quot; value2: &amp;quot;${random.long(100,200)}&amp;quot; value3: &amp;quot;${random.uuid}&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;源码查看&lt;/h2&gt; &lt;p&gt;简单查看一下 &lt;code&gt;RandomValuePropertySource&lt;/code&gt; 源码是怎么实现的&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class RandomValuePropertySource extends PropertySource&amp;lt;Random&amp;gt; {   /**   * Name of the random {@link PropertySource}.   */  public static final String RANDOM_PROPERTY_SOURCE_NAME = &amp;quot;random&amp;quot;;     //定义前缀  private static final String PREFIX = &amp;quot;random.&amp;quot;;   private static final Log logger = LogFactory.getLog(RandomValuePropertySource.class);     //构造函数初始了一个 Random 对象  public RandomValuePropertySource(String name) {   super(name, new Random());  }     //默认无参的构造函数  public RandomValuePropertySource() {   this(RANDOM_PROPERTY_SOURCE_NAME);  }     //最核心的方法，以 random 为前缀配置为例，这里的 name 就是 random. 后面的值，比如int、int(10)  @Override  public Object getProperty(String name) {         //不是指定的前缀直接返回空   if (!name.startsWith(PREFIX)) {    return null;   }   if (logger.isTraceEnabled()) {    logger.trace(&amp;quot;Generating random property for '&amp;quot; + name + &amp;quot;'&amp;quot;);   }         //调用方法处理各种各种类型的随机值   return getRandomValue(name.substring(PREFIX.length()));  }   private Object getRandomValue(String type) {         //处理默认int随机值   if (type.equals(&amp;quot;int&amp;quot;)) {    return getSource().nextInt();   }         //处理默认long随机值   if (type.equals(&amp;quot;long&amp;quot;)) {    return getSource().nextLong();   }         //处理默认指定大小范围int随机值，比如int(10),int(10,100)   String range = getRange(type, &amp;quot;int&amp;quot;);   if (range != null) {    return getNextIntInRange(range);   }   range = getRange(type, &amp;quot;long&amp;quot;);   if (range != null) {    return getNextLongInRange(range);   }         //生成uuid   if (type.equals(&amp;quot;uuid&amp;quot;)) {    return UUID.randomUUID().toString();   }   return getRandomBytes();  }     //解析各种类型的范围  private String getRange(String type, String prefix) {   if (type.startsWith(prefix)) {    int startIndex = prefix.length() + 1;    if (type.length() &amp;gt; startIndex) {     return type.substring(startIndex, type.length() - 1);    }   }   return null;  }     //int指定范围随机值的具体处理方法  private int getNextIntInRange(String range) {   String[] tokens = StringUtils.commaDelimitedListToStringArray(range);   int start = Integer.parseInt(tokens[0]);   if (tokens.length == 1) {    return getSource().nextInt(start);   }   return start + getSource().nextInt(Integer.parseInt(tokens[1]) - start);  }     //long指定范围随机值的具体处理方法  private long getNextLongInRange(String range) {   String[] tokens = StringUtils.commaDelimitedListToStringArray(range);   if (tokens.length == 1) {    return Math.abs(getSource().nextLong() % Long.parseLong(tokens[0]));   }   long lowerBound = Long.parseLong(tokens[0]);   long upperBound = Long.parseLong(tokens[1]) - lowerBound;   return lowerBound + Math.abs(getSource().nextLong() % upperBound);  }   private Object getRandomBytes() {   byte[] bytes = new byte[32];   getSource().nextBytes(bytes);   return DigestUtils.md5DigestAsHex(bytes);  }     //添加到当前的环境，实际自定义的时候可能不会用到此方法  public static void addToEnvironment(ConfigurableEnvironment environment) {   environment.getPropertySources().addAfter(     StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,     new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));   logger.trace(&amp;quot;RandomValuePropertySource add to Environment&amp;quot;);  } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;自定义&lt;/h2&gt; &lt;p&gt;先看看我们自定义的使用方法：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;value1: ${randomKey.key} value2: ${randomKey.key(128)} &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上源码也比较简单，搞清楚大概功能后，我们照葫芦画瓢自定义我们如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class RandomKeyPropertySource extends PropertySource&amp;lt;Random&amp;gt; {     public static final String RANDOM_PROPERTY_SOURCE_NAME = &amp;quot;randomKey&amp;quot;;      private static final String PREFIX = &amp;quot;randomKey.&amp;quot;;      private static final Log logger = LogFactory.getLog(RandomKeyPropertySource.class);     //生成长度小于字符串的随机数字从而生成随机字符串     private static String CONSTANT_STRING = &amp;quot;0123456789abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&amp;quot;;      public RandomKeyPropertySource(String name) {         super(name, new Random());     }      public RandomKeyPropertySource(){         this(RANDOM_PROPERTY_SOURCE_NAME);     }      @Override     public Object getProperty(String name) {         if (!name.startsWith(PREFIX)) {             return null;         }         if (logger.isTraceEnabled()) {             logger.trace(&amp;quot;Generating random property for '&amp;quot; + name + &amp;quot;'&amp;quot;);         }         return getRandomValue(name.substring(PREFIX.length()));     }     private String getRandomValue(String type) {         //默认生成64位的随机字符串         if (type.equals(&amp;quot;key&amp;quot;)) {             return randomString(64);         }         //指定了字符串长度，生成指定长度字符串         String range = getRange(type, &amp;quot;key&amp;quot;);         if (range != null) {             return getNextKeyRange(range);         }         return null;     }     //拷贝源码方法     private String getRange(String type, String prefix) {         if (type.startsWith(prefix)) {             int startIndex = prefix.length() + 1;             if (type.length() &amp;gt; startIndex) {                 return type.substring(startIndex, type.length() - 1);             }         }         return null;     }     //做了简单修改，实际上只能范围只能指定一个固定的值，而不能是一个区间     private String getNextKeyRange(String range) {         String[] tokens = StringUtils.commaDelimitedListToStringArray(range);         int start = Integer.parseInt(tokens[0]);         if (tokens.length == 1) {             return randomString(start);         }         return null;     }     //实际生成随机字符串的方法     private String randomString(int length){         StringBuffer sb = new StringBuffer();         for(int i = 0; i &amp;lt; length; i++){             int number = getSource().nextInt(CONSTANT_STRING.length());             sb.append(CONSTANT_STRING.charAt(number));         }         return sb.toString();     }     //这个方法应该没用，也照葫芦画瓢保留了     public static void addToEnvironment(ConfigurableEnvironment environment) {         environment.getPropertySources().addAfter(                 StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,                 new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));         logger.trace(&amp;quot;RandomValuePropertySource add to Environment&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;配置加载&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration public class PropertySourceConfig {     @Autowired     private ConfigurableEnvironment env;      @PostConstruct     public void init() throws Exception {         env.getPropertySources().addFirst(new RandomKeyPropertySource());     } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;使用&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;application.yml&lt;/code&gt; 中直接使用就可以了&lt;/p&gt; &lt;pre&gt;&lt;code&gt;testKey: ${randomKey.key} &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Demo&lt;/h3&gt; &lt;p&gt;&lt;a href="https://github.com/zealzhangz/custom-properties-random-key" target="_blank"&gt;Demo Github&lt;/a&gt;&lt;/p&gt; &lt;h3&gt;实际运行结果&lt;/h3&gt; &lt;p&gt;在浏览器访问：&lt;code&gt;http://127.0.0.1:8080/test&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;value2&amp;quot;: &amp;quot;fFlgx90qS58F6ljaP5WY4p4F8hdq71396SJiY5WwC0103906PiiQbKBl13tt8an906T082Mrw5177cH04hyB80168leY9ScYa4E8Jo5j519xakXAjlmWTI2o9K49FBd9&amp;quot;,     &amp;quot;value1&amp;quot;: &amp;quot;DP7eg8i27633zvNxzFUvD3hzum5LDJJPY3p77vHV28II2Y1S090dSRbK2S57EcmF&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;注意事项&lt;/h1&gt; &lt;p&gt;我本次使用的场景是微服务的场景，本来是想在 &lt;code&gt;Spring Config Server&lt;/code&gt; 模块生成一个统一的 &lt;code&gt;JWT Token&lt;/code&gt; 签名的密钥，但是发现实际上在 &lt;code&gt;Spring Config Server&lt;/code&gt; 定义改功能后，各个子模块其实也是取不到值的，必须把这个功能分别配置在各个模块，各个模块实际使用的时候各自调用生成各自的随机值。&lt;/p&gt; &lt;p&gt;因此这也我的需求不符，无论如何也 &lt;code&gt;Get&lt;/code&gt; 到了一个新技能&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 04 Jul 2019 12:41:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Security 动手实现 JWT Token 验证 篇五</title>
      <link>https://www.zhangaoo.com/article/spring-security-zuul-jwt</link>
      <content:encoded>&lt;h1&gt;Spring Security 基于 JWT Token 认证&lt;/h1&gt; &lt;p&gt;在开始这篇文章之前，我们似乎应该思考下为什么需要搞清楚 &lt;code&gt;Spring Security&lt;/code&gt; 的内部工作原理？ 按照第二篇文章中的配置，一个简单的表单认证不就达成了吗？ 更有甚者，为什么我们不自己写一个表单认证，用过滤器即可完成，大费周章引入&lt;code&gt;Spring Security&lt;/code&gt;，看起来也并没有方便多少。 对的，在引入 &lt;code&gt;Spring Security&lt;/code&gt; 之前，我们得首先想到，是什么需求让我们引入了 &lt;code&gt;Spring Security&lt;/code&gt;，以及为什么是 &lt;code&gt;Spring Security&lt;/code&gt;，而不是 &lt;code&gt;shiro&lt;/code&gt; 等等其他安全框架。我的理解是有如下几点：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在前文的介绍中，&lt;code&gt;Spring Security&lt;/code&gt; 支持防止 &lt;code&gt;csrf&lt;/code&gt; 攻击，&lt;code&gt;session-fixation protection&lt;/code&gt;，支持表单认证，&lt;code&gt;basic&lt;/code&gt; 认证，&lt;code&gt;rememberMe&lt;/code&gt;…等等一些特性，有很多是开箱即用的功能，而大多特性都可以通过配置灵活的变更，这是它的强大之处。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Spring Security&lt;/code&gt; 的兄弟的项目 &lt;code&gt;Spring Security SSO，OAuth2&lt;/code&gt; 等支持了多种协议，而这些都是基于&lt;code&gt;Spring Security&lt;/code&gt; 的，方便了项目的扩展。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SpringBoot&lt;/code&gt; 的支持，更加保证了 &lt;code&gt;Spring Security&lt;/code&gt; 的开箱即用。&lt;/li&gt; &lt;li&gt;为什么需要理解其内部工作原理?一个有自我追求的程序员都不会满足于浅尝辄止，如果一个开源技术在我们的日常工作中十分常用，那么我偏向于阅读其源码，这样可以让我们即使排查不期而至的问题，也方便日后需求扩展。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Spring&lt;/code&gt; 及其子项目的官方文档是我见过的最良心的文档！相比较于 &lt;code&gt;Apache&lt;/code&gt; 的部分文档&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这一节，为了对之前分析的 &lt;code&gt;Spring Security&lt;/code&gt; 源码和组件有一个清晰的认识，我实现一个 &lt;code&gt;Restful JWT Token&lt;/code&gt; 的认证。&lt;/p&gt; &lt;h2&gt;定义需求&lt;/h2&gt; &lt;p&gt;在表单登录中，一般使用数据库中配置的用户表，权限表，角色表，权限组表…这取决于你的权限粒度，但本质都是借助了一个持久化存储，维护了用户的角色权限，而后给出一个 &lt;code&gt;/login&lt;/code&gt; 作为登录端点，&lt;code&gt;Restful&lt;/code&gt; 接口提交用户名和密码，验证用户名密码后返回一个 &lt;code&gt;accessToken&lt;/code&gt;，和&lt;code&gt;Refresh Token&lt;/code&gt;，在一定时间内可以使用 &lt;code&gt;Refresh Token&lt;/code&gt; 获取新的 &lt;code&gt;accessToken&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;在我们的登录 &lt;code&gt;demo&lt;/code&gt; 中，也是类似的，我们把用户的信息，用户名、密码、权限存储在内存中（&lt;code&gt;ConcurrentHashMap&lt;/code&gt;）来模拟数据库存储。&lt;/p&gt; &lt;h2&gt;设计概述&lt;/h2&gt; &lt;p&gt;整个认证流程如下图： &lt;img src="http://img.zhangaoo.com/2019622153437-spring-security-architecture.jpg" alt="2019622153437-spring-security-architecture" /&gt;&lt;/p&gt; &lt;p&gt;整个 &lt;code&gt;Spring Security&lt;/code&gt; 都是围绕以上这张架构图展开的，最顶层为最核心最抽象的 &lt;code&gt;AuthenticationManager&lt;/code&gt; 接口， &lt;code&gt;ProviderManager&lt;/code&gt; 为 &lt;code&gt;AuthenticationManager&lt;/code&gt; 的一个具体实现，功能如其名字他的作用是管理 &lt;code&gt;Provider&lt;/code&gt; 的， &lt;code&gt;AuthenticationProvider&lt;/code&gt; 才是真正认证的接口，因此我们在实践中要实现我们自己的认证方式，也就是 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 的一个具体实现。当然可以实现多个，如果有多种认证方式，现实中往往也是有多种认证方式。&lt;/p&gt; &lt;p&gt;在此 &lt;code&gt;Demo&lt;/code&gt; 中一共涉及两个 &lt;code&gt;Filter&lt;/code&gt; &lt;code&gt;，LoginProcessingFilter&lt;/code&gt; 处理登录请求获取&lt;code&gt;Access Token 和 Refresh Token&lt;/code&gt; 的过滤器。&lt;code&gt;ApiAuthenticationProcessingFilter&lt;/code&gt; 认证 Token 是否有效以及用户是否有资源访问权限的过滤器。&lt;/p&gt; &lt;p&gt;这两个 &lt;code&gt;Filter&lt;/code&gt; 的都分别对应自己的 &lt;code&gt;Token 、Provider&lt;/code&gt; 实现类，整个逻辑和上图如出一辙。&lt;/p&gt; &lt;p&gt;简单做一个类比：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;LoginProcessingFilter –&amp;gt; UsernamePasswordAuthenticationFilter JwtUsernamePasswordAuthenticationToken –&amp;gt; UsernamePasswordAuthenticationToken ProviderManager –&amp;gt; ProviderManager JwtAuthenticationProvider –&amp;gt; DaoAuthenticationProvider MemoryUserService –&amp;gt; UserDetailsService &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;JwtUsernamePasswordAuthenticationToken&lt;/h3&gt; &lt;p&gt;主要用来存储认证用户名、密码、&lt;code&gt;Refresh Token&lt;/code&gt;，这里扩展了 &lt;code&gt;details&lt;/code&gt; 字段来存储 &lt;code&gt;Refresh Token&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @author Created by https://zhangaoo.com.&amp;lt;br/&amp;gt;  * @version Version: 0.0.1  * @date DateTime: 2019/06/22 16:06:00&amp;lt;br/&amp;gt;  */ public class JwtUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {     /**      * 用来传递刷新 token      */     private Object details;      public JwtUsernamePasswordAuthenticationToken(Object principal, Object credentials,Object details) {         super(principal, credentials);         //for password refresh token         this.details = details;     }      public JwtUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {         super(principal, credentials, authorities);     }      @Override     public Object getDetails(){         return this.details;     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;LoginProcessingFilter&lt;/h3&gt; &lt;p&gt;此 &lt;code&gt;Filter&lt;/code&gt; 主要是对登录用户信息或 &lt;code&gt;Refresh Token&lt;/code&gt; 的简单 &lt;code&gt;check&lt;/code&gt; ，具体检查在对应的 &lt;code&gt;Provider&lt;/code&gt; 中&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @author Created by https://zhangaoo.com.&amp;lt;br/&amp;gt;  * @version Version: 0.0.1  * @date DateTime: 2019/06/22 16:02:00&amp;lt;br/&amp;gt;  */ public class LoginProcessingFilter extends AbstractAuthenticationProcessingFilter {     private final ObjectMapper objectMapper;     private final AuthenticationFailureHandler failureHandler;     private final AuthenticationSuccessHandler successHandler;      public  LoginProcessingFilter(String defaultFilterProcessesUrl,                                  ObjectMapper objectMapper,                                  AuthenticationSuccessHandler successHandler,                                  AuthenticationFailureHandler failureHandler) {         super(defaultFilterProcessesUrl);         this.objectMapper = objectMapper;         this.failureHandler = failureHandler;         this.successHandler = successHandler;     }      @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)             throws AuthenticationException, IOException, ServletException {         LoginRequest loginRequest;         try {             loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);         } catch (Exception e){             throw new BadCredentialsException(&amp;quot;Username or Password or refresh_token is invalid&amp;quot;);         }         //对用户名密码或刷新 token 做初步的认证         if(!checkUserInfo(loginRequest)){             throw new BadCredentialsException(&amp;quot;Username or Password or refresh_token is invalid&amp;quot;);         }         JwtUsernamePasswordAuthenticationToken token = new JwtUsernamePasswordAuthenticationToken(                 loginRequest.getUsername(),                 loginRequest.getPassword(),                 loginRequest.getRefreshToken());         return this.getAuthenticationManager().authenticate(token);     }      @Override     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)             throws IOException, ServletException {         successHandler.onAuthenticationSuccess(request,response,authResult);     }      @Override     protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)             throws IOException, ServletException {         SecurityContextHolder.clearContext();         failureHandler.onAuthenticationFailure(request, response, failed);     }      private boolean checkUserInfo(LoginRequest loginRequest){         //check refresh token first         if(StringUtils.isNotBlank(loginRequest.getRefreshToken())){             return true;         } else {             //check username or password             if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {                 return false;             } else {                 return true;             }         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;defaultFilterProcessesUrl&lt;/code&gt;：用来传递登录接口地址，改过滤器只处理登录和刷新 &lt;code&gt;Token&lt;/code&gt; 这两个操作，这里的登录接口的地址是 &lt;code&gt;/api/tokens&lt;/code&gt;。 &lt;code&gt;AuthenticationSuccessHandler&lt;/code&gt;：认证成功的自定义处理接口，具体的操作就是颁发 &lt;code&gt;Token&lt;/code&gt; &lt;code&gt;AuthenticationFailureHandler&lt;/code&gt;：认证失败的处理方法，返回友好的提示信息给调用者，具体实现可参考代码。&lt;/p&gt; &lt;h3&gt;JwtAuthenticationProvider&lt;/h3&gt; &lt;p&gt;登录具体的认证，用户密码检查，用户状态检查或 &lt;code&gt;Refresh Token&lt;/code&gt; 检查. 这里注意返回的是也是 &lt;code&gt;Authentication&lt;/code&gt; &lt;code&gt;Token&lt;/code&gt; 自定义实现类，包括 &lt;code&gt;LoginProcessingFilter&lt;/code&gt; 也是，注意两个地方返回是调用的是是两个不同的方法 &lt;code&gt;LoginProcessingFilter&lt;/code&gt; 返回是认证还没结束父类方法中会执行 &lt;code&gt;setAuthenticated(false);&lt;/code&gt;，等&lt;code&gt;JwtAuthenticationProvider&lt;/code&gt; 认证结束后才是完整的认证结束，其父类方法中会调用 &lt;code&gt;super.setAuthenticated(true);&lt;/code&gt;，标识已经认证通过了。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @author Created by https://zhangaoo.com.&amp;lt;br/&amp;gt;  * @version Version: 0.0.1  * @date DateTime: 2019/06/22 16:23:00&amp;lt;br/&amp;gt;  */ @Slf4j @Component public class JwtAuthenticationProvider implements AuthenticationProvider {     @Autowired     private JwtTokenHelper tokenHelper;     @Autowired     private MemoryUserService memoryUserService;      @Override     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         Assert.notNull(authentication, &amp;quot;No authentication data provided&amp;quot;);         //username and password         String username = (String) authentication.getPrincipal();         String password = (String) authentication.getCredentials();         String refreshToken = (String) authentication.getDetails();         //当本次请求是刷新 Token 时         if (StringUtils.isNotBlank(refreshToken)) {             // decode and verify refresh token is valid             Jws&amp;lt;Claims&amp;gt; jwsClaims = tokenHelper.parseClaims(refreshToken);             //check token type only access_token             String tokenType = jwsClaims.getBody().get(&amp;quot;type&amp;quot;).toString();             if (!REFRESH_TOKEN.equals(tokenType)) {                 log.warn(&amp;quot;Invalid token type,must be a refresh token,token:&amp;quot; + refreshToken + &amp;quot;,type:&amp;quot; + tokenType);                 throw new InvalidJwtToken(&amp;quot;Invalid token type,must be a refresh token&amp;quot;);             }             UserContext userContext = UserContext.create(jwsClaims.getBody().get(&amp;quot;userId&amp;quot;, String.class),                     jwsClaims.getBody().getSubject(), Collections.emptyList(),                     Constant.REFRESH_TOKEN);             return new JwtUsernamePasswordAuthenticationToken(userContext, null, Collections.emptyList());         } else {             //本次请求是获取 Access Token 和 Refresh Token             MyUserDetails userDetails = memoryUserService.loadUserByUsername(username);             //Check password             if (!userDetails.getPassword().equals(password)) {                 throw new BadCredentialsException(&amp;quot;Authentication Failed. Invalid username or password.&amp;quot;);             }             //检查用户状态             if (!userDetails.isAccountNonExpired() || !userDetails.isAccountNonLocked() || !userDetails.isCredentialsNonExpired() || !userDetails.isEnabled()) {                 log.warn(&amp;quot;User was deleted please contact administrator: &amp;quot; + userDetails.getUsername());                 throw new UserDisabledException(&amp;quot;User was disabled please contact administrator&amp;quot;);             }             UserContext userContext = UserContext.create(userDetails.getUserId(), userDetails.getUsername(), userDetails.getAuthorities(), Constant.ACCESS_TOKEN);             //认证通过，传递自定义的用户上下文（注意两个构造器的区别）             return new JwtUsernamePasswordAuthenticationToken(userContext, null, Collections.emptyList());         }     }     // 标识该 Provider 只处理 JwtUsernamePasswordAuthenticationToken 一种 Token     @Override     public boolean supports(Class&amp;lt;?&amp;gt; authentication) {         return (JwtUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;配置 RestSecurityConfig&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @author Created by https://zhangaoo.com.&amp;lt;br/&amp;gt;  * @version Version: 0.0.1  * @date DateTime: 2019/06/28 10:01:00&amp;lt;br/&amp;gt;  */ @Slf4j @Configuration @EnableWebSecurity public class RestSecurityConfig extends WebSecurityConfigurerAdapter {     //认证用户身份的provider（处理登录的provider）     @Autowired     private JwtAuthenticationProvider jwtAuthenticationProvider;     //访问控制，检查 Token 权限的Provider     @Autowired     private ApiAuthenticationProvider apiAuthenticationProvider;     //认证通过的自定义处理方法     @Autowired     private AuthenticationSuccessHandler successHandler;     //认证失败的自定义处理方法     @Autowired     private AuthenticationFailureHandler failureHandler;     //jackson 序列化用的工具类     @Autowired     private ObjectMapper objectMapper;     //Spring Security 默认帮我们实现好的Provider Manager     @Autowired     private AuthenticationManager authenticationManager;     //解析、认证、生成Token的工具类     @Autowired     private JwtTokenHelper jwtTokenHelper;     //获取所有接口权限的服务     @Autowired     private AuthorityService authorityService;     //权限不足访问拒绝处理类     @Autowired     private AjaxAccessDeniedHandler ajaxAccessDeniedHandler;      /**      * 注意不要忘记注入这个 Bean ，否则做具体认证时加载不到实现认证的Provider，比如这里的 jwtAuthenticationProvider      * @return      * @throws Exception      */     @Bean     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }      /**      * Filter for login API      * @param loginEntryPoint      * @return      * @throws Exception      */     protected LoginProcessingFilter buildLoginProcessingFilter(String loginEntryPoint) throws Exception {         LoginProcessingFilter filter = new LoginProcessingFilter(loginEntryPoint, objectMapper,successHandler, failureHandler );         filter.setAuthenticationManager(this.authenticationManager);         return filter;     }      /**      * Filter for resource request API      * @param pathsToSkip      * @param pattern      * @return      * @throws Exception      */     protected ApiAuthenticationProcessingFilter buildApiAuthenticationProcessingFilter(List&amp;lt;String&amp;gt; pathsToSkip, List&amp;lt;String&amp;gt; pattern) throws Exception {         SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, pattern);         ApiAuthenticationProcessingFilter filter                 = new ApiAuthenticationProcessingFilter(failureHandler, matcher, jwtTokenHelper);         filter.setAuthenticationManager(this.authenticationManager);         return filter;     }     //注入我们自己实现的 Provider      @Override     public void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.authenticationProvider(jwtAuthenticationProvider);         auth.authenticationProvider(apiAuthenticationProvider);     }     //核心配置     @Override     protected void configure(HttpSecurity http) throws Exception {         List&amp;lt;String&amp;gt; permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL);         //模拟从数据库获取接口的访问权限，并初始化         List&amp;lt;ApiAuthority&amp;gt; authorities = authorityService.getAllAuthority();         for(ApiAuthority rule : authorities){             http                     .authorizeRequests()                     .antMatchers(HttpMethod.resolve(rule.getMethod()), rule.getApiUrl()).hasAuthority(rule.getAuthority());         }         http                 .csrf().disable()                 .exceptionHandling().accessDeniedHandler(ajaxAccessDeniedHandler)             .and()                 .sessionManagement()                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)//使用 JWT Token 接口无状态             .and()                 .authorizeRequests()                 .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()//登录接口不需要认证             .and()                 .authorizeRequests()                 .antMatchers(API_ROOT_URL).authenticated()//除了登录接口，所有满足/api/**都需要认证             .and()                 .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)                 //注册 LoginProcessingFilter  注意放置的顺序 这很关键                 .addFilterBefore(buildLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)                 .addFilterBefore(buildApiAuthenticationProcessingFilter(permitAllEndpointList,Arrays.asList(API_ROOT_URL)), UsernamePasswordAuthenticationFilter.class);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;WebSecurityConfigAdapter&lt;/code&gt; 提供了我们很大的便利，不需要关注 &lt;code&gt;AuthenticationManager&lt;/code&gt; 什么时候被创建，只需要使用其暴露的&lt;code&gt;configure(AuthenticationManagerBuilder auth)&lt;/code&gt; 便可以添加我们自定义的 &lt;code&gt;Provider&lt;/code&gt; 。剩下的一些细节，注释中基本都写了出来。&lt;/p&gt; &lt;h2&gt;运行效果&lt;/h2&gt; &lt;h3&gt;获取 Access Token 和 Refresh Token&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜  ~ curl -H 'Content-Type: application/json' -X POST -d '{&amp;quot;username&amp;quot;: &amp;quot;admin&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;admin123456&amp;quot;}' http://127.0.0.1:8808/api/tokens {&amp;quot;code&amp;quot;:200,&amp;quot;data&amp;quot;:{&amp;quot;access_token&amp;quot;:&amp;quot;eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInNjb3BlcyI6W10sInVzZXJJZCI6IjEiLCJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiaXNzIjoiemhhbmdhb28uY29tIiwiaWF0IjoxNTYxOTA0MTM2LCJleHAiOjE1NjE5MDc3MzZ9.OLD1IcMG4pLRzW4uQtDs4TCF18lsBSB7-BJAbEaZStOeFMxGHSfyG2D3G960R7Qn0i7SKq8ryxc-FrqiwOMsVg&amp;quot;,&amp;quot;refresh_token&amp;quot;:&amp;quot;eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJ0eXBlIjoicmVmcmVzaF90b2tlbiIsImlzcyI6InpoYW5nYW9vLmNvbSIsImp0aSI6IjliMDBiNzAwLTRmNDYtNDczZS04ODk3LTBkY2U4YzE5ZDRkYiIsImlhdCI6MTU2MTkwNDEzNiwiZXhwIjoxNTYxOTMyOTM2fQ.WRoQkZE8-bP6ENUCKqbUTO5DNo0gu7z7J-EnLqC9cDjgh6EfopOx0wM0QDCp7iU_B1186tfAXYw-tczdbHEFDw&amp;quot;}}%  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;刷新 Token&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜  ~ curl -H 'Content-Type: application/json' -X POST -d '{&amp;quot;refresh_token&amp;quot;: &amp;quot;eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJ0eXBlIjoicmVmcmVzaF90b2tlbiIsImlzcyI6InpoYW5nYW9vLmNvbSIsImp0aSI6IjliMDBiNzAwLTRmNDYtNDczZS04ODk3LTBkY2U4YzE5ZDRkYiIsImlhdCI6MTU2MTkwNDEzNiwiZXhwIjoxNTYxOTMyOTM2fQ.WRoQkZE8-bP6ENUCKqbUTO5DNo0gu7z7J-EnLqC9cDjgh6EfopOx0wM0QDCp7iU_B1186tfAXYw-tczdbHEFDw&amp;quot;}' http://127.0.0.1:8808/api/tokens {&amp;quot;code&amp;quot;:200,&amp;quot;data&amp;quot;:{&amp;quot;access_token&amp;quot;:&amp;quot;eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInNjb3BlcyI6W10sInVzZXJJZCI6IjEiLCJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiaXNzIjoiemhhbmdhb28uY29tIiwiaWF0IjoxNTYxOTA0MzE4LCJleHAiOjE1NjE5MDc5MTh9.TJ-0SrIbiVW81nA9Ep91Wq1GEGVhMqFIq9ppi9ZRiYkbk88W1DG84YhA1m9KIvWgOmb6tYxPVIvLsM_9QSH06Q&amp;quot;}}%  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;接口权限测试&lt;/h3&gt; &lt;p&gt;测试代码在内存中写死了两个用户，&lt;code&gt;admin/admin123456&lt;/code&gt;，&lt;code&gt;testUser/testUser123456&lt;/code&gt;，&lt;code&gt;admin&lt;/code&gt; 同时具有 &lt;code&gt;admin&lt;/code&gt; 和 &lt;code&gt;user&lt;/code&gt; 两个权限，&lt;code&gt;testUser&lt;/code&gt; 只有 &lt;code&gt;user&lt;/code&gt; 权限。 访问 &lt;code&gt;/api/user/all&lt;/code&gt; 和 &lt;code&gt;/api/user/admin&lt;/code&gt; 需要 &lt;code&gt;admin&lt;/code&gt; 权限，而访问 &lt;code&gt;/api/user/testUser&lt;/code&gt; 只需要 &lt;code&gt;user&lt;/code&gt; 权限。&lt;/p&gt; &lt;h4&gt;正常测试&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;admin&lt;/code&gt; 但访问 &lt;code&gt;/api/user/all&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜  ~ curl -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInNjb3BlcyI6W10sInVzZXJJZCI6IjEiLCJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiaXNzIjoiemhhbmdhb28uY29tIiwiaWF0IjoxNTYxOTA0MzE4LCJleHAiOjE1NjE5MDc5MTh9.TJ-0SrIbiVW81nA9Ep91Wq1GEGVhMqFIq9ppi9ZRiYkbk88W1DG84YhA1m9KIvWgOmb6tYxPVIvLsM_9QSH06Q' http://127.0.0.1:8808/api/user/all  {&amp;quot;code&amp;quot;:200,&amp;quot;data&amp;quot;:{&amp;quot;admin&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;username&amp;quot;:&amp;quot;admin&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;admin123456&amp;quot;,&amp;quot;accountNonExpired&amp;quot;:true,&amp;quot;accountNonLocked&amp;quot;:true,&amp;quot;credentialsNonExpired&amp;quot;:true,&amp;quot;enabled&amp;quot;:true},&amp;quot;testUser&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;2&amp;quot;,&amp;quot;username&amp;quot;:&amp;quot;testUser&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;testUser123456&amp;quot;,&amp;quot;accountNonExpired&amp;quot;:true,&amp;quot;accountNonLocked&amp;quot;:true,&amp;quot;credentialsNonExpired&amp;quot;:true,&amp;quot;enabled&amp;quot;:true}}}%   &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;testUser&lt;/code&gt; 访问 &lt;code&gt;/api/user/testUser&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;➜  ~ curl -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0VXNlciIsInNjb3BlcyI6W10sInVzZXJJZCI6IjIiLCJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiaXNzIjoiemhhbmdhb28uY29tIiwiaWF0IjoxNTYxOTA0NzU3LCJleHAiOjE1NjE5MDgzNTd9.BIFKr7yznjVClWeiamiB8WfkYk3iVoGDEM0f90Hy7idrTOJ8udBkTLtAIPrnMgg1w_Hk_60EEPx00lNXDb337Q' http://127.0.0.1:8808/api/user/testUser {&amp;quot;code&amp;quot;:200,&amp;quot;data&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;2&amp;quot;,&amp;quot;username&amp;quot;:&amp;quot;testUser&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;testUser123456&amp;quot;,&amp;quot;accountNonExpired&amp;quot;:true,&amp;quot;accountNonLocked&amp;quot;:true,&amp;quot;credentialsNonExpired&amp;quot;:true,&amp;quot;enabled&amp;quot;:true}}%   &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;拒绝访问测试&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;curl -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0VXNlciIsInNjb3BlcyI6W10sInVzZXJJZCI6IjIiLCJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiaXNzIjoiemhhbmdhb28uY29tIiwiaWF0IjoxNTYxOTA0NzU3LCJleHAiOjE1NjE5MDgzNTd9.BIFKr7yznjVClWeiamiB8WfkYk3iVoGDEM0f90Hy7idrTOJ8udBkTLtAIPrnMgg1w_Hk_60EEPx00lNXDb337Q' http://127.0.0.1:8808/api/user/all       {     &amp;quot;code&amp;quot;: 5000,     &amp;quot;data&amp;quot;: &amp;quot;Access is denied&amp;quot; }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果要自定义拒绝访问返回信息，一个例子如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Slf4j @Component public class AjaxAccessDeniedHandler implements AccessDeniedHandler {     private final ObjectMapper mapper;     @Autowired     public AjaxAccessDeniedHandler(ObjectMapper mapper) {         this.mapper = mapper;     }      @Override     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {         response.setStatus(HttpStatus.FORBIDDEN.value());         response.setContentType(MediaType.APPLICATION_JSON_VALUE);         mapper.writeValue(response.getWriter(), new ResponseData(ResponseData.FAIL, AuthMessages.AUTH_009));     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上代码完整的实现了一个&lt;code&gt;Spring Security&lt;/code&gt; 整合 &lt;code&gt;JWT Token&lt;/code&gt; 例子，上述 &lt;code&gt;Demo&lt;/code&gt; 未详细介绍的 &lt;code&gt;Spring Cloud&lt;/code&gt; 相关的技术还包括 zuul 网关 Feign Client 、Eureka等，后续会继续介绍。&lt;/p&gt; &lt;p&gt;源代码参考 &lt;a href="https://github.com/zealzhangz/Spring-Security-Zuul-JWT" target="_blank"&gt;GitHub&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 30 Jun 2019 14:42:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Security 核心过滤器 篇四</title>
      <link>https://www.zhangaoo.com/article/spring-security-filter</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019622141857-spring-security.jpg" alt="2019622141857-spring-security" /&gt;&lt;/p&gt; &lt;h1&gt;核心过滤器源码分析&lt;/h1&gt; &lt;p&gt;前面的部分，我们关注了 &lt;code&gt;Spring Security&lt;/code&gt; 是如何完成认证工作的，但是另外一部分核心的内容：过滤器，一直没有提到，我们已经知道&lt;code&gt;Spring Security&lt;/code&gt; 使用了springSecurityFillterChian作为了安全过滤的入口，这一节主要分析一下这个过滤器链都包含了哪些关键的过滤器，并且各自的使命是什么。&lt;/p&gt; &lt;h2&gt;核心过滤器概述&lt;/h2&gt; &lt;p&gt;由于过滤器链路中的过滤较多，即使是 &lt;code&gt;Spring Security&lt;/code&gt; 的官方文档中也并未对所有的过滤器进行介绍，在之前，《Spring Security 篇二》入门指南中我们配置了一个表单登录的 &lt;code&gt;demo&lt;/code&gt;，以此为例，来看看这过程中 &lt;code&gt;Spring Security&lt;/code&gt; 都帮我们自动配置了哪些过滤器。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Creating filter chain: o.s.s.web.util.matcher.AnyRequestMatcher@1,  [o.s.s.web.context.SecurityContextPersistenceFilter@8851ce1,  o.s.s.web.header.HeaderWriterFilter@6a472566,  o.s.s.web.csrf.CsrfFilter@61cd1c71,  o.s.s.web.authentication.logout.LogoutFilter@5e1d03d7,  o.s.s.web.authentication.UsernamePasswordAuthenticationFilter@122d6c22,  o.s.s.web.savedrequest.RequestCacheAwareFilter@5ef6fd7f,  o.s.s.web.servletapi.SecurityContextHolderAwareRequestFilter@4beaf6bd,  o.s.s.web.authentication.AnonymousAuthenticationFilter@6edcad64,  o.s.s.web.session.SessionManagementFilter@5e65afb6,  o.s.s.web.access.ExceptionTranslationFilter@5b9396d3,  o.s.s.web.access.intercept.FilterSecurityInterceptor@3c5dbdf8] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述的 &lt;code&gt;log&lt;/code&gt; 信息是我从 &lt;code&gt;springboot&lt;/code&gt; 启动的日志中 &lt;code&gt;CV&lt;/code&gt; 所得，&lt;code&gt;spring security&lt;/code&gt; 的过滤器日志有一个特点：&lt;code&gt;log&lt;/code&gt; 打印顺序与实际配置顺序符合，也就意味着 &lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt; 是整个过滤器链的第一个过滤器，而  &lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 则是末置的过滤器。另外通过观察过滤器的名称，和所在的包名，可以大致地分析出他们各自的作用，如&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 明显便是与使用用户名和密码登录相关的过滤器，而 &lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 我们似乎看不出它的作用，但是其位于 &lt;code&gt;web.access&lt;/code&gt; 包下，大致可以分析出他与访问限制相关。第四篇文章主要就是介绍这些常用的过滤器，对其中关键的过滤器进行一些源码分析。先大致介绍下每个过滤器的作用：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt; 两个主要职责：请求来临时，创建 &lt;code&gt;SecurityContext&lt;/code&gt; 安全上下文信息，请求结束时清空&lt;code&gt;SecurityContextHolder&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;HeaderWriterFilter&lt;/code&gt;  用来给 &lt;code&gt;http&lt;/code&gt; 响应添加一些 &lt;code&gt;Header&lt;/code&gt;,比如 &lt;code&gt;X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;&lt;code&gt;CsrfFilter&lt;/code&gt; 在 &lt;code&gt;spring4&lt;/code&gt; 这个版本中被默认开启的一个过滤器，用于防止 &lt;code&gt;csrf&lt;/code&gt; 攻击，了解前后端分离的人一定不会对这个攻击方式感到陌生，前后端使用 &lt;code&gt;json&lt;/code&gt; 交互需要注意的一个问题。&lt;/li&gt; &lt;li&gt;&lt;code&gt;LogoutFilter&lt;/code&gt; 顾名思义，处理注销的过滤器。&lt;/li&gt; &lt;li&gt;&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 这个会重点分析，表单提交了 &lt;code&gt;username&lt;/code&gt; 和 &lt;code&gt;password&lt;/code&gt;，被封装成 &lt;code&gt;token&lt;/code&gt; 进行一系列的认证，便是主要通过这个过滤器完成的，在表单认证的方法中，这是最最关键的过滤器。&lt;/li&gt; &lt;li&gt;&lt;code&gt;RequestCacheAwareFilter&lt;/code&gt;  内部维护了一个 &lt;code&gt;RequestCache&lt;/code&gt;，用于缓存 &lt;code&gt;request&lt;/code&gt; 请求&lt;/li&gt; &lt;li&gt;&lt;code&gt;SecurityContextHolderAwareRequestFilter&lt;/code&gt; 此过滤器对 &lt;code&gt;ServletRequest&lt;/code&gt; 进行了一次包装，使得 &lt;code&gt;request&lt;/code&gt; 具有更加丰富的 &lt;code&gt;API&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;AnonymousAuthenticationFilter&lt;/code&gt; 匿名身份过滤器，这个过滤器个人认为很重要，需要将它与&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 放在一起比较理解，&lt;code&gt;spring security&lt;/code&gt; 为了兼容未登录的访问，也走了一套认证流程，只不过是一个匿名的身份。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SessionManagementFilter&lt;/code&gt; 和 &lt;code&gt;session&lt;/code&gt; 相关的过滤器，内部维护了一个 &lt;code&gt;SessionAuthenticationStrategy&lt;/code&gt;，两者组合使用，常用来防止 &lt;code&gt;session-fixation protection attack&lt;/code&gt;，以及限制同一用户开启多个会话的数量。&lt;/li&gt; &lt;li&gt;&lt;code&gt;ExceptionTranslationFilter&lt;/code&gt; 直译成异常翻译过滤器，还是比较形象的，这个过滤器本身不处理异常，而是将认证过程中出现的异常交给内部维护的一些类去处理，具体是那些类下面详细介绍&lt;/li&gt; &lt;li&gt;&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 这个过滤器决定了访问特定路径应该具备的权限，访问的用户的角色，权限是什么？访问的路径需要什么样的角色和权限？这些判断和处理都是由该类进行的。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;其中核心过滤器有如下几个： &lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt;、&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;、&lt;code&gt;AnonymousAuthenticationFilter&lt;/code&gt;、&lt;code&gt;ExceptionTranslationFilter&lt;/code&gt;、&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;SecurityContextPersistenceFilter&lt;/h3&gt; &lt;p&gt;试想一下，如果我们不使用 &lt;code&gt;Spring Security&lt;/code&gt;，如果保存用户信息呢，大多数情况下会考虑使用 &lt;code&gt;Session&lt;/code&gt; 对吧？在&lt;code&gt;Spring Security&lt;/code&gt; 中也是如此，用户在登录过一次之后，后续的访问便是通过 &lt;code&gt;sessionId&lt;/code&gt; 来识别，从而认为用户已经被认证。具体在何处存放用户信息，便是第一篇文章中提到的 &lt;code&gt;SecurityContextHolder&lt;/code&gt;；认证相关的信息是如何被存放到其中的，便是通过&lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt;。在上篇中也提到了，&lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt; 的两个主要作用便是请求来临时，创建 &lt;code&gt;SecurityContext&lt;/code&gt; 安全上下文信息和请求结束时清空 &lt;code&gt;SecurityContextHolder&lt;/code&gt;。顺带提一下：微服务的一个设计理念需要实现服务通信的无状态，而 &lt;code&gt;http&lt;/code&gt; 协议中的无状态意味着不允许存在 &lt;code&gt;session&lt;/code&gt;，这可以通过&lt;code&gt;setAllowSessionCreation(false)&lt;/code&gt; 实现，这并不意味着 &lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt; 变得无用，因为它还需要负责清除用户信息。在 &lt;code&gt;Spring Security&lt;/code&gt; 中，虽然安全上下文信息被存储于 &lt;code&gt;Session&lt;/code&gt; 中，但我们在实际使用中不应该直接操作 &lt;code&gt;Session&lt;/code&gt;，而应当使用 &lt;code&gt;SecurityContextHolder&lt;/code&gt;。&lt;/p&gt; &lt;h4&gt;源码分析&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class SecurityContextPersistenceFilter extends GenericFilterBean {   static final String FILTER_APPLIED = &amp;quot;__spring_security_scpf_applied&amp;quot;;   //上下文存储的仓库  private SecurityContextRepository repo;   private boolean forceEagerSessionCreation = false;   public SecurityContextPersistenceFilter() {       //HttpSessionSecurityContextRepository是SecurityContextRepository接口的一个实现类       //使用HttpSession来存储SecurityContext   this(new HttpSessionSecurityContextRepository());  }   public SecurityContextPersistenceFilter(SecurityContextRepository repo) {   this.repo = repo;  }   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)    throws IOException, ServletException {   HttpServletRequest request = (HttpServletRequest) req;   HttpServletResponse response = (HttpServletResponse) res;    if (request.getAttribute(FILTER_APPLIED) != null) {    // ensure that filter is only applied once per request    chain.doFilter(request, response);    return;   }    final boolean debug = logger.isDebugEnabled();    request.setAttribute(FILTER_APPLIED, Boolean.TRUE);    if (forceEagerSessionCreation) {    HttpSession session = request.getSession();     if (debug &amp;amp;&amp;amp; session.isNew()) {     logger.debug(&amp;quot;Eagerly created session: &amp;quot; + session.getId());    }   }     //包装request，response   HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,     response);     //从Session中获取安全上下文信息   SecurityContext contextBeforeChainExecution = repo.loadContext(holder);   try {       //请求开始时，设置安全上下文信息，这样就避免了用户直接从Session中获取安全上下文信息    SecurityContextHolder.setContext(contextBeforeChainExecution);    chain.doFilter(holder.getRequest(), holder.getResponse());   }   finally {       //请求结束后，清空安全上下文信息    SecurityContext contextAfterChainExecution = SecurityContextHolder      .getContext();    // Crucial removal of SecurityContextHolder contents - do this before anything    // else.    SecurityContextHolder.clearContext();    repo.saveContext(contextAfterChainExecution, holder.getRequest(),      holder.getResponse());    request.removeAttribute(FILTER_APPLIED);    if (debug) {     logger.debug(&amp;quot;SecurityContextHolder now cleared, as request processing completed&amp;quot;);    }   }  }  public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {   this.forceEagerSessionCreation = forceEagerSessionCreation;  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;过滤器一般负责核心的处理流程，而具体的业务实现，通常交给其中聚合的其他实体类，这在 &lt;code&gt;Filter&lt;/code&gt; 的设计中很常见，同时也符合职责分离模式。例如存储安全上下文和读取安全上下文的工作完全委托给了 &lt;code&gt;HttpSessionSecurityContextRepository&lt;/code&gt; 去处理，而这个类中也有几个方法可以稍微解读下，方便我们理解内部的工作流程&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;HttpSessionSecurityContextRepository&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class HttpSessionSecurityContextRepository implements SecurityContextRepository {    // 'SPRING_SECURITY_CONTEXT'是安全上下文默认存储在Session中的键值    public static final String SPRING_SECURITY_CONTEXT_KEY = &amp;quot;SPRING_SECURITY_CONTEXT&amp;quot;;    ...    private final Object contextObject = SecurityContextHolder.createEmptyContext();    private boolean allowSessionCreation = true;    private boolean disableUrlRewriting = false;    private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;     private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();     //从当前request中取出安全上下文，如果session为空，则会返回一个新的安全上下文    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {       HttpServletRequest request = requestResponseHolder.getRequest();       HttpServletResponse response = requestResponseHolder.getResponse();       HttpSession httpSession = request.getSession(false);       SecurityContext context = readSecurityContextFromSession(httpSession);       if (context == null) {          context = generateNewContext();       }       ...       return context;    }     ...     public boolean containsContext(HttpServletRequest request) {       HttpSession session = request.getSession(false);       if (session == null) {          return false;       }       return session.getAttribute(springSecurityContextKey) != null;    }     private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {       if (httpSession == null) {          return null;       }       ...       // Session存在的情况下，尝试获取其中的SecurityContext       Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);       if (contextFromSession == null) {          return null;       }       ...       return (SecurityContext) contextFromSession;    }     //初次请求时创建一个新的SecurityContext实例    protected SecurityContext generateNewContext() {       return SecurityContextHolder.createEmptyContext();    } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;SecurityContextPersistenceFilter&lt;/code&gt; 和 &lt;code&gt;HttpSessionSecurityContextRepository&lt;/code&gt; 配合使用，构成了 &lt;code&gt;Spring Security&lt;/code&gt; 整个调用链路的入口，为什么将它放在最开始的地方也是显而易见的，后续的过滤器中大概率会依赖 &lt;code&gt;Session&lt;/code&gt; 信息和安全上下文信息。&lt;/p&gt; &lt;h3&gt;UsernamePasswordAuthenticationFilter&lt;/h3&gt; &lt;p&gt;表单认证是最常用的一个认证方式，一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录，而这背后的&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;，在整个 &lt;code&gt;Spring Security&lt;/code&gt; 的认证体系中则扮演着至关重要的角色。&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019620204316-user-authentication.jpg" alt="2019620204316-user-authentication" /&gt;&lt;/p&gt; &lt;p&gt;上述的时序图，可以看出 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 主要肩负起了调用身份认证器，校验身份的作用，至于认证的细节，在前面几章花了很大篇幅进行了介绍，到这里，其实 &lt;code&gt;Spring Security&lt;/code&gt; 的基本流程就已经走通了。&lt;/p&gt; &lt;p&gt;&lt;code&gt;org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; public UsernamePasswordAuthenticationFilter() {       //该过滤器默认拦截 /login 的 POST 请求   super(new AntPathRequestMatcher(&amp;quot;/login&amp;quot;, &amp;quot;POST&amp;quot;));  }    //做简单的认证  public Authentication attemptAuthentication(HttpServletRequest request,    HttpServletResponse response) throws AuthenticationException {       //判断请求登录请求方法是否是POST   if (postOnly &amp;amp;&amp;amp; !request.getMethod().equals(&amp;quot;POST&amp;quot;)) {    throw new AuthenticationServiceException(      &amp;quot;Authentication method not supported: &amp;quot; + request.getMethod());   }       //获取请求中的用户名密码   String username = obtainUsername(request);   String password = obtainPassword(request);   if (username == null) {    username = &amp;quot;&amp;quot;;   }   if (password == null) {    password = &amp;quot;&amp;quot;;   }   username = username.trim();       //组装成 username+password 形式的token，把改Token传递到具体的 Provider 做认证   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(     username, password);       //如果需要传递而外的信息到Provider可设置改字段   // Allow subclasses to set the &amp;quot;details&amp;quot; property   setDetails(request, authRequest);       //交给内部的AuthenticationManager去认证，并返回认证信息   return this.getAuthenticationManager().authenticate(authRequest);  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 本身的代码只包含了上述这么一个方法，非常简略，而在其父类  &lt;code&gt;AbstractAuthenticationProcessingFilter&lt;/code&gt; 中包含了大量的细节，值得我们分析：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean       implements ApplicationEventPublisherAware, MessageSourceAware {  //包含了一个身份认证器  private AuthenticationManager authenticationManager;  //用于实现remeberMe  private RememberMeServices rememberMeServices = new NullRememberMeServices();  private RequestMatcher requiresAuthenticationRequestMatcher;  //这两个Handler很关键，分别代表了认证成功和失败相应的处理器  private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();  private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)    throws IOException, ServletException {    HttpServletRequest request = (HttpServletRequest) req;   HttpServletResponse response = (HttpServletResponse) res;   ...   Authentication authResult;   try {    //此处实际上就是调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法    authResult = attemptAuthentication(request, response);    if (authResult == null) {     //子类未完成认证，立刻返回     return;    }    sessionStrategy.onAuthentication(authResult, request, response);   }   //在认证过程中可以直接抛出异常，在过滤器中，就像此处一样，进行捕获   catch (InternalAuthenticationServiceException failed) {    //内部服务异常    unsuccessfulAuthentication(request, response, failed);    return;   }   catch (AuthenticationException failed) {    //认证失败    unsuccessfulAuthentication(request, response, failed);    return;   }   //认证成功   if (continueChainBeforeSuccessfulAuthentication) {    chain.doFilter(request, response);   }   //注意，认证成功后过滤器把authResult结果也传递给了成功处理器   successfulAuthentication(request, response, chain, authResult);  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;整个流程理解起来也并不难，主要就是内部调用了 &lt;code&gt;authenticationManager&lt;/code&gt; 完成认证，根据认证结果执行 &lt;code&gt;successfulAuthentication&lt;/code&gt; 或者 &lt;code&gt;unsuccessfulAuthentication&lt;/code&gt; ，无论成功失败，一般的实现都是转发或者重定向等处理，不再细究 &lt;code&gt;AuthenticationSuccessHandler&lt;/code&gt; 和 &lt;code&gt;AuthenticationFailureHandler&lt;/code&gt; ，有兴趣的朋友，可以去看看两者的实现类。&lt;/p&gt; &lt;h3&gt;AnonymousAuthenticationFilter&lt;/h3&gt; &lt;p&gt;匿名认证过滤器，可能有人会想：匿名了还有身份？我自己对于 &lt;code&gt;Anonymous&lt;/code&gt; 匿名身份的理解是 &lt;code&gt;Spirng Security&lt;/code&gt; 为了整体逻辑的统一性，即使是未通过认证的用户，也给予了一个匿名身份。而 &lt;code&gt;AnonymousAuthenticationFilter&lt;/code&gt; 该过滤器的位置也是非常的科学的，它位于常用的身份认证过滤器（如 &lt;code&gt;UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter&lt;/code&gt; ）之后，意味着只有在上述身份过滤器执行完毕后，&lt;code&gt;SecurityContext&lt;/code&gt; 依旧没有用户信息，&lt;code&gt;AnonymousAuthenticationFilter&lt;/code&gt; 该过滤器才会有意义—-基于用户一个匿名身份。&lt;/p&gt; &lt;p&gt;&lt;code&gt;org.springframework.security.web.authentication.AnonymousAuthenticationFilter&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package org.springframework.security.web.authentication;  .....  /**  * Detects if there is no {@code Authentication} object in the  * {@code SecurityContextHolder}, and populates it with one if needed.  *  * @author Ben Alex  * @author Luke Taylor  */ public class AnonymousAuthenticationFilter extends GenericFilterBean implements   InitializingBean {  private AuthenticationDetailsSource&amp;lt;HttpServletRequest, ?&amp;gt; authenticationDetailsSource = new WebAuthenticationDetailsSource();  private String key;  private Object principal;  private List&amp;lt;GrantedAuthority&amp;gt; authorities;  /**   * Creates a filter with a principal named &amp;quot;anonymousUser&amp;quot; and the single authority   * &amp;quot;ROLE_ANONYMOUS&amp;quot;.   *   * @param key the key to identify tokens created by this filter   */     //自动创建一个&amp;quot;anonymousUser&amp;quot;的匿名用户,其具有ANONYMOUS角色  public AnonymousAuthenticationFilter(String key) {   this(key, &amp;quot;anonymousUser&amp;quot;, AuthorityUtils.createAuthorityList(&amp;quot;ROLE_ANONYMOUS&amp;quot;));  }   /**   *   * @param key key the key to identify tokens created by this filter   * @param principal the principal which will be used to represent anonymous users   * @param authorities the authority list for anonymous users   */  public AnonymousAuthenticationFilter(String key, Object principal,    List&amp;lt;GrantedAuthority&amp;gt; authorities) {   Assert.hasLength(key, &amp;quot;key cannot be null or empty&amp;quot;);   Assert.notNull(principal, &amp;quot;Anonymous authentication principal must be set&amp;quot;);   Assert.notNull(authorities, &amp;quot;Anonymous authorities must be set&amp;quot;);   this.key = key;   this.principal = principal;   this.authorities = authorities;  }   @Override  public void afterPropertiesSet() {   Assert.hasLength(key, &amp;quot;key must have length&amp;quot;);   Assert.notNull(principal, &amp;quot;Anonymous authentication principal must be set&amp;quot;);   Assert.notNull(authorities, &amp;quot;Anonymous authorities must be set&amp;quot;);  }   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)    throws IOException, ServletException {       //过滤器链都执行到匿名认证过滤器这儿了还没有身份信息，塞一个匿名身份进去   if (SecurityContextHolder.getContext().getAuthentication() == null) {    SecurityContextHolder.getContext().setAuthentication(      createAuthentication((HttpServletRequest) req));     if (logger.isDebugEnabled()) {     logger.debug(&amp;quot;Populated SecurityContextHolder with anonymous token: '&amp;quot;       + SecurityContextHolder.getContext().getAuthentication() + &amp;quot;'&amp;quot;);    }   }   else {    if (logger.isDebugEnabled()) {     logger.debug(&amp;quot;SecurityContextHolder not populated with anonymous token, as it already contained: '&amp;quot;       + SecurityContextHolder.getContext().getAuthentication() + &amp;quot;'&amp;quot;);    }   }    chain.doFilter(req, res);  }    //创建一个 AnonymousAuthenticationToken  protected Authentication createAuthentication(HttpServletRequest request) {   AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,     principal, authorities);   auth.setDetails(authenticationDetailsSource.buildDetails(request));    return auth;  }   public void setAuthenticationDetailsSource(    AuthenticationDetailsSource&amp;lt;HttpServletRequest, ?&amp;gt; authenticationDetailsSource) {   Assert.notNull(authenticationDetailsSource,     &amp;quot;AuthenticationDetailsSource required&amp;quot;);   this.authenticationDetailsSource = authenticationDetailsSource;  }   public Object getPrincipal() {   return principal;  }   public List&amp;lt;GrantedAuthority&amp;gt; getAuthorities() {   return authorities;  } }   &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;其实对比 &lt;code&gt;AnonymousAuthenticationFilter&lt;/code&gt; 和 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 就可以发现一些门道了， &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 对应 &lt;code&gt;AnonymousAuthenticationToken&lt;/code&gt; ，他们都是 &lt;code&gt;Authentication&lt;/code&gt; 的实现类，而 &lt;code&gt;Authentication&lt;/code&gt; 则是被 &lt;code&gt;SecurityContextHolder(SecurityContext)&lt;/code&gt; 持有的，一切都被串联在了一起。&lt;/p&gt; &lt;h3&gt;ExceptionTranslationFilter&lt;/h3&gt; &lt;p&gt;&lt;code&gt;ExceptionTranslationFilter&lt;/code&gt; 异常转换过滤器位于整个 &lt;code&gt;springSecurityFilterChain&lt;/code&gt; 的后方，用来转换整个链路中出现的异常，将其转化，顾名思义，转化以意味本身并不处理。一般其只处理两大类异常：&lt;code&gt;AccessDeniedException&lt;/code&gt; 访问异常和 &lt;code&gt;AuthenticationException&lt;/code&gt; 认证异常。&lt;/p&gt; &lt;p&gt;这个过滤器非常重要，因为它将 &lt;code&gt;Java&lt;/code&gt; 中的异常和 &lt;code&gt;HTTP&lt;/code&gt; 的响应连接在了一起，这样在处理异常时，我们不用考虑密码错误该跳到什么页面，账号锁定该如何，只需要关注自己的业务逻辑，抛出相应的异常便可。如果该过滤器检测到 &lt;code&gt;AuthenticationException&lt;/code&gt; ，则将会交给内部的 &lt;code&gt;AuthenticationEntryPoint&lt;/code&gt; 去处理，如果检测到 &lt;code&gt;AccessDeniedException&lt;/code&gt;，需要先判断当前用户是不是匿名用户，如果是匿名访问，则和前面一样运行&lt;code&gt;AuthenticationEntryPoint&lt;/code&gt;，否则会委托给&lt;code&gt;AccessDeniedHandler&lt;/code&gt; 去处理，而 &lt;code&gt;AccessDeniedHandler&lt;/code&gt; 的默认实现，是 &lt;code&gt;AccessDeniedHandlerImpl&lt;/code&gt; 。所以 &lt;code&gt;ExceptionTranslationFilter&lt;/code&gt; 内部的 &lt;code&gt;AuthenticationEntryPoint&lt;/code&gt; 是至关重要的，顾名思义：认证的入口点。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class ExceptionTranslationFilter extends GenericFilterBean {   //处理异常转换的核心方法   private void handleSpringSecurityException(HttpServletRequest request,         HttpServletResponse response, FilterChain chain, RuntimeException exception)         throws IOException, ServletException {      if (exception instanceof AuthenticationException) {         //重定向到登录端点         sendStartAuthentication(request, response, chain,               (AuthenticationException) exception);      }      else if (exception instanceof AccessDeniedException) {         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();         if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {     //重定向到登录端点            sendStartAuthentication(                  request,                  response,                  chain,                  new InsufficientAuthenticationException(                        &amp;quot;Full authentication is required to access this resource&amp;quot;));         }         else {            //交给accessDeniedHandler处理            accessDeniedHandler.handle(request, response,                  (AccessDeniedException) exception);         }      }   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;剩下的便是要搞懂 &lt;code&gt;AuthenticationEntryPoint&lt;/code&gt; 和 &lt;code&gt;AccessDeniedHandler&lt;/code&gt; 就可以了。 &lt;img src="http://img.zhangaoo.com/20196211823-AuthenticationEntryPoint-interface.jpg" alt="20196211823-AuthenticationEntryPoint-interface" /&gt;&lt;/p&gt; &lt;p&gt;选择了几个常用的登录端点，以其中第一个为例来介绍，看名字就能猜到是认证失败之后，让用户跳转到登录页面。还记得我们一开始怎么配置表单登录页面的吗？&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {      @Override     protected void configure(HttpSecurity http) throws Exception {         http             .authorizeRequests()                 .antMatchers(&amp;quot;/&amp;quot;, &amp;quot;/home&amp;quot;).permitAll()                 .anyRequest().authenticated()                 .and()             .formLogin()//FormLoginConfigurer                 .loginPage(&amp;quot;/login&amp;quot;)                 .permitAll()                 .and()             .logout()                 .permitAll();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们顺着 &lt;code&gt;formLogin&lt;/code&gt; 返回的 &lt;code&gt;FormLoginConfigurer&lt;/code&gt; 往下找，看看能发现什么，最终在 &lt;code&gt;FormLoginConfigurer&lt;/code&gt; 的父类 &lt;code&gt;AbstractAuthenticationFilterConfigurer&lt;/code&gt; 中有了不小的收获：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public abstract class AbstractAuthenticationFilterConfigurer extends ...{    ...    //formLogin不出所料配置了AuthenticationEntryPoint    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;    //认证失败的处理器    private AuthenticationFailureHandler failureHandler;    ... } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;具体如何配置的就不看了，我们得出了结论，&lt;code&gt;formLogin()&lt;/code&gt; 配置了之后最起码做了两件事，其一，为 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 设置了相关的配置，其二配置了 &lt;code&gt;AuthenticationEntryPoint&lt;/code&gt; 。&lt;/p&gt; &lt;p&gt;登录端点还有 &lt;code&gt;Http401AuthenticationEntryPoint&lt;/code&gt; ，&lt;code&gt;Http403ForbiddenEntryPoint&lt;/code&gt; 这些都是很简单的实现，有时候我们访问受限页面，又没有配置登录，就看到了一个空荡荡的默认错误页面，上面显示着401,403，就是这两个入口起了作用。&lt;/p&gt; &lt;p&gt;还剩下一个 &lt;code&gt;AccessDeniedHandler&lt;/code&gt; 访问决策器未被讲解，简单提一下：&lt;code&gt;AccessDeniedHandlerImpl&lt;/code&gt; 这个默认实现类会根据 &lt;code&gt;errorPage&lt;/code&gt; 和状态码来判断，最终决定跳转的页面&lt;/p&gt; &lt;p&gt;&lt;code&gt;org.springframework.security.web.access.AccessDeniedHandlerImpl#handle&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void handle(HttpServletRequest request, HttpServletResponse response,       AccessDeniedException accessDeniedException) throws IOException,       ServletException {    if (!response.isCommitted()) {       if (errorPage != null) {          // Put exception into request scope (perhaps of use to a view)          request.setAttribute(WebAttributes.ACCESS_DENIED_403,                accessDeniedException);          // Set the 403 status code.          response.setStatus(HttpServletResponse.SC_FORBIDDEN);          // forward to error page.          RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);          dispatcher.forward(request, response);       }       else {          response.sendError(HttpServletResponse.SC_FORBIDDEN,                accessDeniedException.getMessage());       }    } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;自己在实际项目中处理 &lt;code&gt;AuthenticationException&lt;/code&gt; 异常是在个入口Filter中去做的，比如 &lt;code&gt;AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter&lt;/code&gt;，通过看 &lt;code&gt;AbstractAuthenticationProcessingFilter&lt;/code&gt; 源码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)    throws IOException, ServletException {   HttpServletRequest request = (HttpServletRequest) req;   HttpServletResponse response = (HttpServletResponse) res;   if (!requiresAuthentication(request, response)) {    chain.doFilter(request, response);    return;   }   if (logger.isDebugEnabled()) {    logger.debug(&amp;quot;Request is to process authentication&amp;quot;);   }   Authentication authResult;   try {    authResult = attemptAuthentication(request, response);    if (authResult == null) {     // return immediately as subclass has indicated that it hasn't completed     // authentication     return;    }    sessionStrategy.onAuthentication(authResult, request, response);   }   catch (InternalAuthenticationServiceException failed) {    logger.error(      &amp;quot;An internal error occurred while trying to authenticate the user.&amp;quot;,      failed);          //注意这里的异常交给了unsuccessfulAuthentication处理    unsuccessfulAuthentication(request, response, failed);     return;   }   catch (AuthenticationException failed) {          //同样这里的AuthenticationException也是    // Authentication failed    unsuccessfulAuthentication(request, response, failed);     return;   }   // Authentication success   if (continueChainBeforeSuccessfulAuthentication) {    chain.doFilter(request, response);   }   successfulAuthentication(request, response, chain, authResult);  }    //最终能暴露给用户自定义处理异常的接口 failureHandle(AuthenticationFailureHandler)  protected void unsuccessfulAuthentication(HttpServletRequest request,    HttpServletResponse response, AuthenticationException failed)    throws IOException, ServletException {   SecurityContextHolder.clearContext();       ......   rememberMeServices.loginFail(request, response);   failureHandler.onAuthenticationFailure(request, response, failed);  } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;FilterSecurityInterceptor&lt;/h3&gt; &lt;p&gt;想想整个认证安全控制流程还缺了什么？我们已经有了认证，有了请求的封装，有了 &lt;code&gt;Session&lt;/code&gt; 的关联…还缺一个：由什么控制哪些资源是受限的，这些受限的资源需要什么权限，需要什么角色…这一切和访问控制相关的操作，都是由 &lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 完成的。&lt;/p&gt; &lt;p&gt;&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 的工作流程用笔者的理解可以理解如下：&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt; 从 &lt;code&gt;SecurityContextHolder&lt;/code&gt; 中获取 &lt;code&gt;Authentication&lt;/code&gt; 对象，然后比对用户拥有的权限和资源所需的权限。前者可以通过 &lt;code&gt;Authentication&lt;/code&gt; 对象直接获得，而后者则需要引入我们之前一直未提到过的两个类：&lt;code&gt;SecurityMetadataSource&lt;/code&gt;， &lt;code&gt;AccessDecisionManager&lt;/code&gt; 。理解清楚决策管理器的整个创建流程和 &lt;code&gt;SecurityMetadataSource&lt;/code&gt; 的作用需要花很大一笔功夫，这里，暂时只介绍其大概的作用。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;JavaConfig&lt;/code&gt; 的配置中，我们通常如下配置路径的访问控制一般有两种方式，第一种是直接写死角色或权限，第二种是动态从数据库加载，第一种粒度比较大，灵活性不太好。第二种有较高的灵活性，在程序启动的时候从数据库加载数据，但是像做到在运行时 &lt;code&gt;reload&lt;/code&gt; 权限，目前来看除非在运行时 &lt;code&gt;reload&lt;/code&gt; 相关的 &lt;code&gt;bean&lt;/code&gt;（木有经过实战）&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     protected void configure(HttpSecurity http) throws Exception {         http             .authorizeRequests()      .antMatchers(&amp;quot;/&amp;quot;, &amp;quot;/home&amp;quot;).permitAll()      .antMatchers(&amp;quot;/admin/**&amp;quot;).hasRole(&amp;quot;ADMIN&amp;quot;)      .antMatchers(&amp;quot;/db/**&amp;quot;).access(&amp;quot;hasRole('ADMIN') and hasRole('DBA')&amp;quot;)      .anyRequest().authenticated()                 .and()      .formLogin()      .loginPage(&amp;quot;/login&amp;quot;)      .permitAll()                 .and()      .logout()      .permitAll();     } &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;总结&lt;/h1&gt; &lt;p&gt;本篇文章在介绍过滤器时，顺便进行了一些源码的分析，目的是方便理解整个 &lt;code&gt;Spring Security&lt;/code&gt; 的工作流。伴随着整个过滤器链的介绍，安全框架的轮廓应该已经浮出水面了，下面的章节，主要打算通过自定义一些需求，再次分析其他组件的源码，学习应该如何改造 &lt;code&gt;Spring Security&lt;/code&gt;，为我们所用。&lt;/p&gt; &lt;p&gt;原文链接：http://blog.didispace.com/xjf-spring-security-4/&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 22 Jun 2019 06:19:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Security 核心配置解读 篇三</title>
      <link>https://www.zhangaoo.com/article/spring-security-core-config</link>
      <content:encoded>&lt;h1&gt;核心配置解读&lt;/h1&gt; &lt;h2&gt;配置介绍&lt;/h2&gt; &lt;p&gt;回顾篇二中的 Security 安全核心配置&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override     protected void configure(HttpSecurity http) throws Exception {         http             .authorizeRequests()                 .antMatchers(&amp;quot;/&amp;quot;, &amp;quot;/home&amp;quot;).permitAll()                 .anyRequest().authenticated()                 .and()             .formLogin()                 .loginPage(&amp;quot;/login&amp;quot;).permitAll()                 .and()             .logout()                 .permitAll();     }      @Autowired     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {         auth             .inMemoryAuthentication()                 .withUser(&amp;quot;admin&amp;quot;).password(&amp;quot;admin&amp;quot;).roles(&amp;quot;USER&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当配置了上述的javaconfig之后，我们的应用便具备了如下的功能：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;除了“/”,”/home”(首页),”/login”(登录),”/logout”(注销),之外，其他路径都需要认证。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;指定“/login”该路径为登录页面，当未认证的用户尝试访问任何受保护的资源时，都会跳转到“/login”。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;默认指定“/logout”为注销页面&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;配置一个内存中的用户认证器，使用admin/admin作为用户名和密码，具有USER角色&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;防止CSRF攻击&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Session Fixation protection&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Security Header(添加一系列和Header相关的控制)&lt;/p&gt; &lt;ul&gt; &lt;li&gt;HTTP Strict Transport Security for secure requests&lt;/li&gt; &lt;li&gt;集成X-Content-Type-Options&lt;/li&gt; &lt;li&gt;缓存控制&lt;/li&gt; &lt;li&gt;集成X-XSS-Protection.aspx)&lt;/li&gt; &lt;li&gt;X-Frame-Options integration to help prevent Clickjacking(iframe被默认禁止使用)&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;为Servlet API集成了如下的几个方法&lt;/p&gt; &lt;ul&gt; &lt;li&gt;HttpServletRequest#getRemoteUser())&lt;/li&gt; &lt;li&gt;HttpServletRequest.html#getUserPrincipal())&lt;/li&gt; &lt;li&gt;HttpServletRequest.html#isUserInRole(java.lang.String))&lt;/li&gt; &lt;li&gt;HttpServletRequest.html#login(java.lang.String, java.lang.String))&lt;/li&gt; &lt;li&gt;HttpServletRequest.html#logout())&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;一个 Restful 配置&lt;/h3&gt; &lt;p&gt;动态从数据库加载权限，这边因为 &lt;code&gt;Spring Security&lt;/code&gt; 的机制是在模块启动的时候进行加载的，如果想要动态 &lt;code&gt;reload&lt;/code&gt; 权限，调查来看Spring 并没有提供相关的接口，需要动态 &lt;code&gt;reload Spring&lt;/code&gt; 相关的 &lt;code&gt;bean&lt;/code&gt; 是一种比较危险暴力的做法，需要多加注意。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     protected void configure(HttpSecurity http) throws Exception {         List&amp;lt;String&amp;gt; permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL,&amp;quot;/api/*/webjars/**&amp;quot;,&amp;quot;/api/*/swagger**&amp;quot;,&amp;quot;/api/*/swagger-resources/**&amp;quot;,&amp;quot;/api/*/v2/api-docs&amp;quot;);         //load Authorities from DB         ResponseData&amp;lt;List&amp;lt;Authority&amp;gt;&amp;gt; authorityRules = authorityService.getAuthority();                  try {             for (AuthorityModel rule : authorityRules.getData()) {                 //这里因为 Restful API url 有相同的情况，因此需要URL和方法名组合来区分，注意一种特殊情况，                 //因为是使用Ant语法来匹配URL，如果出现请求方法相同且URL是匹配子集关系时，要把最具体的URL放在                 //前面，比如同是 GET 方法，Api1:/api/user/paged 和 Api2:/api/user/{id}，在Ant语法里面                 //Api2是能匹配Api1的，如果用户有Api2的权限而没有Api1的权限，在如下初始化权限时Api2初始化的                 //顺序在Api1的前面，就会导致用户即使没有Api1的权限也能访问，因此要确保数据库加载的时候Api1在Api2                 //前面                 if (HttpMethod.POST.name().equals(rule.getMethod().name())) {                     http                             .authorizeRequests()                             .antMatchers(HttpMethod.POST, rule.getPattern()).hasAuthority(rule.getSystemName());                 } else if (HttpMethod.GET.name().equals(rule.getMethod().name())) {                     http                             .authorizeRequests()                             .antMatchers(HttpMethod.GET, rule.getPattern()).hasAuthority(rule.getSystemName());                 } else if (HttpMethod.PUT.name().equals(rule.getMethod().name())) {                     http                             .authorizeRequests()                             .antMatchers(HttpMethod.PUT, rule.getPattern()).hasAuthority(rule.getSystemName());                 } else if (HttpMethod.DELETE.name().equals(rule.getMethod().name())) {                     http                             .authorizeRequests()                             .antMatchers(HttpMethod.DELETE, rule.getPattern()).hasAuthority(rule.getSystemName());                 }             }         } catch (Exception e) {             log.error(&amp;quot;&amp;quot;, e);         }          http             .csrf().disable() // We don't need CSRF for JWT based authentication             .exceptionHandling().authenticationEntryPoint(this.authenticationEntryPoint)                 //自定义没有权限时的处理，一般根据业务封装自定义的返回结果                 .accessDeniedHandler(ajaxAccessDeniedHandler)              .and()                 //Restful API 完全无状态，使用JWT token                 .sessionManagement()                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)             .and()                 //定义哪些API不需要认证，比如获取 Token 的接口                 .authorizeRequests()                 .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()             .and()                 // Protected API End-points                 .authorizeRequests()                 .antMatchers(API_ROOT_URL, API_ATTACHMENT_URL).authenticated()             .and()                 //处理跨域过滤器                 .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)                 //处理登录获取token的过滤器                 .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)                 //验证用户token，以及用户是否有接口访问权限的过滤器                 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;具体解释见代码上的注释&lt;/p&gt; &lt;h2&gt;@EnableWebSecurity&lt;/h2&gt; &lt;p&gt;我们自己定义的配置类 &lt;code&gt;WebSecurityConfig&lt;/code&gt; 加上了 &lt;code&gt;@EnableWebSecurity&lt;/code&gt; 注解，同时继承了 &lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt;。你可能会在想谁的作用大一点，毫无疑问 &lt;code&gt;@EnableWebSecurity&lt;/code&gt; 起到决定性的配置作用，它其实是个组合注解。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value = { java.lang.annotation.ElementType.TYPE }) @Documented @Import({ WebSecurityConfiguration.class,// &amp;lt;2&amp;gt;   SpringWebMvcImportSelector.class })// &amp;lt;1&amp;gt; @EnableGlobalAuthentication // &amp;lt;3&amp;gt; @Configuration public @interface EnableWebSecurity {  boolean debug() default false; } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&amp;lt;1&amp;gt; &lt;code&gt;@Import&lt;/code&gt; 是 &lt;code&gt;springboot&lt;/code&gt; 提供的用于引入外部的配置的注解，可以理解为：&lt;code&gt;@EnableWebSecurity&lt;/code&gt; 注解激活了 &lt;code&gt;@Import&lt;/code&gt; 注解中包含的配置类。&lt;/li&gt; &lt;li&gt;&amp;lt;2&amp;gt; &lt;code&gt;WebSecurityConfiguration&lt;/code&gt; 顾名思义，是用来配置 &lt;code&gt;web&lt;/code&gt; 安全的，下面的小节会详细介绍。&lt;/li&gt; &lt;li&gt;&amp;lt;3&amp;gt; &lt;code&gt;@EnableGlobalAuthentication&lt;/code&gt; 注解的源码如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Import(AuthenticationConfiguration.class) @Configuration public @interface EnableGlobalAuthentication { } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意点同样在 &lt;code&gt;@Import&lt;/code&gt; 之中，它实际上激活了 &lt;code&gt;AuthenticationConfiguration&lt;/code&gt; 这样的一个配置类，用来配置认证相关的核心类。&lt;/p&gt; &lt;p&gt;也就是说：&lt;code&gt;@EnableWebSecurity&lt;/code&gt;完成的工作便是加载了&lt;code&gt;WebSecurityConfiguration&lt;/code&gt;，&lt;code&gt;AuthenticationConfiguration&lt;/code&gt; 这两个核心配置类，也就此将 &lt;code&gt;spring security&lt;/code&gt; 的职责划分为了配置安全信息，配置认证信息两部分。&lt;/p&gt; &lt;h3&gt;WebSecurityConfiguration&lt;/h3&gt; &lt;p&gt;在这个配置类中，有一个非常重要的Bean被注册了。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration public class WebSecurityConfiguration {  //DEFAULT_FILTER_NAME = &amp;quot;springSecurityFilterChain&amp;quot;  @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)     public Filter springSecurityFilterChain() throws Exception {      ...     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在未使用&lt;code&gt;springboot&lt;/code&gt;之前，大多数人都应该对&lt;code&gt;springSecurityFilterChain&lt;/code&gt;这个名词不会陌生，他是&lt;code&gt;spring security&lt;/code&gt;的核心过滤器，是整个认证的入口。在曾经的&lt;code&gt;XML&lt;/code&gt;配置中，想要启用&lt;code&gt;spring security&lt;/code&gt;，需要在&lt;code&gt;web.xml&lt;/code&gt;中进行如下配置：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-xml"&gt; &amp;lt;!-- Spring Security --&amp;gt;    &amp;lt;filter&amp;gt;        &amp;lt;filter-name&amp;gt;springSecurityFilterChain&amp;lt;/filter-name&amp;gt;        &amp;lt;filter-class&amp;gt;org.springframework.web.filter.DelegatingFilterProxy&amp;lt;/filter-class&amp;gt;    &amp;lt;/filter&amp;gt;     &amp;lt;filter-mapping&amp;gt;        &amp;lt;filter-name&amp;gt;springSecurityFilterChain&amp;lt;/filter-name&amp;gt;        &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;    &amp;lt;/filter-mapping&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;而在&lt;code&gt;springboot&lt;/code&gt;集成之后，这样的&lt;code&gt;XML&lt;/code&gt;被&lt;code&gt;java&lt;/code&gt;配置取代。&lt;code&gt;WebSecurityConfiguration&lt;/code&gt;中完成了声明&lt;code&gt;springSecurityFilterChain&lt;/code&gt;的作用，并且最终交给&lt;code&gt;DelegatingFilterProxy&lt;/code&gt;这个代理类，负责拦截请求（注意&lt;code&gt;DelegatingFilterProxy&lt;/code&gt;这个类不是&lt;code&gt;spring security&lt;/code&gt;包中的，而是存在于&lt;code&gt;web&lt;/code&gt;包中，&lt;code&gt;spring&lt;/code&gt;使用了代理模式来实现安全过滤的解耦）。&lt;/p&gt; &lt;h3&gt;AuthenticationConfiguration&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @Import(ObjectPostProcessorConfiguration.class) public class AuthenticationConfiguration {    @Bean  public AuthenticationManagerBuilder authenticationManagerBuilder(    ObjectPostProcessor&amp;lt;Object&amp;gt; objectPostProcessor) {   return new AuthenticationManagerBuilder(objectPostProcessor);  }    public AuthenticationManager getAuthenticationManager() throws Exception {      ...     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;AuthenticationConfiguration&lt;/code&gt; 的主要任务，便是负责生成全局的身份认证管理者 &lt;code&gt;AuthenticationManager&lt;/code&gt;。还记得在《初识 Spring Security 篇一》中，介绍了 &lt;code&gt;Spring Security&lt;/code&gt; 的认证体系，&lt;code&gt;AuthenticationManager&lt;/code&gt; 便是最核心的身份认证管理器。&lt;/p&gt; &lt;h2&gt;WebSecurityConfigurerAdapter&lt;/h2&gt; &lt;p&gt;适配器模式在 &lt;code&gt;spring&lt;/code&gt; 中被广泛的使用，在配置中使用 &lt;code&gt;Adapter&lt;/code&gt; 的好处便是，我们可以选择性的配置想要修改的那一部分配置，而不用覆盖其他不相关的配置。&lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt; 中我们可以选择自己想要修改的内容，来进行重写，而其提供了三个 &lt;code&gt;configure&lt;/code&gt; 重载方法，是我们主要关心的：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    /**WebSecurityConfigurerAdapter**/   protected void configure(AuthenticationManagerBuilder auth) throws Exception {   this.disableLocalConfigureAuthenticationBldr = true;  }     ...      /**   * Override this method to configure {@link WebSecurity}. For example, if you wish to   * ignore certain requests.   */  public void configure(WebSecurity web) throws Exception {  }  /**   * Override this method to configure the {@link HttpSecurity}. Typically subclasses   * should not invoke this method by calling super as it may override their   * configuration. The default configuration is:   *   * &amp;lt;pre&amp;gt;   * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();   * &amp;lt;/pre&amp;gt;   *   * @param http the {@link HttpSecurity} to modify   * @throws Exception if an error occurs   */  // @formatter:off  protected void configure(HttpSecurity http) throws Exception {   logger.debug(&amp;quot;Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).&amp;quot;);    http    .authorizeRequests()     .anyRequest().authenticated()     .and()    .formLogin().and()    .httpBasic();  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;由参数就可以知道，分别是对 &lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt;，&lt;code&gt;WebSecurity&lt;/code&gt;，&lt;code&gt;HttpSecurity&lt;/code&gt;进行个性化的配置。&lt;/p&gt; &lt;h3&gt;HttpSecurity常用配置&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {        @Override     protected void configure(HttpSecurity http) throws Exception {         http             .authorizeRequests()                 .antMatchers(&amp;quot;/resources/**&amp;quot;, &amp;quot;/signup&amp;quot;, &amp;quot;/about&amp;quot;).permitAll()                 .antMatchers(&amp;quot;/admin/**&amp;quot;).hasRole(&amp;quot;ADMIN&amp;quot;)                 .antMatchers(&amp;quot;/db/**&amp;quot;).access(&amp;quot;hasRole('ADMIN') and hasRole('DBA')&amp;quot;)                 .anyRequest().authenticated()                 .and()             .formLogin()                 .usernameParameter(&amp;quot;username&amp;quot;)                 .passwordParameter(&amp;quot;password&amp;quot;)                 .failureForwardUrl(&amp;quot;/login?error&amp;quot;)                 .loginPage(&amp;quot;/login&amp;quot;)                 .permitAll()                 .and()             .logout()                 .logoutUrl(&amp;quot;/logout&amp;quot;)                 .logoutSuccessUrl(&amp;quot;/index&amp;quot;)                 .permitAll()                 .and()             .httpBasic()                 .disable();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述是一个使用 &lt;code&gt;Java Configuration&lt;/code&gt; 配置 &lt;code&gt;HttpSecurity&lt;/code&gt; 的典型配置，其中 &lt;code&gt;http&lt;/code&gt; 作为根开始配置，每一个 &lt;code&gt;and()&lt;/code&gt; 对应了一个模块的配置（等同于xml配置中的结束标签），并且 &lt;code&gt;and()&lt;/code&gt; 返回了 &lt;code&gt;HttpSecurity&lt;/code&gt; 本身，于是可以连续进行配置。他们配置的含义也非常容易通过变量本身来推测&lt;/p&gt; &lt;ul&gt; &lt;li&gt;authorizeRequests()配置路径拦截，表明路径访问所对应的权限，角色，认证信息。&lt;/li&gt; &lt;li&gt;formLogin()对应表单认证相关的配置&lt;/li&gt; &lt;li&gt;logout()对应了注销相关的配置&lt;/li&gt; &lt;li&gt;httpBasic()可以配置basic登录&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;他们分别代表了 &lt;code&gt;http&lt;/code&gt; 请求相关的安全配置，这些配置项无一例外的返回了 &lt;code&gt;Configurer&lt;/code&gt; 类，而所有的 &lt;code&gt;http&lt;/code&gt; 相关配置可以通过查看&lt;code&gt;HttpSecurity&lt;/code&gt;的主要方法得知：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; public LogoutConfigurer&amp;lt;HttpSecurity&amp;gt; logout() throws Exception {   return getOrApply(new LogoutConfigurer&amp;lt;HttpSecurity&amp;gt;());  }     ......      public CsrfConfigurer&amp;lt;HttpSecurity&amp;gt; csrf() throws Exception {   ApplicationContext context = getContext();   return getOrApply(new CsrfConfigurer&amp;lt;HttpSecurity&amp;gt;(context));  }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;需要对 &lt;code&gt;http&lt;/code&gt; 协议有一定的了解才能完全掌握所有的配置，不过，&lt;code&gt;springboot&lt;/code&gt;和&lt;code&gt;spring security&lt;/code&gt;的自动配置已经足够使用了。其中每一项 &lt;code&gt;Configurer&lt;/code&gt;（e.g.FormLoginConfigurer,CsrfConfigurer）都是 &lt;code&gt;HttpConfigurer&lt;/code&gt; 的细化配置项。&lt;/p&gt; &lt;h3&gt;WebSecurityBuilder&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override     public void configure(WebSecurity web) throws Exception {         web             .ignoring()             .antMatchers(&amp;quot;/resources/**&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以笔者的经验，这个配置中并不会出现太多的配置信息。&lt;/p&gt; &lt;h3&gt;AuthenticationManagerBuilder&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {          @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth             .inMemoryAuthentication()             .withUser(&amp;quot;admin&amp;quot;).password(&amp;quot;admin&amp;quot;).roles(&amp;quot;USER&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;想要在 &lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt; 中进行认证相关的配置，可以使用 &lt;code&gt;configure(AuthenticationManagerBuilder auth)&lt;/code&gt; 暴露一个 &lt;code&gt;AuthenticationManager&lt;/code&gt; 的建造器：&lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt; 。如上所示，我们便完成了内存中用户的配置。&lt;/p&gt; &lt;p&gt;细心的朋友会发现，在前面的文章中我们配置内存中的用户时，似乎不是这么配置的，而是：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Autowired     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {         auth             .inMemoryAuthentication()                 .withUser(&amp;quot;admin&amp;quot;).password(&amp;quot;admin&amp;quot;).roles(&amp;quot;USER&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果你的应用只有唯一一个 &lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt; ，那么他们之间的差距可以被忽略，从方法名可以看出两者的区别：使用&lt;code&gt;@Autowired&lt;/code&gt;注入的 &lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt; 是全局的身份认证器，作用域可以跨越多个&lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt;，以及影响到基于&lt;code&gt;Method&lt;/code&gt;的安全控制；而 &lt;code&gt;protected configure()&lt;/code&gt;的方式则类似于一个匿名内部类，它的作用域局限于一个&lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt;内部。关于这一点的区别，可以参考我曾经提出的issuespring-security#issues4571。官方文档中，也给出了配置多个&lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt;的场景以及&lt;code&gt;demo&lt;/code&gt;，将在该系列的后续文章中解读&lt;/p&gt; &lt;p&gt;原文链接：https://www.cnkirito.moe/2017/09/20/spring-security-3/&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 15 Jun 2019 08:15:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Security 入门上手篇二</title>
      <link>https://www.zhangaoo.com/article/spring-security-second</link>
      <content:encoded>&lt;h1&gt;Spring Security 入门上手&lt;/h1&gt; &lt;h2&gt;依赖引入&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;parent&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;         &amp;lt;version&amp;gt;1.5.20.RELEASE&amp;lt;/version&amp;gt;         &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;     &amp;lt;/parent&amp;gt;      &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;     &amp;lt;artifactId&amp;gt;spring-security-guide&amp;lt;/artifactId&amp;gt;     &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;name&amp;gt;spring-security-guide&amp;lt;/name&amp;gt;     &amp;lt;description&amp;gt;Demo project for Spring Security&amp;lt;/description&amp;gt;      &amp;lt;properties&amp;gt;         &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;     &amp;lt;/properties&amp;gt;      &amp;lt;dependencies&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-starter-thymeleaf&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt;      &amp;lt;build&amp;gt;         &amp;lt;plugins&amp;gt;             &amp;lt;plugin&amp;gt;                 &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;             &amp;lt;/plugin&amp;gt;         &amp;lt;/plugins&amp;gt;     &amp;lt;/build&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2.2 创建一个不受安全限制的web应用&lt;/h2&gt; &lt;p&gt;这是一个首页，不受安全限制&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html xmlns=&amp;quot;http://www.w3.org/1999/xhtml&amp;quot; xmlns:th=&amp;quot;http://www.thymeleaf.org&amp;quot; xmlns:sec=&amp;quot;http://www.thymeleaf.org/thymeleaf-extras-springsecurity3&amp;quot;&amp;gt; &amp;lt;head&amp;gt;     &amp;lt;title&amp;gt;Spring Security Example&amp;lt;/title&amp;gt; &amp;lt;/head&amp;gt; &amp;lt;body&amp;gt; &amp;lt;h1&amp;gt;Welcome!&amp;lt;/h1&amp;gt;  &amp;lt;p&amp;gt;Click &amp;lt;a th:href=&amp;quot;@{/hello}&amp;quot;&amp;gt;here&amp;lt;/a&amp;gt; to see a greeting.&amp;lt;/p&amp;gt; &amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个简单的页面上包含了一个链接，跳转到”/hello”。对应如下的页面&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html xmlns=&amp;quot;http://www.w3.org/1999/xhtml&amp;quot; xmlns:th=&amp;quot;http://www.thymeleaf.org&amp;quot;       xmlns:sec=&amp;quot;http://www.thymeleaf.org/thymeleaf-extras-springsecurity3&amp;quot;&amp;gt;     &amp;lt;head&amp;gt;         &amp;lt;title&amp;gt;Hello World!&amp;lt;/title&amp;gt;     &amp;lt;/head&amp;gt;     &amp;lt;body&amp;gt;         &amp;lt;h1&amp;gt;Hello world!&amp;lt;/h1&amp;gt;     &amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;接下来配置Spring MVC，使得我们能够访问到页面。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration public class MvcConfig extends WebMvcConfigurerAdapter {     @Override     public void addViewControllers(ViewControllerRegistry registry) {         registry.addViewController(&amp;quot;/home&amp;quot;).setViewName(&amp;quot;home&amp;quot;);         registry.addViewController(&amp;quot;/&amp;quot;).setViewName(&amp;quot;home&amp;quot;);         registry.addViewController(&amp;quot;/hello&amp;quot;).setViewName(&amp;quot;hello&amp;quot;);         registry.addViewController(&amp;quot;/login&amp;quot;).setViewName(&amp;quot;login&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2.3 配置 Spring Security&lt;/h2&gt; &lt;p&gt;一个典型的安全配置如下所示：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Configuration @EnableWebSecurity &amp;lt;1&amp;gt; public class WebSecurityConfig extends WebSecurityConfigurerAdapter { &amp;lt;1&amp;gt;     @Override     protected void configure(HttpSecurity http) throws Exception {         http &amp;lt;2&amp;gt;             .authorizeRequests()                 .antMatchers(&amp;quot;/&amp;quot;, &amp;quot;/home&amp;quot;).permitAll()                 .anyRequest().authenticated()                 .and()             .formLogin()                 .loginPage(&amp;quot;/login&amp;quot;)                 .permitAll()                 .and()             .logout()                 .permitAll();     }      @Autowired     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {         auth &amp;lt;3&amp;gt;             .inMemoryAuthentication()                 .withUser(&amp;quot;admin&amp;quot;).password(&amp;quot;admin&amp;quot;).roles(&amp;quot;USER&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;&amp;lt;1&amp;gt; &lt;code&gt;@EnableWebSecurity&lt;/code&gt; 注解使得 &lt;code&gt;SpringMVC&lt;/code&gt; 集成了 &lt;code&gt;Spring Security&lt;/code&gt; 的 &lt;code&gt;web&lt;/code&gt; 安全支持。另外，&lt;code&gt;WebSecurityConfig&lt;/code&gt; 配置类同时集成了 &lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt; ，重写了其中的特定方法，用于自定义 &lt;code&gt;Spring Security&lt;/code&gt; 配置。整个 &lt;code&gt;Spring Security&lt;/code&gt; 的工作量，其实都是集中在该配置类，不仅仅是这个 &lt;code&gt;guides&lt;/code&gt;，实际项目中也是如此。&lt;/li&gt; &lt;li&gt;&amp;lt;2&amp;gt; &lt;code&gt;configure(HttpSecurity)&lt;/code&gt; 定义了哪些URL路径应该被拦截，如字面意思所描述：”/“, “/home”允许所有人访问，”/login”作为登录入口，也被允许访问，而剩下的”/hello”则需要登陆后才可以访问。&lt;/li&gt; &lt;li&gt;&amp;lt;3&amp;gt; &lt;code&gt;configureGlobal(AuthenticationManagerBuilder)&lt;/code&gt; 在内存中配置一个用户，&lt;code&gt;admin/admin&lt;/code&gt; 分别是用户名和密码，这个用户拥有 &lt;code&gt;USER&lt;/code&gt; 角色。&lt;/li&gt; &lt;li&gt;以上配置隐含部分登录登出的细节，配置了使用Form表单方式登录，登录的接口是 /login&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;创建登录页面如下&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html xmlns=&amp;quot;http://www.w3.org/1999/xhtml&amp;quot; xmlns:th=&amp;quot;http://www.thymeleaf.org&amp;quot;       xmlns:sec=&amp;quot;http://www.thymeleaf.org/thymeleaf-extras-springsecurity3&amp;quot;&amp;gt;     &amp;lt;head&amp;gt;         &amp;lt;title&amp;gt;Spring Security Example &amp;lt;/title&amp;gt;     &amp;lt;/head&amp;gt;     &amp;lt;body&amp;gt;         &amp;lt;div th:if=&amp;quot;${param.error}&amp;quot;&amp;gt;             Invalid username and password.         &amp;lt;/div&amp;gt;         &amp;lt;div th:if=&amp;quot;${param.logout}&amp;quot;&amp;gt;             You have been logged out.         &amp;lt;/div&amp;gt;         &amp;lt;form th:action=&amp;quot;@{/login}&amp;quot; method=&amp;quot;post&amp;quot;&amp;gt;             &amp;lt;div&amp;gt;&amp;lt;label&amp;gt; User Name : &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;username&amp;quot;/&amp;gt; &amp;lt;/label&amp;gt;&amp;lt;/div&amp;gt;             &amp;lt;div&amp;gt;&amp;lt;label&amp;gt; Password: &amp;lt;input type=&amp;quot;password&amp;quot; name=&amp;quot;password&amp;quot;/&amp;gt; &amp;lt;/label&amp;gt;&amp;lt;/div&amp;gt;             &amp;lt;div&amp;gt;&amp;lt;input type=&amp;quot;submit&amp;quot; value=&amp;quot;Sign In&amp;quot;/&amp;gt;&amp;lt;/div&amp;gt;         &amp;lt;/form&amp;gt;     &amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个 &lt;code&gt;Thymeleaf&lt;/code&gt; 模板提供了一个用于提交用户名和密码的表单,其中 name=”username”，name=”password” 是默认的表单值，并发送到“/login”。 在默认配置中，Spring Security 提供了一个拦截该请求并验证用户的过滤器。 如果验证失败，该页面将重定向到“/login?error”，并显示相应的错误消息。 当用户选择注销，请求会被发送到“/login?logout”。&lt;/p&gt; &lt;p&gt;在 hello.html 添加一些内容，用于展示用户信息。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html xmlns=&amp;quot;http://www.w3.org/1999/xhtml&amp;quot; xmlns:th=&amp;quot;http://www.thymeleaf.org&amp;quot;       xmlns:sec=&amp;quot;http://www.thymeleaf.org/thymeleaf-extras-springsecurity3&amp;quot;&amp;gt; &amp;lt;head&amp;gt;     &amp;lt;title&amp;gt;Hello World!&amp;lt;/title&amp;gt; &amp;lt;/head&amp;gt; &amp;lt;body&amp;gt; &amp;lt;h1 th:inline=&amp;quot;text&amp;quot;&amp;gt;Hello [[${#httpServletRequest.remoteUser}]]!&amp;lt;/h1&amp;gt; &amp;lt;form th:action=&amp;quot;@{/logout}&amp;quot; method=&amp;quot;post&amp;quot;&amp;gt;     &amp;lt;input type=&amp;quot;submit&amp;quot; value=&amp;quot;Sign Out&amp;quot;/&amp;gt; &amp;lt;/form&amp;gt; &amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们使用 Spring Security 之后，HttpServletRequest#getRemoteUser() 可以用来获取用户名。 登出请求将被发送到“/logout”。 成功注销后，会将用户重定向到“/login?logout”。&lt;/p&gt; &lt;h2&gt;添加启动类&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@SpringBootApplication public class GuideApplication {     public static void main(String[] args) {         SpringApplication.run(GuideApplication.class, args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;添加配置文件&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   application:     name: spring-security-guide server:   port: 8990 &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;github 源码地址&lt;/h2&gt; &lt;p&gt;&lt;a href="https://github.com/zealzhangz/spring-security-guide" target="_blank"&gt;github 源码地址&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;测试&lt;/h2&gt; &lt;p&gt;访问地址 &lt;code&gt;http://localhost:8990/&lt;/code&gt;&lt;/p&gt; &lt;p&gt;原文链接：http://blog.didispace.com/xjf-spring-security-2&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 15 Jun 2019 07:18:00 GMT</pubDate>
    </item>
    <item>
      <title>初识 Spring Security 篇一</title>
      <link>https://www.zhangaoo.com/article/spring-security-first</link>
      <content:encoded>&lt;h1&gt;Spring Security&lt;/h1&gt; &lt;p&gt;学习 Spring Security 用法，架构、设计模式等。&lt;/p&gt; &lt;h1&gt;核心组件&lt;/h1&gt; &lt;p&gt;主要介绍 &lt;code&gt;Spring Security&lt;/code&gt; 中常见核心 &lt;code&gt;Java&lt;/code&gt; 类以及他们之间的依赖关系，以及整个架构的设计原理。&lt;/p&gt; &lt;h2&gt;SecurityContextHolder&lt;/h2&gt; &lt;p&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt; 用于存储安全上下文&lt;code&gt;（security context）&lt;/code&gt;的信息。当前操作的用户是谁，该用户是否已经被认证，他拥有哪些角色权限…这些都被保存在 &lt;code&gt;SecurityContextHolder&lt;/code&gt;中。&lt;code&gt;SecurityContextHolder&lt;/code&gt; 默认使用 &lt;code&gt;ThreadLocal&lt;/code&gt; 策略来存储认证信息。看到 &lt;code&gt;ThreadLocal&lt;/code&gt; 也就意味着，这是一种与线程绑定的策略。&lt;code&gt;Spring Security&lt;/code&gt; 在用户登录时自动绑定认证信息到当前线程，在用户退出时，自动清除当前线程的认证信息。但这一切的前提，是你在 &lt;code&gt;web&lt;/code&gt; 场景下使用 &lt;code&gt;Spring Security&lt;/code&gt;，而如果是 &lt;code&gt;Swing&lt;/code&gt; &lt;code&gt;界面，Spring&lt;/code&gt; 也提供了支持，&lt;code&gt;SecurityContextHolder&lt;/code&gt; 的策略则需要被替换，鉴于我的初衷是基于 &lt;code&gt;web&lt;/code&gt; 来介绍 &lt;code&gt;Spring Security&lt;/code&gt; ，所以这里以及后续，非 &lt;code&gt;web&lt;/code&gt; 的相关的内容都一笔带过。&lt;/p&gt; &lt;h3&gt;获取当前用户的信息&lt;/h3&gt; &lt;p&gt;因为身份信息是与线程绑定的，所以可以在程序的任何地方使用静态方法获取用户信息。一个例子如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();  if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;getAuthentication()&lt;/code&gt; 返回了认证信息，再次 &lt;code&gt;getPrincipal()&lt;/code&gt; 返回了身份信息，&lt;code&gt;UserDetails&lt;/code&gt; 便是 &lt;code&gt;Spring&lt;/code&gt; 对身份信息封装的一个接口。&lt;code&gt;Authentication&lt;/code&gt; 和 &lt;code&gt;UserDetails&lt;/code&gt; 的介绍在下面的小节具体讲解，本节重要的内容是介绍 &lt;code&gt;SecurityContextHolder&lt;/code&gt; 这个容器。&lt;/p&gt; &lt;h2&gt;Authentication&lt;/h2&gt; &lt;p&gt;直接上源码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package org.springframework.security.core;// &amp;lt;1&amp;gt;  public interface Authentication extends Principal, Serializable { // &amp;lt;1&amp;gt;     Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities(); // &amp;lt;2&amp;gt;      Object getCredentials();// &amp;lt;2&amp;gt;      Object getDetails();// &amp;lt;2&amp;gt;      Object getPrincipal();// &amp;lt;2&amp;gt;      boolean isAuthenticated();// &amp;lt;2&amp;gt;      void setAuthenticated(boolean var1) throws IllegalArgumentException; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&amp;lt;1&amp;gt; Authentication 是 spring security 包中的接口，直接继承自 Principal 类，而 Principal 是位于 java.security 包中的。可以见得，Authentication 在 spring security 中是最高级别的身份/认证的抽象。&lt;/p&gt; &lt;p&gt;&amp;lt;2&amp;gt; 由这个顶级接口，我们可以得到用户拥有的权限信息列表，密码，用户细节信息，用户身份信息，认证信息。&lt;/p&gt; &lt;p&gt;还记得1.1节中，authentication.getPrincipal()返回了一个 Object，我们将 Principal 强转成了 Spring Security 中最常用的UserDetails，这在 Spring Security 中非常常见，接口返回 Object，使用 instanceof 判断类型，强转成对应的具体实现类。接口详细解读如下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;getAuthorities()，权限信息列表，默认是 GrantedAuthority 接口的一些实现类，通常是代表权限信息的一系列字符串。&lt;/li&gt; &lt;li&gt;getCredentials()，密码信息，用户输入的密码字符串，在认证过后通常会被移除，用于保障安全。&lt;/li&gt; &lt;li&gt;getDetails()，细节信息，web 应用中的实现接口通常为 WebAuthenticationDetails，它记录了访问者的 ip 地址和 sessionId 的值。&lt;/li&gt; &lt;li&gt;getPrincipal()，敲黑板！！！最重要的身份信息，大部分情况下返回的是 UserDetails 接口的实现类，也是框架中的常用接口之一。UserDetails 接口将会在下面的小节重点介绍。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Spring Security是如何完成身份认证的？&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;用户名和密码被过滤器获取到，封装成 &lt;code&gt;Authentication&lt;/code&gt; ,通常情况下是 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 这个实现类。&lt;/li&gt; &lt;li&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt; 身份管理器负责验证这个 &lt;code&gt;Authentication&lt;/code&gt;&lt;/li&gt; &lt;li&gt;认证成功后，&lt;code&gt;AuthenticationManager&lt;/code&gt; 身份管理器返回一个被填充满了信息的（包括上面提到的权限信息，身份信息，细节信息，但密码通常会被移除）&lt;code&gt;Authentication&lt;/code&gt; 实例。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt; 安全上下文容器将第3步填充了信息的 &lt;code&gt;Authentication&lt;/code&gt;，通过 &lt;code&gt;SecurityContextHolder.getContext().setAuthentication(…)&lt;/code&gt; 方法，设置到其中。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;这是一个抽象的认证流程，而整个过程中，如果不纠结于细节，其实只剩下一个 &lt;code&gt;AuthenticationManager&lt;/code&gt; 是我们没有接触过的了，这个身份管理器我们在后面的小节介绍。&lt;/p&gt; &lt;h2&gt;AuthenticationManager&lt;/h2&gt; &lt;p&gt;初次接触 &lt;code&gt;Spring Security&lt;/code&gt; 的朋友相信会被 &lt;code&gt;AuthenticationManager&lt;/code&gt;，&lt;code&gt;ProviderManager&lt;/code&gt; ，&lt;code&gt;AuthenticationProvider&lt;/code&gt; …这么多相似的 &lt;code&gt;Spring&lt;/code&gt; 认证类搞得晕头转向，但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。&lt;code&gt;AuthenticationManager&lt;/code&gt;（接口）是认证相关的核心接口，也是发起认证的出发点，因为在实际需求中，我们可能会允许用户使用用户名+密码登录，同时允许用户使用邮箱+密码，手机号码+密码登录，甚至，可能允许用户使用指纹登录（还有这样的操作？没想到吧），所以说 &lt;code&gt;AuthenticationManager&lt;/code&gt; 一般不直接认证，&lt;code&gt;AuthenticationManager&lt;/code&gt; 接口的常用实现类 &lt;code&gt;ProviderManager&lt;/code&gt; 内部会维护一个 &lt;code&gt;List&amp;lt;AuthenticationProvider&amp;gt;&lt;/code&gt; 列表，存放多种认证方式，实际上这是委托者模式的应用（&lt;code&gt;Delegate&lt;/code&gt;）。也就是说，核心的认证入口始终只有一个：&lt;code&gt;AuthenticationManager&lt;/code&gt;，不同的认证方式：用户名+密码（&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;），邮箱+密码，手机号码+密码登录则对应了三个 &lt;code&gt;AuthenticationProvider&lt;/code&gt;。这样一来四不四就好理解多了？熟悉 &lt;code&gt;shiro&lt;/code&gt; 的朋友可以把&lt;code&gt;AuthenticationProvider&lt;/code&gt; 理解成 &lt;code&gt;Realm&lt;/code&gt;。在默认策略下，只需要通过一个 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 的认证，即可被认为是登录成功。&lt;/p&gt; &lt;p&gt;ProviderManager 关键部分源码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class ProviderManager implements AuthenticationManager, MessageSourceAware,   InitializingBean {      // 维护一个AuthenticationProvider列表     private List&amp;lt;AuthenticationProvider&amp;gt; providers = Collections.emptyList();                public Authentication authenticate(Authentication authentication)           throws AuthenticationException {        Class&amp;lt;? extends Authentication&amp;gt; toTest = authentication.getClass();        AuthenticationException lastException = null;        Authentication result = null;         // 依次认证        for (AuthenticationProvider provider : getProviders()) {           if (!provider.supports(toTest)) {              continue;           }           try {              result = provider.authenticate(authentication);               if (result != null) {                 copyDetails(authentication, result);                 break;              }           }           ...           catch (AuthenticationException e) {              lastException = e;           }        }        // 如果有Authentication信息，则直接返回        if (result != null) {    if (eraseCredentialsAfterAuthentication      &amp;amp;&amp;amp; (result instanceof CredentialsContainer)) {                 //移除密码     ((CredentialsContainer) result).eraseCredentials();    }              //发布登录成功事件    eventPublisher.publishAuthenticationSuccess(result);    return result;     }     ...        //执行到此，说明没有认证成功，包装异常信息        if (lastException == null) {           lastException = new ProviderNotFoundException(messages.getMessage(                 &amp;quot;ProviderManager.providerNotFound&amp;quot;,                 new Object[] { toTest.getName() },                 &amp;quot;No AuthenticationProvider found for {0}&amp;quot;));        }        prepareException(lastException, authentication);        throw lastException;     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;ProviderManager&lt;/code&gt; 中的 &lt;code&gt;List&lt;/code&gt; ，会依照次序去认证，认证成功则立即返回，若认证失败则返回 &lt;code&gt;null&lt;/code&gt;，下一个 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 会继续尝试认证，如果所有认证器都无法认证成功，则 &lt;code&gt;ProviderManager&lt;/code&gt; &lt;code&gt;会抛出一个ProviderNotFoundException&lt;/code&gt; 异常。&lt;/p&gt; &lt;p&gt;以上已经把 Spring Security 的整个认证流程都讲述了一遍，简单小结如下：身份信息的存放容器 &lt;code&gt;SecurityContextHolder&lt;/code&gt; ，身份信息的抽象 &lt;code&gt;Authentication&lt;/code&gt; ，身份认证器 &lt;code&gt;AuthenticationManager&lt;/code&gt; 及其认证流程。姑且在这里做一个分隔线。下面来介绍下 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 接口的具体实现。&lt;/p&gt; &lt;h2&gt;DaoAuthenticationProvider&lt;/h2&gt; &lt;p&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt; 最最最常用的一个实现便是 &lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; 。顾名思义，&lt;code&gt;Dao&lt;/code&gt; 正是数据访问层的缩写，也暗示了这个身份认证器的实现思路。由于本文是一个 &lt;code&gt;Overview&lt;/code&gt; ，姑且只给出其 &lt;code&gt;UML&lt;/code&gt; 类图： &lt;img src="http://img.zhangaoo.com/2019415194423-spring-security-DaoAuthenticationProvider.jpg" alt="2019415194423-spring-security-DaoAuthenticationProvider" /&gt;&lt;/p&gt; &lt;p&gt;按照我们最直观的思路，怎么去认证一个用户呢？用户前台提交了用户名和密码，而数据库中保存了用户名和密码，认证便是负责比对同一个用户名，提交的密码和保存的密码是否相同便是了。在&lt;code&gt;Spring Security&lt;/code&gt; 中。提交的用户名和密码，被封装成了&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; ，而根据用户名加载用户的任务则是交给了 &lt;code&gt;UserDetailsService&lt;/code&gt; ，在&lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; 中，对应的方法便是 &lt;code&gt;retrieveUser&lt;/code&gt; ，虽然有两个参数，但是 &lt;code&gt;retrieveUser&lt;/code&gt; 只有第一个参数起主要作，返回一个 &lt;code&gt;UserDetails&lt;/code&gt;。还需要完成 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 和 &lt;code&gt;UserDetails&lt;/code&gt; 密码的比对，这便是交给 &lt;code&gt;additionalAuthenticationChecks&lt;/code&gt; 方法完成的，如果这个 &lt;code&gt;void&lt;/code&gt; 方法没有抛异常，则认为比对成功。比对密码的过程，用到了&lt;code&gt;PasswordEncoder&lt;/code&gt; 和 &lt;code&gt;SaltSource&lt;/code&gt; ，密码加密和盐的概念相信不用我赘述了，它们为保障安全而设计，都是比较基础的概念。&lt;/p&gt; &lt;p&gt;如果你已经被这些概念搞得晕头转向了，不妨这么理解 &lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; ：它获取用户提交的用户名和密码，比对其正确性，如果正确，返回一个数据库中的用户信息（假设用户信息被保存在数据库中）。&lt;/p&gt; &lt;h2&gt;UserDetails 与 UserDetailsService&lt;/h2&gt; &lt;p&gt;上面不断提到了 &lt;code&gt;UserDetails&lt;/code&gt; 这个接口，它代表了最详细的用户信息，这个接口涵盖了一些必要的用户信息字段，具体的实现类对它进行了扩展。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface UserDetails extends Serializable {     Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();     String getPassword();     String getUsername();     boolean isAccountNonExpired();     boolean isAccountNonLocked();     boolean isCredentialsNonExpired();     boolean isEnabled(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;它和 &lt;code&gt;Authentication&lt;/code&gt; 接口很类似，比如它们都拥有 &lt;code&gt;username&lt;/code&gt; ，&lt;code&gt;authorities&lt;/code&gt; ，区分他们也是本文的重点内容之一。&lt;code&gt;Authentication&lt;/code&gt; 的 &lt;code&gt;getCredentials()&lt;/code&gt; 与 &lt;code&gt;UserDetails&lt;/code&gt; 中的 &lt;code&gt;getPassword()&lt;/code&gt; 需要被区分对待，前者是用户提交的密码凭证，后者是用户正确的密码，认证器其实就是对这两者的比对。&lt;code&gt;Authentication&lt;/code&gt; 中的 &lt;code&gt;getAuthorities()&lt;/code&gt; 实际是由 &lt;code&gt;UserDetails&lt;/code&gt; 的 &lt;code&gt;getAuthorities()&lt;/code&gt; 传递而形成的。还记得 &lt;code&gt;Authentication&lt;/code&gt; 接口中的 &lt;code&gt;getUserDetails()&lt;/code&gt; 方法吗？其中的 &lt;code&gt;UserDetails&lt;/code&gt; 用户详细信息便是经过了 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 之后被填充的。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface UserDetailsService {    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;UserDetailsService&lt;/code&gt; 和 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 两者的职责常常被人们搞混，关于他们的问题在文档的 &lt;code&gt;FAQ&lt;/code&gt; 和 &lt;code&gt;issues&lt;/code&gt; 中屡见不鲜。记住一点即可，敲黑板！！！&lt;code&gt;UserDetailsService&lt;/code&gt; 只负责从特定的地方（通常是数据库）加载用户信息，仅此而已，记住这一点，可以避免走很多弯路。&lt;code&gt;UserDetailsService&lt;/code&gt; 常见的实现类有 &lt;code&gt;JdbcDaoImpl&lt;/code&gt;，&lt;code&gt;InMemoryUserDetailsManager&lt;/code&gt;，前者从数据库加载用户，后者从内存中加载用户，也可以自己实现 &lt;code&gt;UserDetailsService&lt;/code&gt;，通常这更加灵活。&lt;/p&gt; &lt;h2&gt;架构概览图&lt;/h2&gt; &lt;p&gt;为了更加形象的理解上述我介绍的这些核心类，附上一张按照我的理解，所画出 &lt;code&gt;Spring Security&lt;/code&gt; 的一张非典型的 &lt;code&gt;UML&lt;/code&gt; 图&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.2cto.com/uploadfile/2018/0802/20180802094833581.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;整个Spring Security 都是围绕以上这张架构图展开的，最顶层为最核心最抽象的 &lt;code&gt;AuthenticationManager&lt;/code&gt; 接口，  &lt;code&gt;ProviderManager&lt;/code&gt; 为 &lt;code&gt;AuthenticationManager&lt;/code&gt; 的一个具体实现，功能如其名字他的作用是管理 &lt;code&gt;Provider&lt;/code&gt; 的， &lt;code&gt;AuthenticationProvider&lt;/code&gt; 才是真正认证的接口，因此我们在实践中要实现我们自己的认证方式，也就是 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 的一个具体实现。当然可以实现多个，如果有多种认证方式，现实中往往也是有多种认证方式。&lt;/p&gt; &lt;p&gt;注意 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 接口中的 &lt;code&gt;supports&lt;/code&gt; 方法，&lt;code&gt;ProviderManager&lt;/code&gt; 里面其实是维护了一个 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 的 &lt;code&gt;List&lt;/code&gt; 因此到底使用哪个  &lt;code&gt;Provider&lt;/code&gt; 来做验证，可以使用 &lt;code&gt;Filter&lt;/code&gt; 返回的 &lt;code&gt;Authentication&lt;/code&gt; 实现类来限定，部分代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//Filter 代码     @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)             throws AuthenticationException, IOException, ServletException {             ......         return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));     } //Provider 代码     @Override     public boolean supports(Class&amp;lt;?&amp;gt; authentication) {         return (JwtAuthenticationToken.class.isAssignableFrom(authentication));     }  //在 ProviderManager 中验证时有如下代码 for (AuthenticationProvider provider : getProviders()) {    if (!provider.supports(toTest)) {       continue;    } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果对&lt;code&gt;Spring Security&lt;/code&gt; 的这些概念感到理解不能，不用担心，因为这是 &lt;code&gt;Architecture First&lt;/code&gt; 导致的必然结果，先过个眼熟。后续的文章会秉持 &lt;code&gt;Code First&lt;/code&gt; 的理念，陆续详细地讲解这些实现类的使用场景，源码分析，以及最基本的：如何配置 &lt;code&gt;Spring Security&lt;/code&gt; ，在后面的文章中可以不时翻看这篇文章，找到具体的类在整个架构中所处的位置，这也是本篇文章的定位。另外，一些 &lt;code&gt;Spring Security&lt;/code&gt; 的过滤器还未囊括在架构概览中，如将表单信息包装成 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 的过滤器，考虑到这些虽然也是架构的一部分，但是真正重写他们的可能性较小，所以打算放到后面的章节讲解。&lt;/p&gt; &lt;p&gt;原文链接：http://blog.didispace.com/xjf-spring-security-1/&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 15 Jun 2019 07:00:00 GMT</pubDate>
    </item>
    <item>
      <title>《时生》读书笔记</title>
      <link>https://www.zhangaoo.com/article/toki-o</link>
      <content:encoded>&lt;h1&gt;《时生》读书笔记&lt;/h1&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201969171310-shisheng.jpg" alt="201969171310-shisheng" /&gt;&lt;/p&gt; &lt;p&gt;悲观也没用。谁都想生在好人家，可无法选择父母。发给你什么样的牌，你就只能尽量打好它。&lt;/p&gt; &lt;p&gt;然而，变化的确还是降临了。一家人如铁链般连在一起的心，渐渐地开始脱钩。&lt;/p&gt; &lt;p&gt;“名字就叫时生。”宫本抱着刚出生的孩子道，“时间的时，出生的生，可以吧？”&lt;/p&gt; &lt;p&gt;现在你只顾眼前，才说这样的话，但人会随着时间而改变。刚开始，你会觉得只要两个人在一起就行，可时间一长，就会想要孩子了，尤其是朋友、亲戚家里添了小孩的时候。&lt;/p&gt; &lt;p&gt;她的出走并非无缘无故。她直到现在才突然离开，应该说是自己的幸运了，但想不通她为什么走得这么突然。&lt;/p&gt; &lt;p&gt;梦总是突然醒的，就像泡沫一般，越吹越大，最后啪地破灭，什么也没有，除了空虚。没有脚踏实地建立起来的东西，就无法形成精神和物质上的支撑。&lt;/p&gt; &lt;p&gt;确信自己喜欢的人能好好地活着，即便面对死亡，也看到了未来。对你父亲来说，你母亲就是未来。人不论在什么时候都会感受到未来。无论是怎样短暂的一个瞬间，只要有活着的感觉，就有未来。我告诉你，未来不仅仅是明天。未来在人心中。&lt;/p&gt; &lt;p&gt;只要心中有未来，人就能幸福起来。因为有人教了你母亲这个，她才将你生下来。可你看看自己，整天牢骚满腹，不思进取。你感受不到未来不能怪别人，要怪你自己，因为你是个浑蛋！”&lt;/p&gt; &lt;p&gt;明明知道会发生什么，却什么也不做，也办不到。&lt;/p&gt; &lt;p&gt;他知道过去是改变不了的，但无法袖手旁观。&lt;/p&gt; &lt;p&gt;他对我说：“一定要努力活下去，因为有美好的人生在等着你。”&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 09 Jun 2019 09:13:00 GMT</pubDate>
    </item>
    <item>
      <title>《最怕你一生碌碌而为，还安慰自己平凡可贵》读书笔记</title>
      <link>https://www.zhangaoo.com/article/busy-loser</link>
      <content:encoded>&lt;h1&gt;最怕你一生碌碌而为，还安慰自己平凡可贵&lt;/h1&gt; &lt;h2&gt;体面的爱情&lt;/h2&gt; &lt;p&gt;其实，任何体面的爱情都要求双方是势均力敌、旗鼓相当。&lt;/p&gt; &lt;p&gt;凭什么会和一个邋遢、懒惰的你厮守一辈子?难道就因为你想得。&lt;/p&gt; &lt;p&gt;你想要体面的爱情，就得先努力让自己成为一个体面的人。幸福的路，没有捷径&lt;/p&gt; &lt;p&gt;你所谓的体面，不过是清闲的日子，不过是你心底按捺不住的懒!你所引以为傲的体面，不过是你自导自演的幻影，不过是你灵魂中溢出来的&lt;/p&gt; &lt;p&gt;在这个平凡的世界里，拒绝平庸才是我们体面生活的&lt;/p&gt; &lt;h2&gt;拒绝平庸&lt;/h2&gt; &lt;p&gt;更可怕的是，你的身体就像一块劣质的电池，充电需要充很久，不但充不满，而且用得快。&lt;/p&gt; &lt;p&gt;你看，你学了二十几年的对错，可现如今才明白，现实只讲输赢。&lt;/p&gt; &lt;p&gt;在我看来，保持热情，保持好奇心，努力争取正当的财富，实实在在地做事，而且是勤勤恳恳地去做，而不是在空谈道理。这就是最有尊严、最体面的生活。&lt;/p&gt; &lt;p&gt;真正让人绝望的，并不是未知的险境、艰辛，也不是世人的褊狭和一事无成的狼狈，而是终其一生，你都不知道自己究竟想要什么。&lt;/p&gt; &lt;p&gt;你把你的未来，统统都交给一个人。你说这是爱，还是懒？&lt;/p&gt; &lt;p&gt;还有一股暗黑的力量就是，如果现在不努力，怎么能让前任后悔，让暗恋开口，让现任长脸！&lt;/p&gt; &lt;p&gt;你得过且过从春天走来，你大概过不完冬天就要离开。&lt;/p&gt; &lt;p&gt;有用的社交，一定势均力敌，同等的实力，才能平等地对话。&lt;/p&gt; &lt;p&gt;大多时候你以为的熟络，不过是你一厢情愿的自Heigh。&lt;/p&gt; &lt;p&gt;你总不能说，因为自己是一只美丽的羚羊，就有资格在草原上翩翩起舞，无所顾忌。你还得问问狼和狮子，会不会因为你长得好看，就不吃你。&lt;/p&gt; &lt;p&gt;自己的懒惰居然打败了爱财。&lt;/p&gt; &lt;p&gt;有一个残酷的事实是:勤奋可以弥补聪明的不足，但聪明却是无法弥补懒惰的缺陷。&lt;/p&gt; &lt;p&gt;可是社会没有义务惯着你、养活你，哪怕你文艺得飞上天，你总有回到地面上吃喝拉撒的时候。&lt;/p&gt; &lt;p&gt;不论男女，每个人的强大都是在用青春和汗水去换取的。青春终会在某一天失去，有的人成为自己喜欢的样子，而有的人，已面目全非。&lt;/p&gt; &lt;p&gt;一个人真的不能轻易对生活妥协，你以为是妥协一次，很可能就妥协了一生。而你退缩得越多，能让你喘息的空间就越有限；你表现的越将就，一些幸福的东西就会离你越远。有些时候退一步可以海阔天空，有些时候退一步可能是万丈深渊。&lt;/p&gt; &lt;p&gt;你所设定的底限决定了你不会失去什么。一旦你丧失了底限，很快就会溃不成军，你所在乎的东西，也 会跟着一样样地失去。&lt;/p&gt; &lt;h2&gt;妥协的标志&lt;/h2&gt; &lt;p&gt;毫不夸张地说，你身上的每一寸赘肉，都是向生活妥协的标志。&lt;/p&gt; &lt;p&gt;如果你对自己下不了狠手，就轮到生活对你下狠手。但凡是活得很怂的人，都是因为对自己太好了。&lt;/p&gt; &lt;p&gt;干什么都是好的，但要干出个样子来，这才是人的价值和尊严所在。&lt;/p&gt; &lt;p&gt;所谓“怀才不遇”的人只有两类，一类是不懂得自我推销的人，这类人把自己埋在土里，等人来挖掘和赏识；另一类是不够优秀，不够努力，却自以为很优秀。&lt;/p&gt; &lt;p&gt;别动不动就说把一切交给时间，时间才懒得收拾你的烂摊子。&lt;/p&gt; &lt;p&gt;每个人都应该有这样的意识，在应该努力的时候，不能急着要结果。这就好比在该磨刀的时候，不能急着去砍柴，因为这会伤了刀，更会伤了手。&lt;/p&gt; &lt;p&gt;每个人的内心都渴望被理解、被赏识。但没有人会赏识一块烂木头，你要努力让自己开出花来，才有资格被赏识。怕就怕，你横溢的不是才华，而是肥肉。&lt;/p&gt; &lt;p&gt;任何不做计划的“努力学习”，都只是作秀而已;任何没有走心的“自强不息”，都只是看起来很努力。&lt;/p&gt; &lt;p&gt;努力是给自己看的。&lt;/p&gt; &lt;p&gt;比不上你的，才议论你;比你强的，人家忙着赶路，根本就懒得多看你一眼。&lt;/p&gt; &lt;p&gt;在年纪轻轻的时候，急着岁月静好是丑陋的，与之同等丑陋的就是“假装很忙”。骗观众的点赞容易，骗自己很忙更容易，只是，骗这个世界的因果，有点难。&lt;/p&gt; &lt;p&gt;是的，这个世界确实存在着很多不公平，但最公平的就是，每个人每天都拥有二十四小时。你不努力，永远不会有人对你公平，只有你努力了，有了资源，有了话语权以后，你才可能为自己争取公平的机会。&lt;/p&gt; &lt;p&gt;虽然世界会有“不公平”，但“不公平”从来都不是“不努力”的理由。&lt;/p&gt; &lt;p&gt;痛苦来临时不要总问:‘为什么偏偏是我?’因为快乐降临时，你可没有问过这个问题。&lt;/p&gt; &lt;p&gt;生活不会在意你的自尊，人们看的只是你的成绩。生活也不会特别地对你温柔，它既功利，又尖锐，还有点儿不近人情，不过，在实力说话面前，一切又很平等。&lt;/p&gt; &lt;p&gt;弱者只是你实力的注解，并不是用来要挟这个世界的道具。&lt;/p&gt; &lt;p&gt;当然了，生活中难免遇到“恃弱逞凶”的人，最好的战略方针是:你弱你有理，我强不理你。&lt;/p&gt; &lt;p&gt;不管你了不了解这个世界，这个世界都不会弯腰来迁就你。就算你无止尽地坠入谷底。&lt;/p&gt; &lt;p&gt;成功的人各有各的方法，但蹉跎的人，却有着惊人的相似:无非是懒！&lt;/p&gt; &lt;p&gt;在与众不同的背后，往往是一些不足与外人道的辛苦。每一个与众不同的人，其实都是以无人能及的勤 奋为前提的，要么是血，要么是汗，要么是大把大把的、无人问津的寂寞时光。&lt;/p&gt; &lt;p&gt;世界上最让人觉得惊慌的事情，莫过于比你优秀的人还比你努力。&lt;/p&gt; &lt;p&gt;其实，不论是什么地位、什么层次的年轻人，都需要长大三次:第一次是在发现自己不是世界中心的时候;第二次是在发现即使再怎么努力，终究还是有些事令人无能为力的时候;第三次是在明知道有些事可能会无能为力，但还是会尽力争取的时候。&lt;/p&gt; &lt;p&gt;死活不肯放走清纯，还脾气比胸大，还不肯承认没有完美的人，你的任何一个问题捞出来都比你的年龄问题要严重，就别拿年龄当挡箭牌了。&lt;/p&gt; &lt;p&gt;真正的情怀，绝对不是无能、懦弱或逃避，而应该是实力、勇气和担当。&lt;/p&gt; &lt;h2&gt;实力&lt;/h2&gt; &lt;p&gt;实力不行，不仅没资格谈情怀，连愤怒、尊重都没法享有。换句话说，一个人最好的状态，就是让你的本事配得上你的情怀。这时候你既可以脚踏实地，又可以仰望星空，从容不迫的与岁月相处。&lt;/p&gt; &lt;p&gt;生存的前提是因为你能创造价值。&lt;/p&gt; &lt;p&gt;我希望你最后能变成这样的人:该有主见的时候能掷地有声地镇得住场，该沉默的时候也能心安理得地躲起来不吭声；会关心和牵挂他人，但绝不黏人；能为在乎的人放下身段，但除他们之外，你可以自由到不看任何人的脸色行事；为了证明自己的价值而保持努力，但不期待任何人的夸奖。&lt;/p&gt; &lt;p&gt;如果问我在爱情和面包之间选择什么，我会说:“你给我爱情就好，面包我自己买”。&lt;/p&gt; &lt;p&gt;感情这件事，不是倾尽所有，就会有好结局，因为它并不奉行“天道酬勤”的游戏规则，但工作不一 样，只要你是真的努力了，并且朝着正确的方向坚持下去，它定不会辜负你。&lt;/p&gt; &lt;p&gt;我更想说的是:这仍然是一个相信汗水的世界，所有的“被看到”的、所有的“能发光”的人和物，都不过是在灰头土面的日子里，不动声色、全力以赴的结果。&lt;/p&gt; &lt;p&gt;只有当你有了财务上的自由，才能有选择上的自由，也才会有人格上的自由。所以那些不努力、混日子的人，被人念叨、被人指责、被人歧视，纯属活该。&lt;/p&gt; &lt;p&gt;有时候，遇见笨蛋、白痴都不可怕，可怕的是遇见玻璃心的人。自己不行，还心里阴暗，阴暗到接受不了周围的人出色。自己躺在尘埃里，一动不动，还希望把身边的人都摁倒在地，陪他一起低到尘埃里。&lt;/p&gt; &lt;p&gt;敏感并不是智慧的证明，傻瓜甚至疯子有时也会格外敏感。&lt;/p&gt; &lt;p&gt;一个人最丑陋的样子莫过于一边痛恨所谓的不公平，一边又安于现状，像一条晒干了的咸鱼。&lt;/p&gt; &lt;p&gt;成长是一场近乎惨烈的淘汰赛，它绝对公平，评判胜负的标准就是努力程度。在拥挤的人潮中，你不向前，就只有退后。&lt;/p&gt; &lt;p&gt;其实，我更想说的是，那个独自一人在大城市里打拼、承受着孤独和委屈、能够格外努力的你，在多年后，会变得更坚强，你的世界会变得更大、视野会更广。对你而言，努力就是最好的天赋，就是最硬的后台。&lt;/p&gt; &lt;p&gt;当你不够强时，你发的一切飙和牢骚，你落的任何眼泪和汗水，在别人看来都只是个笑话。&lt;/p&gt; &lt;p&gt;不要告诉我分娩有多么痛苦，把孩子抱来给我看看。&lt;/p&gt; &lt;p&gt;那些专心做事的人，往往是在别人大声喊口号的时候，默默地将子弹上膛，下一秒，“嘭”的一声，整个喧闹的世界就安静了。&lt;/p&gt; &lt;p&gt;漫无目的的辛苦流浪，并不能换来成长。&lt;/p&gt; &lt;p&gt;我会推着你前进，也可以拖累你直至失败。我完全听命于你，而你做的事情，也会有一半要交给我，因为，我总是能够快速而正确地完成任务。我是所有伟人的奴仆，唉，我也是所有失败者的帮凶。伟人之所以伟大，得益于我的鼎力帮助，失败者之所以失败，我的罪责同样不可推卸。我就是习惯。&lt;/p&gt; &lt;p&gt;人懒惰久了， 稍微努力一下子， 就以为是在拼命。&lt;/p&gt; &lt;p&gt;因为我知道每个人的生活都不会太容易，如果我把日子过得容易了，那一定有人在替我承担本就属于我的那份不容易。这就好比两个人用扁担抬箱子，谁的肩膀离箱子远，谁就更轻松，可箱子还是那么沉啊，重量自然加到了另一个人肩上。替我承担的，就是我的父母啊。&lt;/p&gt; &lt;p&gt;当初你问我为什么这么努力，我回答得有点啰唆，其实简单来说，就是为了让自己成功的速度，超过父母老去的速度。&lt;/p&gt; &lt;p&gt;最后你是把生活过成了诗，却让父母活在了水深火热之中。&lt;/p&gt; &lt;h2&gt;爱不是天经地义&lt;/h2&gt; &lt;p&gt;要记住，爱从来就不是天经地义的。有人给你爱，是因为他愿意，不是必需的。&lt;/p&gt; &lt;p&gt;一段甜蜜恋情的继续，如果你懈怠，那势必是要另一半承担更多的委屈、难过、伤心。&lt;/p&gt; &lt;p&gt;你的岁月静好是建立在别人的负累之上的，而那些苦，你却浑然不觉。&lt;/p&gt; &lt;p&gt;这个世界很残酷，努力不一定有结果，但是不努力一定没结果。更残酷的是，如果你选择了偷懒，命运就会安排另一个人替你受罪。所以，你不努力，就是自私！&lt;/p&gt; &lt;p&gt;你越差劲的时候，对你指手画脚的人会更多!因为你个子矮啊，随便哪个高个子轻轻松松就可以戳你的脑门。&lt;/p&gt; &lt;p&gt;世界上可以不劳而获的只有贫穷和年龄。&lt;/p&gt; &lt;p&gt;买的房子像脱靶的气枪，钉在了十环以外......&lt;/p&gt; &lt;p&gt;天地间本没有草，有的只是人心的贪婪。&lt;/p&gt; &lt;p&gt;其实你最大的损失不是失去他们，而是你用这些过去绑架自己的未来。别人是辜负了你一段情感，你却辜负了自己剩余的时光。&lt;/p&gt; &lt;p&gt;恋爱时最可笑的事情就是，他才陪你去了一次公园，给你做了一顿饭，跟你说了一句晚安，他就成了“对我最好的人”了;失恋时最滑稽的一句话就是“我再也遇不到对我这么好的人了”。哪有那么多“最好的人”。你才见过几个人？&lt;/p&gt; &lt;p&gt;毕竟离开的只是风景，留下的才是人生。&lt;/p&gt; &lt;p&gt;已经发生了，你从它那里汲取完经验，给它鞠个躬，你就要赶赴下一段旅行。&lt;/p&gt; &lt;p&gt;在笼子里长大的小鸟，会以为飞翔是一种病。&lt;/p&gt; &lt;p&gt;可以不是那个最有见识的人，但千万千万，不要成为最没见识的那一类。&lt;/p&gt; &lt;p&gt;有人说了，没有什么事情是一个包包不能解决的。如果有，那就两个！&lt;/p&gt; &lt;p&gt;那时我们有梦，关于文学，关于爱情，关于穿越世界的旅行。如今我们深夜饮酒，杯子碰到一起，都是梦破碎的声音。&lt;/p&gt; &lt;p&gt;身强体壮，但灵魂还没断奶。&lt;/p&gt; &lt;h2&gt;梦和想&lt;/h2&gt; &lt;p&gt;没有坚持的梦想，只是梦和想罢了。&lt;/p&gt; &lt;p&gt;所谓坚持，就是犹疑着、胆怯着、畏缩着，但还在往前走。&lt;/p&gt; &lt;p&gt;一个人至少拥有一个梦想，有一个理由去坚强。心若没有栖息的地方，到哪里都是在流浪。&lt;/p&gt; &lt;p&gt;想要什么，就马上去要，因为世界上再也没有比“计划”更可笑了。&lt;/p&gt; &lt;p&gt;你知道瘦下来很美好，可落实到瘦身上，节食变得很难，运动太累了，素食太难吃了，吃不饱就更懒 了......那我能说什么?继续胖吧，把跑鞋、健身卡都扔掉，反正你又用不上！&lt;/p&gt; &lt;p&gt;梦想是一个说出来就显得矫情的东西，它是生长在暗地里的一颗种子，只有破土而出，拔节而长，终有一日开出花来，才能光明正大的让所有人知道。&lt;/p&gt; &lt;p&gt;精彩的生活本来就是需要一次次尝试、一次次跳出舒适安逸的圈子啊!我可不想在二十几岁的时候，就看见自己五十来岁的样子。&lt;/p&gt; &lt;p&gt;明明不喜欢安稳，又不得不过这样的生活。最后只能咬牙佯装自己很喜欢，再用一些虚假的“安稳”之词来骗自己。&lt;/p&gt; &lt;p&gt;女孩子，在最好的年华，别任性，别浪费，别把最好的时间押宝给一个男人。男孩子也是一样，别把未来交给父母，而应该把最好的时光投资给自己，有一技之长。至少要具备随时能离开任何人、任何体制的能力;至少有一个健康的体魄、融洽的朋友圈和精神上的追求。&lt;/p&gt; &lt;p&gt;最经常的运动是呼吸，最极限的运动是翻身。&lt;/p&gt; &lt;p&gt;没有特别幸运，请先特别努力，别因为懒惰而失败，还矫情地归因于自己倒霉。不管你有多大的梦想，有多牛的想法，有再多的兴趣爱好，一懒毁终生。&lt;/p&gt; &lt;p&gt;总是间歇性踌躇满志，持续性混吃等死。说得难听一点，你是像猪一样懒，却无法像猪一样懒得心安理得。&lt;/p&gt; &lt;p&gt;“本想把日子过成诗，时而简单，时而精致。不料日子却过成了我的歌，时而不靠谱，时而不着调。”&lt;/p&gt; &lt;p&gt;乔布斯曾说过一段让无数年轻人动容的话:“‘记住你即将死去’是我一生中遇到的最重要箴言。它帮我指明了生命中重要的选择。因为几乎所有的事情在死亡面前都会消失。我看到的是留下的真正重要的东西。你有时候会思考你将会失去某些东西，‘记住你即将死去’是我知道的避免这些想法的最好办法。你已经赤身裸体了，你没有理由不去跟随自己内心的声音。”&lt;/p&gt; &lt;p&gt;甘于平凡的人有三个特征: 第一个是读书少，书是通往智慧的直线。&lt;/p&gt; &lt;p&gt;我曾经拥有着的一切，转眼都飘散如烟，我曾经失落失望，失掉所有方向，直到看见平凡才是唯一的答案&lt;/p&gt; &lt;p&gt;还没高调的资格呢，就嚷嚷着低调;还没活明白呢，就开始要去伪存真。这是一种最损己不利人的行为，自己活的假别人看着特别累。&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 07 Jun 2019 07:31:00 GMT</pubDate>
    </item>
    <item>
      <title>《皮囊》读书笔记</title>
      <link>https://www.zhangaoo.com/article/my-skin</link>
      <content:encoded>&lt;h1&gt;皮囊读书笔记&lt;/h1&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201961131248-皮囊.jpg" alt="201961131248-皮囊" /&gt;&lt;/p&gt; &lt;h2&gt;皮囊&lt;/h2&gt; &lt;p&gt;人生际遇的好与坏，关键往往在于生命里碰到甚么人，只要能对你有所启发，都是明灯&lt;/p&gt; &lt;p&gt;我们的生命本来多轻盈，都是被这肉体和各种欲望的污浊给拖住&lt;/p&gt; &lt;p&gt;我知道，即使那房子终究被拆了，即使我有一段时间里买不起北京的房子，但我知道，我这一辈子，都有家可回。&lt;/p&gt; &lt;h2&gt;残疾&lt;/h2&gt; &lt;p&gt;我好久没见父亲。当他被堂哥们扛着从车里出来的时候，我觉得说不出的陌生：手术的需要，头发被剪短了，身体像被放掉气的气球，均匀地干瘪下去——说不出哪里瘦了，但就感觉，他被疾病剃掉了整整一圈&lt;/p&gt; &lt;p&gt;他误以为自己还是以前的那个人，早上想马上坐直身，起床，一不小心，偏瘫的左侧身体跟不上动作。整个人就这样被自己摔在地上。说着说着，我看见憋不住的泪珠就在他眼眶里打转&lt;/p&gt; &lt;p&gt;不习惯自己的身体，我不习惯看他哭。我别过头假装没看见他的狼狈，死命去拖他。当时一百斤左右的我，怎么用力也拖不起一百六十多斤的他。他也死命地出力，想帮自己的儿子一把，终于还是失败。&lt;/p&gt; &lt;p&gt;我因此开始想象，当自己驾驭不了身体的时候，到底是怎么样的境况。我觉得有必要体验到其中种种感受，才能照顾好这样的父亲。&lt;/p&gt; &lt;p&gt;我不知道自己是在问谁，我老觉得有双眼睛在看着这一切，然后我问了第二句：故事到底要怎么走？&lt;/p&gt; &lt;p&gt;我至今感谢父亲的坚强，那几乎是最快乐的时光。虽然或许结局注定是悲剧，但一家人都乐于享受父亲建立的这虚幻的秩序&lt;/p&gt; &lt;p&gt;我可以看到，挫败感从那一个个细微的点开始滋长，终于长成一支军队，一部分一部分攻陷他。但他假装不知道。我们也假装不知道。&lt;/p&gt; &lt;h2&gt;重症病房里的圣诞节&lt;/h2&gt; &lt;p&gt;我知道那是双痛彻后的眼睛，是被眼泪洗干净的眼睛。因为，那种眼睛我也有。&lt;/p&gt; &lt;p&gt;每个病人都像个小太阳一样。当然，代价是燃烧自己本来不多的生命力。&lt;/p&gt; &lt;p&gt;每个人都明白了，是大家共同熟悉而亲近的朋友带走了这两个小孩。&lt;/p&gt; &lt;p&gt;那个朋友的名字谁也不想提，因为谁都可能随时被带走。&lt;/p&gt; &lt;p&gt;一切轻薄得，好像从来没发生过。&lt;/p&gt; &lt;h2&gt;张美丽&lt;/h2&gt; &lt;p&gt;但谁都知道，随着财富的沸腾，每个人的内心都有各种欲求在涌动。财富解决了饥饿感和贫穷感，放松了人。以前，贫穷像一个设置在内心的安全阀门，让每个人都对隐藏在其中的各种欲望不闻不问，然而现在，每个人就要直接面对自己了。&lt;/p&gt; &lt;h2&gt;阿小和阿小&lt;/h2&gt; &lt;p&gt;慢慢地，我注意到他留起了长头发，每次他开摩托车经过我家门口，我总在想，他是在努力成为香港阿小想成为的那个人吗？&lt;/p&gt; &lt;p&gt;偌大的城市，充满焦灼感的生活，每次走在地铁拥挤的人群里，我总会觉得自己要被吞噬，觉得人怎么都这么渺小。而在小镇，每个人都那么复杂而有生趣，觉得人才像人。&lt;/p&gt; &lt;p&gt;“你知道吗，我竟然觉得，那个我看不起的小镇才是我家。”说完他就自嘲起来了，“显然，那是我一厢情愿。我哪有家？”&lt;/p&gt; &lt;p&gt;“城市很恶心的，我爸一病，什么朋友都没有了。他去世的时候，葬礼只有我和母亲。”&lt;/p&gt; &lt;h2&gt;天才文展&lt;/h2&gt; &lt;p&gt;不知道别人的经历如何，据我观察，人到十二三岁就会特别喜欢使用“人生”、“梦想”这类词。这样的词句在当时的我念起来，会不自觉悸动。所以我内心波动了一下：“没什么可聊的，你别来吵我，我只是觉得一切很无聊而已。”&lt;/p&gt; &lt;p&gt;“你得想好自己要拥有什么样的人生，然后细化到一步步做具体规划。”这次他回答我了。他显然认为，我是这附近孩子中唯一有资格和他进行这种精神对话的人。&lt;/p&gt; &lt;h2&gt;厚朴&lt;/h2&gt; &lt;p&gt;一个人顶着这样的名字，和名字这样的含义，究竟会活得多奇葩？特别是他还似乎以此为荣。&lt;/p&gt; &lt;p&gt;他很用力地打招呼，很用力地介绍自己。看到活得这么用力的人，我总会不舒服，仿佛对方在时时提醒我要思考如何生活。&lt;/p&gt; &lt;p&gt;不清楚真实的标准时，越用力就越让人觉得可笑。&lt;/p&gt; &lt;p&gt;其实我自己都搞不清楚，哪个才是我应该坚持的活法，哪个才是真实。&lt;/p&gt; &lt;p&gt;生存现实和自我期待的差距太大，容易让人会开发出不同的想象来安放自己。&lt;/p&gt; &lt;p&gt;他在说这些话的时候，大概以为自己是马丁·路德·金。“多么贫瘠的想象力，连想象的样本都是中学课本里的。”我在心里这样嘲笑着。&lt;/p&gt; &lt;p&gt;不合时宜的东西，如果自己虚弱，终究会成为人们嘲笑的对象，但有力量了，或坚持久了，或许反而能成为众人追捧的魅力和个性&lt;/p&gt; &lt;p&gt;虽然不愿意承认，但在那一刹那，我竟然被触动到了，竟然很认真地想：自己是否也可以活得无所顾忌、畅快淋漓。&lt;/p&gt; &lt;p&gt;厚朴确实在用生命追求一种想象，可能是追索得太用力了，那种来自他生命的最简单的情感确实很容易感染人，然后有人也跟着相信了，所以厚朴成了他想象的那个世界的代言人。&lt;/p&gt; &lt;p&gt;但我最终没打这个电话，我没搞清楚，是否每个人都要像我这样看得那么清楚。我也没把握，看得清楚究竟是把生活过得开心，还是让自己活得闷闷不乐。&lt;/p&gt; &lt;p&gt;王子怡没理解到的是，学校里的这种乐队，贩卖的从来不是音乐，是所谓“自由的感觉”。或许厚朴也没理解到。&lt;/p&gt; &lt;p&gt;静宜的关系到底要如何发展，我确实在很理性地考虑。让我经常愧疚的是，我不是把她单独作为一个原因来考虑，而是把她纳入我整个人生的计划来考量，思考到底我是不是要选择这样的人生。&lt;/p&gt; &lt;p&gt;那个晚上，我没安慰厚朴。在我看来，这是必然，王子怡已经完全知道，在厚朴身上她完成不了反叛，厚朴不是那个真正自由的人，而王子怡真正想得到的恋人其实是叛逆。&lt;/p&gt; &lt;p&gt;后来才意识到，在那很长一段时间里，我那倦乏的、对一切提不起兴趣、似乎感冒一样的状态，是爱情小说里写的所谓心碎。我原本以为，这种矫情的情节不会发生在我身上。&lt;/p&gt; &lt;p&gt;弹了没几下，他放弃了。坐在架子鼓的椅子上，顽固地打着精神，但消沉的感觉悄悄蔓在一段时间里，我觉得这个城市里的很多人都长得像蚂蚁：巨大的脑袋装着一个个庞大的梦想，用和这个梦想不匹配的瘦小身躯扛着，到处奔走在一个个尝试里。而我也在不自觉中成为了其中一员。 延开。&lt;/p&gt; &lt;p&gt;我说不上愤怒，更多的是，我清楚，目前的自己没有能力让厚朴明白过来他的处境。&lt;/p&gt; &lt;p&gt;厚朴的父亲不知道，同学们不知道，王子怡也不知道，但我知道，住在厚朴脑子里的怪兽，是他用想象喂大的那个过度膨胀的理想幻象。我还知道，北京不只是他想要求医的地方，还是他为自己开出的最后药方。&lt;/p&gt; &lt;h2&gt;海是藏不住的&lt;/h2&gt; &lt;p&gt;期许自己要活得更真实也更诚实，要更接受甚至喜欢自己身上起伏的每部分，才能更喜欢这世界。我希望自己懂得处理、欣赏各种欲求，各种人性的丑陋与美妙，找到和它们相处的最好方式。我也希望自己能把这一路看到的风景，最终能全部用审美的笔触表达出来。&lt;/p&gt; &lt;h2&gt;愿每个城市都不被阉割&lt;/h2&gt; &lt;p&gt;城市里似乎太多已知，我老家的一个小水池都有好多未知。&lt;/p&gt; &lt;p&gt;当时小孩子的我一直在心里庆幸还好自己不是这里的人，&lt;/p&gt; &lt;p&gt;生长在这样环境里的人，除了维护秩序或者反抗秩序，似乎也难接受第二层次的思维了。&lt;/p&gt; &lt;h2&gt;我始终要回答的问题&lt;/h2&gt; &lt;p&gt;你只是用这个事情来掩饰或者逃避自己不想回答的问题。&lt;/p&gt; &lt;p&gt;你根本还不知道怎么生活，也始终没勇气回答这个问题。”&lt;/p&gt; &lt;p&gt;他想说的是，在不知道怎么生活的情况下，我会采用的是一种现成的、狭隘的、充满功利而且市侩的逻辑——怎么能尽快挣钱以及怎么能尽量成名，用好听的词汇就是所谓“梦想”和“责任”。&lt;/p&gt; &lt;p&gt;我，或许许多人，都在不知道如何生活的情况下，往往采用最容易掩饰或者最常用的借口——理想或者责任。&lt;/p&gt; &lt;p&gt;好好想想怎么生活，怎么去享受生活。我知道他的意思，他或许想说，生活从来不是那么简单的梦想以及磨难，不是简单的所谓理想还有阴谋，生活不是那么简单的概念，真实的生活要过成什么样是要我们自己完成和回答的。&lt;/p&gt; &lt;p&gt;当我看到我给你的唯一一张照片，被你摸到都已经发白的时候，才知道自己恰恰剥夺了我所能给你的、最好的东西。&lt;/p&gt; &lt;h2&gt;回家&lt;/h2&gt; &lt;p&gt;我知道那种舒服，我认识这里的每块石头，这里的每块石头也认识我；我知道这里的每个角落，怎么被岁月堆积成现在这样的光景，这里的每个角落也知道我，如何被时间滋长出这样的模样。&lt;/p&gt; &lt;p&gt;只不过，以前我是最小的那一个孩子，现在一群孩子围着我喊叔叔，他们有的长成一米八五的身高，有的甚至和我讨论国家大事。&lt;/p&gt; &lt;p&gt;不仅仅是一个房子、几个建筑物，家，就是这片和我血脉相连、亲人一样的土地。&lt;/p&gt; &lt;p&gt;冷静的时候，我确实会看到，这个小镇平凡无奇，建筑乱七八糟没有规划，许多房子下面是石头，上面加盖着钢筋水泥。那片红色砖头的华侨房里，突然夹着干打垒堆成的土房子；而那边房子的屋顶，有外来的打工仔在上面养鸭。&lt;/p&gt; &lt;h2&gt;火车他要开到哪里&lt;/h2&gt; &lt;p&gt;作为游客，惬意的是，任何东西快速地滑过，因为一切都是轻巧、美好的，但这种快意是有罪恶的。快速的一切都可以成为风景，无论对当事者多么惊心动魄。&lt;/p&gt; &lt;p&gt;任何事情只要时间一长，都显得格外残忍。&lt;/p&gt; &lt;p&gt;生活中，我一直尝试着旅客的心态，我一次次看着列车窗外的人，以及他们的生活迎面而来，然后狂啸而过，我一次次告诉自己要不为所动，因为你无法阻止这窗外故事的逝去，而且他们注定要逝去。我真以为，自己已经很胜任游客这一角色，已经学会了淡然，已经可以把这种旅游过成生活。&lt;/p&gt; &lt;p&gt;太多人的一生，被抹除得这么迅速、干净。他们被时光抛下列车，迅速得看不到一点踪影，我找不到他们的一点气息，甚至让我凭吊的地方也没有。&lt;/p&gt; &lt;p&gt;而对于还在那列车中的我，再怎么声嘶力竭都没用。其中好几次，我真想打破那个玻璃，停下来，亲吻那个我想亲吻的人，拥抱着那些我不愿意离开的人。但我如何地反抗，一切都是徒然。&lt;/p&gt; &lt;p&gt;我不相信成熟能让我们接受任何东西，成熟只是让我们更能自欺欺人。&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 01 Jun 2019 05:13:00 GMT</pubDate>
    </item>
    <item>
      <title>认识 JWT(JSON Web Token)</title>
      <link>https://www.zhangaoo.com/article/json-web-token</link>
      <content:encoded>&lt;h1&gt;认识 JWT&lt;/h1&gt; &lt;h2&gt;JSON Web Token（JWT） 是什么&lt;/h2&gt; &lt;p&gt;&lt;code&gt;JSON Web Token&lt;/code&gt; (JWT)是一个开放标准&lt;a href="https://tools.ietf.org/html/rfc7519" target="_blank"&gt;RFC 7519&lt;/a&gt;，它定义了一种紧凑的、自包含的方式，用于作为&lt;code&gt;JSON&lt;/code&gt;对象在各方之间安全地传输信息。该信息可以被验证和信任，因为它是数字签名的。&lt;/p&gt; &lt;p&gt;更通俗的讲： 为了在你的 &lt;code&gt;app&lt;/code&gt;（web或者移动端）中辨识或授权用户，在 &lt;code&gt;header&lt;/code&gt; 或者页面（或者 &lt;code&gt;API&lt;/code&gt;）的 &lt;code&gt;url&lt;/code&gt; 中放置一个基于标准的 &lt;code&gt;token&lt;/code&gt;，它表明了这个用户已经登录并且被允许获取到他想要的内容。&lt;/p&gt; &lt;h2&gt;JSON Web Tokens 使用场景&lt;/h2&gt; &lt;h3&gt;认证授权(Authentication)&lt;/h3&gt; &lt;p&gt;这是使用 &lt;code&gt;JWT&lt;/code&gt; 的最常见场景。一旦用户登录，后续每个请求都将包含&lt;code&gt;JWT&lt;/code&gt;，允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 &lt;code&gt;JWT&lt;/code&gt; 的一个特性，因为它的开销很小，并且可以轻松地跨域使用。&lt;/p&gt; &lt;h3&gt;信息交换(Exchange)&lt;/h3&gt; &lt;p&gt;对于安全的在各方之间传输信息而言，&lt;code&gt;JSON Web Tokens&lt;/code&gt; 无疑是一种很好的方式。因为 &lt;code&gt;JWTs&lt;/code&gt; 可以被签名，例如，用公钥/私钥对，你可以确定发送人就是它们所说的那个人。另外，由于签名是使用头和有效负载计算的，您还可以验证内容没有被篡改。因此对于这种场景，你的信息是可以透明公开的，但是又不希望信息被篡改的场合，这种场景使用 &lt;code&gt;JWT&lt;/code&gt; 是比较合适的。&lt;/p&gt; &lt;h2&gt;JWT 的原理(JWT Logic)&lt;/h2&gt; &lt;h3&gt;长什么样子(What Like)&lt;/h3&gt; &lt;p&gt;&lt;code&gt;JSON Web Token&lt;/code&gt; 由三部分组成，它们之间用圆点(.)连接。头部和负载是对应的 &lt;code&gt;JSON&lt;/code&gt; 通过 &lt;code&gt;Base64&lt;/code&gt; 编码来的，因此解码后就能看到原信息。这就是为什么不推荐把私密信息放在 JWT 中的原因。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.                                       // 头部 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. // 载荷 SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c                                 // 签名  &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;头部(Header)&lt;/h4&gt; &lt;p&gt;&lt;code&gt;header&lt;/code&gt; 典型的由两部分组成：token的类型（“JWT”）和算法名称（比如：HMAC SHA256或者RSA等等）。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{   &amp;quot;alg&amp;quot;: &amp;quot;HS256&amp;quot;,   &amp;quot;typ&amp;quot;: &amp;quot;JWT&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;载荷(Payload)&lt;/h4&gt; &lt;p&gt;JWT 的第二部分是 payload，它包含声明（要求）。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Registered claims : 这里有一组预定义的声明，它们不是强制的，但是推荐。比如：iss (issuer), exp (expiration time), sub (subject), aud (audience)等。&lt;/li&gt; &lt;li&gt;Public claims : 可以随意定义。&lt;/li&gt; &lt;li&gt;Private claims : 用于在同意使用它们的各方之间共享信息，并且不是注册的或公开的声明。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{   &amp;quot;sub&amp;quot;: &amp;quot;1234567890&amp;quot;,   &amp;quot;name&amp;quot;: &amp;quot;John Doe&amp;quot;,   &amp;quot;iat&amp;quot;: 1516239022 } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;签名(Signature)&lt;/h4&gt; &lt;p&gt;其实就是对 &lt;code&gt;header&lt;/code&gt; 和 &lt;code&gt;payload&lt;/code&gt; 进行签名，签名公式如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;HMACSHA256(base64UrlEncode(header) + &amp;quot;.&amp;quot; + base64UrlEncode(payload), secret) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;签名是用于验证消息在传递过程中有没有被更改，并且，对于使用私钥签名的 &lt;code&gt;token&lt;/code&gt;，它还可以验证 &lt;code&gt;JWT&lt;/code&gt; 的发送方是否为它所称的发送方。&lt;/p&gt; &lt;p&gt;看一个我们实际项目中的 Token&lt;/p&gt; &lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbmlzdHJhdG9yIiwic2NvcGVzIjpbXSwidXNlcklkIjoiOGE0OThkZGQ2OWIzNTY3YzAxNjliMzcwMGY1ZDAwMDAiLCJpc3MiOiJpbHV2YXRhci5haSIsImlhdCI6MTU1ODM0NDQyNSwiZXhwIjoxNTU4MzUxNjI1fQ.JUy0yhjM8c7IR3lkJVGeYNJQ9HaqysrxeuzA1x-TfS3h7-qUnfyBOCVokccbcA-MF8O9Dh9JfG16y0wtvV2cPw &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;解码内容如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;# header {   &amp;quot;alg&amp;quot;: &amp;quot;HS512&amp;quot; } # payload {   &amp;quot;sub&amp;quot;: &amp;quot;administrator&amp;quot;,   &amp;quot;scopes&amp;quot;: [],   &amp;quot;userId&amp;quot;: &amp;quot;8a498ddd69b3567c0169b3700f5d0000&amp;quot;,   &amp;quot;iss&amp;quot;: &amp;quot;iluvatar.ai&amp;quot;,   &amp;quot;iat&amp;quot;: 1558344425,   &amp;quot;exp&amp;quot;: 1558351625 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;内容很简单，&lt;code&gt;header&lt;/code&gt; 中说明了签名算法是 &lt;code&gt;SHA256&lt;/code&gt;，类似于 &lt;code&gt;MD5&lt;/code&gt; 哈希算法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;sub (subject) 可理解为对象，这里就是我们的用户名。&lt;/li&gt; &lt;li&gt;scopes 可理解为权限限制的范围，也就是可以把用户权限放里面。结合我们的使用场景，对权限的要求的实时性，每次从数据库查询。&lt;/li&gt; &lt;li&gt;userId 为自定义的字段，用户主键&lt;/li&gt; &lt;li&gt;iss token 的发行商&lt;/li&gt; &lt;li&gt;iat token 的发行时间。可以用于判断 &lt;code&gt;JWT&lt;/code&gt; 的发行时间长。&lt;/li&gt; &lt;li&gt;exp (expiration time) 到期时间戳（已过期的令牌会被拒绝）。注意：如规范中所定义，以秒为单位。&lt;/li&gt; &lt;li&gt;jti (JWT ID) 编号&lt;/li&gt; &lt;li&gt;nbf (Not Before)生效时间&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;工作逻辑(Logic)&lt;/h2&gt; &lt;p&gt;在认证的时候，当用户用他们的凭证成功登录以后，一个 &lt;code&gt;JSON Web Token&lt;/code&gt; 将会被返回。此后，&lt;code&gt;token&lt;/code&gt; 就是用户凭证了，你必须非常小心以防止出现安全问题。一般而言，你保存令牌的时候不应该超过你所需要它的时间。之前研究 &lt;code&gt;keycloak JWT Token&lt;/code&gt; 的有效时间只有5分钟。我们当前系统设计为1小时。&lt;/p&gt; &lt;p&gt;无论何时用户想要访问受保护的路由或者资源的时候，用户代理（通常是浏览器）都应该带上 &lt;code&gt;JWT&lt;/code&gt;，典型的，通常放在 &lt;code&gt;Authorization header中&lt;/code&gt;，用&lt;code&gt;Bearer schema&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9.eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;服务器上的受保护的路由将会检查 &lt;code&gt;Authorization header&lt;/code&gt; 中的 &lt;code&gt;JWT&lt;/code&gt; 是否有效，如果有效，则用户可以访问受保护的资源。如果 &lt;code&gt;JWT&lt;/code&gt; 包含足够多的必需的数据，那么就可以减少对某些操作的数据库查询的需要，尽管可能并不总是如此。&lt;/p&gt; &lt;p&gt;如果 &lt;code&gt;token&lt;/code&gt; 是在授权头（&lt;code&gt;Authorization header&lt;/code&gt;）中发送的，那么跨源资源共享(&lt;code&gt;CORS&lt;/code&gt;)将不会成为问题，因为它不使用 &lt;code&gt;cookie&lt;/code&gt;。 下面这张图显示了如何获取JWT以及使用它来访问 &lt;code&gt;APIs&lt;/code&gt; 或者资源：&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019520202948-jwt.png" alt="2019520202948-jwt" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;应用（或者客户端）想授权服务器请求授权。例如，如果用授权码流程的话，就是/oauth/authorize&lt;/li&gt; &lt;li&gt;当授权被许可以后，授权服务器返回一个 &lt;code&gt;access token&lt;/code&gt; 给应用&lt;/li&gt; &lt;li&gt;应用使用 &lt;code&gt;access token&lt;/code&gt; 访问受保护的资源（比如：API）&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;基于Token的身份认证 与 基于服务器的身份认证(Trandition And Token Authentication)&lt;/h2&gt; &lt;h3&gt;基于服务器的身份认证(Trandition Authentication)&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;用户向服务器发送用户名和密码。&lt;/li&gt; &lt;li&gt;服务器验证通过后，在当前对话（&lt;code&gt;session&lt;/code&gt;）里面保存相关数据，比如用户角色、登录时间等等。&lt;/li&gt; &lt;li&gt;服务器向用户返回一个 &lt;code&gt;session_id&lt;/code&gt;，写入用户的 &lt;code&gt;Cookie&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;用户随后的每一次请求，都会通过 &lt;code&gt;Cookie&lt;/code&gt;，将 &lt;code&gt;session_id&lt;/code&gt; 传回服务器。&lt;/li&gt; &lt;li&gt;服务器收到 &lt;code&gt;session_id&lt;/code&gt;，找到前期保存的数据，由此得知用户的身份。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;这种模式的问题在于，扩展性（&lt;code&gt;scaling&lt;/code&gt;）不好。单机当然没有问题，如果是服务器集群，或者是跨域的服务导向架构，就要求 &lt;code&gt;session&lt;/code&gt; 数据共享，每台服务器都能够读取 &lt;code&gt;session&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;每次用户认证通过以后，服务器需要创建一条记录保存用户信息，通常是在内存中，随着认证通过的用户越来越多，服务器的在这里的开销就会越来越大。&lt;/li&gt; &lt;li&gt;用户很容易受到 &lt;code&gt;CSRF&lt;/code&gt; 攻击。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;基于 JWT Token 的身份认证(JWT Token Authentation)&lt;/h3&gt; &lt;p&gt;相同点是，它们都是存储用户信息；然而，&lt;code&gt;Session&lt;/code&gt; 是在服务器端的，而 &lt;code&gt;JWT&lt;/code&gt; 是在客户端的。&lt;code&gt;Session&lt;/code&gt; 方式存储用户信息的最大问题在于要占用大量服务器内存，增加服务器的开销。而JWT方式将用户状态分散到了客户端中，可以明显减轻服务端的内存压力。&lt;code&gt;Session&lt;/code&gt; 的状态是存储在服务器端，客户端只有 &lt;code&gt;session id&lt;/code&gt;；而 &lt;code&gt;Token&lt;/code&gt; 的状态是存储在客户端。&lt;/p&gt; &lt;p&gt;主要流程如下：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;用户携带用户名和密码请求访问&lt;/li&gt; &lt;li&gt;服务器校验用户凭据&lt;/li&gt; &lt;li&gt;应用提供一个 &lt;code&gt;token&lt;/code&gt; 给客户端&lt;/li&gt; &lt;li&gt;客户端存储 &lt;code&gt;token&lt;/code&gt;，并且在随后的每一次请求中都带着它&lt;/li&gt; &lt;li&gt;服务器校验 &lt;code&gt;token&lt;/code&gt; 并返回数据&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019521101948-jwt-sequence-diagram-2.png" alt="2019521101948-jwt-sequence-diagram-2" /&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;无状态和可扩展性：&lt;code&gt;Tokens&lt;/code&gt; 存储在客户端。完全无状态，可扩展。我们的负载均衡器可以将用户传递到任意服务器，因为在任何地方都没有状态或会话信息。&lt;/li&gt; &lt;li&gt;安全：&lt;code&gt;Token&lt;/code&gt; 不是 &lt;code&gt;Cookie&lt;/code&gt;。（The token, not a cookie.）每次请求的时候 &lt;code&gt;Token&lt;/code&gt; 都会被发送。而且，由于没有 &lt;code&gt;Cookie&lt;/code&gt; 被发送，还有助于防止 &lt;code&gt;CSRF&lt;/code&gt; 攻击。即使在你的实现中将 &lt;code&gt;token&lt;/code&gt; 存储到客户端的 &lt;code&gt;Cookie&lt;/code&gt; 中，这个 &lt;code&gt;Cookie&lt;/code&gt; 也只是一种存储机制，而非身份认证机制。没有基于会话的信息可以操作，因为我们没有会话!&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;还有一点，&lt;code&gt;token&lt;/code&gt; 在一段时间以后会过期，这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫 &lt;code&gt;token&lt;/code&gt; 撤销，它允许我们根据相同的授权许可使特定的 &lt;code&gt;token&lt;/code&gt; 甚至一组 &lt;code&gt;token&lt;/code&gt; 无效。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;JWT&lt;/code&gt; 默认是不加密，但也是可以加密的。生成原始 &lt;code&gt;Token&lt;/code&gt; 以后，可以用密钥再加密一次。&lt;/li&gt; &lt;li&gt;&lt;code&gt;JWT&lt;/code&gt; 不加密的情况下，不能将秘密数据写入 &lt;code&gt;JWT&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;JWT&lt;/code&gt; 不仅可以用于认证，也可以用于交换信息。有效使用 &lt;code&gt;JWT&lt;/code&gt;，可以降低服务器查询数据库的次数。&lt;/li&gt; &lt;li&gt;&lt;code&gt;JWT&lt;/code&gt; 的最大缺点是，由于服务器不保存 &lt;code&gt;session&lt;/code&gt; 状态，因此无法在使用过程中废止某个&lt;code&gt;token&lt;/code&gt;，或者更改 &lt;code&gt;token&lt;/code&gt; 的权限。也就是说，一旦 &lt;code&gt;JWT&lt;/code&gt; 签发了，在到期之前就会始终有效，除非服务器部署额外的逻辑，一般使用黑名单机制，将逻辑上废除的 Token 存储到数据库或内存中。&lt;/li&gt; &lt;li&gt;JWT 本身包含了认证信息，一旦泄露，任何人都可以获得该令牌的所有权限。为了减少盗用，JWT 的有效期应该设置得比较短。对于一些比较重要的权限，使用时应该再次对用户进行认证。&lt;/li&gt; &lt;li&gt;为了减少盗用，&lt;code&gt;JWT&lt;/code&gt; 不应该使用 &lt;code&gt;HTTP&lt;/code&gt; 协议明码传输，要使用 &lt;code&gt;HTTPS&lt;/code&gt; 协议传输。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;JWT与 OAuth2 的区别&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;OAuth2 是一种授权框架 ，JWT 是一种认证协议（安全标准）&lt;/li&gt; &lt;li&gt;OAuth2 提供了一套详细的授权机制（指导）。用户或应用可以通过公开的或私有的设置，授权第三方应用访问特定资源。&lt;/li&gt; &lt;li&gt;JWT 应用场景主要是无状态的分布式 API&lt;/li&gt; &lt;li&gt;OAuth2 包含四种授权方式，可以结合传统的有状态的场景使用，也能结合 JWT 在无状态的场景使用&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;参考链接(Refer)&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html&lt;/li&gt; &lt;li&gt;https://github.com/dwyl/learn-json-web-tokens/blob/master/README-zh_CN.md&lt;/li&gt; &lt;li&gt;https://mp.weixin.qq.com/s/E0cDiRZsPHVQDR72hktxsA&lt;/li&gt; &lt;li&gt;https://tools.ietf.org/html/rfc7519&lt;/li&gt; &lt;li&gt;https://www.jianshu.com/p/1870f456b334&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sat, 25 May 2019 05:06:00 GMT</pubDate>
    </item>
    <item>
      <title>读《当你老了》有感</title>
      <link>https://www.zhangaoo.com/article/when-you-are-old</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/2019520101421-when-you-are-old.jpg" alt="2019520101421-when-you-are-old" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;树  树非菩提  一切由心  卸下包袱  无论幸运与否  辛劳总有留痕 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;读完有感，借鉴以及自己胡思乱想写了几句，共勉。&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 20 May 2019 01:58:00 GMT</pubDate>
    </item>
    <item>
      <title>诡异的博客首页慢加载排查</title>
      <link>https://www.zhangaoo.com/article/index-page-slow-load</link>
      <content:encoded>&lt;h1&gt;背景&lt;/h1&gt; &lt;p&gt;自己在阿里云上搭建了一个个人 &lt;a href="https://zhangaoo.com" target="_blank"&gt;Blog&lt;/a&gt;，用的是开源的 &lt;a href="https://github.com/otale/tale" target="_blank"&gt;Tale&lt;/a&gt;，一个 &lt;code&gt;Java&lt;/code&gt; 开发的支持 &lt;code&gt;Markdown&lt;/code&gt; 博客。一直用着都挺好的，直到有一天我发现首页加载出奇的慢，平均要17、18秒的样子。然后其他文章页面却比较正常，也就1、2秒的样子。&lt;/p&gt; &lt;h2&gt;问题定位&lt;/h2&gt; &lt;h3&gt;前端排查&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;因为一直以来我就只更改了部分前端的内容，后端 &lt;code&gt;Java&lt;/code&gt; 代码根本就没动过，首先怀疑是不是前端什么 &lt;code&gt;JS&lt;/code&gt; 资源被我改坏了，一直要等待超时才能响应。&lt;/li&gt; &lt;li&gt;通过 &lt;code&gt;Chrome&lt;/code&gt; 开发者工具调试观察，并没有发现什么问题。&lt;/li&gt; &lt;li&gt;还好我用 &lt;code&gt;Git&lt;/code&gt; 管理源码，不管三七二十一直接还原到最初的版本，重新部署发现问题依旧，一下子陷入懵逼。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201951395253-blog-index-slow-load.jpg" alt="201951395253-blog-index-slow-load" /&gt;&lt;/p&gt; &lt;h3&gt;Nginx 排查&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;做了以上前端排查后，怀疑是是不是 &lt;code&gt;Nginx&lt;/code&gt; 有缓存，强制清理缓存后发现问题依旧。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;CURL 辅助排查&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;经过以上排查，我逐渐把排查的方向转移到后端。&lt;/li&gt; &lt;li&gt;使用 &lt;code&gt;curl&lt;/code&gt; 直接在内网请求首页，发现依旧很慢，这基本排除不可能是前端或 &lt;code&gt;JS&lt;/code&gt;等静态资源的问题，因为我是直接把请求内容保存到文本文件的。前端脚本什么的都不会渲染。为了统计 &lt;code&gt;CURL&lt;/code&gt; 请求的时间，定制了一下请求参数，保存在 curl-format.txt 文件：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;    time_namelookup:  %{time_namelookup}\n        time_connect:  %{time_connect}\n     time_appconnect:  %{time_appconnect}\n    time_pretransfer:  %{time_pretransfer}\n       time_redirect:  %{time_redirect}\n  time_starttransfer:  %{time_starttransfer}\n                     ----------\n          time_total:  %{time_total}\n &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;一次完整的请求如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;$ curl -w &amp;quot;@curl-format.txt&amp;quot; -o /dev/null -s &amp;quot;http://127.0.0.1:9000&amp;quot;     time_namelookup:  0.001        time_connect:  0.001     time_appconnect:  0.000    time_pretransfer:  0.001       time_redirect:  0.000  time_starttransfer:  16.760                     ----------          time_total:  16.760 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;经过上面的排查我基本确定，问题应该出现在后端&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;后端排查&lt;/h3&gt; &lt;p&gt;因为我使用 &lt;code&gt;tale&lt;/code&gt; 默认的工具 &lt;code&gt;tool&lt;/code&gt; 管理博客进程，看了一下默认启动默认分配 &lt;code&gt;256M&lt;/code&gt; 的内存&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 停止 ./tool stop # 启动 ./ tool start # 启动后看到的情况 java -Xms256m -Xmx256m -Dfile.encoding=UTF-8 -jar tale-latest.jar --app.env=prod &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;top 命令辅助排除&lt;/h4&gt; &lt;p&gt;使用 &lt;code&gt;top&lt;/code&gt; 命令辅助排查内存和 &lt;code&gt;CPU&lt;/code&gt;，不查不知道，一查吓一跳，当请求首页的时候，&lt;code&gt;CPU&lt;/code&gt; 飙升到接近 &lt;code&gt;100%&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt; PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                       10815 zhanga    20   0 2406m 225m  13m S 99.2 16.7  22:22.87 java                                           29864 root      20   0 2005m  74m 3244 S  0.3  7.5   3077:37 java                                           26135 root       0 -20  123m 8480 5628 S  0.3  0.8 107:22.87 AliYunDun                                       &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;一瞬间怀疑是不是遭受 &lt;code&gt;DDOS&lt;/code&gt; 攻击了，但是除了首页其他页面都正常，基本可以排除了，几百兆内存跑起来的博客也有人攻击那就太没天理了。&lt;/li&gt; &lt;li&gt;于是怀疑是内存给小了，是不是引起频繁 &lt;code&gt;GC&lt;/code&gt; 导致 &lt;code&gt;CPU&lt;/code&gt; 飙升的&lt;/li&gt; &lt;li&gt;基于以上怀疑自己写启动脚本调大内存配置至 &lt;code&gt;400M&lt;/code&gt;，并记录 &lt;code&gt;GC&lt;/code&gt; 日志&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 新的启动脚本 nohup java -Xms400m -Xmx400m -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Dfile.encoding=UTF-8 -jar tale-latest.jar --app.env=prod &amp;gt; tale.log 2&amp;gt;&amp;amp;1 &amp;amp; echo &amp;quot;$!&amp;quot; &amp;gt; pid &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-log"&gt;2019-05-13T10:17:10.902+0800: 37622.895: [GC (Allocation Failure) 2019-05-13T10:17:10.902+0800: 37622.895: [DefNew: 109585K-&amp;gt;282K(122880K), 0.0020507 secs] 117507K-&amp;gt;8205K(395968K), 0.0021385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2019-05-13T10:21:43.112+0800: 37895.104: [GC (Allocation Failure) 2019-05-13T10:21:43.112+0800: 37895.104: [DefNew: 109530K-&amp;gt;268K(122880K), 0.0018163 secs] 117453K-&amp;gt;8191K(395968K), 0.0018836 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分析一下上面的 &lt;code&gt;GC&lt;/code&gt; 日志：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;首先每次请求都会有一两条如上的 &lt;code&gt;GC&lt;/code&gt; 日志，&lt;code&gt;Allocation Failure&lt;/code&gt; 表示向 &lt;code&gt;Young generation(eden)&lt;/code&gt;给新对象申请空间。简单来讲不是 &lt;code&gt;Full GC&lt;/code&gt; 并且 &lt;code&gt;GC&lt;/code&gt; 的时间很快，对系统性能影响有限。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;下面对整个第一条 &lt;code&gt;GC&lt;/code&gt; 日志做个详细的分析：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;2019-05-13T10:17:10.902+0800&lt;/code&gt; 代表 &lt;code&gt;GC&lt;/code&gt; 日志开始的时间点&lt;/li&gt; &lt;li&gt;&lt;code&gt;37622.895&lt;/code&gt; &lt;code&gt;GC&lt;/code&gt; 事件的开始时间,相对于 &lt;code&gt;JVM&lt;/code&gt; 的启动时间，单位是秒&lt;/li&gt; &lt;li&gt;&lt;code&gt;GC&lt;/code&gt; 用来区分(distinguish)是 &lt;code&gt;Minor GC&lt;/code&gt; 还是 &lt;code&gt;Full GC&lt;/code&gt; 的标志(Flag). 这里的 &lt;code&gt;GC&lt;/code&gt; 表明本次发生的是 &lt;code&gt;Minor GC&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;Allocation Failure&lt;/code&gt; 引起垃圾回收的原因. 本次 &lt;code&gt;GC&lt;/code&gt; 是因为年轻代中（&lt;code&gt;Young Generation&lt;/code&gt;）没有任何合适的区域能够存放需要分配的数据结构而触发的&lt;/li&gt; &lt;li&gt;&lt;code&gt;DefNew&lt;/code&gt; 使用的垃圾收集器的名字，&lt;code&gt;DefNew&lt;/code&gt; 这个名字代表的是: 单线程(&lt;code&gt;single-threaded&lt;/code&gt;), 采用标记复制(&lt;code&gt;mark-copy&lt;/code&gt;)算法的, 使整个 &lt;code&gt;JVM&lt;/code&gt; 暂停运行(&lt;code&gt;stop-the-world&lt;/code&gt;)的年轻代(&lt;code&gt;Young generation&lt;/code&gt;) 垃圾收集器(&lt;code&gt;garbage collector&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;109585K-&amp;gt;282K&lt;/code&gt; 在本次垃圾收集之前和之后的年轻代内存使用情况(&lt;code&gt;Usage&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;122880K&lt;/code&gt; 年轻代的总的大小(&lt;code&gt;Total size&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;117507K-&amp;gt;8205K&lt;/code&gt; 本次垃圾收集之前和之后整个堆内存的使用情况(&lt;code&gt;Total used heap&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;&lt;code&gt;395968K&lt;/code&gt; 总的可用的堆内存(Total available heap).&lt;/li&gt; &lt;li&gt;&lt;code&gt;0.0021385 secs&lt;/code&gt;  GC事件的持续时间(Duration),单位是秒&lt;/li&gt; &lt;li&gt;&lt;code&gt;Times user&lt;/code&gt; 此次垃圾回收, 垃圾收集线程消耗的所有CPU时间(Total CPU time)&lt;/li&gt; &lt;li&gt;&lt;code&gt;sys&lt;/code&gt; 操作系统调用(OS call) 以及等待系统事件的时间(waiting for system event)&lt;/li&gt; &lt;li&gt;&lt;code&gt;real&lt;/code&gt; 应用程序暂停的时间(Clock time). 由于串行垃圾收集器(Serial Garbage Collector)只会使用单个线程, 所以 real time 等于 user 以及 system time 的总和&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;通过上面分析可以计算出：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;在垃圾收集之前，堆内存总的使用了 &lt;code&gt;117507K&lt;/code&gt; 内存。其中, 年轻代使用了 &lt;code&gt;109585K&lt;/code&gt;，可以算出老年代使用的内存为: &lt;code&gt;117507K - 109585K = 7922K&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;年轻代在经过垃圾回收后由 &lt;code&gt;109585K&lt;/code&gt; 减少到 &lt;code&gt;282K&lt;/code&gt;，下降了 &lt;code&gt;109303K&lt;/code&gt;。总堆栈由 &lt;code&gt;117507K&lt;/code&gt; 减少到 &lt;code&gt;8205K&lt;/code&gt;，下降了 &lt;code&gt;109302K&lt;/code&gt;，通过这一点我们可以计算出有 &lt;code&gt;109303K - 109302K = 1K&lt;/code&gt; 的年轻代对象被提升到老年代(&lt;code&gt;Old&lt;/code&gt;)中。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;以上年轻代内存回收比例比较大，大概率大部分被回收的是缓存的内容，以上分析权当父复习一下 GC 的相关内容。 大致过程可展示为如下：、&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/201992910816-jvm-model.png" alt="GC" /&gt;&lt;/p&gt; &lt;p&gt;罗里吧嗦分析了这么多，很明显问题也不是出在 &lt;code&gt;GC&lt;/code&gt; 上，但 &lt;code&gt;CPU&lt;/code&gt; 飙高，事出反常必有妖，还有待继续追踪。&lt;/p&gt; &lt;h3&gt;定位高CUP占用&lt;/h3&gt; &lt;p&gt;查看博客进程&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;$ jps -ml 18731 sun.tools.jps.Jps -ml 10815 tale-latest.jar --app.env=prod &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;监控进程所有线程，访问首页触发高 CPU 占用&lt;/p&gt; &lt;pre&gt;&lt;code class="language-bash"&gt;# 触发高CPU占用 curl -w &amp;quot;@curl-format.txt&amp;quot; -o /dev/null -s &amp;quot;http://127.0.0.1:9000&amp;quot; # 监控所有线程 top -n 1 -H -p 10815 # 同时用 jstack 记录堆栈信息 jstack 10815 &amp;gt; jstack-output.txt &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;线程监控结果&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Tasks:  20 total,   1 running,  19 sleeping,   0 stopped,   0 zombie Cpu(s):  4.3%us,  1.1%sy,  0.0%ni, 94.6%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st Mem:   1019988k total,   781700k used,   238288k free,    83848k buffers Swap:        0k total,        0k used,        0k free,   283120k cached    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                 10838 zhanga    20   0 2407m 228m  13m R 97.8 23.0  22:57.07 java                                            10815 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.00 java                                            10816 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.29 java                                            10817 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:03.45 java                                            10818 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.00 java                                            10819 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.15 java                                            10820 zhanga    20   0 2407m 228m  13m S  0.0 23.0   0:00.00 java                                             &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看到线程 &lt;code&gt;ID&lt;/code&gt; 为 &lt;code&gt;10838&lt;/code&gt; 占用了所有 &lt;code&gt;CPU&lt;/code&gt;，把 PID 转为 16 进制&lt;/p&gt; &lt;pre&gt;&lt;code&gt;十进制    十六进制 10838    2a56 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看 &lt;code&gt;jstack-output.txt&lt;/code&gt; 搜索关键词 &lt;code&gt;2a56&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;&amp;quot;worker@threadㄧ1&amp;quot; #18 prio=5 os_prio=0 tid=0x00007fd8d4009000 nid=0x2a56 runnable [0x00007fd8dd284000]    java.lang.Thread.State: RUNNABLE         at java.util.regex.Pattern$BnM.match(Pattern.java:5464)         at java.util.regex.Matcher.search(Matcher.java:1248)         at java.util.regex.Matcher.find(Matcher.java:637)         at java.util.regex.Matcher.replaceAll(Matcher.java:951)         at java.lang.String.replace(String.java:2240)         at com.vdurmont.emoji.EmojiParser.parseToUnicode(EmojiParser.java:129)         at com.tale.extension.Commons.emoji(Commons.java:240)         at com.tale.utils.TaleUtils.mdToHtml(TaleUtils.java:170)         at com.tale.extension.Theme.intro(Theme.java:299)         at com.tale.extension.Theme.intro(Theme.java:281)         at sun.reflect.GeneratedMethodAccessor108.invoke(Unknown Source)         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;分析如上调用堆栈，发现正则匹配相关逻辑，很可疑&lt;/li&gt; &lt;li&gt;还好博客是开源的，定位到源码 &lt;code&gt;com.tale.utils.TaleUtils.mdToHtml&lt;/code&gt; 这个方法，从名字可知是 &lt;code&gt;Markdown&lt;/code&gt; 转 &lt;code&gt;HTML&lt;/code&gt; 的功能&lt;/li&gt; &lt;li&gt;于是不加思索，埋点统计一下执行时间，代码如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static String mdToHtml(String markdown) {         long start = System.currentTimeMillis();         if (StringKit.isBlank(markdown)) {             return &amp;quot;&amp;quot;;         }          List&amp;lt;Extension&amp;gt; extensions = Arrays.asList(TablesExtension.create());         Parser          parser     = Parser.builder().extensions(extensions).build();         Node            document   = parser.parse(markdown);         HtmlRenderer renderer = HtmlRenderer.builder()                 .attributeProviderFactory(context -&amp;gt; new LinkAttributeProvider())                 .extensions(extensions).build();          String content = renderer.render(document);         content = Commons.emoji(content);          ......          log.error(&amp;quot;mdToHtml millis:&amp;quot; + (System.currentTimeMillis() - start));         return content;     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;编译打包部署重启，请求首页 log 如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-log"&gt;2019/05/13 23:23:10 DEBUG [   worker@threadㄧ1 ]                       o.s.Query : Execute SQL =&amp;gt; SELECT * FROM t_contents WHERE type = ? AND status = ? ORDER BY created DESC LIMIT 0,12 2019/05/13 23:23:10 DEBUG [   worker@threadㄧ1 ]                       o.s.Query : Parameters  =&amp;gt; [post, publish] 2019/05/13 23:23:10 DEBUG [   worker@threadㄧ1 ]                       o.s.Query : Total       =&amp;gt; 6 ms, execution: 1 ms, reading and parsing: 5 ms; executed [null] 2019/05/13 23:23:10 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:56 2019/05/13 23:23:10 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:55 2019/05/13 23:23:10 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:55 2019/05/13 23:23:11 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:482 2019/05/13 23:23:11 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:506 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:481 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:68 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:68 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:68 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:29 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:28 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:28 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:36 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:33 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:35 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:31 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:30 2019/05/13 23:23:12 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:32 2019/05/13 23:23:13 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:679 2019/05/13 23:23:14 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:684 2019/05/13 23:23:15 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:996 2019/05/13 23:23:15 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:44 2019/05/13 23:23:15 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:42 2019/05/13 23:23:15 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:44 2019/05/13 23:23:16 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1089 2019/05/13 23:23:17 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1044 2019/05/13 23:23:18 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1079 2019/05/13 23:23:18 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:330 2019/05/13 23:23:19 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:327 2019/05/13 23:23:19 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:327 2019/05/13 23:23:21 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1559 2019/05/13 23:23:22 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1527 2019/05/13 23:23:24 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:1558 2019/05/13 23:23:24 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:689 2019/05/13 23:23:25 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:680 2019/05/13 23:23:26 ERROR [   worker@threadㄧ1 ]                 c.t.u.TaleUtils : mdToHtml millis:688 2019/05/13 23:23:26 DEBUG [   worker@threadㄧ1 ]                       o.s.Query : Execute SQL =&amp;gt; SELECT COUNT(*) FROM t_contents WHERE type = ? AND status = ? 2019/05 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;终于真相大白了&lt;/p&gt; &lt;h2&gt;结论&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;每次进首页的时候都会加载 &lt;code&gt;36&lt;/code&gt; 条记录，也就是是 &lt;code&gt;36&lt;/code&gt; 片博客，然后把 &lt;code&gt;Markdown&lt;/code&gt; 转为 &lt;code&gt;HTML&lt;/code&gt;，有些博文很长，加上阿里云本身 &lt;code&gt;1G&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;核、&lt;code&gt;1M&lt;/code&gt; 带宽的配置。&lt;/li&gt; &lt;li&gt;这也解释了为什么首页会慢，单个博客就不是很慢的原因。&lt;/li&gt; &lt;li&gt;按理说有缓存的话也就慢一次，后面都会很快，难道是缓存没生效，还是缓存每次都被 &lt;code&gt;GC&lt;/code&gt; 释放了呢，后面再做详细调查&lt;/li&gt; &lt;li&gt;从后台日志来看目前每刷新一次都会查询数据库，都会 调用 &lt;code&gt;mdToHtml&lt;/code&gt; 转换一次，查数据库本省时间不太长，主要时间还是花在了 &lt;code&gt;mdToHtml&lt;/code&gt; 方法上&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Mon, 13 May 2019 15:57:00 GMT</pubDate>
    </item>
    <item>
      <title>《自在独行》读书笔记</title>
      <link>https://www.zhangaoo.com/article/zi-zai-du-xing</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20195521433-DSC_0337.JPG" alt="20195521433-DSC_0337" /&gt;&lt;/p&gt; &lt;h1&gt;祭父&lt;/h1&gt; &lt;p&gt;父亲是极不甘心地离开了我们，他一直是在悲苦和疼痛中挣扎。我在那时真希望他是个哲学家或是个基督教徒，能悟透人生，能将死自认为一种解脱&lt;/p&gt; &lt;h1&gt;静虚村记&lt;/h1&gt; &lt;p&gt;那么高的楼，人住进去，如鸟悬窠，上不着天，下不踏地，可怜怜掬得一抔黄土，插几株花草，自以为风光宜人了。&lt;/p&gt; &lt;h1&gt;孤独的走向未来&lt;/h1&gt; &lt;p&gt;好多人都在说自己孤独，说自己孤独的人其实并不孤独。孤独不是受到了冷落和遗弃，而是无知己，不被理解。真正的孤独者不言孤独，偶尔做些长啸，如我们看到的兽。&lt;/p&gt; &lt;p&gt;弱者都是群居者，所以有芸芸众生。弱者奋斗的目的是转化为强者，像蛹像蛾的转化，但一旦转化成功了，就失去了原本满足和享受欲望的要求。国王是这样，名人也是这样，巨富们的挣钱成了一种职业，种猪们的配种更不是为了爱情。&lt;/p&gt; &lt;h1&gt;读好书&lt;/h1&gt; &lt;p&gt;手上何必戴那么重的金银，金矿是矿，手铐也是矿嘛！老婆的脸上何必让涂那么厚的脂粉，狐狸正是太爱惜它的皮毛，世间才有了打猎的职业！&lt;/p&gt; &lt;p&gt;住楼就住顶层吧，居高却能望远，看戏就坐后排吧，坐后排看不清戏缺看得清看戏的人。&lt;/p&gt; &lt;h1&gt;看人&lt;/h1&gt; &lt;p&gt;男人是创造世界的，女人是征服男人的，事情就是这样。&lt;/p&gt; &lt;p&gt;人既然如蚂蚁一样来到世上，忽生忽死，忽聚忽散，短短数十年里，该自在就自在吧，该潇洒就潇洒吧，各自完满自己的一段生命，这就是生存的全部意义了。&lt;/p&gt; &lt;h1&gt;谈花钱&lt;/h1&gt; &lt;p&gt;钱对于我们来说，来者不拒，去者不惜，花多花少皆不受累。&lt;/p&gt; &lt;h1&gt;关于父子&lt;/h1&gt; &lt;p&gt;作为男人的一生，是儿子也是父亲，前半生儿子是父亲的影子，后半生父亲是儿子的影子。&lt;/p&gt; &lt;h1&gt;关于女人&lt;/h1&gt; &lt;p&gt;沈从文说过，女人是天使和魔鬼的合作的产物，甚至胡适先生谈佛的戒色，主张见到美女就立即想她老了的形象，想她死后的一副骷髅。&lt;/p&gt; &lt;p&gt;女人之所以要做真正的女人，首先要懂得男人的秉性：男人是朝三暮四的，是喜新厌旧的，是吃着碗里的看着锅里的，不胡思乱想的男人不是男人，所谓的在性上的高尚与卑下的男人之分是克制力量的强弱，是环境的允许与限制，是文化重负下的忧郁与果断。&lt;/p&gt; &lt;p&gt;独立做女人的人格，热情的对待生活，对待自己，为自己而活，活得美好，女人会对男人产生永久的吸引，这就是平等，与男人的平等是真正地活出了女人味。&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 05 May 2019 15:04:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Security 篇一初步认识</title>
      <link>https://www.zhangaoo.com/article/spring-security-1</link>
      <content:encoded>&lt;h1&gt;Spring Security&lt;/h1&gt; &lt;p&gt;学习 Spring Security 用法，架构、设计模式等。&lt;/p&gt; &lt;h1&gt;核心组件&lt;/h1&gt; &lt;p&gt;主要介绍 &lt;code&gt;Spring Security&lt;/code&gt; 中常见核心 &lt;code&gt;Java&lt;/code&gt; 类以及他们之间的依赖关系，以及整个架构的设计原理。&lt;/p&gt; &lt;h2&gt;SecurityContextHolder&lt;/h2&gt; &lt;p&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt; 用于存储安全上下文&lt;code&gt;（security context）&lt;/code&gt;的信息。当前操作的用户是谁，该用户是否已经被认证，他拥有哪些角色权限…这些都被保存在 &lt;code&gt;SecurityContextHolder&lt;/code&gt;中。&lt;code&gt;SecurityContextHolder&lt;/code&gt; 默认使用 &lt;code&gt;ThreadLocal&lt;/code&gt; 策略来存储认证信息。看到 &lt;code&gt;ThreadLocal&lt;/code&gt; 也就意味着，这是一种与线程绑定的策略。&lt;code&gt;Spring Security&lt;/code&gt; 在用户登录时自动绑定认证信息到当前线程，在用户退出时，自动清除当前线程的认证信息。但这一切的前提，是你在 &lt;code&gt;web&lt;/code&gt; 场景下使用 &lt;code&gt;Spring Security&lt;/code&gt;，而如果是 &lt;code&gt;Swing&lt;/code&gt; &lt;code&gt;界面，Spring&lt;/code&gt; 也提供了支持，&lt;code&gt;SecurityContextHolder&lt;/code&gt; 的策略则需要被替换，鉴于我的初衷是基于 &lt;code&gt;web&lt;/code&gt; 来介绍 &lt;code&gt;Spring Security&lt;/code&gt; ，所以这里以及后续，非 &lt;code&gt;web&lt;/code&gt; 的相关的内容都一笔带过。&lt;/p&gt; &lt;h3&gt;获取当前用户的信息&lt;/h3&gt; &lt;p&gt;因为身份信息是与线程绑定的，所以可以在程序的任何地方使用静态方法获取用户信息。一个例子如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();  if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;getAuthentication()&lt;/code&gt; 返回了认证信息，再次 &lt;code&gt;getPrincipal()&lt;/code&gt; 返回了身份信息，&lt;code&gt;UserDetails&lt;/code&gt; 便是 &lt;code&gt;Spring&lt;/code&gt; 对身份信息封装的一个接口。&lt;code&gt;Authentication&lt;/code&gt; 和 &lt;code&gt;UserDetails&lt;/code&gt; 的介绍在下面的小节具体讲解，本节重要的内容是介绍 &lt;code&gt;SecurityContextHolder&lt;/code&gt; 这个容器。&lt;/p&gt; &lt;h2&gt;Authentication&lt;/h2&gt; &lt;p&gt;直接上源码：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package org.springframework.security.core;// &amp;lt;1&amp;gt;  public interface Authentication extends Principal, Serializable { // &amp;lt;1&amp;gt;     Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities(); // &amp;lt;2&amp;gt;      Object getCredentials();// &amp;lt;2&amp;gt;      Object getDetails();// &amp;lt;2&amp;gt;      Object getPrincipal();// &amp;lt;2&amp;gt;      boolean isAuthenticated();// &amp;lt;2&amp;gt;      void setAuthenticated(boolean var1) throws IllegalArgumentException; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&amp;lt;1&amp;gt; Authentication 是 spring security 包中的接口，直接继承自 Principal 类，而 Principal 是位于 java.security 包中的。可以见得，Authentication 在 spring security 中是最高级别的身份/认证的抽象。&lt;/p&gt; &lt;p&gt;&amp;lt;2&amp;gt; 由这个顶级接口，我们可以得到用户拥有的权限信息列表，密码，用户细节信息，用户身份信息，认证信息。&lt;/p&gt; &lt;p&gt;还记得1.1节中，authentication.getPrincipal()返回了一个 Object，我们将 Principal 强转成了 Spring Security 中最常用的UserDetails，这在 Spring Security 中非常常见，接口返回 Object，使用 instanceof 判断类型，强转成对应的具体实现类。接口详细解读如下：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;getAuthorities()，权限信息列表，默认是 GrantedAuthority 接口的一些实现类，通常是代表权限信息的一系列字符串。&lt;/li&gt; &lt;li&gt;getCredentials()，密码信息，用户输入的密码字符串，在认证过后通常会被移除，用于保障安全。&lt;/li&gt; &lt;li&gt;getDetails()，细节信息，web 应用中的实现接口通常为 WebAuthenticationDetails，它记录了访问者的 ip 地址和 sessionId 的值。&lt;/li&gt; &lt;li&gt;getPrincipal()，敲黑板！！！最重要的身份信息，大部分情况下返回的是 UserDetails 接口的实现类，也是框架中的常用接口之一。UserDetails 接口将会在下面的小节重点介绍。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Spring Security是如何完成身份认证的？&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;用户名和密码被过滤器获取到，封装成 &lt;code&gt;Authentication&lt;/code&gt; ,通常情况下是 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 这个实现类。&lt;/li&gt; &lt;li&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt; 身份管理器负责验证这个 &lt;code&gt;Authentication&lt;/code&gt;&lt;/li&gt; &lt;li&gt;认证成功后，&lt;code&gt;AuthenticationManager&lt;/code&gt; 身份管理器返回一个被填充满了信息的（包括上面提到的权限信息，身份信息，细节信息，但密码通常会被移除）&lt;code&gt;Authentication&lt;/code&gt; 实例。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt; 安全上下文容器将第3步填充了信息的 &lt;code&gt;Authentication&lt;/code&gt;，通过 &lt;code&gt;SecurityContextHolder.getContext().setAuthentication(…)&lt;/code&gt; 方法，设置到其中。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;这是一个抽象的认证流程，而整个过程中，如果不纠结于细节，其实只剩下一个 &lt;code&gt;AuthenticationManager&lt;/code&gt; 是我们没有接触过的了，这个身份管理器我们在后面的小节介绍。&lt;/p&gt; &lt;h2&gt;AuthenticationManager&lt;/h2&gt; &lt;p&gt;初次接触 &lt;code&gt;Spring Security&lt;/code&gt; 的朋友相信会被 &lt;code&gt;AuthenticationManager&lt;/code&gt;，&lt;code&gt;ProviderManager&lt;/code&gt; ，&lt;code&gt;AuthenticationProvider&lt;/code&gt; …这么多相似的 &lt;code&gt;Spring&lt;/code&gt; 认证类搞得晕头转向，但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。&lt;code&gt;AuthenticationManager&lt;/code&gt;（接口）是认证相关的核心接口，也是发起认证的出发点，因为在实际需求中，我们可能会允许用户使用用户名+密码登录，同时允许用户使用邮箱+密码，手机号码+密码登录，甚至，可能允许用户使用指纹登录（还有这样的操作？没想到吧），所以说 &lt;code&gt;AuthenticationManager&lt;/code&gt; 一般不直接认证，&lt;code&gt;AuthenticationManager&lt;/code&gt; 接口的常用实现类 &lt;code&gt;ProviderManager&lt;/code&gt; 内部会维护一个 &lt;code&gt;List&amp;lt;AuthenticationProvider&amp;gt;&lt;/code&gt; 列表，存放多种认证方式，实际上这是委托者模式的应用（&lt;code&gt;Delegate&lt;/code&gt;）。也就是说，核心的认证入口始终只有一个：&lt;code&gt;AuthenticationManager&lt;/code&gt;，不同的认证方式：用户名+密码（&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;），邮箱+密码，手机号码+密码登录则对应了三个 &lt;code&gt;AuthenticationProvider&lt;/code&gt;。这样一来四不四就好理解多了？熟悉 &lt;code&gt;shiro&lt;/code&gt; 的朋友可以把&lt;code&gt;AuthenticationProvider&lt;/code&gt; 理解成 &lt;code&gt;Realm&lt;/code&gt;。在默认策略下，只需要通过一个 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 的认证，即可被认为是登录成功。&lt;/p&gt; &lt;p&gt;ProviderManager 关键部分源码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class ProviderManager implements AuthenticationManager, MessageSourceAware,   InitializingBean {      // 维护一个AuthenticationProvider列表     private List&amp;lt;AuthenticationProvider&amp;gt; providers = Collections.emptyList();                public Authentication authenticate(Authentication authentication)           throws AuthenticationException {        Class&amp;lt;? extends Authentication&amp;gt; toTest = authentication.getClass();        AuthenticationException lastException = null;        Authentication result = null;         // 依次认证        for (AuthenticationProvider provider : getProviders()) {           if (!provider.supports(toTest)) {              continue;           }           try {              result = provider.authenticate(authentication);               if (result != null) {                 copyDetails(authentication, result);                 break;              }           }           ...           catch (AuthenticationException e) {              lastException = e;           }        }        // 如果有Authentication信息，则直接返回        if (result != null) {    if (eraseCredentialsAfterAuthentication      &amp;amp;&amp;amp; (result instanceof CredentialsContainer)) {                 //移除密码     ((CredentialsContainer) result).eraseCredentials();    }              //发布登录成功事件    eventPublisher.publishAuthenticationSuccess(result);    return result;     }     ...        //执行到此，说明没有认证成功，包装异常信息        if (lastException == null) {           lastException = new ProviderNotFoundException(messages.getMessage(                 &amp;quot;ProviderManager.providerNotFound&amp;quot;,                 new Object[] { toTest.getName() },                 &amp;quot;No AuthenticationProvider found for {0}&amp;quot;));        }        prepareException(lastException, authentication);        throw lastException;     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;ProviderManager&lt;/code&gt; 中的 &lt;code&gt;List&lt;/code&gt; ，会依照次序去认证，认证成功则立即返回，若认证失败则返回 &lt;code&gt;null&lt;/code&gt;，下一个 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 会继续尝试认证，如果所有认证器都无法认证成功，则 &lt;code&gt;ProviderManager&lt;/code&gt; &lt;code&gt;会抛出一个ProviderNotFoundException&lt;/code&gt; 异常。&lt;/p&gt; &lt;p&gt;以上已经把 Spring Security 的整个认证流程都讲述了一遍，简单小结如下：身份信息的存放容器 &lt;code&gt;SecurityContextHolder&lt;/code&gt; ，身份信息的抽象 &lt;code&gt;Authentication&lt;/code&gt; ，身份认证器 &lt;code&gt;AuthenticationManager&lt;/code&gt; 及其认证流程。姑且在这里做一个分隔线。下面来介绍下 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 接口的具体实现。&lt;/p&gt; &lt;h2&gt;DaoAuthenticationProvider&lt;/h2&gt; &lt;p&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt; 最最最常用的一个实现便是 &lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; 。顾名思义，&lt;code&gt;Dao&lt;/code&gt; 正是数据访问层的缩写，也暗示了这个身份认证器的实现思路。由于本文是一个 &lt;code&gt;Overview&lt;/code&gt; ，姑且只给出其 &lt;code&gt;UML&lt;/code&gt; 类图： &lt;img src="https://img.zhangaoo.com/2019415194423-spring-security-DaoAuthenticationProvider.jpg" alt="2019415194423-spring-security-DaoAuthenticationProvider" /&gt;&lt;/p&gt; &lt;p&gt;按照我们最直观的思路，怎么去认证一个用户呢？用户前台提交了用户名和密码，而数据库中保存了用户名和密码，认证便是负责比对同一个用户名，提交的密码和保存的密码是否相同便是了。在&lt;code&gt;Spring Security&lt;/code&gt; 中。提交的用户名和密码，被封装成了&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; ，而根据用户名加载用户的任务则是交给了 &lt;code&gt;UserDetailsService&lt;/code&gt; ，在&lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; 中，对应的方法便是 &lt;code&gt;retrieveUser&lt;/code&gt; ，虽然有两个参数，但是 &lt;code&gt;retrieveUser&lt;/code&gt; 只有第一个参数起主要作，返回一个 &lt;code&gt;UserDetails&lt;/code&gt;。还需要完成 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 和 &lt;code&gt;UserDetails&lt;/code&gt; 密码的比对，这便是交给 &lt;code&gt;additionalAuthenticationChecks&lt;/code&gt; 方法完成的，如果这个 &lt;code&gt;void&lt;/code&gt; 方法没有抛异常，则认为比对成功。比对密码的过程，用到了&lt;code&gt;PasswordEncoder&lt;/code&gt; 和 &lt;code&gt;SaltSource&lt;/code&gt; ，密码加密和盐的概念相信不用我赘述了，它们为保障安全而设计，都是比较基础的概念。&lt;/p&gt; &lt;p&gt;如果你已经被这些概念搞得晕头转向了，不妨这么理解 &lt;code&gt;DaoAuthenticationProvider&lt;/code&gt; ：它获取用户提交的用户名和密码，比对其正确性，如果正确，返回一个数据库中的用户信息（假设用户信息被保存在数据库中）。&lt;/p&gt; &lt;h2&gt;UserDetails 与 UserDetailsService&lt;/h2&gt; &lt;p&gt;上面不断提到了 &lt;code&gt;UserDetails&lt;/code&gt; 这个接口，它代表了最详细的用户信息，这个接口涵盖了一些必要的用户信息字段，具体的实现类对它进行了扩展。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface UserDetails extends Serializable {     Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();     String getPassword();     String getUsername();     boolean isAccountNonExpired();     boolean isAccountNonLocked();     boolean isCredentialsNonExpired();     boolean isEnabled(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;它和 &lt;code&gt;Authentication&lt;/code&gt; 接口很类似，比如它们都拥有 &lt;code&gt;username&lt;/code&gt; ，&lt;code&gt;authorities&lt;/code&gt; ，区分他们也是本文的重点内容之一。&lt;code&gt;Authentication&lt;/code&gt; 的 &lt;code&gt;getCredentials()&lt;/code&gt; 与 &lt;code&gt;UserDetails&lt;/code&gt; 中的 &lt;code&gt;getPassword()&lt;/code&gt; 需要被区分对待，前者是用户提交的密码凭证，后者是用户正确的密码，认证器其实就是对这两者的比对。&lt;code&gt;Authentication&lt;/code&gt; 中的 &lt;code&gt;getAuthorities()&lt;/code&gt; 实际是由 &lt;code&gt;UserDetails&lt;/code&gt; 的 &lt;code&gt;getAuthorities()&lt;/code&gt; 传递而形成的。还记得 &lt;code&gt;Authentication&lt;/code&gt; 接口中的 &lt;code&gt;getUserDetails()&lt;/code&gt; 方法吗？其中的 &lt;code&gt;UserDetails&lt;/code&gt; 用户详细信息便是经过了 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 之后被填充的。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface UserDetailsService {    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;UserDetailsService&lt;/code&gt; 和 &lt;code&gt;AuthenticationProvider&lt;/code&gt; 两者的职责常常被人们搞混，关于他们的问题在文档的 &lt;code&gt;FAQ&lt;/code&gt; 和 &lt;code&gt;issues&lt;/code&gt; 中屡见不鲜。记住一点即可，敲黑板！！！&lt;code&gt;UserDetailsService&lt;/code&gt; 只负责从特定的地方（通常是数据库）加载用户信息，仅此而已，记住这一点，可以避免走很多弯路。&lt;code&gt;UserDetailsService&lt;/code&gt; 常见的实现类有 &lt;code&gt;JdbcDaoImpl&lt;/code&gt;，&lt;code&gt;InMemoryUserDetailsManager&lt;/code&gt;，前者从数据库加载用户，后者从内存中加载用户，也可以自己实现 &lt;code&gt;UserDetailsService&lt;/code&gt;，通常这更加灵活。&lt;/p&gt; &lt;h2&gt;架构概览图&lt;/h2&gt; &lt;p&gt;为了更加形象的理解上述我介绍的这些核心类，附上一张按照我的理解，所画出 &lt;code&gt;Spring Security&lt;/code&gt; 的一张非典型的 &lt;code&gt;UML&lt;/code&gt; 图&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.2cto.com/uploadfile/2018/0802/20180802094833581.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;如果对&lt;code&gt;Spring Security&lt;/code&gt; 的这些概念感到理解不能，不用担心，因为这是 &lt;code&gt;Architecture First&lt;/code&gt; 导致的必然结果，先过个眼熟。后续的文章会秉持 &lt;code&gt;Code First&lt;/code&gt; 的理念，陆续详细地讲解这些实现类的使用场景，源码分析，以及最基本的：如何配置 &lt;code&gt;Spring Security&lt;/code&gt; ，在后面的文章中可以不时翻看这篇文章，找到具体的类在整个架构中所处的位置，这也是本篇文章的定位。另外，一些 &lt;code&gt;Spring Security&lt;/code&gt; 的过滤器还未囊括在架构概览中，如将表单信息包装成 &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; 的过滤器，考虑到这些虽然也是架构的一部分，但是真正重写他们的可能性较小，所以打算放到后面的章节讲解。&lt;/p&gt; &lt;p&gt;本文作者：徐靖峰 原文链接：http://blog.didispace.com/xjf-spring-security-1/ 版权归作者所有&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 15 Apr 2019 11:39:00 GMT</pubDate>
    </item>
    <item>
      <title>摄影知识积累</title>
      <link>https://www.zhangaoo.com/article/photo-study</link>
      <content:encoded>&lt;h1&gt;单反摄影入门学习&lt;/h1&gt; &lt;h2&gt;档位认识&lt;/h2&gt; &lt;p&gt;以尼康D3200为例&lt;/p&gt; &lt;ul&gt; &lt;li&gt;M(manual) 手动 完全手动设置光圈和快门速度，难度较大&lt;/li&gt; &lt;li&gt;A(aperture)光圈 用户选择光圈，相机自动选择快门速度以达到最佳效果&lt;/li&gt; &lt;li&gt;S(shutter)快门 用户选择快门，相机自动选择光圈以达到最佳效果&lt;/li&gt; &lt;li&gt;P(Program)程序自动 在拍摄快照以及没有时间调整相机的情况下使用该模式&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;光圈优先&lt;/h2&gt; &lt;p&gt;大多数人在拍人像以及风景时，光圈优先就是手动定义光圈的大小，相机会根据这个光圈值确定快门速度。由于光圈的大小直接影响着景深，因此在平常的拍摄中此模式使用最为广泛。&lt;/p&gt; &lt;p&gt;在拍摄人像时，我一般采用大光圈，长焦距而使背景得以虚化，获取较浅景深的作用，这样可以突出主体。同时，较大的光圈，也能得到较快的快门值，从而提高手持拍摄的稳定。&lt;/p&gt; &lt;p&gt;利用大光圈拍摄的人物图像，景深小，使得远处背景虚化，强化人像的效果。 而在拍摄风景这一类的照片时，往往需要采用较小的光圈，这样景深的范围比较广，可以使远处和近处的景物都清晰，同样，这一点技巧在拍摄夜景时也适用。&lt;/p&gt; &lt;p&gt;利用小光圈，大景深的原理，使得远处的风景和近处的一样清淅。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;总结：拍人适合大光圈，快门速度较快，拍出的照片比较稳定&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;快门优先&lt;/h2&gt; &lt;p&gt;快门优先时，适用于拍摄运动的物体上，例如体育运动、行使中的车辆、瀑布、飞行中的物体、烟花、水滴等等。 这个与光圈优先恰恰相反，快门优先是在手动定义快门的情况下通过相机测光而获取光圈值。&lt;/p&gt; &lt;p&gt;快门优先多用于拍摄运动的物体上，特别是在体育运动拍摄中最常用。很多朋友在拍摄运动物体时发现，往往拍摄出来的主体是模糊的，这多半就是因为快门的速度不够快。在这种情况下你可以使用快门优先模式，大概确定一个快门值，然后进行拍摄。并且物体的运行一般都是有规律的，那么快门的数值也可以大概估计，例如拍摄行人，快门速度只需要1/125秒就差不多了，而拍摄下落的水滴则需要1/1000秒。快门越快，抓拍的瞬间就越清淅，可以抓拍水漾起的水珠。&lt;/p&gt; &lt;p&gt;总之，在光圈优先的情况下，我们可以通过改变光圈的大小来轻松地控制景深，而在快门优先的情况下，利用不同的光圈对运动的物体能达到很好的拍摄效果。这两者都要灵活运用，满足我们不同情况下的拍摄要求。&lt;/p&gt; &lt;h2&gt;景深&lt;/h2&gt; &lt;p&gt;景深是啥玩意？ 我们就不那么专业了，简单来说就是对焦位置前后的清晰范围。清晰范围越大，就说明景深越大，景深越深，清晰范围越小呢，就是景深越小，今景深也就越浅。画面背景有着强烈的虚化效果。&lt;/p&gt; &lt;p&gt;景深的大小不仅仅与光圈有关和焦距还有拍摄的距离都有着密切的联系。如果距离的近，用长焦或者大光圈拍摄就会有很强烈的虚化效果，但是如果距离远用短焦拍摄即使光圈开到很大，也并不会有很好的虚化效果。&lt;/p&gt; &lt;h1&gt;参考链接&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="http://www.canon.com.cn/special/canon_portrait/2.html" target="_blank"&gt;canon摄影技巧&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sun, 14 Apr 2019 02:35:00 GMT</pubDate>
    </item>
    <item>
      <title>石塘竹海</title>
      <link>https://www.zhangaoo.com/article/birthday</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_1.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_2.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_3.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_4.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_5.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_6.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_7.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190323_8.JPG" alt="alt" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 25 Mar 2019 16:11:00 GMT</pubDate>
    </item>
    <item>
      <title>灰色玄武湖</title>
      <link>https://www.zhangaoo.com/article/xuanwu-lake</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-1.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-2.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-3.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-4.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-5.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-6.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-7.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-8.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-9.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190303-10.JPG" alt="alt" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 03 Mar 2019 13:08:00 GMT</pubDate>
    </item>
    <item>
      <title>冬日紫金山</title>
      <link>https://www.zhangaoo.com/article/winter-zijinshan</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-8.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-2.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-3.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-4.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-5.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-6.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-7.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-1.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/20190224-9.JPG" alt="alt" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 24 Feb 2019 16:05:00 GMT</pubDate>
    </item>
    <item>
      <title>从 Future 到 CompletableFuture</title>
      <link>https://www.zhangaoo.com/article/completable-future</link>
      <content:encoded>&lt;h1&gt;CompletableFuture Start&lt;/h1&gt; &lt;p&gt;上篇已经介绍了 &lt;code&gt;Future&lt;/code&gt;，因为 &lt;code&gt;Future&lt;/code&gt; 有如下局限性：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;将多个异步计算的结果合并成一个&lt;/li&gt; &lt;li&gt;等待 &lt;code&gt;Future&lt;/code&gt; 集合中的所有任务都完成&lt;/li&gt; &lt;li&gt;&lt;code&gt;Future&lt;/code&gt; 完成事件（即，任务完成以后触发执行动作）&lt;/li&gt; &lt;li&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 是 &lt;code&gt;Java8&lt;/code&gt; 提供的新特性，提供了函数式编程的能力(面向对象编程-&amp;gt;抽象数据，函数式编程-&amp;gt;抽象行为)，可以通过回调的方式处理计算结果。&lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;CompletableFuture 特点&lt;/h1&gt; &lt;h2&gt;部分源码&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * .....  * @author Doug Lea  * @since 1.8  */ public class CompletableFuture&amp;lt;T&amp;gt; implements Future&amp;lt;T&amp;gt;, CompletionStage&amp;lt;T&amp;gt; {     ...... } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 实现了 &lt;code&gt;Future&lt;/code&gt; 和 &lt;code&gt;CompletionStage&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;CompletionStage&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;CompletionStage&lt;/code&gt; 代表异步计算过程中的某一个阶段，一个阶段完成以后可能会触发另外一个阶段&lt;/li&gt; &lt;li&gt;一个阶段的计算执行可以是一个 &lt;code&gt;Function&lt;/code&gt; ，&lt;code&gt;Consumer&lt;/code&gt; 或者 &lt;code&gt;Runnable&lt;/code&gt; 。比如：&lt;code&gt;stage.thenApply(x -&amp;gt; square(x)).thenAccept(x -&amp;gt; System.out.print(x)).thenRun(() -&amp;gt; System.out.println())&lt;/code&gt;&lt;/li&gt; &lt;li&gt;一个阶段的执行可能是被单个阶段的完成触发，也可能是由多个阶段一起触发&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;CompletableFuture&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在 &lt;code&gt;Java8&lt;/code&gt; 中，&lt;code&gt;CompletableFuture&lt;/code&gt; 提供了非常强大的 &lt;code&gt;Future&lt;/code&gt; 的扩展功能，可以帮助我们简化异步编程的复杂性，并且提供了函数式编程的能力，可以通过回调的方式处理计算结果，也提供了转换和组合 &lt;code&gt;CompletableFuture&lt;/code&gt; 的方法。&lt;/li&gt; &lt;li&gt;它可能代表一个明确完成的 &lt;code&gt;Future&lt;/code&gt;，也有可能代表一个完成阶段（ &lt;code&gt;CompletionStage&lt;/code&gt; ），它支持在计算完成以后触发一些函数或执行某些动作。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;CompletableFuture 基本用法&lt;/h3&gt; &lt;h4&gt;源码提供一下创建 &lt;code&gt;CompletableFuture&lt;/code&gt; 的方法&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    /**      * Creates a new incomplete CompletableFuture.      */     public CompletableFuture() {     }      /**      * Creates a new complete CompletableFuture with given encoded result.      */     private CompletableFuture(Object r) {         this.result = r;     }      /**      * Returns a new CompletableFuture that is asynchronously completed      * by a task running in the {@link ForkJoinPool#commonPool()} with      * the value obtained by calling the given Supplier.      *      * @param supplier a function returning the value to be used      * to complete the returned CompletableFuture      * @param &amp;lt;U&amp;gt; the function's return type      * @return the new CompletableFuture      */     public static &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; supplyAsync(Supplier&amp;lt;U&amp;gt; supplier) {         return asyncSupplyStage(asyncPool, supplier);     }      /**      * Returns a new CompletableFuture that is asynchronously completed      * by a task running in the given executor with the value obtained      * by calling the given Supplier.      *      * @param supplier a function returning the value to be used      * to complete the returned CompletableFuture      * @param executor the executor to use for asynchronous execution      * @param &amp;lt;U&amp;gt; the function's return type      * @return the new CompletableFuture      */     public static &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; supplyAsync(Supplier&amp;lt;U&amp;gt; supplier,                                                        Executor executor) {         return asyncSupplyStage(screenExecutor(executor), supplier);     }      /**      * Returns a new CompletableFuture that is asynchronously completed      * by a task running in the {@link ForkJoinPool#commonPool()} after      * it runs the given action.      *      * @param runnable the action to run before completing the      * returned CompletableFuture      * @return the new CompletableFuture      */     public static CompletableFuture&amp;lt;Void&amp;gt; runAsync(Runnable runnable) {         return asyncRunStage(asyncPool, runnable);     }      /**      * Returns a new CompletableFuture that is asynchronously completed      * by a task running in the given executor after it runs the given      * action.      *      * @param runnable the action to run before completing the      * returned CompletableFuture      * @param executor the executor to use for asynchronous execution      * @return the new CompletableFuture      */     public static CompletableFuture&amp;lt;Void&amp;gt; runAsync(Runnable runnable,                                                    Executor executor) {         return asyncRunStage(screenExecutor(executor), runnable);     }      /**      * Returns a new CompletableFuture that is already completed with      * the given value.      *      * @param value the value      * @param &amp;lt;U&amp;gt; the type of the value      * @return the completed CompletableFuture      */     public static &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; completedFuture(U value) {         return new CompletableFuture&amp;lt;U&amp;gt;((value == null) ? NIL : value);     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;&lt;code&gt;thenApply&lt;/code&gt;&lt;/h4&gt; &lt;p&gt;当前阶段正常完成以后执行，而且当前阶段的执行的结果会作为下一阶段的输入参数。&lt;code&gt;thenApplyAsync&lt;/code&gt; 默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。&lt;code&gt;thenApply&lt;/code&gt; 相当于回调函数（&lt;code&gt;callback&lt;/code&gt;）.源码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; thenApply(         Function&amp;lt;? super T,? extends U&amp;gt; fn) {         return uniApplyStage(null, fn);     }      public &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; thenApplyAsync(         Function&amp;lt;? super T,? extends U&amp;gt; fn) {         return uniApplyStage(asyncPool, fn);     }      public &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; thenApplyAsync(         Function&amp;lt;? super T,? extends U&amp;gt; fn, Executor executor) {         return uniApplyStage(screenExecutor(executor), fn);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;例1 一个简单的链式执行例子&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCompletable(){         CompletableFuture.supplyAsync(()-&amp;gt;1)                 .thenApply(i -&amp;gt; i+1)                 .thenApply(i -&amp;gt; 1 * i)                 .whenComplete((r,e)-&amp;gt; System.out.println(r+&amp;quot;,&amp;quot;+e));     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;例2 两个耗时异步计算同时执行&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/** * 具体的计算类 **/ public class SquareCalculator {     private ExecutorService executor             = Executors.newFixedThreadPool(4);      public Future&amp;lt;Integer&amp;gt; calculate(Integer input) {         return executor.submit(() -&amp;gt; {             Thread.sleep(2000);             return input * input;         });     }      public CompletableFuture&amp;lt;Integer&amp;gt; completableFutureCalculate(Integer input, Long sleep){         CompletableFuture&amp;lt;Integer&amp;gt; future = new CompletableFuture&amp;lt;&amp;gt;();         executor.execute(() -&amp;gt; {             try{                 Thread.sleep(sleep);                 Integer result =  input * input;                 future.complete(result);             } catch (InterruptedException e){                 System.out.println(e);             }         });         return future;     } }  /** ** CompletableFuture 处理两个异步计算 **/     @Test     public void testFuture() throws Exception{         SquareCalculator sqa = new SquareCalculator();         System.out.println(&amp;quot;Calculating ... &amp;quot;);         long start = System.currentTimeMillis();         CompletableFuture&amp;lt;Integer&amp;gt; reslt = sqa.completableFutureCalculate(11, 2000L);         CompletableFuture&amp;lt;Integer&amp;gt; reslt1 = sqa.completableFutureCalculate(22, 4000L);         CompletableFuture.allOf(reslt,reslt1).join();         System.out.println(reslt.get());         System.out.println(reslt1.get());         System.out.println(System.currentTimeMillis() - start);     }     /**      一个计算结果      Calculating ...       121      484      4058     **/ &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;thenAccept 与 thenRun&lt;/h4&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;可以看到，&lt;code&gt;thenAccept&lt;/code&gt; 和 &lt;code&gt;thenRun&lt;/code&gt; 都是无返回值的。如果说 &lt;code&gt;thenApply&lt;/code&gt; 是不停的输入输出的进行生产，那么 &lt;code&gt;thenAccept&lt;/code&gt; 和 &lt;code&gt;thenRun&lt;/code&gt; 就是在进行消耗。它们是整个计算的最后两个阶段。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;同样是执行指定的动作，同样是消耗，二者也有区别：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;thenAccept&lt;/code&gt; 接收上一阶段的输出作为本阶段的输入 　&lt;/li&gt; &lt;li&gt;&lt;code&gt;thenRun&lt;/code&gt; 根本不关心前一阶段的输出，根本不不关心前一阶段的计算结果，因为它不需要输入参数&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;例3 &lt;code&gt;thenAccept&lt;/code&gt; 的一个简单的例子&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCompletable3() {         CompletableFuture.supplyAsync(() -&amp;gt; 8)                 .thenApply(i -&amp;gt; i + 1)                 .thenAccept(i -&amp;gt; System.out.println(i * i));     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;例4 &lt;code&gt;thenRun&lt;/code&gt; 的一个简单的例子&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCompletable4() {         CompletableFuture.supplyAsync(()-&amp;gt; 8)                 .thenRun(() -&amp;gt; System.out.println(&amp;quot;Hello World&amp;quot;));     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;thenCombine 整合两个计算结果&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;源码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public &amp;lt;U,V&amp;gt; CompletableFuture&amp;lt;V&amp;gt; thenCombine(         CompletionStage&amp;lt;? extends U&amp;gt; other,         BiFunction&amp;lt;? super T,? super U,? extends V&amp;gt; fn) {         return biApplyStage(null, other, fn);     }      public &amp;lt;U,V&amp;gt; CompletableFuture&amp;lt;V&amp;gt; thenCombineAsync(         CompletionStage&amp;lt;? extends U&amp;gt; other,         BiFunction&amp;lt;? super T,? super U,? extends V&amp;gt; fn) {         return biApplyStage(asyncPool, other, fn);     }      public &amp;lt;U,V&amp;gt; CompletableFuture&amp;lt;V&amp;gt; thenCombineAsync(         CompletionStage&amp;lt;? extends U&amp;gt; other,         BiFunction&amp;lt;? super T,? super U,? extends V&amp;gt; fn, Executor executor) {         return biApplyStage(screenExecutor(executor), other, fn);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;例5 整合两个计算结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCompletable5() {         CompletableFuture.supplyAsync(() -&amp;gt; &amp;quot;Hello&amp;quot;)                 .thenApply(s -&amp;gt; s + &amp;quot; Word &amp;quot;)                 .thenApply(String::toUpperCase)                 .thenCombine(CompletableFuture.completedFuture(&amp;quot;Java&amp;quot;),                         (s1,s2)-&amp;gt;s1+s2)                 .thenAccept(System.out::println);     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;whenComplete&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;等待多个异步任务完成后执行相应的处理，这里要着重理解 &lt;code&gt;whenComplete&lt;/code&gt; 返回的是一个新的 &lt;code&gt;Future&lt;/code&gt;，如果需要回调需要按顺序执行那么同样需要用 &lt;code&gt;CompletableFuture.allOf&lt;/code&gt; 重新组合新他们。&lt;/li&gt; &lt;li&gt;要等待 &lt;code&gt;Group&lt;/code&gt; 里面的 &lt;code&gt;Future&lt;/code&gt; 执行完成，需要调用 &lt;code&gt;join()&lt;/code&gt; 或 &lt;code&gt;get()&lt;/code&gt;，两者的区别下面会说明&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCompletable9() {         Runnable dummyTask = () -&amp;gt; {             try {                 Thread.sleep(200);             } catch (InterruptedException ignored) {             }         };          CompletableFuture&amp;lt;Void&amp;gt; f1 = CompletableFuture.runAsync(dummyTask);         CompletableFuture&amp;lt;Void&amp;gt; f2 = CompletableFuture.runAsync(dummyTask);         f1 = f1.whenComplete((aVoid, throwable) -&amp;gt; System.out.println(&amp;quot;Completed f1&amp;quot;));         f2 = f2.whenComplete((aVoid, throwable) -&amp;gt; System.out.println(&amp;quot;Completed f2&amp;quot;));         CompletableFuture[] all = {f1, f2};         CompletableFuture&amp;lt;Void&amp;gt; allOf = CompletableFuture.allOf(all);         allOf.whenComplete((aVoid, throwable) -&amp;gt; {             System.out.println(&amp;quot;Completed allOf&amp;quot;);         });         allOf.join();         System.out.println(&amp;quot;Joined&amp;quot;);     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;join 与 get 区别联系&lt;/h3&gt; &lt;p&gt;两者的作用相同，都是等待 &lt;code&gt;Future&lt;/code&gt; 完成然后返回结果。不同点是 &lt;code&gt;join&lt;/code&gt; 不会被 &lt;code&gt;interrupt&lt;/code&gt;，但是 &lt;code&gt;get&lt;/code&gt; 可能会被中断，使用时需要捕捉异常，接口定义如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;V get() throws InterruptedException, ExecutionException; &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;两个实际场景&lt;/h1&gt; &lt;h2&gt;多个异步请求全部完成&lt;/h2&gt; &lt;p&gt;多个异步请求全部完成获取所有结果&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/** controller 代码**/     @RequestMapping(method = RequestMethod.GET, value = &amp;quot;/test&amp;quot;)     public long test() {         long sleep = randomSleep();         return sleep;     }      private long randomSleep(){         long time = (int)(1+Math.random()*(1000-1+1));         try{             System.out.println(&amp;quot;Sleep &amp;quot; + time + &amp;quot;ms&amp;quot;);             Thread.sleep(time);         }catch (InterruptedException e){             e.printStackTrace();         }         return time;     } /** okhttp3 client获取异步消息代码**/  public class AsynchronousMessage {     private static ExecutorService executor = Executors.newFixedThreadPool(4);     private static OkHttpClient okHttpClient = (new OkHttpClient.Builder()).connectionPool(new ConnectionPool(5, 1, TimeUnit.MINUTES)).             retryOnConnectionFailure(true).connectTimeout(5000, TimeUnit.MILLISECONDS).readTimeout(5000, TimeUnit.MILLISECONDS).build();      public static CompletableFuture&amp;lt;Long&amp;gt; getAsynchronousMessage() {         CompletableFuture&amp;lt;Long&amp;gt; future = new CompletableFuture&amp;lt;&amp;gt;();         Request req = new Request.Builder().url(&amp;quot;http://10.201.0.39:8080/datanode/test/test&amp;quot;).get().build();         executor.execute(() -&amp;gt; {             Response response = null;             try {                 response = okHttpClient.newCall(req).execute();                 if (response.isSuccessful()) {                     String res = response.body().string();                     if (res != null) {                         future.complete(Long.valueOf(res));                     }                 }             } catch (IOException e) {                 e.printStackTrace();             } finally {                 if (null != response) {                     response.close();                 }             }         });         return future;     } } /** CompletableFuture 调用处理所有结果**/        @Test     public void testCompletable12() {         long start = System.currentTimeMillis();         StringBuilder result = new StringBuilder();          CompletableFuture fut1 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture fut2 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture fut3 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture futures =  CompletableFuture.allOf(fut1,fut2,fut3);         System.out.println(fut1.join() + &amp;quot;,&amp;quot; + fut2.join() + &amp;quot;,&amp;quot; + fut3.join());         System.out.println(&amp;quot;Process Time:&amp;quot; + (System.currentTimeMillis() - start));     }         &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;多个异步请求其中之一完成&lt;/h2&gt; &lt;p&gt;多个异步请求全部完成获取完成的结果&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**     共通代码参考上面的例子 **/     @Test     public void testCompletable13() {         long start = System.currentTimeMillis();         CompletableFuture fut1 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture fut2 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture fut3 = AsynchronousMessage.getAsynchronousMessage();         CompletableFuture futures =  CompletableFuture.anyOf(fut1,fut2,fut3);         System.out.println(futures.join());//anyOf 能取到完成任务的结果，而 allOf 不行         System.out.println(&amp;quot;Process Time:&amp;quot; + (System.currentTimeMillis() - start));     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;参考资料&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Future 各种用法&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;https://juejin.im/post/5abc9e59f265da239f077460&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 19 Feb 2019 15:17:00 GMT</pubDate>
    </item>
    <item>
      <title>二月赏梅</title>
      <link>https://www.zhangaoo.com/article/plum-blossom</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/1.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/2.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/3.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/4.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/5.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/6.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/7.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/8.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/9.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/10.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/11.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/12.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/13.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/14.JPG" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://img.zhangaoo.com/15.JPG" alt="alt" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 17 Feb 2019 16:17:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇八之使用Lambda表达式编写并发程序</title>
      <link>https://www.zhangaoo.com/article/java8-concurrent</link>
      <content:encoded>&lt;h1&gt;第 9 章 使用Lambda表达式编写并发程序&lt;/h1&gt; &lt;p&gt;前面讨论了如何并行化处理数据，本章讨论如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式编写并发应用，高效传递信息和非阻塞式 &lt;code&gt;I/O&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;9.1 为什么要使用非阻塞式I/O&lt;/h2&gt; &lt;p&gt;在介绍并行化处理时，讲了很多关于如何高效利用多核 &lt;code&gt;CPU&lt;/code&gt; 的内容。这种方式很管用，但在处理大量数据时，它并不是唯一可用的线程模型。&lt;/p&gt; &lt;p&gt;假设要编写一个支持大量用户的聊天程序。每当用户连接到聊天服务器时，都要和服务器建立一个 &lt;code&gt;TCP&lt;/code&gt; 连接。使用传统的线程模型，每次向用户写数据时，都要调用一个方法向用户传输数据，这个方法会阻塞当前线程。&lt;/p&gt; &lt;p&gt;这种 &lt;code&gt;I/O&lt;/code&gt; 方式叫阻塞式 &lt;code&gt;I/O&lt;/code&gt;，是一种通用且易于理解的方式，因为和程序用户的交互通常符合这样一种顺序执行的方式。缺点是，将系统扩展至支持大量用户时，需要和服务器建立大量 &lt;code&gt;TCP&lt;/code&gt; 连接，因此扩展性不是很好。&lt;/p&gt; &lt;p&gt;非阻塞式 &lt;code&gt;I/O&lt;/code&gt;，有时也叫异步 &lt;code&gt;I/O&lt;/code&gt;，可以处理大量并发网络连接，而且一个线程可以为多个连接服务。和阻塞式 &lt;code&gt;I/O&lt;/code&gt; 不同，对聊天程序客户端的读写调用立即返回，真正的读写操作则在另一个独立的线程执行，这样就可以同时执行其他任务了。如何使用这些省下来的&lt;code&gt;CPU&lt;/code&gt; 周期完全取决于程序员，可以选择读入更多数据，也可以玩一局 &lt;code&gt;Minecraft&lt;/code&gt; 游戏。&lt;/p&gt; &lt;p&gt;到目前为止，我避免使用代码来描述这两种 &lt;code&gt;I/O&lt;/code&gt; 方式，因为根据 &lt;code&gt;API&lt;/code&gt; 的不同，它们有多种实现方式。&lt;code&gt;Java&lt;/code&gt; 标准类库的 &lt;code&gt;NIO&lt;/code&gt; 提供了非阻塞式 &lt;code&gt;I/O&lt;/code&gt; 的接口，&lt;code&gt;NIO&lt;/code&gt; 的最初版本用到了 &lt;code&gt;Selector&lt;/code&gt; 的概念，让一个线程管理多个通信管道，比如向客户端写数据的网络套接字。&lt;/p&gt; &lt;p&gt;然而这种方式压根儿就没有在 &lt;code&gt;Java&lt;/code&gt; 程序员中流行起来，它编写出来的代码难于理解和调试。引入 &lt;code&gt;Lambda&lt;/code&gt; 表达式后，设计和实现没有这些缺点的 &lt;code&gt;API&lt;/code&gt; 就顺手多了。&lt;/p&gt; &lt;h2&gt;9.2 回调&lt;/h2&gt; &lt;p&gt;为了展示非阻塞式 &lt;code&gt;I/O&lt;/code&gt; 的原则，我们将运行一个极其简单的聊天应用，没有那些花里胡哨的功能。当用户第一次连接应用时，需要设定用户名，随后便可通过应用收发信息。&lt;/p&gt; &lt;p&gt;我们将使用 &lt;code&gt;Vert.x&lt;/code&gt; 框架实现该应用，并且在实施过程中根据需要，引入其他一些必需的技术。让我们先来写一段接收 &lt;code&gt;TCP&lt;/code&gt; 连接的代码，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;接收 TCP 连接&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class MainVerticle extends AbstractVerticle {    @Override   public void start(Future&amp;lt;Void&amp;gt; startFuture) throws Exception {     vertx.createHttpServer().requestHandler(req -&amp;gt; {       req.response()         .putHeader(&amp;quot;content-type&amp;quot;, &amp;quot;text/plain&amp;quot;)         .end(&amp;quot;Hello from Vert.x!&amp;quot;);     }).listen(8080, http -&amp;gt; {       if (http.succeeded()) {         startFuture.complete();         System.out.println(&amp;quot;HTTP server started on http://localhost:8080&amp;quot;);       } else {         startFuture.fail(http.cause());       }     });   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;读者可将 &lt;code&gt;Verticle&lt;/code&gt; 想成 &lt;code&gt;Servlet&lt;/code&gt; —— 它是 &lt;code&gt;Vert.x&lt;/code&gt; 框架中部署的原子单元。上述代码的入口是 &lt;code&gt;start&lt;/code&gt; 方法，它和普通 &lt;code&gt;Java&lt;/code&gt; 程序中的 &lt;code&gt;main&lt;/code&gt; 方法类似。在聊天应用中，我们用它建立一 个接收 &lt;code&gt;TCP&lt;/code&gt; 连接的服务器。&lt;/p&gt; &lt;p&gt;然后向 &lt;code&gt;requestHandler&lt;/code&gt; 方法输入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，每当有用户连接到聊天应用时，都会调用该 &lt;code&gt;Lambda&lt;/code&gt; 表达式。这就是一个回调，与在第 1 章中介绍的 &lt;code&gt;Swing&lt;/code&gt; 中的回调类似。 这种方式的好处是，应用不必控制线程模型 —— &lt;code&gt;Vert.x&lt;/code&gt; 框架为我们管理线程，打理好了一切相关复杂性，程序员只需考虑事件和回调就够了。&lt;/p&gt; &lt;p&gt;我们的应用还通过 &lt;code&gt;dataHandler&lt;/code&gt; 方法注册了另外一个回调，每当从网络套接字读取数据时，该回调就会被调用。在本例中，我们希望提供更复杂的功能，因此没有使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式， 而是传入一个常规的 &lt;code&gt;User&lt;/code&gt; 类，该类实现了相关的函数接口。&lt;code&gt;User&lt;/code&gt; 类的定义如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;处理用户连接&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class User implements Handler&amp;lt;Buffer&amp;gt; {     private static  final Pattern newline = Pattern.compile(&amp;quot;\\n&amp;quot;);      private  final NetSocket socket;      private  nal Set&amp;lt;String&amp;gt; names;     private  nal EventBus eventBus;     private Optional&amp;lt;String&amp;gt; name;      public User(NetSocket socket, Verticle verticle) {         Vertx vertx = verticle.getVertx();         this.socket = socket;         names = vertx.sharedData().getSet(&amp;quot;names&amp;quot;);         eventBus = vertx.eventBus();         name = Optional.empty();     } @Override public void handle(Buffer buffer) {             newline.splitAsStream(buffer.toString())                    .forEach(line -&amp;gt; {                         if (!name.isPresent())                              setName(line);                         else                             handleMessage(line);                     });   }   // Class continues... &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;变量 &lt;code&gt;buffer&lt;/code&gt; 包含了网络连接写入的数据，我们使用的是一个分行的文本协议，因此需要先将其转换成一个字符串，然后依换行符分割。&lt;/p&gt; &lt;p&gt;这里使用了正则表达式 &lt;code&gt;java.util.regex.Pattern&lt;/code&gt; 的一个实例 &lt;code&gt;newline&lt;/code&gt; 来匹配换行符。尤为方便的是，&lt;code&gt;Java 8&lt;/code&gt; 为&lt;code&gt;Pattern&lt;/code&gt; 类新增了一个 &lt;code&gt;splitAsStream&lt;/code&gt; 方法，该方法使用正则表达式将字符串分割好后，生成一个包含分割结果的流对象。&lt;/p&gt; &lt;p&gt;用户连上聊天服务器后，首先要做的事是设置用户名。如果用户名未知，则执行设置用户名的逻辑;否则正常处理聊天消息。&lt;/p&gt; &lt;p&gt;还需要接收来自其他用户的消息，并且将它们传递给聊天程序客户端，让接收者能够读取 消息。为了实现该功能，在设置当前用户用户名的同时，我们注册了另外一个回调，用来写入消息&lt;/p&gt; &lt;ul&gt; &lt;li&gt;注册聊天消息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    eventBus.registerHandler(name, (Message&amp;lt;String&amp;gt; msg) -&amp;gt; {             sendClient(msg.body());     }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述代码使用了 &lt;code&gt;Vert.x&lt;/code&gt; 的事件总线，它允许在 &lt;code&gt;verticle&lt;/code&gt; 对象之间以非阻塞式 &lt;code&gt;I/O&lt;/code&gt; 的方式传递消息(如下图所示)。&lt;code&gt;registerHandler&lt;/code&gt; 方法将一个处理程序和一个地址关联，有消息发送给该地址时，就将之作为参数传递给处理程序，并且自动调用处理程序。这里使用用户名作为地址。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/vf9qqe2kdsivcohobm9un436bt.png" alt="使用事件总线传递消息" /&gt;&lt;/p&gt; &lt;p&gt;通过为地址注册处理程序并发消息的方式，可以构建非常复杂和解耦的服务，它们之间完全以非阻塞式 &lt;code&gt;I/O&lt;/code&gt; 方式响应。需要注意的是，在我们的设计中没有共享状态。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Vert.x&lt;/code&gt; 的事件总线允许发送多种类型的消息，但是它们都要使用 &lt;code&gt;Message&lt;/code&gt; 对象进行封装。 点对点的消息传递由 &lt;code&gt;Message&lt;/code&gt; 对象本身完成，它们可能持有消息发送方的应答处理程序。 在这种情况下，我们想要的是消息体，也就是文字本身，则只需调用 &lt;code&gt;body&lt;/code&gt; 方法。我们通过将消息写入 &lt;code&gt;TCP&lt;/code&gt; 连接，把消息发送给了用户聊天客户端。&lt;/p&gt; &lt;p&gt;当应用想要把消息从一个用户发送给另一个用户时，就使用代表另一个用户的地址(如下代码所示)，这里使用了用户的用户名。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;发送聊天信息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    eventBus.send(user, name.get() +‘&amp;gt;’+ message); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;让我们扩展这个基础聊天服务器，向关注你的用户群发消息，为此，需要实现两个新命令。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;代表群发命令的感叹号，它能将信息群发给关注你的用户。如果Bob键入“!hello followers”，则所有关注 Bob 的用户都会收到该条信息:“Bob&amp;gt;hello followers”。&lt;/li&gt; &lt;li&gt;关注命令，用来关注一个用户，比如“followBob”。 一旦解析了命令，就可以着手实现 &lt;code&gt;broadcastMessage&lt;/code&gt; 和 &lt;code&gt;followUser&lt;/code&gt; 方法，它们分别代表了这两个命令。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这里的通信模式略有不同，除了给单个用户发消息，现在还拥有了群发信息的能力。幸好，&lt;code&gt;Vert.x&lt;/code&gt; 的事件总线允许我们将一条信息发布给多个处理程序(见下图)，让我们得以沿用一种类似的方式。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/v5tshvqqkmgq8otp6q1q6hi4i8.png" alt="使用消息总线发布" /&gt;&lt;/p&gt; &lt;p&gt;代码的唯一变化是使用了事件总线的 &lt;code&gt;publish&lt;/code&gt; 方法，而不是先前的 &lt;code&gt;send&lt;/code&gt; 方法。为了避免用户使用 ! 命令时和已有的地址冲突，在用户名后紧跟 .followers。比如 Bob 发布一条消息 时，所有注册到 bob.followers 的处理程序都会收到消息(如下代码所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;向关注者群发消息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private void broadcastMessage(String message) {     String name = this.name.get();     eventBus.publish(name + &amp;quot;.followers&amp;quot;, name +‘&amp;gt;’+ message); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在处理程序里，我们希望和早先的操作一样:将消息传递给客户&lt;/p&gt; &lt;ul&gt; &lt;li&gt;接收群发的消息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private void followUser(String user) { eventBus.registerHandler(user + &amp;quot;.followers&amp;quot;, (Message&amp;lt;String&amp;gt; message) -&amp;gt; {              sendClient(message.body());          }); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;如果将消息发送到有多个处理程序监听的地址，则会轮询决定哪个处理程序会接收到消息。这意味着在注册地址时要多加小心。&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;9.3 消息传递架构&lt;/h2&gt; &lt;p&gt;这里我们要讨论的是一种基于消息传递的架构，我用它实现了一个简单的聊天客户端。聊天客户端的细节并不重要，重要的是这个模式，那就让我们来谈谈消息传递本身吧。&lt;/p&gt; &lt;p&gt;首先要注意的是我们的设计里不共享任何状态。&lt;code&gt;verticle&lt;/code&gt; 对象之间通过向事件总线发送消 息通信，这就是说我们不需要保护任何共享状态，因此根本不需要在代码中添加锁或使用 &lt;code&gt;synchronized&lt;/code&gt; 关键字，编写并发程序变得更加简单。&lt;/p&gt; &lt;p&gt;为了确保不在 &lt;code&gt;verticle&lt;/code&gt; 对象之间共享状态，我们对事件总线上传递的消息做了某些限制。例子中使用的消息是普通的 &lt;code&gt;Java&lt;/code&gt; 字符串，它们天生就是不可变的，因此可以安全地在 &lt;code&gt;verticle&lt;/code&gt; 对象之间传递。 接收处理程序无法改变 &lt;code&gt;String&lt;/code&gt; 对象的状态，因此不会和消息发送者互相干扰。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Vert.x&lt;/code&gt; 没有限制只能使用字符串传递消息，我们可以使用更复杂的 &lt;code&gt;JSON&lt;/code&gt; 对象，甚至使用 &lt;code&gt;Buffer&lt;/code&gt; 类构建自己的消息。这些消息是可变的，也就是说如果使用不当，消息发送者和接收者可以通过读写消息共享状态。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Vert.x&lt;/code&gt; 框架通过在发送消息时复制消息的方式来避免这种问题。这样既保证接收者得到了正确的结果，又不会共享状态。无论是否使用 &lt;code&gt;Vert.x&lt;/code&gt;，确保消息不会共享状态都是最重要的。不可变消息是最简单的解决方式，但通过复制消息也能解决该问题。&lt;/p&gt; &lt;p&gt;使用 &lt;code&gt;verticle&lt;/code&gt; 对象模型开发的并发系统易于测试，因为每个 &lt;code&gt;verticle&lt;/code&gt; 对象都可以通过发送消息、验证返回值的方式单独测试。然后使用这些经过测试的模块组合成一个复杂系统，而不用担心使用共享的可变状态通信在集成时会遇到大量问题。当然，点对点的测试还是必须的，确保系统和预期的行为一致。&lt;/p&gt; &lt;p&gt;基于消息传递的系统让隔离错误变得简单，也便于编写可靠的代码。如果一个消息处理程序发生错误，可以选择重启本地 &lt;code&gt;verticle&lt;/code&gt; 对象，而不用去重启整个 &lt;code&gt;JVM&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;在第 6 章中，我们看到了如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式和 &lt;code&gt;Stream&lt;/code&gt; 类库编写并行处理数据代码。 并行机制让处理海量数据的速度更快，消息传递和稍后将会介绍的响应式编程是问题的另 一面:我们希望在有限的并行运行的线程里，执行更多的 &lt;code&gt;I/O&lt;/code&gt; 操作，比如连接更多的聊天客户端。无论哪种情况，解决方案都是一样的:使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式表示行为，构建 &lt;code&gt;API&lt;/code&gt; 来管理并发。聪明的类库意味着简单的应用代码。&lt;/p&gt; &lt;h2&gt;9.4 末日金字塔&lt;/h2&gt; &lt;p&gt;读者已经看到了如何使用回调和事件编写非阻塞的并发代码，但是我还没提起房间里的大象。如果编写代码时使用了大量的回调，代码会变得难于阅读，即便使用了 &lt;code&gt;Lambda&lt;/code&gt; 表达式也是如此。让我们通过一个具体例子来更好地理解这个问题。&lt;/p&gt; &lt;p&gt;在编写聊天程序服务器端代码时，我写了很多测试，从客户端的角度描述了 &lt;code&gt;verticle&lt;/code&gt; 对象 的行为。如下代码中的 &lt;code&gt;messageFriend&lt;/code&gt; 测试所示:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;检测聊天服务器上两个朋友是否能发消息的测试&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void messageFriend() {          withModule(() -&amp;gt; {              withConnection(richard -&amp;gt; {                  richard.dataHandler(data -&amp;gt; {                      assertEquals(&amp;quot;bob&amp;gt;oh its you!&amp;quot;, data.toString());                      moduleTestComplete();             });             richard.write(&amp;quot;richard\n&amp;quot;);             withConnection(bob -&amp;gt; {                      bob.dataHandler(data -&amp;gt; {                          assertEquals(&amp;quot;richard&amp;gt;hai&amp;quot;, data.toString());                          bob.write(&amp;quot;richard&amp;lt;oh its you!&amp;quot;);                      });                      bob.write(&amp;quot;bob\n&amp;quot;);                      vertx.setTimer(6, id -&amp;gt; richard.write(&amp;quot;bob&amp;lt;hai&amp;quot;));             });          });     });  }      &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我连上两个客户端，分别是 &lt;code&gt;Richard&lt;/code&gt; 和 &lt;code&gt;Bob，Richard&lt;/code&gt; 对 &lt;code&gt;Bob&lt;/code&gt; 说“嗨”，&lt;code&gt;Bob&lt;/code&gt; 回答“哦，是 你啊”。我已经将建立连接的通用代码重构，即使这样，读者依然会注意到那些嵌套的回调形成了一个末日金字塔。代码不断地向屏幕右方挤过去，就像一座金字塔。(别看我，这名字又不是我起的!)这是一个众所周知的反模式，让代码难于阅读和理解。同时，将代码的逻辑分散在了多个方法里。&lt;/p&gt; &lt;p&gt;上一章我们讨论过如何通过将一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式传给 &lt;code&gt;with&lt;/code&gt; 方法的方式来管理资源。读者会注意到，在测试代码中我多次用到了该方法。&lt;code&gt;withModule&lt;/code&gt; 方法部署 &lt;code&gt;Vert.x&lt;/code&gt; 模块，运行一 些代码然后关闭模块。还有一个 &lt;code&gt;withConnection&lt;/code&gt; 方法连接到 &lt;code&gt;ChatVerticle&lt;/code&gt;，使用完毕后关掉连接。&lt;/p&gt; &lt;p&gt;这里使用 &lt;code&gt;with&lt;/code&gt; 方法，而不使用 &lt;code&gt;try-with-resources&lt;/code&gt; 的方式，好处是它符合本章我们使用的非阻塞线程模型。我们可以重构代码，让它变得易于理解，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;分成多个方法后的测试代码，测试聊天服务器上两个朋友是否能发消息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void canMessageFriend() {     withModule(this::messageFriendWithModule);  }  private void messageFriendWithModule() {      withConnection(richard -&amp;gt; {              checkBobReplies(richard);              richard.write(&amp;quot;richard\n&amp;quot;);              messageBob(richard);     });  } private void messageBob(NetSocket richard) {      withConnection(messageBobWithConnection(richard)); } private Handler&amp;lt;NetSocket&amp;gt; messageBobWithConnection(NetSocket richard) {     return bob -&amp;gt; {         checkRichardMessagedYou(bob);         bob.write(&amp;quot;bob\n&amp;quot;);         vertx.setTimer(6, id -&amp;gt; richard.write(&amp;quot;bob&amp;lt;hai&amp;quot;));     }; } private void checkRichardMessagedYou(NetSocket bob) {      bob.dataHandler(data -&amp;gt; {              assertEquals(&amp;quot;richard&amp;gt;hai&amp;quot;, data.toString());              bob.write(&amp;quot;richard&amp;lt;oh its you!&amp;quot;);          }); } private void checkBobReplies(NetSocket richard) {      richard.dataHandler(data -&amp;gt; {              assertEquals(&amp;quot;bob&amp;gt;oh its you!&amp;quot;, data.toString());              moduleTestComplete();          }); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上面的代码中的重构将测试逻辑分散在了多个方法里，解决了末日金字塔问题。不再是一个方 法只能有一个功能，我们将一个功能分散在了多个方法里!代码还是难于阅读，不过这次换了一个方式。&lt;/p&gt; &lt;p&gt;想要链接或组合的操作越多，问题就会越严重，我们需要一个更好的解决方案。&lt;/p&gt; &lt;h2&gt;9.5 Future&lt;/h2&gt; &lt;p&gt;构建复杂并行操作的另外一种方案是使用 &lt;code&gt;Future&lt;/code&gt;。&lt;code&gt;Future&lt;/code&gt; 像一张欠条，方法不是返回一个值，而是返回一个 &lt;code&gt;Future&lt;/code&gt; 对象，该对象第一次创建时没有值，但以后能拿它“换回”一个值。&lt;/p&gt; &lt;p&gt;调用 &lt;code&gt;Future&lt;/code&gt; 对象的 &lt;code&gt;get&lt;/code&gt; 方法获取值，它会阻塞当前线程，直到返回值。可惜，和回调一样，组合 &lt;code&gt;Future&lt;/code&gt; 对象时也有问题，我们会快速浏览这些可能碰到的问题。&lt;/p&gt; &lt;p&gt;我们要考虑的场景是从外部网站查找某专辑的信息。我们需要找出专辑上的曲目列表和艺术家，还要保证有足够的权限访问登录等各项服务，或者至少确保已经登录。&lt;/p&gt; &lt;p&gt;如下代码使用 &lt;code&gt;Future API&lt;/code&gt; 解决了该问题。在 ① 处登录提供曲目和艺术家信息的服务，这时会 返回一个 &lt;code&gt;Future&amp;lt;Credentials&amp;gt;&lt;/code&gt; 对象，该对象包含登录信息。&lt;code&gt;Future&lt;/code&gt; 接口支持泛型，可将 &lt;code&gt;Future&amp;lt;Credentials&amp;gt;&lt;/code&gt; 看作是 &lt;code&gt;Credentials&lt;/code&gt; 对象的一张欠条。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Override public Album lookupByName(String albumName) {     Future&amp;lt;Credentials&amp;gt; trackLogin = loginTo(&amp;quot;track&amp;quot;);①      Future&amp;lt;Credentials&amp;gt; artistLogin = loginTo(&amp;quot;artist&amp;quot;);     try {         Future&amp;lt;List&amp;lt;Track&amp;gt;&amp;gt; tracks = lookupTracks(albumName, trackLogin.get());②         Future&amp;lt;List&amp;lt;Artist&amp;gt;&amp;gt; artists = lookupArtists(albumName, artistLogin.get());         return new Album(albumName, tracks.get(), artists.get()); ③     } catch (InterruptedException | ExecutionException e) {         throw new AlbumLookupException(e.getCause());   ④     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 ② 处使用登录后的凭证查询曲目和艺术家信息，通过调用 &lt;code&gt;Future&lt;/code&gt; 对象的 &lt;code&gt;get&lt;/code&gt; 方法获取凭证信息。在 ③ 处构建待返回的专辑对象，这里同样调用 &lt;code&gt;get&lt;/code&gt; 方法以阻塞 &lt;code&gt;Future&lt;/code&gt; 对象。如果有异常，我们在 ④ 处将其转化为一个待解问题域内的异常，然后将其抛出。&lt;/p&gt; &lt;p&gt;读者将会看到，如果要将 &lt;code&gt;Future&lt;/code&gt; 对象的结果传给其他任务，会阻塞当前线程的执行。这会成为一个性能问题，任务不是平行执行了，而是(意外地)串行执行。&lt;/p&gt; &lt;p&gt;以上面的例子来说，这意味着在登录两个服务之前，我们无法启动任何查找任务。没必要这样: &lt;code&gt;lookupTracks&lt;/code&gt; 只需要自己的登录凭证，&lt;code&gt;lookupArtists&lt;/code&gt; 也是一样。我们将理想的行为用如下图描述出来。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/12/v92lcuiev8jfsr5e6ea96dhutp.png" alt="alt" /&gt; 查询操作不必等待&lt;strong&gt;所有&lt;/strong&gt;登录操作完成后才能执行&lt;/p&gt; &lt;p&gt;可以将对 &lt;code&gt;get&lt;/code&gt; 的调用放到 &lt;code&gt;lookupTracks&lt;/code&gt; 和 &lt;code&gt;lookupArtists&lt;/code&gt; 方法的中间，这能解决问题，但是代码丑陋，而且无法在多次调用之间重用登录凭证。&lt;/p&gt; &lt;p&gt;我们真正需要的是不必调用 &lt;code&gt;get&lt;/code&gt; 方法阻塞当前线程，就能操作 &lt;code&gt;Future&lt;/code&gt; 对象返回的结果。我们需要将 &lt;code&gt;Future&lt;/code&gt; 和回调结合起来使用。&lt;/p&gt; &lt;h2&gt;9.6 CompletableFuture&lt;/h2&gt; &lt;p&gt;这些问题的解决之道是 &lt;code&gt;CompletableFuture&lt;/code&gt;，它结合了 &lt;code&gt;Future&lt;/code&gt; 对象打欠条的主意和使用回调处理事件驱动的任务。其要点是可以组合不同的实例，而不用担心末日金字塔问题。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;你以前可能接触过 CompletableFuture 对象背后的概念，在其他语言中这被 叫作延迟对象或约定。在Google Guava类库和Spring框架中，这被叫作 ListenableFutures。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;在如下代码中，我会使用 &lt;code&gt;CompletableFuture&lt;/code&gt; 重写上面的例子来展示它的用法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;CompletableFuture&lt;/code&gt; 从外部网站下载专辑信息&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public Album lookupByName(String albumName) {      CompletableFuture&amp;lt;List&amp;lt;Artist&amp;gt;&amp;gt; artistLookup = loginTo(&amp;quot;artist&amp;quot;)         .thenCompose(artistLogin -&amp;gt; lookupArtists(albumName, artistLogin)); ①     return loginTo(&amp;quot;track&amp;quot;)         .thenCompose(trackLogin -&amp;gt; lookupTracks(albumName, trackLogin))②         .thenCombine(artistLookup, (tracks, artists)             -&amp;gt; new Album(albumName, tracks, artists)) ③         .join(); ④ } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在上面的代码中，&lt;code&gt;loginTo&lt;/code&gt;、&lt;code&gt;lookupArtists&lt;/code&gt; 和 &lt;code&gt;lookupTracks&lt;/code&gt; 方 法 均 返 回 &lt;code&gt;CompletableFuture&lt;/code&gt; ， 而不是 &lt;code&gt;Future&lt;/code&gt;。&lt;code&gt;CompletableFuture API&lt;/code&gt; 的技巧是注册 &lt;code&gt;Lambda&lt;/code&gt; 表达式，并且把高阶函数链接起来。方法不同，但道理和 &lt;code&gt;Stream API&lt;/code&gt; 的设计是相通的。&lt;/p&gt; &lt;p&gt;在 ① 处使用 &lt;code&gt;thenCompose&lt;/code&gt; 方法将 &lt;code&gt;Credentials&lt;/code&gt; 对象转换成包含艺术家信息的 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象，这就像和朋友借了点钱，然后在亚马逊上花了。你不会马上拿到新买的书——亚马逊会发给你一封电子邮件，告诉你新书正在运送途中，又是一张欠条!&lt;/p&gt; &lt;p&gt;在 ② 处还是使用了 &lt;code&gt;thenCompose&lt;/code&gt; 方法，通过登录 &lt;code&gt;Track API&lt;/code&gt;，将 &lt;code&gt;Credentials&lt;/code&gt; 对象转换成包 含曲目信息的 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象。这里引入了一个新方法 &lt;code&gt;thenCombine&lt;/code&gt; ③，该方法将一个 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象的结果和另一个 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象组合起来。组合操作是由用户提供的 &lt;code&gt;Lambda&lt;/code&gt; 表达式完成，这里我们要使用曲目信息和艺术家信息构建一个 &lt;code&gt;Album&lt;/code&gt; 对象。&lt;/p&gt; &lt;p&gt;这时我有必要提醒大家，和使用 &lt;code&gt;Stream API&lt;/code&gt; 一样，现在还没真正开始做事呢，只是定义好了做事的规则。在调用最终的方法之前，无法保证 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象已经生成结果。&lt;code&gt;CompletableFuture&lt;/code&gt; 对象实现了 &lt;code&gt;Future&lt;/code&gt; 接口，可以调用 &lt;code&gt;get&lt;/code&gt; 方法获取值。 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象包含 &lt;code&gt;join&lt;/code&gt; 方法，我们在 ④ 处调用了该方法，它的作用和 &lt;code&gt;get&lt;/code&gt; 方法 是一样的，而且它没有使用 &lt;code&gt;get&lt;/code&gt; 方法时令人倒胃口的检查异常。&lt;/p&gt; &lt;p&gt;读者现在可能已经掌握了使用 &lt;code&gt;CompletableFuture&lt;/code&gt; 的基础，但是如何创建它们又是另外一 回事。创建 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象分两部分:创建对象和传给它欠客户代码的值。&lt;/p&gt; &lt;p&gt;如下代码所示，创建 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象非常简单，调用它的构造函数就够了。现在就可以将该对象传给客户代码，用来将操作链接在一起。我们同时保留了对该对象的引用，以便在另一个线程里继续执行任务。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;为 &lt;code&gt;Future&lt;/code&gt; 提供值&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;CompletableFuture&amp;lt;Artist&amp;gt; createFuture(String id) {      CompletableFuture&amp;lt;Artist&amp;gt; future = new CompletableFuture&amp;lt;&amp;gt;();      startJob(future); return future; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一旦任务完成，不管是在哪个线程里执行的，都需要告诉 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象那个值， 这份工作可以由各种线程模型完成。比如，可以 &lt;code&gt;submit&lt;/code&gt; 一个任务给 &lt;code&gt;ExecutorService&lt;/code&gt;，或 者使用类似 &lt;code&gt;Vert.x&lt;/code&gt; 这样基于事件循环的系统，或者直接启动一个线程来执行任务。在如下例子中，为了告诉 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象值已就绪，需要调用 &lt;code&gt;complete&lt;/code&gt; 方法，是时候还债了，如下图所示。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/12/6d9jm8d13mg4goqekjbhf60fac.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;一个可完成的 Future 是一张可以被处理的欠条&lt;/p&gt; &lt;p&gt;当然，&lt;code&gt;CompletableFuture&lt;/code&gt; 的常用情境之一是异步执行一段代码，该段代码计算并返回一个值。为了避免大家重复实现同样的代码，有一个工厂方法 &lt;code&gt;supplyAsync&lt;/code&gt;，用来创建 &lt;code&gt;CompletableFuture&lt;/code&gt; 实例，如下例子所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;异步创建 CompletableFuture 实例的示例代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;CompletableFuture&amp;lt;Track&amp;gt; lookupTrack(String id) {     return CompletableFuture.supplyAsync(() -&amp;gt; {         // 这里会做一些繁重的工作 ①         // ...         return track; //②     }, service)//③ } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;supplyAsync&lt;/code&gt; 方法接受一个 &lt;code&gt;Supplier&lt;/code&gt; 对象作为参数，然后执行它。如 ① 处所示，这里的要点是能执行一些耗时的任务，同时不会阻塞当前线程——这就是方法名中 &lt;code&gt;Async&lt;/code&gt; 的含义。② 处的返回值用来完成 &lt;code&gt;CompletableFuture&lt;/code&gt;。在 ③ 处我们提供了一个叫作 &lt;code&gt;service&lt;/code&gt; 的 &lt;code&gt;Executor&lt;/code&gt;，告诉 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象在哪里执行任务。如果没有提供 &lt;code&gt;Executor&lt;/code&gt;，就会使 用相同的 &lt;code&gt;fork/join&lt;/code&gt; 线程池并行执行。&lt;/p&gt; &lt;p&gt;当然，不是所有的欠条都能兑现。有时候碰上异常，我们无力偿还，如下代码所示， &lt;code&gt;CompletableFuture&lt;/code&gt; 为此提供了 &lt;code&gt;completeExceptionally&lt;/code&gt;，用于处理异常情况。该方法可以视作 &lt;code&gt;complete&lt;/code&gt; 方法的备选项，但不能同时调用 &lt;code&gt;complete&lt;/code&gt; 和 &lt;code&gt;completeExceptionally&lt;/code&gt; 方法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;出现错误时完成 Future&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;future.completeExceptionally(new AlbumLookupException(&amp;quot;Unable to find &amp;quot; + name)); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;完整讨论 &lt;code&gt;CompletableFuture&lt;/code&gt; 接口已经超出了本章的范围，很多时候它是一个隐藏大礼包。 该接口有很多有用的方法，可以用你想到的任何方式组合 &lt;code&gt;CompletableFuture&lt;/code&gt; 实例。现在， 读者应该能熟练地使用高阶函数链接各种操作，告诉计算机应该做什么了吧&lt;/p&gt; &lt;p&gt;让我们简单看一下其中的一些用例。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;如果你想在链的末端执行一些代码而不返回任何值，比如 &lt;code&gt;Consumer&lt;/code&gt; 和R &lt;code&gt;unnable&lt;/code&gt;，那就 看看 &lt;code&gt;thenAccept&lt;/code&gt; 和 &lt;code&gt;thenRun&lt;/code&gt; 方法。&lt;/li&gt; &lt;li&gt;可使用 &lt;code&gt;thenApply&lt;/code&gt; 方法转换 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象的值，有点像使用 &lt;code&gt;Stream&lt;/code&gt; 的 &lt;code&gt;map&lt;/code&gt; 方法。&lt;/li&gt; &lt;li&gt;在 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象出现异常时，可使用 &lt;code&gt;exceptionally&lt;/code&gt; 方法恢复，可以将一个函数注册到该方法，返回一个替代值。&lt;/li&gt; &lt;li&gt;如果你想有一个 &lt;code&gt;map&lt;/code&gt;，包含异常情况和正常情况，请使用 &lt;code&gt;handle&lt;/code&gt; 方法。&lt;/li&gt; &lt;li&gt;要找出 &lt;code&gt;CompletableFuture&lt;/code&gt; 对象到底出了什么问题，可使用 &lt;code&gt;isDone&lt;/code&gt; 和 &lt;code&gt;isCompleted-Exceptionally&lt;/code&gt; 方法辅助调查。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 对于处理并发任务非常有用，但这并不是唯一的办法。下面要学习的概念提供了更多的灵活性，但是代码也更复杂。&lt;/p&gt; &lt;h2&gt;9.7 响应式编程&lt;/h2&gt; &lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 背后的概念可以从单一的返回值推广到数据流，这就是响应式编程。响应式编程其实是一种声明式编程方法，它让程序员以自动流动的变化和数据流来编程。&lt;/p&gt; &lt;p&gt;你可以将电子表格想象成一个使用响应式编程的例子。如果在单元格 &lt;code&gt;C1&lt;/code&gt; 中键入 &lt;code&gt;=B1+5&lt;/code&gt;，其实是在告诉电子表格将 &lt;code&gt;B1&lt;/code&gt; 中的值加 &lt;code&gt;5&lt;/code&gt;，然后将结果存入 &lt;code&gt;C1&lt;/code&gt;。而且，将来 &lt;code&gt;B1&lt;/code&gt; 中的值变化后，电子表格会自动刷新 &lt;code&gt;C1&lt;/code&gt; 中的值。&lt;/p&gt; &lt;p&gt;&lt;code&gt;RxJava&lt;/code&gt; 类库将这种响应式的理念移植到了&lt;code&gt;JVM&lt;/code&gt;。我们这里不会深入类库，只描述其中的一些关键概念。&lt;code&gt;RxJava&lt;/code&gt; 类库引入了一个叫作 &lt;code&gt;Observable&lt;/code&gt; 的类，该类代表了一组待响应的事件，可以理解为一沓欠条。在 &lt;code&gt;Observable&lt;/code&gt; 对象和第 3 章讲述的 &lt;code&gt;Stream&lt;/code&gt; 接口之间有很强的关联。&lt;/p&gt; &lt;p&gt;两种情况下，都需要使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式将行为和一般的操作关联、都需要将高阶函数链 接起来定义完成任务的规则。实际上，&lt;code&gt;Observable&lt;/code&gt; 定义的很多操作都和 &lt;code&gt;Stream&lt;/code&gt; 的相同: &lt;code&gt;map、filter、reduce&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;最大的不同在于用例。&lt;code&gt;Stream&lt;/code&gt; 是为构建内存中集合的计算流程而设计的，而 &lt;code&gt;RxJava&lt;/code&gt; 则是为了组合异步和基于事件的系统流程而设计的。它没有取数据，而是把数据放进去。换个角度理解 &lt;code&gt;RxJava&lt;/code&gt;，它是处理一组值，而 &lt;code&gt;CompletableFuture&lt;/code&gt; 用来处理一个值。&lt;/p&gt; &lt;p&gt;这次的例子是查找艺术家，如下代码所示。&lt;code&gt;search&lt;/code&gt; 方法根据名字和国籍过滤结果，它在本地缓存了一份艺术家名单，但必须从外部服务上查询艺术家信息，比如国籍。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通过名字和国籍查找艺术家&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public Observable&amp;lt;Artist&amp;gt; search(String searchedName,                                   String searchedNationality,                                  int maxResults) {     return getSavedArtists() ①            .filter(name -&amp;gt; name.contains(searchedName)) ②            .flatMap(this::lookupArtist) ③            .filter(artist -&amp;gt; artist.getNationality() ④                                    .contains(searchedNationality))             .take(maxResults); ⑤                                                   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 ① 处取得一个包含艺术家姓名的 &lt;code&gt;Observable&lt;/code&gt; 对象，该对象的高阶函数和 &lt;code&gt;Stream&lt;/code&gt; 类似，在②和③处使用姓名和国籍做过滤，和使用 &lt;code&gt;Stream&lt;/code&gt; 是一样的。&lt;/p&gt; &lt;p&gt;在 ④ 处将姓名替换为一个 &lt;code&gt;Artist&lt;/code&gt; 对象，如果这只是调用构造函数这么简单，我们显然会使用 &lt;code&gt;map&lt;/code&gt; 操作。但这里我们需要组合调用一系列外部服务，每种服务都可能在它自己的线程或线程池里执行。因此，我们将名字替换为 &lt;code&gt;Observable&lt;/code&gt; 对象，来表示一个或多个艺术家，因此使用了 &lt;code&gt;flatMap&lt;/code&gt; 操作。&lt;/p&gt; &lt;p&gt;我们还需要在查找时限定返回结果的最大值:&lt;code&gt;maxResults&lt;/code&gt;，在 ⑤ 处，我们通过调用 &lt;code&gt;Observable&lt;/code&gt; 对象的 &lt;code&gt;take&lt;/code&gt; 方法来实现该功能。&lt;/p&gt; &lt;p&gt;读者会发现，这个 &lt;code&gt;API&lt;/code&gt; 很像使用 &lt;code&gt;Stream&lt;/code&gt;。它和 &lt;code&gt;Stream&lt;/code&gt; 的最大区别是:&lt;code&gt;Stream&lt;/code&gt; 是为了计算最终结果，而 &lt;code&gt;RxJava&lt;/code&gt; 在线程模型上则像 &lt;code&gt;CompletableFuture。&lt;/code&gt;&lt;/p&gt; &lt;p&gt;使用 &lt;code&gt;CompletableFuture&lt;/code&gt; 时，我们通过给 &lt;code&gt;complete&lt;/code&gt; 方法一个值来偿还欠条。而 &lt;code&gt;Observable&lt;/code&gt; 代表了一个事件流，我们需要有能力传入多个值，如下代码展示了该怎么做。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;给 &lt;code&gt;Observable&lt;/code&gt; 对象传值，并且完成它&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    observer.onNext(&amp;quot;a&amp;quot;);     observer.onNext(&amp;quot;b&amp;quot;);     observer.onNext(&amp;quot;c&amp;quot;);     observer.onCompleted(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们不停地调用 &lt;code&gt;onNext&lt;/code&gt; 方法，&lt;code&gt;Observable&lt;/code&gt; 对象中的每个值都调用一次。这可以在一个循环里做，也可以在任何我们想要生成值的线程里做。一旦完成了产生事件的工作，就调 用 &lt;code&gt;onCompleted&lt;/code&gt; 方法表示任务完成。和使用 &lt;code&gt;Stream&lt;/code&gt; 一样，也有一些静态工厂方法用来从 &lt;code&gt;Future&lt;/code&gt;、迭代器和数组中创建 &lt;code&gt;Observable&lt;/code&gt; 对象。&lt;/p&gt; &lt;p&gt;和 &lt;code&gt;CompletableFuture&lt;/code&gt; 类似，&lt;code&gt;Observable&lt;/code&gt; 也能处理异常。如果出现错误，调用 &lt;code&gt;onError&lt;/code&gt; 方 法，如下代码所示。这里的功能和 &lt;code&gt;CompletableFuture&lt;/code&gt; 略有不同——你能得到异常发生之前所有的事件，但两种情况下，只能正常或异常地终结程序，两者只能选其一。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通知 &lt;code&gt;Observable&lt;/code&gt; 对象有错误发生&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;observer.onError(new Exception()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和介绍 &lt;code&gt;CompletableFuture&lt;/code&gt; 时一样，这里只给出了如何使用和在什么地方使用 &lt;code&gt;Observable&lt;/code&gt; 的一点建议。读者如果想了解跟多细节，请阅读项目文档(https://github.com/ReactiveX/ RxJava/wiki/Getting-Started)。&lt;code&gt;RxJava&lt;/code&gt; 已经开始集成进 Java 类库的生态系统，比如企业 级的集成框架 &lt;code&gt;Apache Camel&lt;/code&gt; 已经加入了一个叫作 [Camel-RX](http://camel.apache.org/ rx.html) 的模块，该模块使得可以在该框架中使用 &lt;code&gt;RxJava&lt;/code&gt;。&lt;code&gt;Vert.x&lt;/code&gt; 项目也启动了一个 &lt;a href="https://github.com/vert-x/mod-rxvertx" target="_blank"&gt;Rx-ify&lt;/a&gt; 它的 &lt;code&gt;API&lt;/code&gt; 项目。&lt;/p&gt; &lt;h2&gt;9.8 何时何地使用新技术&lt;/h2&gt; &lt;p&gt;本章讲解了如何使用非阻塞式和基于事件驱动的系统。这是否意味着大家明天就要扔掉现有的&lt;code&gt;Java EE&lt;/code&gt; 或者 &lt;code&gt;Spring&lt;/code&gt; 企业级 &lt;code&gt;Web&lt;/code&gt; 应用呢?答案当然是否定的。&lt;/p&gt; &lt;p&gt;即使不去考虑 &lt;code&gt;CompletableFuture&lt;/code&gt; 和 &lt;code&gt;RxJava&lt;/code&gt; 相对较新，使用它们依然有一定的复杂度。它们用起来比到处显式使用 &lt;code&gt;Future&lt;/code&gt; 和回调简单，但对很多问题来说，传统的阻塞式 &lt;code&gt;Web&lt;/code&gt; 应 用开发技术就足够了。如果还能用，就别修理。&lt;/p&gt; &lt;p&gt;当然，我也不是说阅读本章会白白浪费您一个美好的下午。事件驱动和响应式应用正在变得越来越流行，而且经常会是为你的问题建模的最好方式之一。响应式编程宣言(http:// www.reactivemanifesto.org/)鼓励大家使用这种方式编写更多应用，如果它适合你的待解问 题，那么就应该使用。相比阻塞式设计，有两种情况可能特别适合使用响应式或事件驱动的方式来思考。&lt;/p&gt; &lt;p&gt;第一种情况是业务逻辑本身就使用事件来描述。&lt;code&gt;Twitter&lt;/code&gt; 就是一个经典例子。&lt;code&gt;Twitter&lt;/code&gt; 是一种订阅文字流信息的服务，用户彼此之间推送信息。使用事件驱动架构编写应用，能准确地为业务建模。图形化展示股票价格可能是另一个例子，每一次价格的变动都可认为是一个事件。&lt;/p&gt; &lt;p&gt;另一种显然的用例是应用需要同时处理大量 &lt;code&gt;I/O&lt;/code&gt; 操作。阻塞式 &lt;code&gt;I/O&lt;/code&gt; 需要同时使用大量线程， 这会导致大量锁之间的竞争和太多的上下文切换。如果想要处理成千上万的连接，非阻塞式 &lt;code&gt;I/O&lt;/code&gt; 通常是更好的选择。&lt;/p&gt; &lt;h2&gt;9.9 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;使用基于 &lt;code&gt;Lambda&lt;/code&gt; 表达式的回调，很容易实现事件驱动架构。&lt;/li&gt; &lt;li&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 代表了 &lt;code&gt;IOU&lt;/code&gt;，使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式能方便地组合、合并。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Observable&lt;/code&gt; 继承了 &lt;code&gt;CompletableFuture&lt;/code&gt; 的概念，用来处理数据流。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 14 Feb 2019 12:36:00 GMT</pubDate>
    </item>
    <item>
      <title>多线程之 Future 学习</title>
      <link>https://www.zhangaoo.com/article/java-future</link>
      <content:encoded>&lt;h1&gt;Future Start&lt;/h1&gt; &lt;p&gt;学习 &lt;code&gt;Future&lt;/code&gt; 之前先简单的学习一下 &lt;code&gt;Callable&lt;/code&gt; 和 &lt;code&gt;Runnable&lt;/code&gt;&lt;/p&gt; &lt;h2&gt;Callable 和 Runnable&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Java&lt;/code&gt; 多线程实现方式主要有四种：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;继承 &lt;code&gt;Thread&lt;/code&gt; 类&lt;/li&gt; &lt;li&gt;实现 &lt;code&gt;Runnable&lt;/code&gt; 接口&lt;/li&gt; &lt;li&gt;实现 &lt;code&gt;Callable&lt;/code&gt; 接口通过 &lt;code&gt;FutureTask&lt;/code&gt; 包装器来创建 &lt;code&gt;Thread&lt;/code&gt; 线程、&lt;/li&gt; &lt;li&gt;使用 &lt;code&gt;ExecutorService、Callable、Future&lt;/code&gt; 实现有返回结果的多线程。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;Runnable&lt;/h3&gt; &lt;p&gt;代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package java.lang;  @FunctionalInterface public interface Runnable {     void run(); } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;由于&lt;code&gt;run()&lt;/code&gt;方法返回值为 &lt;code&gt;void&lt;/code&gt; 类型，所以在执行完任务之后无法返回任何结果。&lt;/li&gt; &lt;li&gt;使用示例1&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class MyTask implements Runnable{         CountDownLatch latch;         public MyTask(CountDownLatch latch){             this.latch = latch;         }         public MyTask(){             this.latch = null;         }         @Override         public void run(){             try{                 System.out.println(&amp;quot;Starting my task&amp;quot;);                 Thread.sleep(1000);                 System.out.println(&amp;quot;Done my task&amp;quot;);             } catch (InterruptedException e){                 System.out.println(e.fillInStackTrace());             } finally {                 if(this.latch != null){                     latch.countDown();                 }             }         }     }     @Test     public void testRunnable()throws InterruptedException{         Thread thread = new Thread(new MyTask());         thread.start();         thread.join();     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用示例2&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public void testRunnable1()throws InterruptedException{         ExecutorService service = Executors.newFixedThreadPool(1);         service.execute(new MyTask());         service.shutdown();         /**主线程等待子线程执行结束再结束**/         try {             service.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);         } catch (InterruptedException e) {             System.out.println(e.fillInStackTrace());         }     }     /**使用CountDownLatch使主进程等待子进程**/         @Test     public void testRunnable2()throws InterruptedException{         CountDownLatch latch = new CountDownLatch(1);         ExecutorService service = Executors.newFixedThreadPool(1);         service.execute(new MyTask(latch));          try {             System.out.println(&amp;quot;Waiting one sub thread ...&amp;quot;);             latch.await();             System.out.println(&amp;quot;Sub thread all donne&amp;quot;);             System.out.println(&amp;quot;Continuing main thread&amp;quot;);         } catch (InterruptedException e) {             e.printStackTrace();         }     }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Callable&lt;/h3&gt; &lt;p&gt;接口定义&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@FunctionalInterface public interface Callable&amp;lt;V&amp;gt; {     /**      * Computes a result, or throws an exception if unable to do so.      *      * @return computed result      * @throws Exception if unable to compute a result      */     V call() throws Exception; } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;这是一个泛型接口，&lt;code&gt;call()&lt;/code&gt;函数返回的类型就是传递进来的 &lt;code&gt;V&lt;/code&gt; 类型。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Callable&lt;/code&gt; 一般与 &lt;code&gt;FutureTask&lt;/code&gt; 配合使用，&lt;code&gt;FutureTask&lt;/code&gt; 使用参照下面的内容&lt;/li&gt; &lt;li&gt;示例1&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class MyCallable implements Callable&amp;lt;String&amp;gt;{         private String context;         public MyCallable(String context){             this.context = context;         }         @Override         public String call(){            try{                Thread.sleep(1000);                return this.context + &amp;quot; append something&amp;quot;;            } catch (InterruptedException e){                e.printStackTrace();            }            return this.context;         }     }      @Test     public void testCallable()throws ExecutionException,InterruptedException{         Callable&amp;lt;String&amp;gt; mCallable = new MyCallable(&amp;quot;Wether is good.&amp;quot;);         FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(mCallable);         Thread thread = new Thread(task);         thread.start();         //调用get方法阻塞主线程         String result = task.get();         System.out.println(result);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;示例2 使用线程池&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCallable2()throws ExecutionException,InterruptedException{         ExecutorService service = Executors.newFixedThreadPool(1);         Callable&amp;lt;String&amp;gt; mCallable = new MyCallable(&amp;quot;Wether is good.&amp;quot;);         FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(mCallable);         service.execute(task);         //调用get方法阻塞主线程         String result = task.get();         System.out.println(result);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;示例3 使用线程池&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCallable3()throws ExecutionException,InterruptedException{         ExecutorService service = Executors.newFixedThreadPool(1);         Callable&amp;lt;String&amp;gt; mCallable = new MyCallable(&amp;quot;Wether is good.&amp;quot;);         Future&amp;lt;String&amp;gt; future = service.submit(mCallable);         //调用get方法阻塞主线程         String result = future.get();         System.out.println(result);     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;FutureTask&lt;/h3&gt; &lt;p&gt;代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface RunnableFuture&amp;lt;V&amp;gt; extends Runnable, Future&amp;lt;V&amp;gt; {     /**      * Sets this Future to the result of its computation      * unless it has been cancelled.      */     void run(); } public class FutureTask&amp;lt;V&amp;gt; implements RunnableFuture&amp;lt;V&amp;gt; {     ... } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;可以看出 &lt;code&gt;RunnableFuture&lt;/code&gt; 继承了 &lt;code&gt;Runnable&lt;/code&gt; 接口和 &lt;code&gt;Future&lt;/code&gt; 接口，而 &lt;code&gt;FutureTask&lt;/code&gt; 实现了 &lt;code&gt;RunnableFuture&lt;/code&gt; 接口。&lt;/li&gt; &lt;li&gt;可见 &lt;code&gt;FutureTask&lt;/code&gt; 是 &lt;code&gt;Runnable&lt;/code&gt; 和 &lt;code&gt;Future&lt;/code&gt; 接口的实现&lt;/li&gt; &lt;li&gt;所以它既可以作为 &lt;code&gt;Runnable&lt;/code&gt; 被线程执行，又可以作为 &lt;code&gt;Future&lt;/code&gt; 得到 &lt;code&gt;Callable&lt;/code&gt; 的返回值。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Future&lt;/h3&gt; &lt;p&gt;如上面的介绍，&lt;code&gt;Future&lt;/code&gt; 的两种典型用法，这两种用法的主要不同点就是线程池使用 &lt;code&gt;execute、submit&lt;/code&gt; 执行的区别。但是 &lt;code&gt;Future&lt;/code&gt; 有个明显的缺点，&lt;code&gt;get&lt;/code&gt; 方法会阻塞当前线程，也就下面的两种用法只能串行执行。这和我们预想的不太一样，这也是下面要学习的 &lt;code&gt;CompletableFuture&lt;/code&gt; 所要解决的问题。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testFuture()throws ExecutionException,InterruptedException{         //用法一         ExecutorService service = Executors.newFixedThreadPool(2);         Callable&amp;lt;String&amp;gt; mCallable = new MyCallable(&amp;quot;Wether is good.&amp;quot;);         FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(mCallable);         service.execute(task);         //调用get方法阻塞主线程         String result = task.get();         System.out.println(result);          //用法二         Callable&amp;lt;String&amp;gt; mCallable1 = new MyCallable(&amp;quot;Wether is good.&amp;quot;);         Future&amp;lt;String&amp;gt; future1 = service.submit(mCallable);         //调用get方法阻塞主线程         String result1 = future1.get();         System.out.println(result1);     } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 14 Feb 2019 12:33:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇七之设计和架构的原则</title>
      <link>https://www.zhangaoo.com/article/java8-lambda-architecture</link>
      <content:encoded>&lt;h1&gt;第8章 设计和架构的原则&lt;/h1&gt; &lt;p&gt;本章旨在帮助大家写出优秀的程序，我会给出一些良好的设计原则和模式，在此基础之上，就能开发出可维护且十分可靠的程序。我们不光会用到 &lt;code&gt;JDK&lt;/code&gt; 提供的崭新类库，而且会教大家如何在自己的领域和应用程序中使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式。&lt;/p&gt; &lt;h2&gt;8.1 Lambda表达式改变了设计模式&lt;/h2&gt; &lt;p&gt;设计模式是人们熟悉的另一种设计思想，它是软件架构中解决通用问题的模板。如果碰到 一个问题，并且恰好熟悉一个与之适应的模式，就能直接应用该模式来解决问题。从某种 程度上来说，设计模式将解决特定问题的最佳实践途径固定了下来。&lt;/p&gt; &lt;p&gt;当然，没有永远的最佳实践。以曾经风靡一时的单例模式为例，该模式确保只产生一个对象实例。在过去十年中，人们批评它让程序变得更脆弱，且难于测试。敏捷开发的流行， 让测试显得更加重要，单例模式的这个问题把它变成了一个反模式:一种应该避免使用的模式。&lt;/p&gt; &lt;p&gt;本书的重点并不是讨论设计模式如何变得过时，相反，我们讨论的是如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式，让现有设计模式变得更好、更简单，或者在某些情况下，有了不同的实现方式。 &lt;code&gt;Java 8&lt;/code&gt; 引入的新语言特性是所有这些设计模式变化的推动因素。&lt;/p&gt; &lt;h3&gt;8.1.1 命令者模式&lt;/h3&gt; &lt;p&gt;命令者是一个对象，它封装了调用另一个方法的所有细节，命令者模式使用该对象，可以 编写出根据运行期条件，顺序调用方法的一般化代码。命令者模式中有四个类参与其中，如下图所示：&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/rid5npdhguj65onoodhcilb275.png" alt="命令着模式" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;命令接收者：执行实际任务。&lt;/li&gt; &lt;li&gt;命令者：封装了所有调用命令执行者的信息。&lt;/li&gt; &lt;li&gt;发起者：控制一个或多个命令的顺序和执行。&lt;/li&gt; &lt;li&gt;客户端：创建具体的命令者实例。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;看一个命令者模式的具体例子，看看如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式改进该模式。假设有一个 &lt;code&gt;GUI Editor&lt;/code&gt; 组件，在上面可以执行 &lt;code&gt;open、save&lt;/code&gt; 等一系列操作，如下图所示。现在我们想实现宏功能——也就是说，可以将一系列操作录制下来，日后作为一个操作执行，这就是我们的命令接收者。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;文本编辑器可能具有的一般功能&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Editor {     public void save();      public void open();      public void close(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在该例子中，像 &lt;code&gt;open、save&lt;/code&gt; 这样的操作称为命令，我们需要一个统一的接口来概括这些不同的操作，我将这个接口叫作 &lt;code&gt;Action&lt;/code&gt;，它代表了一个操作。所有的命令都要实现该接口&lt;/p&gt; &lt;ul&gt; &lt;li&gt;所有操作均实现 &lt;code&gt;Action&lt;/code&gt; 接口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Action {     public void perform();  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在让每个操作都实现该接口，这些类要做的只是在 &lt;code&gt;Action&lt;/code&gt; 接口中调用 &lt;code&gt;Editor&lt;/code&gt; 类中的一 个方法。我将遵循恰当的命名规范，用类名代表操作，比如 &lt;code&gt;save&lt;/code&gt; 方法对应 &lt;code&gt;Save&lt;/code&gt; 类。如下代码是定义好的命令对象。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;保存操作代理给 &lt;code&gt;Editor&lt;/code&gt; 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Save implements Action {     private  final Editor editor;      public Save(Editor editor) {         this.editor = editor;     }     @Override     public void perform() {              editor.save();          } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;打开文件操作代理给 &lt;code&gt;Editor&lt;/code&gt; 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Open implements Action {     private  final Editor editor;     public Open(Editor editor) {          this.editor = editor;     }     @Override     public void perform() {             editor.open();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在可以实现 &lt;code&gt;Macro&lt;/code&gt; 类了，该类 &lt;code&gt;record&lt;/code&gt; 操作，然后一起运行。我们使用 &lt;code&gt;List&lt;/code&gt; 保存操作序列，然后调用 &lt;code&gt;forEach&lt;/code&gt; 方法按顺序执行每一个 &lt;code&gt;Action&lt;/code&gt; 如下代码就是我们的命令发起者。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;包含操作序列的宏，可按顺序执行操作&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Macro {     private  nal List&amp;lt;Action&amp;gt; actions;     public Macro() {     actions = new ArrayList&amp;lt;&amp;gt;();     }     public void record(Action action) {          actions.add(action);     }     public void run() {          actions.forEach(Action::perform);     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在构建宏时，将每一个命令实例加入 &lt;code&gt;Macro&lt;/code&gt; 对象的列表，然后运行宏，就会按顺序执行每一条命令。我是个“懒惰的”程序员，喜欢将通用的工作流定义成宏。我说“懒惰”了吗?我的意思其实是提高工作效率。如下代码展示了如何在用户代码中使用 &lt;code&gt;Macro&lt;/code&gt; 对象。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用命令者模式构建宏&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Macro macro = new Macro();     macro.record(new Open(editor));     macro.record(new Save(editor));     macro.record(new Close(editor));     macro.run(); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式构建宏&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Macro macro = new Macro();      macro.record(() -&amp;gt; editor.open());      macro.record(() -&amp;gt; editor.save());      macro.record(() -&amp;gt; editor.close());      macro.run(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;事实上 ，如果意识到这些 &lt;code&gt;Lambda&lt;/code&gt; 表达式的作用只是调用了一个方法，还能让问题变得更简单。我们可以使用方法引用将命令和宏对象关联起来(如下代码所示)。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Macro macro = new Macro();      macro.record(editor::open());      macro.record(editor::save());      macro.record(editor::close());      macro.run(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;命令者模式只是一个可怜的程序员使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的起点。使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式或是方法引用，能让代码更简洁，去除了大量样板代码，让代码意图更加明显。 宏只是使用命令者模式的一个例子，它被大量用在实现组件化的图形界面系统、撤销功能、线程池、事务和向导中。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;在核心 Java 中，已经有一个和 Action 接口结构一致的函数接口——Runnable。 我们可以在实现上述宏程序中直接使用该接口，但在这个例子中，似乎 Action 是一个更符合我们待解问题的词汇，因此我们创建了自己的接口。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;8.1.2 策略模式&lt;/h3&gt; &lt;p&gt;策略模式能在运行时改变软件的算法行为。如何实现策略模式根据你的情况而定，但其主要思想是定义一个通用的问题，使用不同的算法来实现，然后将这些算法都封装在一个统一接口的背后。 文件压缩就是一个很好的例子。我们提供给用户各种压缩文件的方式，可以使用 &lt;code&gt;zip&lt;/code&gt; 算法，也可以使用 &lt;code&gt;gzip&lt;/code&gt; 算法，我们实现一个通用的 &lt;code&gt;Compressor&lt;/code&gt; 类，能以任何一种算法压缩文件。&lt;/p&gt; &lt;p&gt;首先，为我们的策略定义 &lt;code&gt;API&lt;/code&gt;(参见下图)，我把它叫作 &lt;code&gt;CompressionStrategy&lt;/code&gt;，每一种文件压缩算法都要实现该接口。该接口有一个 &lt;code&gt;compress&lt;/code&gt; 方法，接受并返回一个 &lt;code&gt;OutputStream&lt;/code&gt; 对象，返回的就是压缩后的 &lt;code&gt;OutputStream&lt;/code&gt;(如下代码所示)。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/55phu0lf0mi3lrjnq1qtklbhtc.png" alt="策略模式" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;定义压缩数据的策略接口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface CompressionStrategy {     public OutputStream compress(OutputStream data) throws IOException; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们有两个类实现了该接口，分别代表 &lt;code&gt;gzip&lt;/code&gt; 和 &lt;code&gt;ZIP&lt;/code&gt; 算法，使用 &lt;code&gt;Java&lt;/code&gt; 内置的类实现 &lt;code&gt;gzip&lt;/code&gt; 和 &lt;code&gt;ZIP&lt;/code&gt; 算法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;gzip&lt;/code&gt; 算法压缩数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class GzipCompressionStrategy implements CompressionStrategy {     @Override     public OutputStream compress(OutputStream data) throws IOException {     return new GZIPOutputStream(data);      } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;zip&lt;/code&gt; 算法压缩数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class ZipCompressionStrategy implements CompressionStrategy {     @Override     public OutputStream compress(OutputStream data) throws IOException {     return new ZipOutputStream(data);      } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在可以动手实现 &lt;code&gt;Compressor&lt;/code&gt; 类了，这里就是使用策略模式的地方。该类有一个 &lt;code&gt;compress&lt;/code&gt; 方法，读入文件，压缩后输出。它的构造函数有一个 &lt;code&gt;CompressionStrategy&lt;/code&gt; 参数，调用代 码可以在运行期使用该参数决定使用哪种压缩策略，比如，可以等待用户输入选择(如下代码所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在构造类时提供压缩策略&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Compressor {     private  final CompressionStrategy strategy;     public Compressor(CompressionStrategy strategy) {          this.strategy = strategy;     }     public void compress(Path inFile, File outFile) throws IOException {          try (OutputStream outStream = new FileOutputStream(outFile)) {                     Files.copy(inFile, strategy.compress(outStream));         }     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果使用这种传统的策略模式实现方式，可以编写客户代码创建一个新的 &lt;code&gt;Compressor&lt;/code&gt;，并 且使用任何我们想要的策略(如下代码所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用具体的策略类初始化 &lt;code&gt;Compressor&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Compressor gzipCompressor = new Compressor(new GzipCompressionStrategy());     gzipCompressor.compress(inFile, outFile);     Compressor zipCompressor = new Compressor(new ZipCompressionStrategy());      zipCompressor.compress(inFile, outFile); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和前面讨论的命令者模式一样，使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式或者方法引用可以去掉样板代码。在这里，我们可以去掉具体的策略实现，使用一个方法实现算法，这里的算法由构造函数 中对应的 &lt;code&gt;OutputStream&lt;/code&gt; 实现。使用这种方式，可以完全舍弃 &lt;code&gt;GzipCompressionStrategy&lt;/code&gt; 和 &lt;code&gt;ZipCompressionStrategy&lt;/code&gt; 类。如下代码展示了使用方法引用后的代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用方法引用初始化 Compressor&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);     gzipCompressor.compress(inFile, outFile);     Compressor zipCompressor = new Compressor(ZipOutputStream::new);      zipCompressor.compress(inFile, outFile); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;8.1.3 观察者模式&lt;/h3&gt; &lt;p&gt;观察者模式是另一种可被 &lt;code&gt;Lambda&lt;/code&gt; 表达式简化和改进的行为模式。在观察者模式中，被观察者持有一个观察者列表。当被观察者的状态发生改变，会通知观察者。观察者模式被大量应用于基于 &lt;code&gt;MVC&lt;/code&gt; 的 &lt;code&gt;GUI&lt;/code&gt; 工具中，以此让模型状态发生变化时，自动刷新视图模块，达到二者之间的解耦。 观看 &lt;code&gt;GUI&lt;/code&gt; 模块自动刷新有点枯燥，我们要观察的对象是月球! &lt;code&gt;NASA&lt;/code&gt; 和外星人都对登陆到月球上的东西感兴趣，都希望可以记录这些信息。&lt;code&gt;NASA&lt;/code&gt; 希望确保阿波罗号上的航天员成功登月;外星人则希望在 &lt;code&gt;NASA&lt;/code&gt; 注意力分散之时进犯地球。 让我们先来定义观察者的 &lt;code&gt;API&lt;/code&gt;，这里我将观察者称作 &lt;code&gt;LandingObserver&lt;/code&gt;。它只有一个 &lt;code&gt;observeLanding&lt;/code&gt; 方法，当有东西登陆到月球上时会调用如下方法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;用于观察登陆到月球的组织的接口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface LandingObserver {     public void observeLanding(String name);  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;被观察者是月球 &lt;code&gt;Moon&lt;/code&gt;，它持有一组 &lt;code&gt;LandingObserver&lt;/code&gt; 实例，有东西着陆时会通知这些观察者，还可以增加新的 &lt;code&gt;LandingObserver&lt;/code&gt; 实例观测 &lt;code&gt;Moon&lt;/code&gt; 对象(如下代码)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Moon 类——当然不如现实世界中那么完美&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Moon {     private  final List&amp;lt;LandingObserver&amp;gt; observers = new ArrayList&amp;lt;&amp;gt;();     public void land(String name) {     for (LandingObserver observer : observers) {             observer.observeLanding(name);         }     }     public void startSpying(LandingObserver observer) {          observers.add(observer);     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们有两个具体的类实现了 &lt;code&gt;LandingObserver&lt;/code&gt; 接口，分别代表外星人和 &lt;code&gt;NASA&lt;/code&gt; 检测着陆情况。前面提到过，监测到登陆后它们有不同的反应。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;外星人观察到人类登陆月球&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Aliens implements LandingObserver {     @Override     public void observeLanding(String name) {     if (name.contains(&amp;quot;Apollo&amp;quot;)) {             System.out.println(&amp;quot;They're distracted, lets invade earth!&amp;quot;);         }      } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;NASA 也能观察到有人登陆月球&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Nasa implements LandingObserver {      @Override     public void observeLanding(String name) {          if (name.contains(&amp;quot;Apollo&amp;quot;)) {             System.out.println(&amp;quot;We made it!&amp;quot;);         }     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和前面的模式类似，在传统的例子中，用户代码需要有一层模板类，如果使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式，就不用编写这些类了(如下代码所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用类的方式构建用户代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Moon moon = new Moon();      moon.startSpying(new Nasa());      moon.startSpying(new Aliens());     moon.land(&amp;quot;An asteroid&amp;quot;);     moon.land(&amp;quot;Apollo 11&amp;quot;); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用 Lambda 表达式构建用户代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Moon moon = new Moon(); moon.startSpying(name -&amp;gt; { if (name.contains(&amp;quot;Apollo&amp;quot;))              System.out.println(&amp;quot;We made it!&amp;quot;); });  moon.startSpying(name -&amp;gt; { if (name.contains(&amp;quot;Apollo&amp;quot;))              System.out.println(&amp;quot;They're distracted, lets invade earth!&amp;quot;); });  moon.land(&amp;quot;An asteroid&amp;quot;); moon.land(&amp;quot;Apollo 11&amp;quot;); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;还有一点值得思考，无论使用观察者模式或策略模式，实现时采用 &lt;code&gt;Lambda&lt;/code&gt; 表达式还是传 统的类，取决于策略和观察者代码的复杂度。我这里所举的例子代码很简单，只是一两个 方法调用，很适合展示新的语言特性。然而在有些情况下，观察者本身就是一个很复杂的类，这时将很多代码塞进一个方法中会大大降低代码的可读性。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;从某种角度来说，将大量代码塞进一个方法会让可读性变差是决定如何使用 Lambda 表达式的黄金法则。之所以不在这里过分强调，是因为这也是编写一般方法时的黄金法则!&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;8.1.4 模板方法模式&lt;/h3&gt; &lt;p&gt;开发软件时一个常见的情况是有一个通用的算法，只是步骤上略有不同。我们希望不同的实现能够遵守通用模式，保证它们使用了同一个算法，也是为了让代码更加易读。一旦你从整体上理解了算法，就能更容易理解其各种实现。&lt;/p&gt; &lt;p&gt;模板方法模式是为这些情况设计的:整体算法的设计是一个抽象类，它有一系列抽象方法，代表算法中可被定制的步骤，同时这个类中包含了一些通用代码。算法的每一个变种由具体的类实现，它们重写了抽象方法，提供了相应的实现。&lt;/p&gt; &lt;p&gt;让我们假想一个情境来搞明白这是怎么回事。假设我们是一家银行，需要对公众、公司和 职员放贷。放贷程序大体一致——验明身份、信用记录和收入记录。这些信息来源不一， 衡量标准也不一样。你可以查看一个家庭的账单来核对个人身份;公司都在官方机构注册过，比如美国的 &lt;code&gt;SEC&lt;/code&gt;、英国的 &lt;code&gt;Companies House&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;我们先使用一个抽象类 &lt;code&gt;LoanApplication&lt;/code&gt; 来控制算法结构，该类包含一些贷款调查结果报告的通用代码。根据不同的申请人，有不同的类:&lt;code&gt;CompanyLoanApplication、Personal LoanApplication&lt;/code&gt; 和 &lt;code&gt;EmployeeLoanApplication&lt;/code&gt;。如下代码展示了 &lt;code&gt;LoanApplication&lt;/code&gt; 类的结构。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用模板方法模式描述申请贷款过程&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public abstract class LoanApplication {     public void checkLoanApplication() throws ApplicationDenied {          checkIdentity();         checkCreditHistory();         checkIncomeHistory();         reportFindings();     }     protected abstract void checkIdentity() throws ApplicationDenied;     protected abstract void checkIncomeHistory() throws ApplicationDenied;     protected abstract void checkCreditHistory() throws ApplicationDenied;     private void reportFindings(){} } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;CompanyLoanApplication&lt;/code&gt; 的 &lt;code&gt;checkIdentity&lt;/code&gt; 方法在 &lt;code&gt;Companies House&lt;/code&gt; 等注册公司数据库中 &lt;code&gt;查找相关信息。checkIncomeHistory&lt;/code&gt; 方法评估公司的现有利润、损益表和资产负债表。 &lt;code&gt;checkCreditHistory&lt;/code&gt; 方法则查看现有的坏账和未偿债务。&lt;/p&gt; &lt;p&gt;&lt;code&gt;PersonalLoanApplication&lt;/code&gt; 的 &lt;code&gt;checkIdentity&lt;/code&gt; &lt;code&gt;方法通过分析客户提供的纸本结算单，确认客户地址是否真实有效。checkIncomeHistory&lt;/code&gt; 方法通过检查工资条判断客户是否仍被雇佣。 &lt;code&gt;checkCreditHistory&lt;/code&gt; 方法则会将工作交给外部的信用卡支付提供商。&lt;/p&gt; &lt;p&gt;&lt;code&gt;EmployeeLoanApplication&lt;/code&gt; 就是没有查阅员工历史功能的 &lt;code&gt;PersonalLoanApplication&lt;/code&gt;。为了方便起见，我们的银行在雇佣员工时会查阅所有员工的收入记录(如下代码)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;员工申请贷款是个人申请的一种特殊情况&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class EmployeeLoanApplication extends PersonalLoanApplication {     @Override     protected void checkIncomeHistory() {     // 这是自己人 !      } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式和方法引用，我们能换个角度思考模板方法模式，实现方式也跟以前不一样。模板方法模式真正要做的是将一组方法调用按一定顺序组织起来。如果用函数接口表示函数，用 &lt;code&gt;Lambda&lt;/code&gt; 表达式或者方法引用实现这些接口，相比使用继承构建算法，就会得到极大的灵活性。让我们看看如何使用这种方式实现 &lt;code&gt;LoanApplication&lt;/code&gt; 算法，请看如下代码!&lt;/p&gt; &lt;ul&gt; &lt;li&gt;员工申请贷款的例子&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class LoanApplication {     private  final Criteria identity;      private  final Criteria creditHistory;      private  final Criteria incomeHistory;      public LoanApplication(Criteria identity, Criteria creditHistory,                                 Criteria incomeHistory) {         this.identity = identity;          this.creditHistory = creditHistory;          this.incomeHistory = incomeHistory;     }     public void checkLoanApplication() throws ApplicationDenied {          identity.check();         creditHistory.check();         incomeHistory.check();         reportFindings();     }     private void reportFindings(){} } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;正如读者所见，这里没有使用一系列的抽象方法，而是多出一些属性:&lt;code&gt;identity&lt;/code&gt;、 &lt;code&gt;creditHistory&lt;/code&gt; 和 &lt;code&gt;incomeHistory&lt;/code&gt;。每一个属性都实现了函数接口 &lt;code&gt;Criteria&lt;/code&gt;，该接口检查一 项标准，如果不达标就抛出一个问题域里的异常。我们也可以选择从 &lt;code&gt;check&lt;/code&gt; 方法返回一个 类来表示成功或失败，但是沿用异常更加符合先前的实现(如下所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;如果申请失败，函数接口 Criteria 抛出异常&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Criteria {     public void check() throws ApplicationDenied; }     &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;采用这种方式，而不是基于继承的模式的好处是不需要在 &lt;code&gt;LoanApplication&lt;/code&gt; 及其子类中实现算法，分配功能时有了更大的灵活性。比如，我们想让 &lt;code&gt;Company&lt;/code&gt; 类负责所有的检查，那么 &lt;code&gt;Company&lt;/code&gt; 类就会多出一系列方法，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Company 类中的检查方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void checkIdentity() throws ApplicationDenied; public void checkProfitAndLoss() throws ApplicationDenied; public void checkHistoricalDebt() throws ApplicationDenied; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在只需为 &lt;code&gt;CompanyLoanApplication&lt;/code&gt; 类传入对应的方法引用，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;CompanyLoanApplication&lt;/code&gt; 类声明了对应的检查方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class CompanyLoanApplication extends LoanApplication {     public CompanyLoanApplication(Company company) {          super(company::checkIdentity,                     company::checkHistoricalDebt,                     company::checkProfitAndLoss);     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;将行为分配给 &lt;code&gt;Company&lt;/code&gt; 类的原因是各个国家之间确认公司信息的方式不同。在英国， &lt;code&gt;Companies House&lt;/code&gt; 规范了注册公司信息的地址，但在美国，各个州的政策是不一样的。&lt;/p&gt; &lt;p&gt;使用函数接口实现检查方法并没有排除继承的方式。我们可以显式地在这些类中使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式或者方法引用。&lt;/p&gt; &lt;p&gt;我们也不需要强制 &lt;code&gt;EmployeeLoanApplication&lt;/code&gt; 继承 &lt;code&gt;PersonalLoanApplication&lt;/code&gt; 来达到复用， 可以对同一个方法传递引用。它们之间是否天然存在继承关系取决于员工的借贷是否是普通人借贷这种特殊情况，或者是另外一种不同类型的借贷。因此，使用这种方式能让我们更加紧密地为问题建模。&lt;/p&gt; &lt;h2&gt;8.2 使用Lambda表达式的领域专用语言&lt;/h2&gt; &lt;p&gt;领域专用语言&lt;code&gt;(DSL)&lt;/code&gt;是针对软件系统中某特定部分的编程语言。它们通常比较小巧，表达能力也不如 &lt;code&gt;Java&lt;/code&gt; 这样能应对大多数编程任务的通用语言强。&lt;code&gt;DSL&lt;/code&gt; 高度专用:不求面面俱到，但求有所专长。&lt;/p&gt; &lt;p&gt;人们通常将 &lt;code&gt;DSL&lt;/code&gt; 分为两类:内部 &lt;code&gt;DSL&lt;/code&gt; 和外部 &lt;code&gt;DSL&lt;/code&gt;。外部 &lt;code&gt;DSL&lt;/code&gt; 脱离程序源码编写，然后单独解析和实现。比如级联样式表(&lt;code&gt;CSS&lt;/code&gt;)和正则表达式，就是常用的外部 &lt;code&gt;DSL&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;内部 &lt;code&gt;DSL&lt;/code&gt; 嵌入编写它们的编程语言中。如果读者使用过 &lt;code&gt;JMock&lt;/code&gt; 和 &lt;code&gt;Mockito&lt;/code&gt; 等模拟类库，或用过 &lt;code&gt;SQL&lt;/code&gt; 构建 &lt;code&gt;API&lt;/code&gt;，如 &lt;code&gt;JOOQ&lt;/code&gt; 或 &lt;code&gt;Querydsl&lt;/code&gt;，那么就知道什么是内部 &lt;code&gt;DSL&lt;/code&gt;。从某种角度上说，内部 &lt;code&gt;DSL&lt;/code&gt; 就是普通的类库，提供 &lt;code&gt;API&lt;/code&gt; 方便使用。虽然简单，内部 &lt;code&gt;DSL&lt;/code&gt; 却功能强大，让你的代码 变得更加精炼、易读。理想情况下，使用 &lt;code&gt;DSL&lt;/code&gt; 编写的代码读起来就像描述问题所使用的语言。&lt;/p&gt; &lt;p&gt;有了 &lt;code&gt;Lambda&lt;/code&gt; 表达式，实现 &lt;code&gt;DSL&lt;/code&gt; 就更简单了，那些想尝试 &lt;code&gt;DSL&lt;/code&gt; 的程序员又多了一件趁手的工具。我们将通过实现一个用于行为驱动开发&lt;code&gt;(BDD)&lt;/code&gt;的 &lt;code&gt;DSL&lt;/code&gt;:&lt;code&gt;LambdaBehave&lt;/code&gt;，来探 索其中遇到的各种问题。&lt;/p&gt; &lt;p&gt;&lt;code&gt;BDD&lt;/code&gt; 是测试驱动开发(&lt;code&gt;TDD&lt;/code&gt;)的一个变种，它的重点是描述程序的行为，而非一组需要通过的单元测试。我们的设计灵感源于一个叫&lt;code&gt;Jasmine&lt;/code&gt;的&lt;code&gt;JavaScript BDD&lt;/code&gt;框架，前端开发中会大量使用该框架。如下展示了如何使用 &lt;code&gt;Jasmine&lt;/code&gt; 创建测试用例。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Jasmine&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-jasmine"&gt;    describe(&amp;quot;A suite is just a function&amp;quot;, function() {          it(&amp;quot;and so is a spec&amp;quot;, function() {             var a = true; expect(a).toBe(true);         });      }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果读者不熟悉 &lt;code&gt;JavaScript&lt;/code&gt;，阅读这段代码可能会稍感疑惑。下面我们使用&lt;code&gt;Java 8&lt;/code&gt;实现一 个类似的框架时会一步一步来，只需要记住，在 &lt;code&gt;JavaScript&lt;/code&gt; 中我们使用 &lt;code&gt;function() { ... }&lt;/code&gt; 来表示 &lt;code&gt;Lambda&lt;/code&gt; 表达式。&lt;/p&gt; &lt;p&gt;让我们分别来看看这些概念:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;每一个规则描述了程序的一种行为;&lt;/li&gt; &lt;li&gt;期望是描述应用行为的一种方式，在规则中定义;&lt;/li&gt; &lt;li&gt;多个规则合在一起，形成一个套件。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;这些概念在传统的测试框架，比如 &lt;code&gt;JUnit&lt;/code&gt; 中，都有对应的概念。规则对应一个测试方法， 期望对应断言，套件对应一个测试类。&lt;/p&gt; &lt;h3&gt;8.2.1 使用Java编写DSL&lt;/h3&gt; &lt;p&gt;让我们先看一下实现后的 &lt;code&gt;Java BDD&lt;/code&gt; 框架长什么样子，例 8-28 描述了一个 Stack 的某些行为。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;描述 &lt;code&gt;Stack&lt;/code&gt; 的案例&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class StackSpec {{     describe(&amp;quot;a stack&amp;quot;, it -&amp;gt; {         it.should(&amp;quot;be empty when created&amp;quot;, expect -&amp;gt; {             expect.that(new Stack()).isEmpty();         });         it.should(&amp;quot;push new elements onto the top of the stack&amp;quot;, expect -&amp;gt; {              Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();             stack.push(1);             expect.that(stack.get(0)).isEqualTo(1);         });         it.should(&amp;quot;pop the last element pushed onto the stack&amp;quot;, expect -&amp;gt; {              Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();             stack.push(2);             stack.push(1);             expect.that(stack.pop()).isEqualTo(2);         });     }); }} &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;首先我们使用动词 &lt;code&gt;describe&lt;/code&gt; 为套件起头，然后定义一个名字表明这是描述什么东西的行 为，这里我们使用了 &amp;quot;a stack&amp;quot;。&lt;/p&gt; &lt;p&gt;每一条规则读起来尽可能接近英语中的句子。它们均以 &lt;code&gt;it.should&lt;/code&gt; 打头，其中 &lt;code&gt;it&lt;/code&gt; 指正在描述的对象。然后用一句简单的英语描述行为，最后使用 &lt;code&gt;expect.that&lt;/code&gt; 做前缀，描述期待的行为。&lt;/p&gt; &lt;p&gt;检查规则时，会从命令行得到一个简单的报告，表明是否有规则失败。你会发现 &lt;code&gt;pop&lt;/code&gt; 操作期望 的返回值是 2，而不是 1，因此“pop the last element pushed onto the stack”这条规则就失败了:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;```java     a stack     should pop the last element pushed onto the stack[expected:  but was:  ] should be empty when created     should push new elements onto the top of the stack ``` &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;8.2.2 实现&lt;/h3&gt; &lt;p&gt;读者已经领略了使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的 &lt;code&gt;DSL&lt;/code&gt; 所带来的便利，现在该看看我们是如何实现该框架的。我们希望会让大家看到，自己实现一个这样的框架是多么简单。&lt;/p&gt; &lt;p&gt;描述行为首先看到的是 &lt;code&gt;describe&lt;/code&gt; 这个动词，简单导入一个静态方法就够了。为套件创建一 个 &lt;code&gt;Description&lt;/code&gt; 实例，在此处理各种各样的规则。&lt;code&gt;Description&lt;/code&gt; 类就是我们定义的 &lt;code&gt;DSL&lt;/code&gt; 中 的 &lt;code&gt;it&lt;/code&gt;(如下代码)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;从 &lt;code&gt;describe&lt;/code&gt; 方法开始定义规则&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static void describe(String name, Suite behavior){         Description description = new Description(name);         behavior.specifySuite(description);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;每个套件的规则描述由用户使用一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现，因此我们需要一个 &lt;code&gt;Suite&lt;/code&gt; 函数接口来表示规则组成的套件，如下代码所示。该接口接收一个 &lt;code&gt;Description&lt;/code&gt; 对象作为参数，我们在 &lt;code&gt;describe&lt;/code&gt; 方法里将其传入。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;每个测试套件都由一个实现该接口的 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Suite {     void specifySuite(Description description); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在我们定义的 &lt;code&gt;DSL&lt;/code&gt; 中，不仅套件由 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现，每一条规则也是一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式。它们也需要定义一个函数接口:&lt;code&gt;Specification&lt;/code&gt;(如下代码所示)。示例代码中的 &lt;code&gt;expect&lt;/code&gt; 变量是 &lt;code&gt;Expect&lt;/code&gt; 类的实例，我们稍后描述:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Specification {     void specifyBehaviour(Expect expect); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之前来回传递的 &lt;code&gt;Description&lt;/code&gt; 实例这里就派上用场了。我们希望用户可以使用 &lt;code&gt;it.should&lt;/code&gt; 命名他们的规则，这就是说 &lt;code&gt;Description&lt;/code&gt; 类需要有一个 &lt;code&gt;should&lt;/code&gt; 方法(如下代码所示)。这里是真正做事的地方，该方法通过调用 &lt;code&gt;specifySuite&lt;/code&gt; 执行 &lt;code&gt;Lambda&lt;/code&gt; 表达式。如果规则失败，会 抛出一个标准的 &lt;code&gt;Java AssertionError&lt;/code&gt;，而其他任何 &lt;code&gt;Throwable&lt;/code&gt; 对象则认为是一个错误:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将用 &lt;code&gt;Lambda&lt;/code&gt; 表达式表示的规则传入 &lt;code&gt;should&lt;/code&gt; 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public void should(String description, Specification specification) {         try {             Expect expect = new Expect();             specification.specifyBehaviour(expect);             Runner.current.recordSuccess(suite, description);         } catch (AssertionError cause) {             Runner.current.recordFailure(suite, description, cause);         } catch (Throwable cause) {             Runner.current.recordError(suite, description, cause);         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;规则通过 &lt;code&gt;expect.that&lt;/code&gt; 描述期望的行为，也就是说 &lt;code&gt;Expect&lt;/code&gt; 类需要一个 &lt;code&gt;that&lt;/code&gt; 方法供用户调 用，如下代码所示。这里可以封装传入的对象，然后暴露一些常用的方法，如 &lt;code&gt;isEqualTo。&lt;/code&gt; 如果规则失败，抛出相应的断言。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;期望链的开始&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public  final class Expect {     public BoundExpectation that(Object value) {          return new BoundExpectation(value);     }     // 省去类定义的其他部分  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;读者可能会注意到，我一直忽略了一个细节，该细节与 &lt;code&gt;Lambda&lt;/code&gt; 表达式无关。&lt;code&gt;StackSpec&lt;/code&gt; 类 并没有直接实现任何方法，我直接将代码写在里边。这里我偷了个懒，在类定义的开头和结尾使用了双括号:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class StackSpec {{      ... }} &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这其实是一个匿名构造函数，可以执行任意的 &lt;code&gt;Java&lt;/code&gt; 代码块，所以这等价于一个完整的构造函数，只是少了一些样板代码。这段代码也可以写作:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class StackSpec {      public StackSpec() {         ...     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;要实现一个完整的 &lt;code&gt;BDD&lt;/code&gt; 框架还有很多工作要做，本节只是为了向读者展示如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式创建领域专用语言。我在这里讲解了与 &lt;code&gt;DSL&lt;/code&gt; 中 &lt;code&gt;Lambda&lt;/code&gt; 表达式交互的部分， 以期能帮助读者管中窥豹，了解如何实现这种类型的 &lt;code&gt;DSL&lt;/code&gt;。&lt;/p&gt; &lt;h3&gt;8.2.3 评估&lt;/h3&gt; &lt;p&gt;流畅性的一方面表现在 &lt;code&gt;DSL&lt;/code&gt; 是否是 &lt;code&gt;IDE&lt;/code&gt; 友好的。换句话说，你只需记住少量知识，然后用代码自动补全功能补齐代码。这就是使用 &lt;code&gt;Description&lt;/code&gt; 和 &lt;code&gt;Expect&lt;/code&gt; 对象的原因。当然也可以导入静态方法 &lt;code&gt;it&lt;/code&gt; 或 &lt;code&gt;expect&lt;/code&gt;，一些 &lt;code&gt;DSL&lt;/code&gt; 中就使用了这种方式。如果选择向 &lt;code&gt;Lambda&lt;/code&gt; 表达 式传入对象，而不是导入一个静态方法，就能让 &lt;code&gt;IDE&lt;/code&gt; 的使用者轻松补全代码。&lt;/p&gt; &lt;p&gt;用户唯一要记住的是调用 &lt;code&gt;describe&lt;/code&gt; 方法，这种方式的好处通过单纯阅读可能无法体会，我建议大家创建一个示例项目，亲自体验这个框架。&lt;/p&gt; &lt;p&gt;另一个值得注意的是大多数测试框架提供了大量注释，或者很多外部“魔法”，或者借助 于反射。我们不需要这些技巧，就能直接使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式在 &lt;code&gt;DSL&lt;/code&gt; 中表达行为，就和使 用普通的 &lt;code&gt;Java&lt;/code&gt; 方法一样。&lt;/p&gt; &lt;h2&gt;8.3 使用Lambda表达式的SOLID原则&lt;/h2&gt; &lt;p&gt;&lt;code&gt;SOLID&lt;/code&gt; 原则是设计面向对象程序时的一些基本原则。原则的名字是个简写，分别代表了 下面五个词的首字母:&lt;code&gt;Single responsibility&lt;/code&gt;、&lt;code&gt;Open/closed&lt;/code&gt;、&lt;code&gt;Liskov substitution``、Interface segregation&lt;/code&gt; 和 &lt;code&gt;Dependency inversion&lt;/code&gt;。这些原则能指导你开发出易于维护和扩展的代码。&lt;/p&gt; &lt;p&gt;每种原则都对应着一系列潜在的代码异味，并为其提供了解决方案。有很多图书介绍这个主题，因此我不会详细讲解，而是关注如何在 &lt;code&gt;Lambda&lt;/code&gt; 表达式的环境下应用其中的三条原则。在 &lt;code&gt;Java 8&lt;/code&gt; 中，有些原则通过扩展，已经超出了原来的限制。&lt;/p&gt; &lt;h3&gt;8.3.1 单一功能原则&lt;/h3&gt; &lt;p&gt;程序中的类或方法只能有一个改变的理由。&lt;/p&gt; &lt;p&gt;软件开发中不可避免的情况是需求的改变。这可能是需要增加新功能，也可能是你对问题 的理解或者客户发生变化了，或者你想变得更快，总之，软件会随着时间不断演进。&lt;/p&gt; &lt;p&gt;当软件的需求发生变化，实现这些功能的类和方法也需要变化。如果你的类有多个功能，一个功能引发的代码变化会影响该类的其他功能。这可能会引入缺陷，还会影响代码演进的能力。&lt;/p&gt; &lt;p&gt;让我们看一个简单的示例程序，该程序由资产列表生成 &lt;code&gt;BalanceSheet&lt;/code&gt; 表格，然后输出成一份 &lt;code&gt;PDF&lt;/code&gt; 格式的报告。如果实现时将制表和输出功能都放进同一个类，那么该类就有两个变化的理由。你可能想改变输出功能，输出不同的格式，比如 &lt;code&gt;HTML&lt;/code&gt;，可能还想改变 &lt;code&gt;BalanceSheet&lt;/code&gt; 的细节。这为将问题分解成两个类提供了很好的理由:一个负责将 &lt;code&gt;BalanceSheet&lt;/code&gt; 生成表格，一个负责输出。&lt;/p&gt; &lt;p&gt;单一功能原则不止于此:一个类不仅要功能单一，而且还需将功能封装好。换句话说，如果我想改变输出格式，那么只需改动负责输出的类，而不必关心负责制表的类。&lt;/p&gt; &lt;p&gt;这是强内聚性设计的一部分。说一个类是内聚的，是指它的方法和属性需要统一对待，因为它们紧密相关。如果你试着将一个内聚的类拆分，可能会得到刚才创建的那两个类。&lt;/p&gt; &lt;p&gt;既然你已经知道了什么是单一功能原则，问题来了:这和 &lt;code&gt;Lambda&lt;/code&gt; 表达式有什么关系?&lt;/p&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式在方法级别能更容易实现单一功能原则。让我们看一个例子，该段程序能得出一定范围内有多少个质数(如下代码)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;计算质数个数，一个方法里塞进了多重职责&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public long countPrimes(int upTo) {         long tally = 0;         for (int i = 1; i &amp;lt; upTo; i++) {             boolean isPrime = true;             for (int j = 2; j &amp;lt; i; j++) {                 if (i % j == 0) {                     isPrime = false;                 }             }             if (isPrime) {                 tally++;             }         }         return tally;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;很显然，在上面的例子中我们同时干了两件事:计数和判断一个数是否是质数。在例下面的代码中， 通过简单重构，将两个功能一分为二。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将 &lt;code&gt;isPrime&lt;/code&gt; 重构成另外一个方法后，计算质数个数的方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public long countPrimes(int upTo) {         long tally = 0;         for (int i = 1; i &amp;lt; upTo; i++) {             if (isPrime(i)) {                 tally++;             }         }         return tally;     }      private boolean isPrime(int number) {         for (int i = 2; i &amp;lt; number; i++) {             if (number % i == 0) {                 return false;             }         }         return true;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但我们的代码还是有两个功能。代码中的大部分都在对数字循环，如果我们遵守单一功能 原则，那么迭代过程应该封装起来。改进代码还有一个现实的原因，如果需要对一个很大 的 &lt;code&gt;upTo&lt;/code&gt; 计数，我们希望可以并行操作。没错，线程模型也是代码的职责之一!&lt;/p&gt; &lt;p&gt;我们可以使用 &lt;code&gt;Java 8&lt;/code&gt; 的集合流(如下代码所示)重构上述代码，将循环操作交给类库本身处理。这里使用了 &lt;code&gt;range&lt;/code&gt; 方法从 0 至 upTo 计数，然后 &lt;code&gt;filter&lt;/code&gt; 出质数，最后对结果做 &lt;code&gt;count&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用集合流重构质数计数程序&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countPrimes(int upTo) {      return IntStream.range(1, upTo)                     .filter(this::isPrime) .count(); }  private boolean isPrime(int number) {      return IntStream.range(2, number)                     .allMatch(x -&amp;gt; (number % x) != 0); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果我们想利用更多 &lt;code&gt;CPU&lt;/code&gt; 加速计数操作，可使用 &lt;code&gt;parallelStream&lt;/code&gt; 方法，而不需要修改任何其他代码(如例下代码所示)。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;并行运行基于集合流的质数计数程序&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countPrimes(int upTo) {      return IntStream.range(1, upTo)                     .parallel()                     .filter(this::isPrime) .count(); }  private boolean isPrime(int number) {      return IntStream.range(2, number)                     .allMatch(x -&amp;gt; (number % x) != 0); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;因此，利用高阶函数，可以轻松帮助我们实现功能单一原则。&lt;/p&gt; &lt;h3&gt;8.3.2 开闭原则&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;软件应该对扩展开放，对修改闭合。  —— Bertrand Meyer&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;开闭原则的首要目标和单一功能原则类似:让软件易于修改。一个新增功能或一处改动， 会影响整个代码，容易引入新的缺陷。开闭原则保证已有的类在不修改内部实现的基础上可扩展，这样就努力避免了上述问题。&lt;/p&gt; &lt;p&gt;第一次听说开闭原则时，感觉有点痴人说梦。不改变实现怎么能扩展一个类的功能呢?答案是借助于抽象，可插入新的功能。让我们看一个具体的例子。&lt;/p&gt; &lt;p&gt;我们要写的程序用来衡量系统性能，并且把得到的结果绘制成图形。比如，我们有描述计 算机花在用户空间、内核空间和输入输出上的时间散点图。我将负责显示这些指标的类叫 作 &lt;code&gt;MetricDataGraph&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;设计 &lt;code&gt;MetricDataGraph&lt;/code&gt; 类的方法之一是将代理收集到的各项指标放入该类，该类的公开 &lt;code&gt;API&lt;/code&gt; 如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;MetricDataGraph 类的公开 API&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;class MetricDataGraph {     public void updateUserTime(int value);      public void updateSystemTime(int value);     public void updateIoTime(int value); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但这样的设计意味着每次想往散点图中添加新的时间点，都要修改 &lt;code&gt;MetricDataGraph&lt;/code&gt; 类。通过引入抽象可以解决这个问题，我们使用一个新类 &lt;code&gt;TimeSeries&lt;/code&gt; 来表示各种时间点。这时， &lt;code&gt;MetricDataGraph&lt;/code&gt; 类的公开 API 就得以简化，不必依赖于某项具体指标，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;MetricDataGraph 类简化之后的 API&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;class MetricDataGraph {     public void addTimeSeries(TimeSeries values); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;每项具体指标现在可以实现 &lt;code&gt;TimeSeries&lt;/code&gt; 接口，在需要时能直接插入。比如，我们可能会有如下类:&lt;code&gt;UserTimeSeries、SystemTimeSeries&lt;/code&gt; 和 &lt;code&gt;IoTimeSeries&lt;/code&gt;。 如果要添加的，比如由于虚拟化所浪费的 &lt;code&gt;CPU&lt;/code&gt; 时间，则可增加一个新的实现了 &lt;code&gt;TimeSeries&lt;/code&gt; 接口的类: &lt;code&gt;StealTimeSeries&lt;/code&gt;。这样，就扩展了 &lt;code&gt;MetricDataGraph&lt;/code&gt; 类，但并没有修改它。&lt;/p&gt; &lt;p&gt;高阶函数也展示出了同样的特性:对扩展开放，对修改闭合。前面提到的 &lt;code&gt;ThreadLocal&lt;/code&gt; 类 &lt;code&gt;就是一个很好的例子。ThreadLocal&lt;/code&gt; 有一个特殊的变量，每个线程都有一个该变量的副本 并与之交互。该类的静态方法 &lt;code&gt;withInitial&lt;/code&gt; 是一个高阶函数，传入一个负责生成初始值的 &lt;code&gt;Lambda&lt;/code&gt; 表达式。&lt;/p&gt; &lt;p&gt;这符合开闭原则，因为不用修改 &lt;code&gt;ThreadLocal&lt;/code&gt; 类，就能得到新的行为。给 &lt;code&gt;withInitial&lt;/code&gt; 方法传入不同的工厂方法，就能得到拥有不同行为的 &lt;code&gt;ThreadLocal&lt;/code&gt; 实例。比如，可以使用 &lt;code&gt;ThreadLocal&lt;/code&gt; 生成一个 &lt;code&gt;DateFormatter&lt;/code&gt; 实例，该实例是线程安全的，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;ThreadLocal 日期格式化器&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;// 实现 ThreadLocal&amp;lt;DateFormat&amp;gt; localFormatter = ThreadLocal.withInitial(() -&amp;gt; new SimpleDateFormat());  // 使用 DateFormat formatter = localFormatter.get(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;通过传入不同的 &lt;code&gt;Lambda&lt;/code&gt; 表达式，可以得到完全不同的行为。比如在下面代码中，我们为每个 &lt;code&gt;Java&lt;/code&gt; 线程创建了唯一、有序的标识符。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;ThreadLocal 标识符&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;// 或者这样实现 AtomicInteger threadId = new AtomicInteger(); ThreadLocal&amp;lt;Integer&amp;gt; localId = ThreadLocal.withInitial(() -&amp;gt; threadId.getAndIncrement());  // 使用 int idForThisThread = localId.get(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;对开闭原则的另外一种理解和传统的思维不同，那就是使用不可变对象实现开闭原则。不可变对象是指一经创建就不能改变的对象。&lt;/p&gt; &lt;p&gt;不可变性”一词有两种解释:观测不可变性和实现不可变性。观测不可变性是指在其他对象看来，该类是不可变的;实现不可变性是指对象本身不可变。实现不可变性意味着观测不可变性，反之则不一定成立。&lt;/p&gt; &lt;p&gt;&lt;code&gt;java.lang.String&lt;/code&gt; 宣称是不可变的，但事实上只是观测不可变，因为它在第一次调用 &lt;code&gt;hashCode&lt;/code&gt; 方法时缓存了生成的散列值。在其他类看来，这是完全安全的，它们看不出散列值是每次在构造函数中计算出来的，还是从缓存中返回的。&lt;/p&gt; &lt;p&gt;之所以在这样一本讲解 &lt;code&gt;Lambda&lt;/code&gt; 表达式的书中谈及不可变对象，是因为它们都是函数式编程中耳熟能详的概念，这里也是 &lt;code&gt;Lambda&lt;/code&gt; 表达式的发源地。它们生来就符合我在本书中讲述的编程风格。&lt;/p&gt; &lt;p&gt;我们说不可变对象实现了开闭原则，是因为它们的内部状态无法改变，可以安全地为其增加新的方法。新增加的方法无法改变对象的内部状态，因此对修改是闭合的;但它们又增加了新的行为，因此对扩展是开放的。当然，你还需留意不要改变程序其他部分的状态。&lt;/p&gt; &lt;p&gt;因其天生线程安全的特性，不可变对象引起了人们的格外注意。它们没有内部状态可变， 因此可以安全地在不同线程之间共享。&lt;/p&gt; &lt;p&gt;如果我们回顾这几种方式，会发现已经偏离了传统的开闭原则。事实上，在 &lt;code&gt;Bertrand Meyer&lt;/code&gt; 第一次引入这个原则时，原意是一旦实现后，类就不允许改动了。在现代敏捷开发环境中，完成一个类的说法很明显已经过时了。业务需求和使用方法的变化可能会让一个类的功能和当初设计的不同。当然这不成为忽视这一原则的理由，只是说明了所谓的原则只应作为指导，而不应教条地全盘接受，走向极端。&lt;/p&gt; &lt;p&gt;我认为还有一点值得思考，在&lt;code&gt;Java 8&lt;/code&gt;中，使用抽象插入多个类，或者使用高阶函数来实现开闭原则其实是一样的。因为抽象需要使用一个接口或抽象类来定义方法，这其实就是一 种多态的使用方式。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;Java 8&lt;/code&gt; 中，任何传入高阶函数的 &lt;code&gt;Lambda&lt;/code&gt; 表达式都由一个函数接口表示，高阶函数负责调用其唯一的方法，根据传入 &lt;code&gt;Lambda&lt;/code&gt; 表达式的不同，行为也不同。这其实也是在用多态来实现开闭原则。&lt;/p&gt; &lt;h3&gt;8.3.3 依赖反转原则&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;抽象不应依赖细节，细节应该依赖抽象。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;让程序变得死板、脆弱、难于改变的方法之一是将上层业务逻辑和底层粘合模块的代码混在一起，因为这两样东西都会随着时间发生变化。&lt;/p&gt; &lt;p&gt;依赖反转原则的目的是让程序员脱离底层粘合代码，编写上层业务逻辑代码。这就让上层代码依赖于底层细节的抽象，从而可以重用上层代码。这种模块化和重用方式是双向的: 既可以替换不同的细节重用上层代码，也可以替换不同的业务逻辑重用细节的实现。&lt;/p&gt; &lt;p&gt;让我们看一个具体的、自动化构建地址簿的例子，实现时使用了依赖反转原则达到上层的解耦。该应用以电子卡片作为输入，使用某种存储机制编写地址簿。&lt;/p&gt; &lt;p&gt;显然，我们可将代码分成如下三个基本模块:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;一个能解析电子卡片格式的电子卡片阅读器;&lt;/li&gt; &lt;li&gt;能将地址存为文本文件的地址簿存储模块;&lt;/li&gt; &lt;li&gt;从电子卡片中获取有效信息并将其写入地址簿的编写模块。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;我们用如下图来表示各模块之间的关系。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/r0tqbe7eimi3noci7hp85opb6g.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;在该系统中，重用编写模块很复杂，但是电子卡片阅读器和地址簿存储模块都不依赖于其他模块，因此很容易在其他系统中重用。还可以替换它们，比如用一个其他的阅读器，或者从人们的 &lt;code&gt;Twitter&lt;/code&gt; 账户信息中读取内容;又比如我们不想将地址簿存为一个文本文件， 而是使用数据库存储等其他形式。&lt;/p&gt; &lt;p&gt;为了具备能在系统中替换组件的灵活性，必须保证编写模块不依赖阅读器或存储模块的实现细节。因此我们引入了对阅读信息和输出信息的抽象，编写模块的实现依赖于这种抽象。在运行时传入具体的实现细节，这就是依赖反转原则的工作原理(简单理解为细节要依赖抽象，抽象不依赖细节)。&lt;/p&gt; &lt;p&gt;具体到 &lt;code&gt;Lambda&lt;/code&gt; 表达式，我们之前遇到的很多高阶函数都符合依赖反转原则。比如 &lt;code&gt;map&lt;/code&gt; 函 数重用了在两个集合之间转换的代码。&lt;code&gt;map&lt;/code&gt; 函数不依赖于转换的细节，而是依赖于抽象的概念。在这里，就是依赖函数接口:&lt;code&gt;Function&lt;/code&gt;。（接口可认为是抽象的，因为接口不包含实现细节，当然 &lt;code&gt;Java8&lt;/code&gt; 接口可以有默认实现）&lt;/p&gt; &lt;p&gt;资源管理是依赖反转的另一个更为复杂的例子。显然，可管理的资源很多，比如数据库连接、线程池、文件和网络连接。这里我将以文件为例，因为文件是一种相对简单的资源， 但是背后的原则可以很容易应用到更复杂的资源中。&lt;/p&gt; &lt;p&gt;让我们看一段代码，该段代码从一种假想的标记语言中提取标题，其中标题以冒号( :)结尾。我们的方法先读取文件，逐行检查，滤出标题，然后关闭文件。我们还将和读写文 件有关的异常封装成接近待解决问题的异常:HeadingLookupException，最后的代码如下代码所示：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;解析文件中的标题&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public List&amp;lt;String&amp;gt; findHeadings(Reader input) {         try (BufferedReader reader = new BufferedReader(input)) {         return reader.lines()         .filter(line -&amp;gt; line.endsWith(&amp;quot;:&amp;quot;))                                 .map(line -&amp;gt; line.substring(0, line.length() - 1))                                 .collect(toList());          } catch (IOException e) {             throw new HeadingLookupException(e);          } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可惜，我们的代码将提取标题和资源管理、文件处理混在一起。我们真正想要的是编写提取标题的代码，而将操作文件相关的细节交给另一个方法。可以使用 &lt;code&gt;Stream&amp;lt;String&amp;gt;&lt;/code&gt; 作为抽象，让代码依赖它，而不是文件。&lt;code&gt;Stream&lt;/code&gt; 对象更安全，而且不容易被滥用。我们还想传 入一个函数，在读文件出问题时，可以创建一个问题域里的异常。整个过程如下所示，而且我们将问题域里的异常处理和资源管理的异常处理分开了。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;剥离了文件处理功能后的业务逻辑&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public List&amp;lt;String&amp;gt; findHeadings(Reader input) { return withLinesOf(input, lines -&amp;gt; lines.filter(line -&amp;gt; line.endsWith(&amp;quot;:&amp;quot;)) .map(line -&amp;gt; line.substring(0, line.length()-1))                                           .collect(toList()),                             HeadingLookupException::new); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;是不是想知道 &lt;code&gt;withLinesOf&lt;/code&gt; 方法是什么样的?请看如下代码&lt;/p&gt; &lt;ul&gt; &lt;li&gt;定义 withLinesOf 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private &amp;lt;T&amp;gt; T withLinesOf(Reader input, Function&amp;lt;Stream&amp;lt;String&amp;gt;, T&amp;gt; handler,                                Function&amp;lt;IOException, RuntimeException&amp;gt; error) { try (BufferedReader reader = new BufferedReader(input)) { return handler.apply(reader.lines()); } catch (IOException e) { throw error.apply(e); } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;withLinesOf&lt;/code&gt; 方法接受一个 &lt;code&gt;Reader&lt;/code&gt; 参数处理文件读写，然后将其封装进一个 &lt;code&gt;Buffered-Reader&lt;/code&gt; 对象，这样就可以逐行读取文件了。&lt;code&gt;handler&lt;/code&gt; 函数代表了我们想在该方法中执行的代码，它以文件中的每一行组成的 &lt;code&gt;Stream&lt;/code&gt; 作为参数。另一个参数是 &lt;code&gt;error&lt;/code&gt;，输入输出有异常时会调用该方法，它会构建出与问题域有关的异常，出问题时就抛出该异常。&lt;/p&gt; &lt;p&gt;总结下来，高阶函数提供了反转控制，这就是依赖反转的一种形式，可以很容易地和 &lt;code&gt;Lambda&lt;/code&gt; 表达式一起使用。依赖反转原则另外值得注意的一点是待依赖的抽象不必是接口。 这里我们使用 &lt;code&gt;Stream&lt;/code&gt; 对原始的 &lt;code&gt;Reader&lt;/code&gt; 和文件处理做抽象，这种方式也适用于函数式编程 语言中的资源管理——通常使用高阶函数管理资源，接受一个回调函数使用打开的资源， 然后再关闭资源。事实上，如果&lt;code&gt;Java 7&lt;/code&gt;就有Lambda表达式，那么&lt;code&gt;Java 7&lt;/code&gt;中的&lt;code&gt;try-with-resources&lt;/code&gt; 功能可能只需要一个库函数就能实现。&lt;/p&gt; &lt;h2&gt;8.4 进阶阅读&lt;/h2&gt; &lt;p&gt;本章讨论的很多内容都涉及了更广泛的设计问题，关注程序整体，而不是一个方法。限于本书讨论的重点是 &lt;code&gt;Lambda&lt;/code&gt; 表达式，我们对这些话题的讨论都是浅尝辄止。如果读者想了 解更多细节，可参考相关图书。&lt;/p&gt; &lt;p&gt;长期以来，“Bob 大叔”是 &lt;code&gt;SOLID&lt;/code&gt; 原则的推动者，他撰写了大量有关该主题的文章和书籍， 也多次就该主题举行过演讲。如果你想免费从他那里获取一些相关知识，可访问 &lt;code&gt;Object Mentor&lt;/code&gt; 官方网站(http://www.objectmentor.com/resources/publishedArticles.html)，在“设计 模式”主题下有一系列详述设计原则的文章。&lt;/p&gt; &lt;p&gt;如果你想深入理解领域专用语言，包括内部领域专用语言和外部领域专用语言，推荐大家 阅读&lt;code&gt;Martin Fowler&lt;/code&gt;和&lt;code&gt;Rebecca Parsons&lt;/code&gt;合著的D&lt;code&gt;omain-Speci c Languages&lt;/code&gt;&lt;/p&gt; &lt;h2&gt;8.5 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式能让很多现有设计模式更简单、可读性更强，尤其是命令者模式。&lt;/li&gt; &lt;li&gt;在 &lt;code&gt;Java8&lt;/code&gt; 中，创建领域专用语言有更多的灵活性。-&lt;/li&gt; &lt;li&gt;在 &lt;code&gt;Java8&lt;/code&gt; 中，有应用 &lt;code&gt;SOLID&lt;/code&gt; 原则的新机会。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sun, 25 Nov 2018 11:05:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇六之测试调试和重构</title>
      <link>https://www.zhangaoo.com/article/36</link>
      <content:encoded>&lt;h1&gt;测试、调试和重构&lt;/h1&gt; &lt;p&gt;重构、测试驱动开发&lt;code&gt;(TDD)&lt;/code&gt;和持续集成&lt;code&gt;(CI)&lt;/code&gt;越来越流行，如果我们需要将 &lt;code&gt;Lambda&lt;/code&gt; 表 达式应用于日常编程工作中，就得学会如何为它编写单元测试。&lt;/p&gt; &lt;p&gt;本章主要探讨如何在代码中使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的技术，也会说明什么情况下不应该(直 接)使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式。本章还讲述了如何调试大量使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式和流的程序。&lt;/p&gt; &lt;h2&gt;7.1 重构候选项&lt;/h2&gt; &lt;p&gt;这里有一些要点，可以帮助读者确定什么时候应该 &lt;code&gt;Lambda&lt;/code&gt; 化自己的应用或类库。其中的 每一条都可看作一个局部的反模式或代码异味，借助于 &lt;code&gt;Lambda&lt;/code&gt; 化可以修复。&lt;/p&gt; &lt;h3&gt;7.1.1 进进出出、摇摇晃晃&lt;/h3&gt; &lt;p&gt;如下代码是关于如何在程序中记录日志的，我在第 4 章多次提到这个代码。这段代码先调用 &lt;code&gt;isDebugEnabled&lt;/code&gt; 方法抽取布尔值，用来检查是否启用调试级别，如果启用，则调用 &lt;code&gt;Logger&lt;/code&gt; 对象的相应方法记录日志。如果你发现自己的代码不断地查询和操作某对象，目的只为了在最后给该对象设个值，那么这段代码就本该属于你所操作的对象。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;logger&lt;/code&gt; 对象使用 &lt;code&gt;isDebugEnabled&lt;/code&gt; 属性避免不必要的性能开销&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Logger logger = new Logger();  if (logger.isDebugEnabled()) {          logger.debug(&amp;quot;Look at this: &amp;quot; + expensiveOperation());      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;记录日志本来就是一直以来很难实现的目标，因为地方不同，所需的行为也不一样。本例中，需要根据程序中记录日志的不同位置和要记录的内容生成不同的信息。&lt;/p&gt; &lt;p&gt;这种反模式通过传入代码即数据的方式很容易解决。与其查询并设置一个对象的值，不如传入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，该表达式按照计算得出的值执行相应的行为。我将原来的实现 代码列在下面代码中，以示提醒。当程序处于调试级别，并且检查是否使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的逻辑被封装在 &lt;code&gt;Logger&lt;/code&gt; 对象中时，才会调用 &lt;code&gt;Lambda&lt;/code&gt; 表达式。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式简化记录日志代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Logger logger = new Logger(); logger.debug(() -&amp;gt; &amp;quot;Look at this: &amp;quot; + expensiveOperation()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上述记录日志的例子也展示了如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式更好地面向对象编程&lt;code&gt;(OOP)&lt;/code&gt;，面向对象编程的核心之一是封装局部状态，比如日志的级别。通常这点做得不是很好， &lt;code&gt;isDebugEnabled&lt;/code&gt; 方法暴露了内部状态。如果使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式，外面的代码根本不需要检查日志级别。&lt;/p&gt; &lt;h3&gt;7.1.2 孤独的覆盖&lt;/h3&gt; &lt;p&gt;这个代码异味是使用继承，其目的只是为了覆盖一个方法。&lt;code&gt;ThreadLocal&lt;/code&gt; 就是一个很好的 例子。&lt;code&gt;ThreadLocal&lt;/code&gt; 能创建一个工厂，为每个线程最多只产生一个值。这是确保非线程安全的类在并发环境下安全使用的一种简单方式。假设要在数据库中查询一个艺术家，但希望每个线程只做一次这种查询，写出的代码可能如例下所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在数据库中查找艺术家&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;ThreadLocal&amp;lt;Album&amp;gt; thisAlbum = new ThreadLocal&amp;lt;Album&amp;gt; () {      @Override protected Album initialValue() {     return database.lookupCurrentAlbum(); } }; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 &lt;code&gt;Java 8&lt;/code&gt; 中，可以为工厂方法 &lt;code&gt;withInitial&lt;/code&gt; 传入一个 &lt;code&gt;Supplier&lt;/code&gt; 对象的实例来创建对象，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用工厂方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;ThreadLocal&amp;lt;Album&amp;gt; thisAlbum = ThreadLocal.withInitial(() -&amp;gt; database.lookupCurrentAlbum()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们认为第二个例子优于前一个有以下几个原因。首先，任何已有的 &lt;code&gt;Supplier&amp;lt;Album&amp;gt;&lt;/code&gt; 实例不需要重新封装，就可以在此使用，这鼓励了重用和组合。&lt;/p&gt; &lt;p&gt;在其他都一样的情况下，代码短小精悍就是个优势。更重要的是，这是代码更加清晰的结果，阅读代码时，信噪比降低了。这意味着有更多时间来解决实际问题，而不是把时间花在继承的样板代码上。这样做还有一个优点，&lt;code&gt;JVM&lt;/code&gt; 会少加载一个类。&lt;/p&gt; &lt;p&gt;对每个试图阅读代码，弄明白代码意图的人来说，也清楚了很多。如果你试着大声念出第二个例子中的单词，能很容易听出是干嘛的，但第一个例子就不行了。&lt;/p&gt; &lt;p&gt;有趣的是，在&lt;code&gt;Java 8&lt;/code&gt;以前，这并不是一个反模式，而是惯用的代码编写方式，就像使用匿名内部类传递行为一样，都不是反模式，而是在 &lt;code&gt;Java&lt;/code&gt; 中表达你所想的唯一方式。随着语言的演进，编程习惯也要与时俱进。&lt;/p&gt; &lt;h3&gt;7.1.3 同样的东西写两遍&lt;/h3&gt; &lt;p&gt;不要重复你劳动&lt;code&gt;(Don’t Repeat Yourself，DRY)&lt;/code&gt;是一个众所周知的模式，它的反面是同样 的东西写两遍&lt;code&gt;(Write Everything Twice，WET)&lt;/code&gt;。这种代码异味多见于重复的样板代码，产 生了更多需要测试的代码，这样的代码难于重构，一改就坏。&lt;/p&gt; &lt;p&gt;不是所有 &lt;code&gt;WET&lt;/code&gt; 的情况都适合 &lt;code&gt;Lambda&lt;/code&gt; 化。有时，重复是唯一可以避免系统过紧耦合的方式。什么时候该将 &lt;code&gt;WET&lt;/code&gt; 的代码 &lt;code&gt;Lambda&lt;/code&gt; 化?这里有一个信号可以参考。如果有一个整体上大概相似的模式，只是行为上有所不同，就可以试着加入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式。(意思就是把行为相同的代码块抽成 &lt;code&gt;Lambda&lt;/code&gt; 表达式)&lt;/p&gt; &lt;p&gt;让我们看一个更具体的例子。回到我们有关音乐的问题，我想增加一个简单的 &lt;code&gt;Order&lt;/code&gt; 类来 计算用户购买专辑的一些有用属性，如计算音乐家人数、曲目和专辑时长等。如果使用命 令式 &lt;code&gt;Java&lt;/code&gt;，编写出的代码如下代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Order 类的命令式实现&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static long countRunningTime(List&amp;lt;Album&amp;gt; albums) {         long count = 0;         for (Album album : albums) {             for (Track track : album.getTrackList()) {                 count += track.getLength();             }         }         return count;     }      public long countMusicians(List&amp;lt;Album&amp;gt; albums) {         long count = 0;         for (Album album : albums) {             count += album.getMusicianList().size();         }         return count;     }       public long countTracks(List&amp;lt;Album&amp;gt; albums) {         long count = 0;         for (Album album : albums) {             count += album.getTrackList().size();         }         return count;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;每个方法里，都有样板代码将每个专辑里的属性和总数相加，比如每首曲目的长度或音乐家的人数。我们没有重用共有的概念，写出了更多代码需要测试和维护。可以使用 &lt;code&gt;Stream&lt;/code&gt; 来抽象，使用&lt;code&gt;Java 8&lt;/code&gt;中的集合类库来重写上述代码，使之更紧凑。如果直接将上述命令式 的代码翻译成使用流的形式，则形如下。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用流重构命令式的 &lt;code&gt;Order&lt;/code&gt; 类&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static long countRunningTimeStream(List&amp;lt;Album&amp;gt; albums) {         return albums.stream()                 .mapToLong(album -&amp;gt; album.getTracks()                 .mapToLong(track -&amp;gt; track.getLength()).sum())                 .sum();     }      public long countMusiciansStream(List&amp;lt;Album&amp;gt; albums) {         return albums.stream()                 .mapToLong(album -&amp;gt; album.getMusicians().count())                 .sum();     }      public long countTracksStream(List&amp;lt;Album&amp;gt; albums) {         return albums.stream()                 .mapToLong(album -&amp;gt; album.getTracks().count())                 .sum();     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然而这段代码仍然有重用可读性的问题，因为有一些抽象和共性只能使用领域内的知识来表达。流不会提供一个方法统计每张专辑上的信息——这是程序员要自己编写的领域知识。这也是在 &lt;code&gt;Java 8&lt;/code&gt; 出现之前很难编写的领域方法，因为每个方法都不一样。&lt;/p&gt; &lt;p&gt;想一下如何实现这样一个函数。我们返回一个 &lt;code&gt;long&lt;/code&gt;，统计所有专辑的某些特征，还需要 一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，告诉我们统计专辑上的什么信息。也就是说我们的方法需要一个参数，该参数为每张专辑返回一个&lt;code&gt;long&lt;/code&gt;，方便的是，&lt;code&gt;Java 8&lt;/code&gt;核心类库中已经有了这样一 个类型 &lt;code&gt;ToLongFunction&lt;/code&gt;。如下图所示，它的类型随参数类型，因此我们要使用的类型为 &lt;code&gt;ToLongFunction&amp;lt;Album&amp;gt;&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/sknlthsabiijequ1vm362gfk4r.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;这些都定下来之后，方法体就自然定下来了。我们将专辑转换成流，将专辑映射为 &lt;code&gt;long&lt;/code&gt;， 然后求和。在实现直接面对客户的代码时，比如 &lt;code&gt;countTracks&lt;/code&gt;，传入一个代表了领域知识 的 &lt;code&gt;Lambda&lt;/code&gt; 表达式，在这里，就是将专辑映射为上面的曲目。如下代码使用了这种方式转换之后的代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用领域方法重构 &lt;code&gt;Order&lt;/code&gt; 类&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countFeature(ToLongFunction&amp;lt;Album&amp;gt; function) {      return albums.stream()                  .mapToLong(function)                  .sum(); } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countTracks() { return countFeature(album -&amp;gt; album.getTracks().count()); } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countRunningTime() { return countFeature(album -&amp;gt; album.getTracks()                                 .mapToLong(track -&amp;gt; track.getLength())                                 .sum()); } &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public long countMusicians() { return countFeature(album -&amp;gt; album.getMusicians().count()); } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;7.2 Lambda表达式的单元测试&lt;/h2&gt; &lt;p&gt;通常，在编写单元测试时，怎么在应用中调用该方法，就怎么在测试中调用。给定一些输入或测试替身，调用这些方法，然后验证结果是否和预期的行为一致。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式给单元测试带来了一些麻烦，&lt;code&gt;Lambda&lt;/code&gt; 表达式没有名字，无法直接在测试代 码中调用。&lt;/p&gt; &lt;p&gt;你可以在测试代码中复制 &lt;code&gt;Lambda&lt;/code&gt; 表达式来测试，但这种方式的副作用是测试的不是真正的实现。假设你修改了实现代码，测试仍然通过，而实现可能早已在做另一件事了。&lt;/p&gt; &lt;p&gt;解决该问题有两种方式。第一种是将 &lt;code&gt;Lambda&lt;/code&gt; 表达式放入一个方法测试，这种方式要测那个方法，而不是 &lt;code&gt;Lambda&lt;/code&gt; 表达式本身。如下代码是一个将一组字符串转换成大写的方法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将字符串转换为大写形式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static List&amp;lt;String&amp;gt; allToUpperCase(List&amp;lt;String&amp;gt; words) { return words.stream()                      .map(string -&amp;gt; string.toUpperCase())                      .collect(Collectors.&amp;lt;String&amp;gt;toList()); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在这段代码中，&lt;code&gt;Lambda&lt;/code&gt; 表达式唯一的作用就是调用一个 &lt;code&gt;Java&lt;/code&gt; 方法。将该 &lt;code&gt;Lambda&lt;/code&gt; 表达式 单独测试是不值得的，它的行为太简单了。&lt;/p&gt; &lt;p&gt;如果换我来测试这段代码，我会将重点放在方法的行为上。如下代码测试了流中有多个单词的情况，它们都被转换成对应的大写&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void multipleWordsToUppercase() {          List&amp;lt;String&amp;gt; input = Arrays.asList(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;hello&amp;quot;);          List&amp;lt;String&amp;gt; result = Testing.allToUpperCase(input);          assertEquals(asList(&amp;quot;A&amp;quot;, &amp;quot;B&amp;quot;, &amp;quot;HELLO&amp;quot;), result); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;有时候 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现了复杂的功能，它可能包含多个边界情况、使用了多个属性来 计算一个非常重要的值。你非常想测试该段代码的行为，但它是一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，无法引用。&lt;/p&gt; &lt;p&gt;作为例子，让我们来看一个比大写转换更复杂一点的方法。我们要把字符串的第一个字母转换成大写，其他部分保持不变。使用流和 &lt;code&gt;Lambda&lt;/code&gt; 表达式，编写的代码形如下代码所示。在①处使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式做转换。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将列表中元素的第一个字母转换成大写&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static List&amp;lt;String&amp;gt; elementFirstToUpperCaseLambdas(List&amp;lt;String&amp;gt; words) {      return words.stream()         .map(value -&amp;gt; {①             char firstChar = Character.toUpperCase(value.charAt(0));              return firstChar + value.substring(1);         }).collect(Collectors.&amp;lt;String&amp;gt;toList()); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果要测试这段代码，我们必须创建一个列表，然后将想要测试的各种情况都测试到。如下代码展示了这种方式有多么繁琐，别担心，我们有办法!&lt;/p&gt; &lt;ul&gt; &lt;li&gt;测试字符串包含两个字符的情况，第一个字母被转换为大写&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void twoLetterStringConvertedToUppercaseLambdas() {          List&amp;lt;String&amp;gt; input = Arrays.asList(&amp;quot;ab&amp;quot;);          List&amp;lt;String&amp;gt; result = Testing.elementFirstToUpperCaseLambdas(input);          assertEquals(asList(&amp;quot;Ab&amp;quot;), result); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;别用 &lt;code&gt;Lambda&lt;/code&gt; 表达式。我知道，在一本介绍如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的书里，这个建议有点奇怪，但是方楔子钉不进圆孔。既然如此，大家一定会问如何测试代码，同时享有 &lt;code&gt;Lambda&lt;/code&gt; 表达式带来的便利?&lt;/p&gt; &lt;p&gt;请用方法引用。任何 &lt;code&gt;Lambda&lt;/code&gt; 表达式都能被改写为普通方法，然后使用方法引用直接引用。&lt;/p&gt; &lt;p&gt;如下代码将 &lt;code&gt;Lambda&lt;/code&gt; 表达式重构为一个方法，然后在主程序中使用，主程序负责转换字符串。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将首字母转换为大写，应用到所有列表元素&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static List&amp;lt;String&amp;gt; elementFirstToUppercase(List&amp;lt;String&amp;gt; words) {      return words.stream()                      .map(Testing::firstToUppercase)                      .collect(Collectors.&amp;lt;String&amp;gt;toList()); }  public static String firstToUppercase(String value) { ①      char firstChar = Character.toUpperCase(value.charAt(0));      return firstChar + value.substring(1); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;把处理字符串的的逻辑抽取成一个方法后，就可以测试该方法，把所有的边界情况都覆盖到。新的测试用例如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;测试单独的方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void twoLetterStringConvertedToUppercase() {          String input = &amp;quot;ab&amp;quot;;          String result = Testing.firstToUppercase(input);          assertEquals(&amp;quot;Ab&amp;quot;, result); } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;7.3 在测试替身时使用Lambda表达式&lt;/h2&gt; &lt;p&gt;编写单元测试的常用方式之一是使用测试替身描述系统中其他模块的期望行为。这种方式很有用，因为单元测试可以脱离其他模块来测试你的类或方法，测试替身让你能用单元测试来实现这种隔离。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;测试替身也常被称为模拟，事实上测试存根和模拟都属于测试替身。区别是模拟可以验证代码的行为。读者若想了解更多有关这方面的信息，请阅读 MartinFowler 的相关文章(http://martinfowler.com/articles/mocksArentStubs.html)。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;测试代码时，使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的最简单方式是实现轻量级的测试存根。如果交互的类本身就是一个函数接口，实现这样的存根就非常简单和自然。&lt;/p&gt; &lt;p&gt;在上面的代码中讨论过如何将通用的领域逻辑重构为一个 &lt;code&gt;countFeature&lt;/code&gt; 方法，然后使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现不同的统计行为。如下代码展示了如何对此编写单元测试。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式编写测试替身，传给 &lt;code&gt;countFeature&lt;/code&gt; 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Test public void canCountFeatures() { OrderDomain order = new OrderDomain(asList(                   newAlbum(&amp;quot;Exile on Main St.&amp;quot;),                  newAlbum(&amp;quot;Beggars Banquet&amp;quot;),                  newAlbum(&amp;quot;Aftermath&amp;quot;),                  newAlbum(&amp;quot;Let it Bleed&amp;quot;)));          assertEquals(8, order.countFeature(album -&amp;gt; 2));      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;对于 &lt;code&gt;countFeature&lt;/code&gt; 方法的期望行为是为传入的专辑返回某个数值。这里传入 &lt;code&gt;4&lt;/code&gt; 张专辑，测 试存根中为每张专辑返回 &lt;code&gt;2&lt;/code&gt;，然后断言该方法返回 &lt;code&gt;8&lt;/code&gt;，即 &lt;code&gt;2×4&lt;/code&gt;。如果要向代码传入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，最好确保 &lt;code&gt;Lambda&lt;/code&gt; 表达式也通过测试。&lt;/p&gt; &lt;p&gt;多数的测试替身都很复杂，使用 &lt;code&gt;Mockito&lt;/code&gt; 这样的框架有助于更容易地产生测试替身。让我们考虑一种简单情形，为 &lt;code&gt;List&lt;/code&gt; 生成测试替身。我们不想返回 &lt;code&gt;List&lt;/code&gt; 本上的长度，而是返回另一个 &lt;code&gt;List&lt;/code&gt; 的长度，为了模拟 &lt;code&gt;List&lt;/code&gt; 的 &lt;code&gt;size&lt;/code&gt; 方法，我们不想只给出答案，还想做一些操作，因此传入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;结合 &lt;code&gt;Mockito&lt;/code&gt; 框架使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    //这里使用 otherList 的 size 来代替 list 测试     List&amp;lt;String&amp;gt; list = mock(List.class);     List&amp;lt;String&amp;gt; otherList = Arrays.asList(&amp;quot;123&amp;quot;,&amp;quot;456&amp;quot;,&amp;quot;789&amp;quot;);     Mockito.when(list.size()).thenAnswer(inv -&amp;gt; otherList.size());     assertEquals(3, list.size()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;Mockito&lt;/code&gt; 使用 &lt;code&gt;Answer&lt;/code&gt; 接口允许用户提供其他行为，换句话说，这是我们的老朋友:代码即 数据。之所以在这里能使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式，是因为 &lt;code&gt;Answer&lt;/code&gt; 本身就是一个函数接口。&lt;/p&gt; &lt;h2&gt;7.4 惰性求值和调试&lt;/h2&gt; &lt;p&gt;调试时通常会设置断点，单步跟踪程序的每一步。使用流时，调试可能会变得更加复杂，因为迭代已交由类库控制，而且很多流操作是惰性求值的。&lt;/p&gt; &lt;p&gt;在传统的命令式编程看来，代码就是达到某种目的的一系列行动，在行动前后查看程序状态是有意义的。在 &lt;code&gt;Java 8&lt;/code&gt; 中，你仍然可以使用 &lt;code&gt;IDE&lt;/code&gt; 提供的各种调试工具，但有时需要调整 实现方式，以期达到更好的结果。&lt;/p&gt; &lt;h2&gt;7.5 日志和打印消息&lt;/h2&gt; &lt;p&gt;假设你要在集合上进行大量操作，你要调试代码，你希望看到每一步操作的结果是什么。可以在每一步打印出集合中的值，这在流中很难做到，因为一些中间步骤是惰性求值的。&lt;/p&gt; &lt;p&gt;让我们通过第 3 章介绍的命令式版本的国际报告程序，看看如何记录中间值。考虑到读者可能已经忘记这个程序，我们再来解释一下这个程序的意图，该程序找出了专辑上每位艺术家来自哪个国家。在例如下代码中，我们将找到的国家信息记录到日志中。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;记录中间值，以便调试 &lt;code&gt;for&lt;/code&gt; 循环&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Set&amp;lt;String&amp;gt; nationalities = new HashSet&amp;lt;&amp;gt;();  for (Artist artist : album.getMusicianList()) {     if (artist.getName().startsWith(&amp;quot;The&amp;quot;)) {     String nationality = artist.getNationality();      System.out.println(&amp;quot;Found nationality: &amp;quot; + nationality);      nationalities.add(nationality);     }  } return nationalities; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在可以使用 &lt;code&gt;forEach&lt;/code&gt; 方法打印出流中的值，这同时会触发求值过程。但是这样的操作有 个缺点:我们无法再继续操作流了，流只能使用一次。如果我们还想继续，必须重新创建流。如下代码展示了这样的代码会有多难看。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;forEach&lt;/code&gt; 记录中间值，这种方式有点幼稚&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;album.getMusicians()           .filter(artist -&amp;gt; artist.getName().startsWith(&amp;quot;The&amp;quot;))           .map(artist -&amp;gt; artist.getNationality())           .forEach(nationality -&amp;gt; System.out.println(&amp;quot;Found: &amp;quot; + nationality));  Set&amp;lt;String&amp;gt; nationalities          = album.getMusicians()                 .filter(artist -&amp;gt; artist.getName().startsWith(&amp;quot;The&amp;quot;))                 .map(artist -&amp;gt; artist.getNationality())                 .collect(Collectors.&amp;lt;String&amp;gt;toSet()); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;7.6 解决方案:peak&lt;/h2&gt; &lt;p&gt;遗憾的是，流有一个方法让你能查看每个值，同时能继续操作流。这就是 &lt;code&gt;peek&lt;/code&gt; 方法。如下代码使用 &lt;code&gt;peek&lt;/code&gt; 方法重写了前面的例子，输出流中的值，同时避免了重复的流操作。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;peek&lt;/code&gt; 方法记录中间值&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Set&amp;lt;String&amp;gt; nationalities          = album.getMusicians()                 .filter(artist -&amp;gt; artist.getName().startsWith(&amp;quot;The&amp;quot;))                 .map(artist -&amp;gt; artist.getNationality())                 .peek(nation -&amp;gt; System.out.println(&amp;quot;Found nationality: &amp;quot; + nation))                 .collect(Collectors.&amp;lt;String&amp;gt;toSet()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用 &lt;code&gt;peek&lt;/code&gt; 方法还能以同样的方式，将输出定向到现有的日志系统中，比如 &lt;code&gt;log4j、java&lt;/code&gt;. &lt;code&gt;util.logging&lt;/code&gt; 或者 &lt;code&gt;slf4j&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;7.7 在流中间设置断点&lt;/h2&gt; &lt;p&gt;记录日志这是 &lt;code&gt;peek&lt;/code&gt; 方法的用途之一。为了像调试循环那样一步一步跟踪，可在 &lt;code&gt;peek&lt;/code&gt;&lt;/p&gt; &lt;p&gt;此时，&lt;code&gt;peek&lt;/code&gt; 方法可知包含一个空的方法体，只要能设置断点就行。有一些调试器不允许在 空的方法体中设置断点，此时，我将值简单地映射为其本身，这样就有地方设置断点了， 虽然这样做不够完美，但只要能工作就行。&lt;/p&gt; &lt;h2&gt;7.8 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;重构遗留代码时考虑如何使用Lambda表达式，有一些通用的模式。&lt;/li&gt; &lt;li&gt;如果想要对复杂一点的Lambda表达式编写单元测试，将其抽取成一个常规的方法。&lt;/li&gt; &lt;li&gt;&lt;code&gt;peek&lt;/code&gt; 方法能记录中间值，在调试时非常有用。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 15 Nov 2018 13:07:00 GMT</pubDate>
    </item>
    <item>
      <title>Markdown Add Backquote</title>
      <link>https://www.zhangaoo.com/article/markdown-add-backquote</link>
      <content:encoded>&lt;h1&gt;便捷添加反引号插件&lt;/h1&gt; &lt;p&gt;我个人是 &lt;code&gt;Markdown&lt;/code&gt; 的深度使用者，但是在写 &lt;code&gt;Markdown&lt;/code&gt; 的时候发现手动添加反引号是一件非常麻烦的事情。包括两种情况：一种是单行代码的两个反引号；另一种是使用两组一共六个反引号包含的代码块。因此花了半天研究了一下 &lt;code&gt;VSCode&lt;/code&gt; 的插件开发流程，写了这个简易的小插件。&lt;/p&gt; &lt;h2&gt;Features&lt;/h2&gt; &lt;p&gt;对于选中的文本使用插件会添加单行代码的反引号，不选中文本的情况下会在当前的文本的下一行自动插入代码块的反引号。&lt;/p&gt; &lt;p&gt;使用快捷键添加（推荐）：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Mac: &lt;code&gt;cmd+d&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Windows: &lt;code&gt;ctrl+d&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/0a05bbv3fkglqphtorqg2fsmt6.gif" alt="add-backquote" /&gt;&lt;/p&gt; &lt;h2&gt;安装&lt;/h2&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/1nk66cbtdeh2ioc7sg9mfrgm11.png" alt="install" /&gt;&lt;/p&gt; &lt;h2&gt;Release Notes&lt;/h2&gt; &lt;p&gt;0.0.1 (2018/11/11)&lt;/p&gt; &lt;ul&gt; &lt;li&gt;初始版本&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;0.0.2&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;添加自动生成代码块反引号功能&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;更多&lt;/h2&gt; &lt;p&gt;如果有更好的建议可以在 &lt;code&gt;Github&lt;/code&gt; 或者个人博客留言，后续还会添加更多的功能&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://github.com/zealzhangz/markdown-add-backquote" target="_blank"&gt;zealzhangz Github&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://www.zhangaoo.com/article/markdown-add-backquote" target="_blank"&gt;zealzhangz Blog&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Enjoy!&lt;/strong&gt;&lt;/p&gt; &lt;hr /&gt; &lt;h1&gt;Markdown Add Backquote&lt;/h1&gt; &lt;p&gt;I am a markdown deep user.When writing markdown files, it is very troublesome thing to add backquote manually.There are two scenarios: one is the two backquote of a single line of code;The other is  the block of code contained six backquote.So we used half a day to develop this simple plugin.&lt;/p&gt; &lt;h2&gt;Features&lt;/h2&gt; &lt;p&gt;The selected content will be added to the line backquote.If you do not select text, plugin will add code block backquote.&lt;/p&gt; &lt;p&gt;Use shortcut keys to add backquotes in Markdown:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Mac: &lt;code&gt;cmd+d&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Windows: &lt;code&gt;ctrl+d&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/0a05bbv3fkglqphtorqg2fsmt6.gif" alt="add-backquote" /&gt;&lt;/p&gt; &lt;h2&gt;Install&lt;/h2&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/1nk66cbtdeh2ioc7sg9mfrgm11.png" alt="install" /&gt;&lt;/p&gt; &lt;h2&gt;Release Notes&lt;/h2&gt; &lt;p&gt;0.0.1 (2018/11/11)&lt;/p&gt; &lt;ul&gt; &lt;li&gt;First version&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;0.0.2&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Add auto generated code block backquote function.&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;For more information&lt;/h2&gt; &lt;p&gt;If you have better suggestions, you can leave a message on &lt;code&gt;Github&lt;/code&gt; or my personal blog, and more features will be added later.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://github.com/zealzhangz/markdown-add-backquote" target="_blank"&gt;zealzhangz Github&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://www.zhangaoo.com/article/markdown-add-backquote" target="_blank"&gt;zealzhangz Blog&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Enjoy!&lt;/strong&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 11 Nov 2018 05:35:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇五之数据并行化</title>
      <link>https://www.zhangaoo.com/article/java-lambda-parallel</link>
      <content:encoded>&lt;h1&gt;第 6 章 数据并行化&lt;/h1&gt; &lt;p&gt;本章主要内容并不在于如何更改代码，而是讲述为什么需要并 行化和什么时候会带来性能的提升。要提醒大家的是，本章并不是关于 Java 性能的泛泛之谈，我们只关注 Java 8 轻松提升性能的技术。&lt;/p&gt; &lt;h2&gt;6.1 并行和并发&lt;/h2&gt; &lt;p&gt;并发是两个任务共享时间段，并行则是两个任务在同一时间发生，比如运行在多核 &lt;code&gt;CPU&lt;/code&gt; 上。如果一个程序要运行两个任务，并且只有一个 &lt;code&gt;CPU&lt;/code&gt; 给它们分配了不同的时间片，那么这就是并发，而不是并行。两者之间的区别如下图所示。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/sefv0r00bohb6pr9m1aksn3n0d.png" alt="并发和并行的区别" /&gt;&lt;/p&gt; &lt;p&gt;并行化是指为缩短任务执行时间，将一个任务分解成几部分，然后并行执行。这和顺序执行的任务量是一样的，区别就像用更多的马来拉车，花费的时间自然减少了。实际上，和顺序执行相比，并行化执行任务时，&lt;code&gt;CPU&lt;/code&gt; 承载的工作量更大。&lt;/p&gt; &lt;p&gt;本章会讨论一种特殊形式的并行化:数据并行化。数据并行化是指将数据分成块，为每块数据分配单独的处理单元。还是拿马拉车那个例子打比方，就像从车里取出一些货物，放到另一辆车上，两辆马车都沿着同样的路径到达目的地。&lt;/p&gt; &lt;p&gt;当需要在大量数据上执行同样的操作时，数据并行化很管用。它将问题分解为可在多块数据上求解的形式，然后对每块数据执行运算，最后将各数据块上得到的结果汇总，从而获得最终答案。&lt;/p&gt; &lt;p&gt;人们经常拿任务并行化和数据并行化做比较，在任务并行化中，线程不同，工作各异。我 们最常遇到的Java EE应用容器便是任务并行化的例子之一，每个线程不光可以为不同用 户服务，还可以为同一个用户执行不同的任务，比如登录或往购物车添加商品。&lt;/p&gt; &lt;h2&gt;6.2 为什么并行化如此重要&lt;/h2&gt; &lt;p&gt;在过去十年中，主流的芯片厂商转向了多核处理器。服务器通过几个物理单元搭载 32 或 64 核的情况已不鲜见，而且，这种趋势尚无减弱的征兆。这种变化影响到了软件设计。我们不能再依赖提升 CPU 的时钟频率来提高现有代码的计算能力，需要利用现代 CPU 的架构，而这唯一的办法就是编写并行化的代码。&lt;/p&gt; &lt;p&gt;阿姆达尔定律是一个简单规则，预测了搭载多核处理器的机器提升程序速度的理论最大值。以一段完全串行化的程序为例，如果将其一半改为并行化处理，则不管增加多少处理器，其理论上的最大速度只是原来的 2 倍。有了大量的处理器后，现在这已经是现实了，问题的求解时间将完全取决于它可被分解成几个部分。&lt;/p&gt; &lt;p&gt;以这样的方式思考性能问题，优化任何和计算相关的任务立即变成了如何有效利用现有硬件的问题。当然，不是所有的任务都和计算相关，本章只关注这类和计算相关的问题。&lt;/p&gt; &lt;h2&gt;6.3 并行化流操作&lt;/h2&gt; &lt;p&gt;并行化操作流只需改变一个方法调用。如果已经有一个 &lt;code&gt;Stream&lt;/code&gt; 对象，调用它的 &lt;code&gt;parallel&lt;/code&gt; 方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流，调用 &lt;code&gt;parallelStream&lt;/code&gt; 就能立即获得一个拥有并行能力的流。&lt;/p&gt; &lt;p&gt;让我们先来看一个具体的例子,计算了一组专辑的曲目总长度。它拿到每张专辑的 曲目信息，然后得到曲目长度，最后相加得出曲目总长度。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;串行化计算专辑曲目长度&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public int serialArraySum() { return albums.stream()                       .flatMap(Album::getTracks)                       .mapToInt(Track::getLength)                       .sum(); } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;并行化计算专辑曲目长度&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public int parallrlArraySum(List&amp;lt;Album&amp;gt; albums) {         return albums.parallelStream()                 .flatMap(Album::getTracks)                 .mapToInt(Track::getLength)                 .sum();     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;读到这里，大家的第一反应可能是立即将手头代码中的 &lt;code&gt;stream&lt;/code&gt; 方法替换为 &lt;code&gt;parallelStream&lt;/code&gt; 方法，因为这样做简直太简单了!先别忙，为了将硬件物尽其用，利用好并行化非常重要，但流类库提供的数据并行化只是其中的一种形式。&lt;/p&gt; &lt;p&gt;我们先要问自己一个问题:并行化运行基于流的代码是否比串行化运行更快?这不是一个简单的问题。回到前面的例子，哪种方式花的时间更多取决于串行或并行化运行时的环境。&lt;/p&gt; &lt;p&gt;中的代码为准，在一个四核电脑上，如果有 10 张专辑，串行化代码的速 度是并行化代码速度的 8 倍;如果将专辑数量增至 100 张，串行化和并行化速度相当;如 果将专辑数量增值 10000 张，则并行化代码的速度是串行化代码速度的 2.5 倍。&lt;/p&gt; &lt;p&gt;输入流的大小并不是决定并行化是否会带来速度提升的唯一因素，性能还会受到编写代码的方式和核的数量的影响。6.6 节会详述和性能有关的细节，但现在还是再来看一个更复杂的例子吧。&lt;/p&gt; &lt;h2&gt;6.4 模拟系统&lt;/h2&gt; &lt;p&gt;并行化流操作的用武之地是使用简单操作处理大量数据，比如模拟系统。本节我们会搭建一个简易的模拟系统来理解摇骰子，但其中的原理对于大型、真实的系统也适用。&lt;/p&gt; &lt;p&gt;我们这里要讨论的是蒙特卡洛模拟法。蒙特卡洛模拟法会重复相同的模拟很多次，每次模拟都使用随机生成的种子。每次模拟的结果都被记录下来，汇总得到一个对系统的全面模拟。蒙特卡洛模拟法被大量用在工程、金融和科学计算领域。&lt;/p&gt; &lt;p&gt;如果公平地掷两次骰子，然后将赢的一面上的点数相加，就会得到一个 2~12 的数字。点数的和至少是 2，因为骰子六个面上最小的点数是 1，而我们将骰子掷了两次;点数的和最大超不过 12，因为骰子点数最多的一面也不过 6 点。我们想要得出点数落在 2~12 之间 每个值的概率。&lt;/p&gt; &lt;p&gt;解决该问题的方法之一是求出掷骰子的所有组合，比如，得到 2 点的方式是第一次掷得 1 点，第二次也掷得 1 点。总共有 36 种可能的组合，因此，掷得 2 点的概率就是 1/36。&lt;/p&gt; &lt;p&gt;另外一种解法是使用 1 到 6 的随机数模拟掷骰子事件，然后用得到每个点数的次数除以总的投掷次数。这就是一个简单的蒙特卡洛模拟。模拟投掷骰子的次数越多，得到的结果越准确，因此，我们希望尽可能多地增加模拟次数。&lt;/p&gt; &lt;p&gt;如下代码展示了如何使用流实现蒙特卡洛模拟法。N 代表模拟次数，在➊处使用 IntStream 的range方法创建大小为N的流，在➋处调用parallel方法使用流的并行化操作， twoDiceThrows 函数模拟了连续掷两次骰子事件，返回值是两次点数之和。在➌处使用 mapToObj 方法以便在流上使用该函数。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用蒙特卡洛模拟法并行化模拟掷骰子事件&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static final Integer N = 4;      public static Map&amp;lt;Integer, Double&amp;gt; parallelDiceRolls() {         IntFunction&amp;lt;Integer&amp;gt; intFunction = (a) -&amp;gt; (int) (1 + Math.random() * (6 - 1 + 1)) + (int) (1 + Math.random() * (6 - 1 + 1));         double fraction = 1.0 / N;         return IntStream.range(0, N)                   ①                 .parallel()                            ②                 .mapToObj(intFunction)                 ③                 .collect(groupingBy(side -&amp;gt; side,      ④                         summingDouble(n -&amp;gt; fraction)));⑤     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在➍处得到了需要合并的所有结果的流，使用前一章介绍的 groupingBy 方法将点数一样 的结果合并。我说过要计算每个点数的出现次数，然后除以总的模拟次数 N。在流框架中， 将数字映射为 1/N 并且相加很简单，这和前面说的计算方法是等价的。在➎处我们使用 summingDouble方法完成了这一步。最终的返回值类型是Map&amp;lt;Integer, Double&amp;gt;，是点数之 和到它们的概率的映射。&lt;/p&gt; &lt;p&gt;个人理解： ①产生 0 - （N-1）的流 ②调用parallel方法使用流的并行化操作 ③Lambda 模拟两次掷骰子和，并返回结果 ④⑤根据元素的值groupby，&lt;code&gt;summingDouble(n -&amp;gt; fraction)&lt;/code&gt; 出现多少次，就加多少个 &lt;code&gt;1.0 / N&lt;/code&gt;，也就是出现的概率。&lt;code&gt;summingDouble&lt;/code&gt;就是简单计算和。&lt;/p&gt; &lt;p&gt;我得承认这段代码不算儿戏，但使用 5 行代码即能实现蒙特卡洛模拟法还是很精巧的。重要的是模拟的次数越多，得到的结果越准确，因此我们运行多次模拟的动机就会更加强烈。这是一个很好的并行化案列，并行化能带来速度的提升。&lt;/p&gt; &lt;p&gt;我已经带领读者浏览了整个实现细节，为了对比，下面的代码给出了手动实现并行化蒙特卡洛模拟法的代码。可以看到，大多数代码都在处理调度和等待线程池中的某项任务完成。而 使用并行化的流时，这些都不用程序员手动管理。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;通过手动使用线程模拟掷骰子事件&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//代码太多了，这里就不全部列出来了，具体可参看原文 public class ManualDiceRolls { private static  nal int N = 100000000; private  nal double fraction; private  nal Map&amp;lt;Integer, Double&amp;gt; results; private  nal int numberOfThreads; private  nal ExecutorService executor; private  nal int workPerThread; public static void main(String[] args) {  ManualDiceRolls roles = new ManualDiceRolls();  roles.simulateDiceRoles();     } public ManualDiceRolls() { fraction = 1.0 / N; results = new ConcurrentHashMap&amp;lt;&amp;gt;(); numberOfThreads = Runtime.getRuntime().availableProcessors();  executor = Executors.newFixedThreadPool(numberOfThreads);  workPerThread = N / numberOfThreads;     }  ......    &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;6.5 限制&lt;/h2&gt; &lt;p&gt;之前提到过使用并行流能工作，但这样说有点无耻。虽然只需一点改动，就能让已有代码并行化运行，但前提是代码写得符合约定。为了发挥并行流框架的优势，写代码时必须遵守一些规则和限制。&lt;/p&gt; &lt;p&gt;之前调用 &lt;code&gt;reduce&lt;/code&gt; 方法，初始值可以为任意值，为了让其在并行化时能工作正常，初值必须 为组合函数的恒等值。拿恒等值和其他值做 &lt;code&gt;reduce&lt;/code&gt; 操作时，其他值保持不变。比如，使用 &lt;code&gt;reduce&lt;/code&gt; 操作求和，组合函数为&lt;code&gt;(acc, element) -&amp;gt; acc + element&lt;/code&gt;，则其初值必须为 &lt;code&gt;0&lt;/code&gt;，因为任何数字加 &lt;code&gt;0&lt;/code&gt;，值不变。&lt;/p&gt; &lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; 操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变，组合操作的顺序不重要。有点疑惑?别担心!请看如下代码，我们可以改变加法和乘法的顺序， 但结果是一样的。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;加法和乘法满足结合律&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;(4+2)+1=4+(2+1)=7 (4*2)*1=4*(2*1)=8 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;要避免的是持有锁。流框架会在需要时，自己处理同步操作，因此程序员没有必要为自己的数据结构加锁。如果你执意为流中要使用的数据结构加锁，比如操作的原始集合，那么有可能是自找麻烦。&lt;/p&gt; &lt;p&gt;在前面我还解释过，使用 &lt;code&gt;parallel&lt;/code&gt; 方法能轻易将流转换为并行流。如果读者在阅读本书的同时，还查看了相应的 &lt;code&gt;API&lt;/code&gt;，那么可能会发现还有一个叫 &lt;code&gt;sequential&lt;/code&gt; 的方法。在要对流求值时，不能同时处于两种模式，要么是并行的，要么是串行的。如果同时调用了 &lt;code&gt;parallel&lt;/code&gt; 和 &lt;code&gt;sequential&lt;/code&gt; 方法，最后调用的那个方法起效。&lt;/p&gt; &lt;h2&gt;6.6 性能&lt;/h2&gt; &lt;p&gt;在前面我简要提及了影响并行流是否比串行流快的一些因素，现在让我们仔细看看它们。 理解哪些能工作、哪些不能工作，能帮助在如何使用、什么时候使用并行流这一问题上做出明智的决策。影响并行流性能的主要因素有 5 个，依次分析如下。&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;数据大小 输入数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理，再将结果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多时，并行化处理才有意义。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;源数据结构 每个管道的操作都基于一些初始数据源，通常是集合。将不同的数据源分割相对容易， 这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;装箱 处理基本类型比处理装箱类型要快。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;核的数量 极端情况下，只有一个核，因此完全没必要并行化。显然，拥有的核越多，获得潜在性 能提升的幅度就越大。在实践中，核的数量不单指你的机器上有多少核，更是指运行时 你的机器能使用多少核。这也就是说同时运行的其他进程，或者线程关联性(强制线程 在某些核或 &lt;code&gt;CPU&lt;/code&gt; 上运行)会影响性能。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;单元处理开销 比如数据大小，这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长，并行操作带来的性能提升越明显。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;使用并行流框架，理解如何分解和合并问题是很有帮助的。这让我们能够知悉底层如何工作，但却不必了解框架的细节。&lt;/p&gt; &lt;p&gt;来看一个具体的问题，看看如何分解和合并它。下面并行求和的代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;并行求和&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private int addIntegers(List&amp;lt;Integer&amp;gt; values) {      return values.parallelStream()                       .mapToInt(i -&amp;gt; i)                       .sum(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在底层，并行流还是沿用了 &lt;code&gt;fork/joi&lt;/code&gt;n 框架。&lt;code&gt;fork&lt;/code&gt; 递归式地分解问题，然后每段并行执行， 最终由 &lt;code&gt;join&lt;/code&gt; 合并结果，返回最后的值。过程如下图所示：&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/o972860naoid6pcr095t6tom4r.png" alt="流并行处理" /&gt;&lt;/p&gt; &lt;p&gt;假设并行流将我们的工作分解开，在一个四核的机器上并行执行。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;数据被分成四块。&lt;/li&gt; &lt;li&gt;计算工作在每个线程里并行执行。这包括将每个 &lt;code&gt;Integer&lt;/code&gt; 对象映射为 &lt;code&gt;int&lt;/code&gt; 值，然后在每个线程里将 &lt;code&gt;1/4&lt;/code&gt; 的数字相加。理想情况下，我们希望在这里花的时间越多越好，因为这里是并行操作的最佳场所。&lt;/li&gt; &lt;li&gt;然后合并结果。在上面的并行求和代码中，就是 sum 操作，但这也可能是 &lt;code&gt;reduce&lt;/code&gt;、&lt;code&gt;collect&lt;/code&gt; 或其他终结操作。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;根据问题的分解方式，初始的数据源的特性变得尤其重要，它影响了分解的性能。直观上 看，能重复将数据结构对半分解的难易程度，决定了分解操作的快慢。能对半分解同时意味着待分解的值能够被等量地分解。&lt;/p&gt; &lt;p&gt;我们可以根据性能的好坏，将核心类库提供的通用数据结构分成以下 3 组。&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;性能好 &lt;code&gt;ArrayList&lt;/code&gt;、数组或 &lt;code&gt;IntStream.range&lt;/code&gt;，这些数据结构支持随机读取，也就是说它们能轻而易举地被任意分解。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;性能一般 &lt;code&gt;HashSet&lt;/code&gt;、&lt;code&gt;TreeSet&lt;/code&gt;，这些数据结构不易公平地被分解，但是大多数时候分解是可能的。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;性能差 有些数据结构难于分解，比如，可能要花 O(N) 的时间复杂度来分解问题。其中包括 &lt;code&gt;LinkedList&lt;/code&gt;，对半分解太难了。还有 &lt;code&gt;Streams.iterate&lt;/code&gt; 和 &lt;code&gt;BufferedReader.lines&lt;/code&gt;，它们长度未知，因此很难预测该在哪里分解。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;初始的数据结构影响巨大。举一个极端的例子，对比对 10000 个整数并行求和，使用 &lt;code&gt;ArrayList&lt;/code&gt; 要比使用 &lt;code&gt;LinkedList&lt;/code&gt; 快 10 倍。这不是说业务逻辑的性能情况也会如此，只是说明了数据结构对于性能的影响之大。使用形如 &lt;code&gt;LinkedList&lt;/code&gt; 这样难于分解的数据结构并行运行可能更慢。&lt;/p&gt; &lt;p&gt;理想情况下，一旦流框架将问题分解成小块，就可以在每个线程里单独处理每一小块，线程之间不再需要进一步通信。无奈现实不总遂人愿!&lt;/p&gt; &lt;p&gt;在讨论流中单独操作每一块的种类时，可以分成两种不同的操作:无状态的和有状态的。 无状态操作整个过程中不必维护状态，有状态操作则有维护状态所需的开销和限制。&lt;/p&gt; &lt;p&gt;如果能避开有状态，选用无状态操作，就能获得更好的并行性能。无状态操作包括 &lt;code&gt;map&lt;/code&gt;、 &lt;code&gt;filter&lt;/code&gt; 和 &lt;code&gt;flatMap&lt;/code&gt;，有状态操作包括 &lt;code&gt;sorted&lt;/code&gt;、&lt;code&gt;distinct&lt;/code&gt; 和 &lt;code&gt;limit&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;6.7 并行化数组操作&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Java 8&lt;/code&gt; 还引入了一些针对数组的并行操作，脱离流框架也可以使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式。像流框架上的操作一样，这些操作也都是针对数据的并行化操作。让我们看看如何使用这些操作解决那些使用流框架难以解决的问题。&lt;/p&gt; &lt;p&gt;这些操作都在工具类 &lt;code&gt;Arrays&lt;/code&gt; 中，该类还包括 &lt;code&gt;Java&lt;/code&gt; 以前版本中提供的和数组相关的有用方法，下表总结了新增的并行化操作。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;数组上的并行化操作&lt;/li&gt; &lt;/ul&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th align="left"&gt;方法名&lt;/th&gt;&lt;th align="left"&gt;操作&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td align="left"&gt;parallelPrefix&lt;/td&gt;&lt;td align="left"&gt;任意给定一个函数，计算数组的和&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;parallelSetAll&lt;/td&gt;&lt;td align="left"&gt;使用 Lambda 表达式更新数组元素&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;parallelSort&lt;/td&gt;&lt;td align="left"&gt;并行化对数组元素排序&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;使用一个 for 循环初始化数组。在这里，我们用数 组下标初始化数组中的每个元素。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 for 循环初始化数组&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static double[] imperativeInitilize(int size) {      double[] values = new double[size];     for(int i = 0; i &amp;lt; values.length;i++) {         values[i] = i;     } return values;  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用 &lt;code&gt;parallelSetAll&lt;/code&gt; 方法能轻松地并行化该过程，代码如下所示。首先提供了一个用于操作的数组，然后传入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，根据数组下标计算元素的值。在该例中， 数组下标和元素的值是一样的。使用这些方法有一点要小心:它们改变了传入的数组，而没有创建一个新的数组。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用并行化数组操作初始化数组&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static double [] parallelInitialize(int size){         double [] values = new double[size];         Arrays.parallelSetAll(values,i -&amp;gt;i);         return values;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;parallelPrefix&lt;/code&gt; 操作擅长对时间序列数据做累加，它会更新一个数组，将每一个元素替换为当前元素和其前驱元素的和，这里的“和”是一个宽泛的概念，它不必是加法，可以是任意一个 &lt;code&gt;BinaryOperator&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;使用该方法能计算的例子之一是一个简单的滑动平均数。在时间序列上增加一个滑动窗口，计算出窗口中的平均值。如果输入数据为 0、1、2、3、4、3.5，滑动窗口的大小为 3，则简单滑动平均数为 1、2、3、3.5。如下代码展示了如何计算滑动平均数。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;计算简单滑动平均数&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static double[] simpleMovingAverage(double[] values, int n) {         //不影响原数组，先拷贝一份         double[] sums = Arrays.copyOf(values, values.length); ①         //每一个元素替换为当前元素和其前驱元素的和         Arrays.parallelPrefix(sums, Double::sum);②         int start = n - 1;         //range的作用其实就是产生遍历的下标         return IntStream.range(start, sums.length)③                 .mapToDouble(i -&amp;gt; {                     //第一个窗口的前一个值不存在因此为0                     double prefix = i == start ? 0 : sums[i - n];                     return (sums[i] - prefix) / n;④                 }).toArray();⑤     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里要理解滑动平均值的求法，是先计算和（每一个元素替换为当前元素和其前驱元素的和）然后减去窗口起始位置的元素即可，除以 n 即得到平均值。&lt;/p&gt; &lt;p&gt;这段代码有点复杂，我会分步介绍它是如何工作的。参数 n 是时间窗口的大小，我们据此 计算滑动平均值。由于要使用的并行操作会改变数组内容，为了不修改原有数据，在➊处 复制了一份输入数据。&lt;/p&gt; &lt;p&gt;在➋处执行并行操作，将数组的元素相加。现在 sums 变量中保存了求和结果。比如输入 0、1、2、3、4、3.5，则计算后的值为 0.0、1.0、3.0、6.0、10.0、13.5。&lt;/p&gt; &lt;p&gt;现在有了和，就能计算出时间窗口中的和了，减去窗口起始位置的元素即可，除以 n 即得到 平均值。可以使用已有的流中的方法计算该值，那就让我们来试试吧!使用 Intstream.range 得到包含所需元素下标的流。&lt;/p&gt; &lt;p&gt;在➍处使用总和减去窗口起始值，然后再除以 n 得到平均值。最后在➎处将流转换为数组。&lt;/p&gt; &lt;h2&gt;6.8 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;数据并行化是把工作拆分，同时在多核CPU上执行的方式。&lt;/li&gt; &lt;li&gt;如果使用流编写代码，可通过调用parallel或者parallelStream方法实现数据并行化 操作。&lt;/li&gt; &lt;li&gt;影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的CPU核数量，以及处理每个元素所花的时间。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;6.9 练习&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;下面的代码顺序求流中元素的平方和，将其改为并行处理。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;顺序处理&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static int sequentialSumOfSquares(IntStream range) {      return range.map(x -&amp;gt; x * x).sum(); } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;并行处理&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static int parallelSumOfSquares(IntStream range) {         return range.parallel()                 .map(x -&amp;gt; x * x).sum();     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;下面代码把列表中的数字相乘，然后再将所得结果乘以 5。顺序执行这段程序没有问题，但并行执行时有一个缺陷，使用流并行化执行该段代码，并修复缺陷。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;把列表中的数字相乘，然后再将所得结果乘以 5，该实现有一个缺陷&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static int multiplyThrough(List&amp;lt;Integer&amp;gt; linkedListOfNumbers) {      return linkedListOfNumbers.stream()                        .reduce(5, (acc, x) -&amp;gt; x * acc); } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;修复后的代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static int multiplyThrough(List&amp;lt;Integer&amp;gt; linkedListOfNumbers) {         return linkedListOfNumbers.parallelStream()                 .reduce(1, (acc, x) -&amp;gt; x * acc) * 5;     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;如下代码计算列表中数字的平方和。尝试改进代码性能，但不得牺牲代码质量。 只需要一些简单的改动即可。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;求列表元素的平方和，该实现方式性能不高&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public int slowSumOfSquares() {     return linkedListOfNumbers.parallelStream()                                 .map(x-&amp;gt;x*x)                                 .reduce(0, (acc, x) -&amp;gt; acc + x); } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;修改后&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public int slowSumOfSquares() {     return linkedListOfNumbers.parallelStream()                                 .map(x-&amp;gt;x*x)                                 .sum(); } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 08 Nov 2018 12:58:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇四之高级集合类和收集器</title>
      <link>https://www.zhangaoo.com/article/java-lambda-collector</link>
      <content:encoded>&lt;h1&gt;第5章 高级集合类和收集器&lt;/h1&gt; &lt;p&gt;第3章只介绍了集合类的部分变化，事实上，&lt;code&gt;Java 8&lt;/code&gt; 对集合类的改进不止这些。现在是时 候介绍一些高级主题了，包括新引入的 &lt;code&gt;Collector&lt;/code&gt; 类。同时我还会为大家介绍方法引用，它可以帮助大家在 &lt;code&gt;Lambda&lt;/code&gt; 表达式中轻松使用已有代码。编写大量使用集合类的代码时， 使用方法引用能让程序员获得丰厚的回报。本章还会涉及集合类的一些更高级的主题，比 如流中元素的顺序，以及一些有用的 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;5.1 方法引用&lt;/h2&gt; &lt;p&gt;读者可能已经发现，&lt;code&gt;Lambda&lt;/code&gt; 表达式有一个常见的用法:&lt;code&gt;Lambda&lt;/code&gt; 表达式经常调用参数。比如想得到艺术家的姓名，&lt;code&gt;Lambda&lt;/code&gt; 的表达式如下:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    artist -&amp;gt; artist.getName() &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这种用法如此普遍，因此 &lt;code&gt;Java 8&lt;/code&gt; 为其提供了一个简写语法，叫作方法引用，帮助程序员重用已有方法。用方法引用重写上面的 &lt;code&gt;Lambda&lt;/code&gt; 表达式，代码如下:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Artist::getName &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;标准语法为 &lt;code&gt;Classname::methodName&lt;/code&gt;。需要注意的是，虽然这是一个方法，但不需要在后面加括号，因为这里并不调用该方法。我们只是提供了和 &lt;code&gt;Lambda&lt;/code&gt; 表达式等价的一种结构， 在需要时才会调用。凡是使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的地方，就可以使用方法引用。&lt;/p&gt; &lt;p&gt;构造函数也有同样的缩写形式，如果你想使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式创建一个 &lt;code&gt;Artist&lt;/code&gt; 对象，可能 会写出如下代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    (name, nationality) -&amp;gt; new Artist(name, nationality) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用方法引用，上述代码可写为:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Artist::new &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这段代码不仅比原来的代码短，而且更易阅读。&lt;code&gt;Artist::new&lt;/code&gt; 立刻告诉程序员这是在创建 一个 &lt;code&gt;Artist&lt;/code&gt; 对象，程序员无需看完整行代码就能弄明白代码的意图。另一个要注意的地方是方法引用自动支持多个参数，前提是选对了正确的函数接口。&lt;/p&gt; &lt;p&gt;还可以用这种方式创建数组，下面的代码创建了一个字符串型的数组:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    String[]::new &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从现在开始，我们将在合适的地方使用方法引用，因此读者很快会看到更多的例子。一开始探索 &lt;code&gt;Java 8&lt;/code&gt; 时，有位朋友告诉我，方法引用看起来“就像在作弊”。他的意思是说，了解如何使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式让代码像数据一样在对象间传递之后，这种直接引用方法的方式就像“作弊”。&lt;/p&gt; &lt;p&gt;放心，这不是在作弊。读者只要记住，每次写出形如 &lt;code&gt;x -&amp;gt; a.foo(x)&lt;/code&gt;的&lt;code&gt;Lambda&lt;/code&gt;表达式时， 和直接调用方法 &lt;code&gt;foo&lt;/code&gt; 是一样的。方法引用只不过是基于这样的事实，提供了一种简短的语法而已。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;x -&amp;gt; a.foo(x) &amp;lt;==&amp;gt; A::foo &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;5.2 元素顺序&lt;/h2&gt; &lt;p&gt;另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。读者可能知道，一 些集合类型中的元素是按顺序排列的，比如 &lt;code&gt;List&lt;/code&gt;;而另一些则是无序的，比如 &lt;code&gt;HashSet&lt;/code&gt;。 增加了流操作后，顺序问题变得更加复杂。&lt;/p&gt; &lt;p&gt;直观上看，流是有序的，因为流中的元素都是按顺序处理的。这种顺序称为出现顺序。出现顺序的定义依赖于数据源和对流的操作。&lt;/p&gt; &lt;p&gt;在一个有序集合中创建一个流时，流中的元素就按出现顺序排列，因此，如下代码总是可以通过。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;顺序测试永远通过&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testStreamSort(){         List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1,2,3,4);         List&amp;lt;Integer&amp;gt; sameOrder = numbers.stream()                                             .collect(Collectors.toList());         assertEquals(numbers,sameOrder);      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果集合本身就是无序的，由此生成的流也是无序的。&lt;code&gt;HashSet&lt;/code&gt; 就是一种无序的集合，因此不能保证如下所示的程序每次都通过。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testHashSetOrder(){         Set&amp;lt;Integer&amp;gt; numbers = new HashSet&amp;lt;&amp;gt;(Arrays.asList(4,3,2,1));         List&amp;lt;Integer&amp;gt; sameOrder = numbers.stream()                                             .collect(Collectors.toList());         assertEquals(Arrays.asList(4,3,2,1), sameOrder);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;流的目的不仅是在集合类之间做转换，而且同时提供了一组处理数据的通用操作。有些集合本身是无序的，但这些操作有时会产生顺序，试看如下代码。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testStreamSort1(){         Set&amp;lt;Integer&amp;gt; numbers = new HashSet&amp;lt;&amp;gt;(Arrays.asList(4,3,2,1));         List&amp;lt;Integer&amp;gt; stillOrdered = numbers.stream()                                             .sorted()                                             .collect(Collectors.toList());         assertEquals(Arrays.asList(1,2,3,4),stillOrdered);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一些中间操作会产生顺序，比如对值做映射时，映射后的值是有序的，这种顺序就会保留下来。如果进来的流是无序的，出去的流也是无序的。看如下代码，我们只能断言&lt;code&gt;HashSet&lt;/code&gt;中含有某元素，但对其顺序不能作出任何假设，因为 &lt;code&gt;HashSet&lt;/code&gt; 是无序的，使用了映射操作后，得到的集合仍然是无序的。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testStreamMapSort(){         List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1,2,3,4);         List&amp;lt;Integer&amp;gt; stillOrdered = numbers.stream()                                             .map(x-&amp;gt;x+1)                                             .collect(Collectors.toList());         //顺序得到了保留         assertEquals(Arrays.asList(2, 3, 4, 5), stillOrdered);         Set&amp;lt;Integer&amp;gt; unordered = new HashSet&amp;lt;&amp;gt;(numbers);         List&amp;lt;Integer&amp;gt; stillUnordered = unordered.stream()                                                 .map(x-&amp;gt;x+1)                                                 .collect(Collectors.toList());         // 顺序得不到保证         assertThat(stillUnordered, hasItem(2));         assertThat(stillUnordered, hasItem(3));         assertThat(stillUnordered, hasItem(4));         assertThat(stillUnordered, hasItem(5));      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一些操作在有序的流上开销更大，调用 &lt;code&gt;unordered&lt;/code&gt; 方法消除这种顺序就能解决该问题。大多数操作都是在有序流上效率更高，比如 &lt;code&gt;filter、map&lt;/code&gt; 和 &lt;code&gt;reduce&lt;/code&gt; 等。&lt;/p&gt; &lt;p&gt;这会带来一些意想不到的结果，比如使用并行流时，&lt;code&gt;forEach&lt;/code&gt; 方法不能保证元素是 按顺序处理的(第 6 章会详细讨论这些内容)。如果需要保证按顺序处理，应该使用 &lt;code&gt;forEachOrdered&lt;/code&gt; 方法，它是你的朋友。&lt;/p&gt; &lt;h2&gt;5.3 使用收集器&lt;/h2&gt; &lt;p&gt;前面我们使用过 &lt;code&gt;collect(toList())&lt;/code&gt;，在流中生成列表。显然，&lt;code&gt;List&lt;/code&gt; 是能想到的从流中生成的最自然的数据结构，但是有时人们还希望从流生成其他值，比如 &lt;code&gt;Map&lt;/code&gt; 或 &lt;code&gt;Set&lt;/code&gt;，或者你希望定制一个类将你想要的东西抽象出来。&lt;/p&gt; &lt;p&gt;前面已经讲过，仅凭流上方法的签名，就能判断出这是否是一个及早求值的操作。&lt;code&gt;reduce&lt;/code&gt; 操作就是一个很好的例子，但有时人们希望能做得更多。&lt;/p&gt; &lt;p&gt;这就是收集器，一种通用的、从流生成复杂值的结构。只要将它传给 &lt;code&gt;collect&lt;/code&gt; 方法，所有 的流就都可以使用它了。&lt;/p&gt; &lt;p&gt;标准类库已经提供了一些有用的收集器，让我们先来看看。本章示例代码中的收集器都是从 &lt;code&gt;java.util.stream.Collectors&lt;/code&gt; 类中静态导入的。&lt;/p&gt; &lt;h3&gt;5.3.1 转换成其他集合&lt;/h3&gt; &lt;p&gt;有一些收集器可以生成其他集合。比如前面已经见过的 &lt;code&gt;toList&lt;/code&gt;，生成了 &lt;code&gt;java.util.List&lt;/code&gt; 类 的实例。还有 &lt;code&gt;toSet&lt;/code&gt; 和 &lt;code&gt;toCollection&lt;/code&gt;，分别生成 &lt;code&gt;Set&lt;/code&gt; 和 &lt;code&gt;Collection&lt;/code&gt; 类的实例。到目前为止， 我已经讲了很多流上的链式操作，但总有一些时候，需要最终生成一个集合——比如：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;已有代码是为集合编写的，因此需要将流转换成集合传入;&lt;/li&gt; &lt;li&gt;在集合上进行一系列链式操作后，最终希望生成一个值;&lt;/li&gt; &lt;li&gt;写单元测试时，需要对某个具体的集合做断言。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;通常情况下，创建集合时需要调用适当的构造函数指明集合的具体类型:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Artist&amp;gt; artists = new ArrayList&amp;lt;&amp;gt;(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是调用 &lt;code&gt;toList&lt;/code&gt; 或者 &lt;code&gt;toSet&lt;/code&gt; 方法时，不需要指定具体的类型。&lt;code&gt;Stream&lt;/code&gt; 类库在背后自动为你挑选出了合适的类型。本书后面会讲述如何使用 &lt;code&gt;Stream&lt;/code&gt; 类库并行处理数据，收集并行操作的结果需要的 &lt;code&gt;Set&lt;/code&gt;，和对线程安全没有要求的 &lt;code&gt;Set&lt;/code&gt; 类是完全不同的。&lt;/p&gt; &lt;p&gt;可能还会有这样的情况，你希望使用一个特定的集合收集值，而且你可以稍后指定该集合的类型。比如，你可能希望使用 &lt;code&gt;TreeSet&lt;/code&gt;，而不是由框架在背后自动为你指定一种类型的 &lt;code&gt;Set&lt;/code&gt;。此时就可以使用 &lt;code&gt;toCollection&lt;/code&gt;，它接受一个函数作为参数，来创建集合如下例子所示：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 toCollection，用定制的集合收集元素&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Integer&amp;gt; numbers =  Arrays.asList(1,2,3,4,1);     TreeSet&amp;lt;Integer&amp;gt; treeSet = numbers.stream()                                         .collect(Collectors.toCollection(TreeSet::new));     assertEquals(treeSet.size(),numbers.size() - 1); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;5.3.2 转换成值&lt;/h3&gt; &lt;p&gt;还可以利用收集器让流生成一个值。&lt;code&gt;maxBy&lt;/code&gt; 和 &lt;code&gt;minBy&lt;/code&gt; 允许用户按某种特定的顺序生成一个值。如下代码展示了如何找出成员最多的乐队。它使用一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，将艺术家映射为成员数量，然后定义了一个比较器，并将比较器传入 &lt;code&gt;maxBy&lt;/code&gt; 收集器。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;找出成员最多的乐队&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public Optional&amp;lt;Artist&amp;gt; biggestGroup(Stream&amp;lt;Artist&amp;gt; artists){         Function&amp;lt;Artist,Long&amp;gt; getCount = artist -&amp;gt; artist.getMembers().count();         return artists.collect(Collectors.maxBy(comparing(getCount)));     }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;minBy&lt;/code&gt; 就如它的方法名，是用来找出最小值的。&lt;/p&gt; &lt;p&gt;还有些收集器实现了一些常用的数值运算。让我们通过一个计算专辑曲目平均数的例子来 看看&lt;/p&gt; &lt;ul&gt; &lt;li&gt;找出一组专辑上曲目的平均数&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public double averageNumberOfTracks(List&amp;lt;Album&amp;gt; albums) { return albums.stream()             .collect(averagingInt(album -&amp;gt; album.getTracks().size()));     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和以前一样，通过调用&lt;code&gt;stream&lt;/code&gt;方法让集合生成流，然后调用&lt;code&gt;collect&lt;/code&gt;方法收集结果。 &lt;code&gt;averagingInt&lt;/code&gt; 方法接受一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式作参数，将流中的元素转换成一个整数，然后再计算 平均数。还有和 &lt;code&gt;double&lt;/code&gt; 和 &lt;code&gt;long&lt;/code&gt; 类型对应的重载方法，帮助程序员将元素转换成相应类型的值。&lt;/p&gt; &lt;p&gt;第 4 章介绍过一些特殊的流，如 &lt;code&gt;IntStream&lt;/code&gt;，为数值运算定义了一些额外的方法。事实上， &lt;code&gt;Java 8&lt;/code&gt;也提供了能完成类似功能的收集器，如&lt;code&gt;averagingInt&lt;/code&gt;。可以使用&lt;code&gt;summingInt&lt;/code&gt;及其重载方法求和。&lt;code&gt;SummaryStatistics&lt;/code&gt; 也可以使用 &lt;code&gt;summingInt&lt;/code&gt; 及其组合收集&lt;/p&gt; &lt;h3&gt;5.3.3 数据分块&lt;/h3&gt; &lt;p&gt;另外一个常用的流操作是将其分解成两个集合。假设有一个艺术家组成的流，你可能希望将其分成两个部分，一部分是独唱歌手，另一部分是由多人组成的乐队。可以使用两次过滤操作，分别过滤出上述两种艺术家。&lt;/p&gt; &lt;p&gt;但是这样操作起来有问题。首先，为了执行两次过滤操作，需要有两个流。其次，如果过 滤操作复杂，每个流上都要执行这样的操作，代码也会变得冗余。&lt;/p&gt; &lt;p&gt;幸好我们有这样一个收集器 &lt;code&gt;partitioningBy&lt;/code&gt;，它接受一个流，并将其分成两部分它使用 &lt;code&gt;Predicate&lt;/code&gt; 对象判断一个元素应该属于哪个部分，并根据布尔值返回一个&lt;code&gt;Map&lt;/code&gt;到列表。因此，对于&lt;code&gt;true List&lt;/code&gt;中的元素，&lt;code&gt;Predicat&lt;/code&gt;e返回&lt;code&gt;true&lt;/code&gt;;对其他&lt;code&gt;List&lt;/code&gt;中的 元素，&lt;code&gt;Predicate&lt;/code&gt; 返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;使用它，我们就可以将乐队(有多个成员)和独唱歌手分开了。在本例中，分块函数指明艺术家是否为独唱歌手。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;将艺术家组成的流分成乐队和独唱歌手两部分&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public Map&amp;lt;Boolean, List&amp;lt;Artist&amp;gt;&amp;gt; bandsAndSolo(Stream&amp;lt;Artist&amp;gt; artists) {         return artists.collect(partitioningBy(artist -&amp;gt; artist.isSolo()));     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;也可以使用方法引用代替 Lambda 表达式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public Map&amp;lt;Boolean, List&amp;lt;Artist&amp;gt;&amp;gt; bandsAndSolo(Stream&amp;lt;Artist&amp;gt; artists) {         return artists.collect(partitioningBy(Artist::isSolo);     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;5.3.4 数据分组&lt;/h3&gt; &lt;p&gt;数据分组是一种更自然的分割数据操作，与将数据分成 &lt;code&gt;ture&lt;/code&gt; 和 &lt;code&gt;false&lt;/code&gt; 两部分不同，可以使 用任意值对数据分组。比如现在有一个由专辑组成的流，可以按专辑当中的主唱对专辑分组。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;唱对专辑分组&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public Map&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; albumsByArtist(Stream&amp;lt;Album&amp;gt; albums) {         return albums.collect(groupingBy(album -&amp;gt; album.getMainMusician()));     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;和其他例子一样，调用流的 &lt;code&gt;collect&lt;/code&gt; 方法，传入一个收集器。&lt;code&gt;groupingBy&lt;/code&gt; 收集器(如图 5-2 所示)接受一个分类函数，用来对数据分组，就像 &lt;code&gt;partitioningBy&lt;/code&gt; 一样，接受一个 &lt;code&gt;Predicate&lt;/code&gt; 对象将数据分成 &lt;code&gt;ture&lt;/code&gt; 和 &lt;code&gt;false&lt;/code&gt; 两部分。我们使用的分类器是一个 &lt;code&gt;Function&lt;/code&gt; 对 象，和 &lt;code&gt;map&lt;/code&gt; 操作用到的一样。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;读者可能知道SQL中的group by操作，我们的方法是和这类似的一个概念，只不过在 Stream 类库中实现了而已。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;5.3.5 字符串&lt;/h3&gt; &lt;p&gt;很多时候，收集流中的数据都是为了在最后生成一个字符串。假设我们想将参与制作一张专辑的所有艺术家的名字输出为一个格式化好的列表，以专辑  &lt;code&gt;Let It Be&lt;/code&gt; 为例，期望的输出 为:&amp;quot;[George Harrison, John Lennon, Paul McCartney, Ringo Starr, The Beatles]&amp;quot;。&lt;/p&gt; &lt;p&gt;在 &lt;code&gt;Java 8&lt;/code&gt; 还未发布前，实现该功能的代码可能如下代码所示。通过不断迭代列表，使用一 个  &lt;code&gt;StringBuilder&lt;/code&gt; 对象来记录结果。每一步都取出一个艺术家的名字，追加到 &lt;code&gt;StringBuilder&lt;/code&gt; 对象。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testAppend(){         List&amp;lt;Artist&amp;gt; artists = new ArrayList&amp;lt;&amp;gt;();         StringBuilder builder = new StringBuilder(&amp;quot;[&amp;quot;);         for (Artist artist : artists) {             if (builder.length() &amp;gt; 1) {                 builder.append(&amp;quot;, &amp;quot;);             }             String name = artist.getName();             builder.append(name);         }         builder.append(&amp;quot;]&amp;quot;);         String result = builder.toString();     }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;显然，这段代码不是非常好。如果不一步步跟踪，很难看出这段代码是干什么的。使用 &lt;code&gt;Java 8&lt;/code&gt; 提供的流和收集器就能写出更清晰的代码，如下代码所示。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testStreamJoin(){         List&amp;lt;Artist&amp;gt; artists = new ArrayList&amp;lt;&amp;gt;();         String result = artists.stream()                 .map(Artist::getName)                 .collect(Collectors.joining(&amp;quot;,&amp;quot;, &amp;quot;[&amp;quot;, &amp;quot;]&amp;quot;));              } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里使用 &lt;code&gt;map&lt;/code&gt; 操作提取出艺术家的姓名，然后使用 &lt;code&gt;Collectors.joining&lt;/code&gt; 收集流中的值，该方法 可以方便地从一个流得到一个字符串，允许用户提供分隔符(用以分隔元素)、前缀和后缀。&lt;/p&gt; &lt;h3&gt;5.3.6 组合收集器&lt;/h3&gt; &lt;p&gt;虽然读者现在看到的各种收集器已经很强大了，但如果将它们组合起来，会变得更强大。&lt;/p&gt; &lt;p&gt;之前我们使用主唱将专辑分组，现在来考虑如何计算一个艺术家的专辑数量。一个简单的方案是使用前面的方法对专辑先分组后计数，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;计算每个艺术家专辑数的简单方式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Map&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; albumsByArtist          = albums.collect(groupingBy(album -&amp;gt; album.getMainMusician())); Map&amp;lt;Artist, Integer&amp;gt; numberOfAlbums = new HashMap&amp;lt;&amp;gt;();  for(Entry&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; entry : albumsByArtist.entrySet()) {          numberOfAlbums.put(entry.getKey(), entry.getValue().size());      }          &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这种方式看起来简单，但却有点杂乱无章。这段代码也是命令式的代码，不能自动适应并 行化操作。&lt;/p&gt; &lt;p&gt;这里实际上需要另外一个收集器，告诉 &lt;code&gt;groupingBy&lt;/code&gt; 不用为每一个艺术家生成一个专辑列表，只需要对专辑计数就可以了。幸好，核心类库已经提供了一个这样的收集器: &lt;code&gt;counting&lt;/code&gt;。使用它，可将上述代码重写为如下代码的样子。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Map&amp;lt;Artist,Long&amp;gt; numberOfAlbums(Stream&amp;lt;Album&amp;gt; albums){         return albums.collect(groupingBy(album -&amp;gt; album.getMainMusician(),counting()));     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;groupingBy&lt;/code&gt; 先将元素分成块，每块都与分类函数 &lt;code&gt;getMainMusician&lt;/code&gt; 提供的键值相关联，然后使用下游的另一个收集器收集每块中的元素，最好将结果映射为一个 &lt;code&gt;Map&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;让我们再看一个例子，这次我们不想生成一组专辑，只希望得到专辑名。这个问题仍然可以用前面的方法解决，先将专辑分组，然后再调整生成的 Map 中的值&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用简单方式求每个艺术家的专辑名&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Map&amp;lt;Artist, List&amp;lt;String&amp;gt;&amp;gt; nameOfAlbumsDumb(Stream&amp;lt;Album&amp;gt; albums) {         Map&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; albumsByArtist =                 albums.collect(groupingBy(album -&amp;gt; album.getMainMusician()));         Map&amp;lt;Artist, List&amp;lt;String&amp;gt;&amp;gt; nameOfAlbums = new HashMap&amp;lt;&amp;gt;();         for (Map.Entry&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; entry : albumsByArtist.entrySet()) {             nameOfAlbums.put(entry.getKey(), entry.getValue()                     .stream()                     .map(Album::getName).collect(toList()));         }         return nameOfAlbums;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;同理，我们可以再使用一个收集器，编写出更好、更快、更容易并行处理的代码。我们已经知道，可以使用 &lt;code&gt;groupingBy&lt;/code&gt; 将专辑按主唱分组，但是其输出为一个 &lt;code&gt;Map&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt;&lt;/code&gt; 对象，它将每个艺术家和他的专辑列表关联起来，但这不是我们想要的，我们想要的是一个包含专辑名的字符串列表。&lt;/p&gt; &lt;p&gt;此时，我们真正想做的是将专辑列表映射为专辑名列表，这里不能直接使用流的 &lt;code&gt;map&lt;/code&gt; 操 作，因为列表是由 &lt;code&gt;groupingBy&lt;/code&gt; 生成的。我们需要有一种方法，可以告诉 &lt;code&gt;groupingBy&lt;/code&gt; 将它 的值做映射，生成最终结果。&lt;/p&gt; &lt;p&gt;每个收集器都是生成最终值的一剂良方。这里需要两剂配方，一个传给另一个。谢天谢 地，&lt;code&gt;Oracle&lt;/code&gt; 公司的研究员们已经考虑到这种情况，为我们提供了 &lt;code&gt;mapping&lt;/code&gt; 收集器。&lt;/p&gt; &lt;p&gt;&lt;code&gt;mapping&lt;/code&gt; 允许在收集器的容器上执行类似 &lt;code&gt;map&lt;/code&gt; 的操作。但是需要指明使用什么样的集合类 存储结果，比如 &lt;code&gt;toList&lt;/code&gt;。这些收集器就像乌龟叠罗汉，龟龟相驮以至无穷。&lt;/p&gt; &lt;p&gt;&lt;code&gt;mapping&lt;/code&gt; 收集器和 &lt;code&gt;map&lt;/code&gt; 方法一样，接受一个 &lt;code&gt;Function&lt;/code&gt; 对象作为参数，经过重构后的如下代码所示。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Map&amp;lt;Artist, List&amp;lt;String&amp;gt;&amp;gt; nameOfAlbumsDumb1(Stream&amp;lt;Album&amp;gt; albums) {         Map&amp;lt;Artist, List&amp;lt;String&amp;gt;&amp;gt; nameOfAlbums =                 albums.collect(groupingBy(Album::getMainMusician, mapping(Album::getName, toList())));         return nameOfAlbums;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这两个例子中我们都用到了第二个收集器，用以收集最终结果的一个子集。这些收集器叫作下游收集器。收集器是生成最终结果的一剂配方，下游收集器则是生成部分结果的配方，主收集器中会用到下游收集器。这种组合使用收集器的方式，使得它们在 &lt;code&gt;Stream&lt;/code&gt; 类库 中的作用更加强大。&lt;/p&gt; &lt;p&gt;那些为基本类型特殊定制的函数，如 &lt;code&gt;averagingInt&lt;/code&gt;、&lt;code&gt;summarizingLong&lt;/code&gt; 等，事实上和调用特殊 &lt;code&gt;Stream&lt;/code&gt; 上的方法是等价的，加上它们是为了将它们当作下游收集器来使用的。&lt;/p&gt; &lt;h3&gt;5.3.7 重构和定制收集器&lt;/h3&gt; &lt;p&gt;尽管在常用流操作里，&lt;code&gt;Java&lt;/code&gt; 内置的收集器已经相当好用，但收集器框架本身是极其通用的。&lt;code&gt;JDK&lt;/code&gt; 提供的收集器没有什么特别的，完全可以定制自己的收集器，而且定制起来相当简单，这就是本节要讲的内容。&lt;/p&gt; &lt;p&gt;读者可能还没忘记在之前的代码中，如何使用 &lt;code&gt;Java 7&lt;/code&gt; 连接字符串，尽管形式并不优雅。让我们逐步重构这段代码，最终用合适的收集器实现原有代码功能。在工作中没有必要这样做， &lt;code&gt;JDK&lt;/code&gt; 已经提供了一个完美的收集器 &lt;code&gt;joining&lt;/code&gt;。这里只是为了展示如何定制收集器，以及如何使用 &lt;code&gt;Java 8&lt;/code&gt; 提供的新功能来重构遗留代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;for&lt;/code&gt; 循环和 &lt;code&gt;StringBuilder&lt;/code&gt; 格式化艺术家姓名&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Artist&amp;gt; artists = new ArrayList&amp;lt;&amp;gt;();     StringBuilder builder = new StringBuilder(&amp;quot;[&amp;quot;);     for (Artist artist : artists) {         if (builder.length() &amp;gt; 1) {             builder.append(&amp;quot;, &amp;quot;);         }         String name = artist.getName();         builder.append(name);     }     builder.append(&amp;quot;]&amp;quot;);     String result = builder.toString(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;显然，可以使用 &lt;code&gt;map&lt;/code&gt; 操作，将包含艺术家的流映射为包含艺术家姓名的流。如下代码展示了 使用了流的 &lt;code&gt;map&lt;/code&gt; 操作重构后的代码。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Artist&amp;gt; artists = new ArrayList&amp;lt;&amp;gt;();     StringBuilder builder = new StringBuilder(&amp;quot;[&amp;quot;);     artists.stream().map(Artist::getName).forEach(name -&amp;gt; {         if (builder.length() &amp;gt; 1)             builder.append(&amp;quot;, &amp;quot;);         builder.append(name);     });     builder.append(&amp;quot;]&amp;quot;);     String result = builder.toString(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;将艺术家映射为姓名，就能更快看出最终是要生成什么，这样代码看起来更清楚一点。可惜 &lt;code&gt;forEach&lt;/code&gt; 方法看起来还是有点笨重，这与我们通过组合高级操作让代码变得易读的目标不符。&lt;/p&gt; &lt;p&gt;暂且不必考虑定制一个收集器，让我们想想怎么通过流上已有的操作来解决该问题。和生 成字符串目标最近的操作就是 &lt;code&gt;reduce&lt;/code&gt;，使用它将如下的代码重构如下。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 reduce 和 StringBuilder 格式化艺术家姓名&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    StringBuilder reduced =             artists.stream()                     .map(Artist::getName)                     .reduce(new StringBuilder(), (builder1, name) -&amp;gt; {                         if (builder1.length() &amp;gt; 0) {                             builder1.append(&amp;quot;, &amp;quot;);                         }                         builder1.append(name);                         return builder1;                     }, (left, right) -&amp;gt; left.append(right));     reduced.insert(0, &amp;quot;[&amp;quot;);     reduced.append(&amp;quot;]&amp;quot;);     String result = reduced.toString(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;这里比较疑惑的是第三个参数(left, right) -&amp;gt; left.append(right)，这里的left和right是指什么参数？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;上面👆的问题，经过调查&lt;a href="https://www.cnblogs.com/h9527/p/5685439.html" target="_blank"&gt;调查&lt;/a&gt;，发现在普通 &lt;code&gt;stream&lt;/code&gt; 中第三个 &lt;code&gt;lambda combiner&lt;/code&gt;并不会执行，而在 &lt;code&gt;parallelStream&lt;/code&gt;中会执行，如下解释：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Executing this stream in parallel results in an entirely different execution behavior. Now the combiner is actually called. Since the accumulator is called in parallel, the combiner is needed to sum up the separate accumulated values.&lt;/p&gt; &lt;p&gt;通过并行的方式执行上面的 stream 操作，得到的是另外一种完全不相同的执行动作。在并行 stream 中 combiner 方法会被调用。这是由于累加器是被并行调用的，因此组合器需要对分开的累加操作进行求和。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;我曾经天真地以为上面的重构会让代码变得更清晰，可惜恰好相反，代码看起来比以 前更糟糕。让我们先来看看怎么回事。和前面的例子一样，都调用了 &lt;code&gt;stream&lt;/code&gt; 和 &lt;code&gt;map&lt;/code&gt; 方 法，&lt;code&gt;reduce&lt;/code&gt; 操作生成艺术家姓名列表，艺术家与艺术家之间用“,”分隔。首先创建一 个 &lt;code&gt;StringBuilder&lt;/code&gt; 对象，该对象是 &lt;code&gt;reduce&lt;/code&gt; 操作的初始状态，然后使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式将姓名连接到 &lt;code&gt;builder&lt;/code&gt; 上。&lt;code&gt;reduce&lt;/code&gt; 操作的第三个参数也是一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，接受两个 &lt;code&gt;StringBuilder&lt;/code&gt; 对象做参数，将两者连接起来。最后添加前缀和后缀。&lt;/p&gt; &lt;p&gt;在接下来的重构中，我们还是使用 &lt;code&gt;reduc&lt;/code&gt;e 操作，不过需要将杂乱无章的代码隐藏掉——我 的意思是使用一个 &lt;code&gt;StringCombiner&lt;/code&gt; 类对细节进行抽象。代码如下所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;reduce&lt;/code&gt; 和 &lt;code&gt;StringCombiner&lt;/code&gt; 类格式化艺术家姓名&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public  void testStringCombiner(){         List&amp;lt;Artist&amp;gt; artists = Arrays.asList(new Artist(&amp;quot;Zhangsan&amp;quot;,&amp;quot;China&amp;quot;),new Artist(&amp;quot;Lisi&amp;quot;,&amp;quot;China&amp;quot;));         StringCombiner combiner = artists.stream()                 .map(Artist::getName)                 .reduce(new StringCombiner(&amp;quot;,&amp;quot;,&amp;quot;[&amp;quot;,&amp;quot;]&amp;quot;),                         StringCombiner::add,                         StringCombiner::merge);         String result = combiner.toString();     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;尽管代码看起来和上个例子大相径庭，其实背后做的工作是一样的。我们使用 &lt;code&gt;reduce&lt;/code&gt; 操作将姓名和分隔符连接成一个 &lt;code&gt;StringBuilder&lt;/code&gt; 对象。不过这次连接姓名操作被代理到了 &lt;code&gt;StringCombiner.add&lt;/code&gt; 方法，而连接两个连接器操作被 &lt;code&gt;StringCombiner.merge&lt;/code&gt; 方法代理。让我们现在来看看这些方法，先 从下面中的 &lt;code&gt;add&lt;/code&gt; 方法开始。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;add&lt;/code&gt; 方法返回连接新元素后的结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public StringCombiner add(String element){         if(areAtStart()){             this.builder.append(this.prefix);         } else {             this.builder.append(this.delim);         }         builder.append(element);         return this;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;add&lt;/code&gt; 方法在内部其实将操作代理给一个 &lt;code&gt;StringBuilder&lt;/code&gt; 对象。如果刚开始进行连接，则在最 前面添加前缀，否则添加分隔符，然后再添加新的元素。这里返回一个 &lt;code&gt;StringCombiner&lt;/code&gt; 对 象，因为这是传给 &lt;code&gt;reduce&lt;/code&gt; 操作所需要的类型。合并代码也是同样的道理，内部将操作代理给 &lt;code&gt;StringBuilder&lt;/code&gt; 对象，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;merge&lt;/code&gt; 方法连接两个 &lt;code&gt;StringCombiner&lt;/code&gt; 对象&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public StringCombiner merge(StringCombiner other){         this.builder.append(other.builder);         return this;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; 阶段的重构还差一小步就差不多结束了。我们要在最后调用 &lt;code&gt;toString&lt;/code&gt; 方法，将整个步骤串成一个方法链。这很简单，只需要排列好 &lt;code&gt;reduce&lt;/code&gt; 代码，准备好将其转换为 &lt;code&gt;Collector API&lt;/code&gt; 就行了&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 reduce 操作，将工作代理给 StringCombiner 对象&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Artist&amp;gt; artists = Arrays.asList(new Artist(&amp;quot;Zhangsan&amp;quot;,&amp;quot;China&amp;quot;),new Artist(&amp;quot;Lisi&amp;quot;,&amp;quot;China&amp;quot;));     String result = artists.stream()                 .map(Artist::getName)                 .reduce(new StringCombiner(&amp;quot;,&amp;quot;,&amp;quot;[&amp;quot;,&amp;quot;]&amp;quot;),                         StringCombiner::add,                         StringCombiner::merge).toString(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：实测发现 使用 stream() 的确不会走 merge 方法， 解释同上&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;现在的代码看起来已经差不多完美了，但是在程序中还是不能重用。因此，我们想将 &lt;code&gt;reduce&lt;/code&gt; 操作重构为一个收集器，在程序中的任何地方都能使用。不妨将这个收集器叫作 &lt;code&gt;StringCollector&lt;/code&gt;，让我们重构代码使用这个新的收集器，如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用定制的收集器 &lt;code&gt;StringCollector&lt;/code&gt; 收集字符串&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    String result =         artists.stream()             .map(Artist::getName)             .collect(new StringCollector(&amp;quot;, &amp;quot;, &amp;quot;[&amp;quot;, &amp;quot;]&amp;quot;)); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;既然已经将所有对字符串的连接操作代理给了定制的收集器，应用程序就不需要关心 &lt;code&gt;StringCollector&lt;/code&gt; 对象的任何内部细节，它和框架中其他 &lt;code&gt;Collector&lt;/code&gt; 对象用起来是一样的。&lt;/p&gt; &lt;p&gt;先来实现 &lt;code&gt;Collector&lt;/code&gt; 接口如下代码，由于 &lt;code&gt;Collector&lt;/code&gt; 接口支持泛型，因此先得确定一些具体的类型:&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;待收集元素的类型，这里是 &lt;code&gt;String&lt;/code&gt;;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;累加器的类型 &lt;code&gt;StringCombiner&lt;/code&gt;;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;最终结果的类型，这里依然是 &lt;code&gt;String&lt;/code&gt;;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;定义字符串收集器&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class StringCollector implements Collector&amp;lt;String,StringCombiner,String&amp;gt; {} &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一个收集器由四部分组成。首先是一个 &lt;code&gt;Supplier&lt;/code&gt;，这是一个工厂方法，用来创建容器，在这个例子中，就是 &lt;code&gt;StringCombiner&lt;/code&gt;。和 &lt;code&gt;reduce&lt;/code&gt; 操作中的第一个参数类似，它是后续操作的初值，如下代码所示：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Supplier&lt;/code&gt; 是创建容器的工厂&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     public Supplier&amp;lt;StringCombiner&amp;gt; supplier() {         return () -&amp;gt; new StringCombiner(delim, prefix, suffix);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;让我们一边阅读代码，一边看图，这样就能看清到底是怎么工作的。由于收集器可以并行收集，我们要展示的收集操作在两个容器上(比如 &lt;code&gt;StringCombiners&lt;/code&gt; )并行进行。&lt;code&gt;StringCombiner&lt;/code&gt; 中的 &lt;code&gt;merge&lt;/code&gt; 方法就是为了处理流并行的情况。&lt;/p&gt; &lt;p&gt;收集器的每一个组件都是函数，因此我们使用箭头表示，流中的值用圆圈表示，最终生成的值用椭圆表示。收集操作一开始，&lt;code&gt;Supplier&lt;/code&gt; 先创建出新的容器&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/51jk212sk6j38r2685rduf766v.png" alt="StringCollector" /&gt;&lt;/p&gt; &lt;p&gt;收集器的 &lt;code&gt;accumulator&lt;/code&gt; 的作用和 &lt;code&gt;reduce&lt;/code&gt; 操作的第二个参数一样，它结合之前操作的结果和当前值，生成并返回新的值。这一逻辑已经在 &lt;code&gt;StringCombiner&lt;/code&gt; 的 &lt;code&gt;add&lt;/code&gt; 方法中得以实现， 直接引用就好了&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     public BiConsumer&amp;lt;StringCombiner, String&amp;gt; accumulator() {         return StringCombiner::add;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里的 &lt;code&gt;accumulator&lt;/code&gt; 用来将流中的值叠加入容器中如下图&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/rg19rlag8giour2pb51q8munjq.png" alt="accumulator" /&gt;&lt;/p&gt; &lt;p&gt;&lt;code&gt;combine&lt;/code&gt; 方法很像 &lt;code&gt;reduce&lt;/code&gt; 操作的第三个方法。如果有两个容器，我们需要将其合并。同样，在前面的重构中我们已经实现了该功能，直接使用 &lt;code&gt;StringCombiner.merge&lt;/code&gt; 方法就行了&lt;/p&gt; &lt;ul&gt; &lt;li&gt;combiner 合并两个容器&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     public BinaryOperator&amp;lt;StringCombiner&amp;gt; combiner() {          return StringCombiner::merge;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在收集阶段，容器被 &lt;code&gt;combiner&lt;/code&gt; 方法成对合并进一个容器，直到最后只剩一个容器为止如下图&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/2hmbm9gicqgrjo9b6e6jt9rcbn.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;读者可能还记得，在使用收集器之前，重构的最后一步将 &lt;code&gt;toString&lt;/code&gt; 方法内联到方法链的末端，这就将 &lt;code&gt;StringCombiner&lt;/code&gt; 转换成了我们想要的字符串&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/11/vobmj2tf32gg2on2v7oc870ng8.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;收集器的 &lt;code&gt;finisher&lt;/code&gt; 方法作用相同。我们已经将流中的值叠加入一个可变容器中，但这还不是我们想要的最终结果。这里调用了 &lt;code&gt;finisher&lt;/code&gt; 方法，以便进行转换。在我们想创建字符串等不可变的值时特别有用，这里容器是可变的&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;finisher&lt;/code&gt; 方法返回收集操作的最终结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Override     public Function&amp;lt;StringCombiner, String&amp;gt; finisher() {         return StringCombiner::toString;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从最后剩下的容器中得到最终结果。&lt;/p&gt; &lt;p&gt;关于收集器，还有一点一直没有提及，那就是特征。特征是一组描述收集器的对象，框架 可以对其适当优化。&lt;code&gt;characteristics&lt;/code&gt; 方法定义了特征，实现 &lt;code&gt;Collector&lt;/code&gt; 接口，除了实现上面四个接口还需要实现 &lt;code&gt;Set&amp;lt;Characteristics&amp;gt; characteristics()&lt;/code&gt;  接口。&lt;/p&gt; &lt;p&gt;需要注意上面实现的所有代码只起教学用途，对于拼接字符串的功能系统方法 &lt;code&gt;java.util.StringJoiner&lt;/code&gt; 已经帮我们实现好了。&lt;/p&gt; &lt;p&gt;做这些练习的主要目的不仅在于展示定制收集器的工作原理，而且还在于帮助读者编写自 己的收集器。特别是你有自己特定领域内的类，希望从集合中构建一个操作，而标准的集 合类并没有提供这种操作时，就需要定制自己的收集器。&lt;/p&gt; &lt;p&gt;以 &lt;code&gt;StringCombiner&lt;/code&gt; 为例，收集值的容器和我们想要创建的值(字符串)不一样。如果想要收集的是不可变对象，而不是可变对象，那么这种情况就非常普遍，否则收集操作的每一 步都需要创建一个新值。&lt;/p&gt; &lt;p&gt;想要收集的最终结果和容器一样是完全有可能的。事实上，如果收集的最终结果是集合， 比如 &lt;code&gt;toList&lt;/code&gt; 收集器，就属于这种情况。&lt;/p&gt; &lt;p&gt;此时，&lt;code&gt;finisher&lt;/code&gt; 方法不需要对容器做任何操作。更正式地说，此时的 &lt;code&gt;finisher&lt;/code&gt; 方法其实是 &lt;code&gt;identity&lt;/code&gt; 函数:它返回传入参数的值。如果这样，收集器就展现出 &lt;code&gt;IDENTITY_FINISH&lt;/code&gt; 的特征，需要使用 &lt;code&gt;characteristics&lt;/code&gt; 方法声明。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;小结：读到这里 针对上面的问题: 这里的 left 和 right 是指什么参数？，因为流是可以并行处理的，因此在处理并行 accumulator 结果需要一个 merge 操作。上面的字符串收集器并不能完全处理并行流的情况，因为 merge 操作并未完全考虑并行情况下的 prefix、suffix Merge 操作， 只是简单的 append 操作 。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;5.3.8 对收集器的归一化处理&lt;/h3&gt; &lt;p&gt;就像之前看到的那样，定制收集器其实不难，但如果你想为自己领域内的类定制一个收集器，不妨考虑一下其他替代方案。最容易想到的方案是构建若干个集合对象，作为参数传给领域内类的构造函数。如果领域内的类包含多种集合，这种方式又简单又适用。&lt;/p&gt; &lt;p&gt;当然，如果领域内的类没有这些集合，需要在已有数据上计算，那这种方法就不合适了。 但即使如此，也不见得需要定制一个收集器。你还可以使用 &lt;code&gt;reducing&lt;/code&gt; 收集器，它为流上的归一操作提供了统一实现。如下代码展示了如何使用 &lt;code&gt;reducing&lt;/code&gt; 收集器编写字符串处理程序。&lt;/p&gt; &lt;p&gt;这和上面的例子中讲到的基于 &lt;code&gt;reduce&lt;/code&gt; 操作的实现很像，这点从方法名中就能看出。 区别在于 &lt;code&gt;Collectors.reducing&lt;/code&gt; 的第二个参数，我们为流中每个元素创建了唯一的 &lt;code&gt;StringCombiner&lt;/code&gt;。如果你被这种写法吓到了，或是感到恶心，你不是一个人!这种方式非常低效，这也是我要定制收集器的原因之一。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    String  result =             artists.stream()             .map(Artist::getName)             .collect(Collectors.reducing(                     new StringCombiner(&amp;quot;,&amp;quot;,&amp;quot;[&amp;quot;,&amp;quot;]&amp;quot;),                     name -&amp;gt; new StringCombiner(&amp;quot;, &amp;quot;, &amp;quot;[&amp;quot;, &amp;quot;]&amp;quot;).add(name),                     StringCombiner::merge             )).toString(); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;5.4 一些细节&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式的引入也推动了一些新方法被加入集合类。让我们来看看 &lt;code&gt;Map&lt;/code&gt; 类的一些变化。&lt;/p&gt; &lt;p&gt;构建 &lt;code&gt;Map&lt;/code&gt; 时，为给定值计算键值是常用的操作之一，一个经典的例子就是实现一个缓存。传统的处理方式是先试着从 &lt;code&gt;Map&lt;/code&gt; 中取值，如果没有取到，创建一个新值并返回。&lt;/p&gt; &lt;p&gt;假设使用 &lt;code&gt;Map&amp;lt;String, Artist&amp;gt; artistCache&lt;/code&gt; 定义缓存，我们需要使用费时的数据库操作查询艺术家信息，代码可能如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用显式判断空值的方式缓存&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public Artist getArtist(String name) {      Artist artist = artistCache.get(name);      if (artist == null) {         artist = readArtistFromDB(name);         artistCache.put(name, artist);     } return artist;  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;Java 8&lt;/code&gt; 引入了一个新方法 &lt;code&gt;computeIfAbsent&lt;/code&gt;，该方法接受一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，值不存在时使用该 &lt;code&gt;Lambda&lt;/code&gt; 表达式计算新值。使用该方法，可将上述代码重写为如下所示的形式。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;computeIfAbsent&lt;/code&gt; 缓存&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public Artist getArtist(String name) {     return artistCache.computeIfAbsent(name, this::readArtistFromDB); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;你可能还希望在值不存在时不计算，为 &lt;code&gt;Map&lt;/code&gt; 接口新增的 &lt;code&gt;compute&lt;/code&gt; 和 &lt;code&gt;computeIfAbsent&lt;/code&gt; 就能处理这些情况。&lt;/p&gt; &lt;p&gt;在工作中，你可能尝试过在 &lt;code&gt;Map&lt;/code&gt; 上迭代。过去的做法是使用 &lt;code&gt;value&lt;/code&gt; 方法返回一个值的集合， 然后在集合上迭代。这样的代码不易读。如下代码展示了本章早些时候介绍的一种方式，创建一个 &lt;code&gt;Map&lt;/code&gt;，然后统计每个艺术家专辑的数量。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;一种丑陋的迭代 &lt;code&gt;Map&lt;/code&gt; 的方式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Map&amp;lt;Artist, Integer&amp;gt; countOfAlbums = new HashMap&amp;lt;&amp;gt;();  for(Map.Entry&amp;lt;Artist, List&amp;lt;Album&amp;gt;&amp;gt; entry : albumsByArtist.entrySet()) {     Artist artist = entry.getKey();     List&amp;lt;Album&amp;gt; albums = entry.getValue();     countOfAlbums.put(artist, albums.size()); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;谢天谢地，&lt;code&gt;Java 8&lt;/code&gt; 为 &lt;code&gt;Map&lt;/code&gt; 接口新增了一个 &lt;code&gt;forEach&lt;/code&gt; 方法，该方法接受一个 &lt;code&gt;BiConsumer&lt;/code&gt; 对象为参数(该对象接受两个参数，返回空)，通过内部迭代编写出易于阅读的代码，关于内 部迭代请参考 3.1 节。使用该方法重写后的代码如下代码所示。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用内部迭代遍历 &lt;code&gt;Map&lt;/code&gt; 里的值&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt; Map&amp;lt;Artist, Integer&amp;gt; countOfAlbums = new HashMap&amp;lt;&amp;gt;();      albumsByArtist.forEach((artist, albums) -&amp;gt; {          countOfAlbums.put(artist, albums.size());      }); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;5.5 要点回&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;方法引用是一种引用方法的轻量级语法，形如: &lt;code&gt;ClassName::methodName&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;收集器可用来计算流的最终值，是 &lt;code&gt;reduce&lt;/code&gt; 方法的模拟。&lt;/li&gt; &lt;li&gt;Java 8 提供了收集多种容器类型的方式，同时允许用户自定义收集器。(像前面使用的 &lt;code&gt;toList&lt;/code&gt;、&lt;code&gt;toSet&lt;/code&gt; 都是收集器)&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;5.6 练习&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;方法引用 回顾第 3 章中的例子，使用方法引用改写以下方法: a. 转换大写的 &lt;code&gt;map&lt;/code&gt; 方法;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;String&amp;gt; collected = Stream.of(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;hello&amp;quot;)             .map(String::toUpperCase)             .collect(toList()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;b. 使用 &lt;code&gt;reduce&lt;/code&gt; 实现 &lt;code&gt;count&lt;/code&gt; 方法;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Map&amp;lt;String, Long&amp;gt; countWords(Stream&amp;lt;String&amp;gt; names) {         return names.collect(groupingBy(name -&amp;gt; name, counting()));     }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;c. 使用 &lt;code&gt;flatMap&lt;/code&gt; 连接列表。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; artists.stream()         .flatMap(Artist::getMembers         .reduce(0,(acc,members) -&amp;gt; member.count()) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;收集器 a. 找出名字最长的艺术家，分别使用收集器和第 3 章介绍过的 reduce 高阶函数实现。然后对比二者的异同:哪一种方式写起来更简单，哪一种方式读起来更简单?以下面的参数为例，该方法的正确返回值为 &amp;quot;Stuart Sutcliffe&amp;quot;:&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Stream&amp;lt;String&amp;gt; names = Stream.of(&amp;quot;John Lennon&amp;quot;, &amp;quot;Paul McCartney&amp;quot;,           &amp;quot;George Harrison&amp;quot;, &amp;quot;Ringo Starr&amp;quot;, &amp;quot;Pete Best&amp;quot;, &amp;quot;Stuart Sutcliffe&amp;quot;); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用收集器&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;        Function&amp;lt;String,Integer&amp;gt; getLength = artistName -&amp;gt; artistName.length();         Optional&amp;lt;String&amp;gt; name1 =  names.collect(Collectors.maxBy(comparing(getLength))).orElseThrow(RuntimeException::new);         assertEquals(&amp;quot;Stuart Sutcliffe&amp;quot;,name1.get()); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用 reduce 高阶函数&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;        String name = names.reduce(&amp;quot;&amp;quot;, (acc, ele) -&amp;gt; ele.length() &amp;gt; acc.length() ? ele : acc);         assertEquals(&amp;quot;Stuart Sutcliffe&amp;quot;,name); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;分析：语义上收集器比较清晰，方法名称能准确表达预期的操作&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;b. 假设一个元素为单词的流，计算每个单词出现的次数。假设输入如下，则返回值为一 个形如  [John → 3, Paul → 2, George → 1] 的Map:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Stream&amp;lt;String&amp;gt; names = Stream.of(&amp;quot;John&amp;quot;, &amp;quot;Paul&amp;quot;, &amp;quot;George&amp;quot;, &amp;quot;John&amp;quot;,                                         &amp;quot;Paul&amp;quot;, &amp;quot;John&amp;quot;); &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Stream&amp;lt;String&amp;gt; names = Stream.of(&amp;quot;John&amp;quot;, &amp;quot;Paul&amp;quot;, &amp;quot;George&amp;quot;, &amp;quot;John&amp;quot;,&amp;quot;Paul&amp;quot;, &amp;quot;John&amp;quot;);     Map&amp;lt;String ,Long&amp;gt; result = names.collect(groupingBy(name -&amp;gt; name,counting())); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;c. 用一个定制的收集器实现 &lt;code&gt;Collectors.groupingBy&lt;/code&gt; 方法，不需要提供一个下游收集器，只需实现一个最简单的即可。别看 JDK 的源码，这是作弊!提示:可从下面这行代 码开始:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//以下是参考答案，想了一会实在没太明确思路 package com.insightfullogic.java8.answers.chapter5;  import java.util.*; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector;  public class GroupingBy&amp;lt;T, K&amp;gt; implements Collector&amp;lt;T, Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;, Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;&amp;gt; {      private final static Set&amp;lt;Characteristics&amp;gt; characteristics = new HashSet&amp;lt;&amp;gt;();     static {         characteristics.add(Characteristics.IDENTITY_FINISH);     }      private final Function&amp;lt;? super T, ? extends K&amp;gt; classifier;      public GroupingBy(Function&amp;lt;? super T, ? extends K&amp;gt; classifier) {         this.classifier = classifier;     }      @Override     public Supplier&amp;lt;Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;&amp;gt; supplier() {         return HashMap::new;     }      @Override     public BiConsumer&amp;lt;Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;, T&amp;gt; accumulator() {         return (map, element) -&amp;gt; {             K key = classifier.apply(element);             List&amp;lt;T&amp;gt; elements = map.computeIfAbsent(key, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;());             elements.add(element);         };     }     @Override     public BinaryOperator&amp;lt;Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;&amp;gt; combiner() {         return (left, right) -&amp;gt; {             right.forEach((key, value) -&amp;gt; {                 left.merge(key, value, (leftValue, rightValue) -&amp;gt; {                     leftValue.addAll(rightValue);                     return leftValue;                 });             });             return left;         };     }     @Override     public Function&amp;lt;Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;, Map&amp;lt;K, List&amp;lt;T&amp;gt;&amp;gt;&amp;gt; finisher() {         return map -&amp;gt; map;     }      @Override     public Set&amp;lt;Characteristics&amp;gt; characteristics() {         return characteristics;     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;改进Map 使用 Map 的 computeIfAbsent 方法高效计算斐波那契数列。这里的“高效”是指避免将那些较小的序列重复计算多次。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Map&amp;lt;Integer,BigDecimal&amp;gt; fibCache = new HashMap&amp;lt;&amp;gt;();     public static BigDecimal optimiseFib(Integer i){         if (i == 1 || i== 2){             return new BigDecimal(1);         }         Function&amp;lt;Integer,BigDecimal&amp;gt; compute = j -&amp;gt; {             BigDecimal left = optimiseFib(j - 1);             BigDecimal right = optimiseFib(j - 2);             BigDecimal result =  left.add(right);             fibCache.put(i,result);             return result;         };         return fibCache.computeIfAbsent(i,compute);     }      @Test     public  void testOptimiseFib(){         System.out.println(optimiseFib(1500));     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;斐波那契数列的而外探索 发现使用上面递归且加 &lt;code&gt;map&lt;/code&gt; 缓存的方法，在计算第 &lt;code&gt;2000&lt;/code&gt; 个斐波那契数的时候就报栈溢出了，如果不使用缓存发现求第 &lt;code&gt;50&lt;/code&gt; 个斐波那契数都求不出来。 简单想了一下使用 &lt;code&gt;for&lt;/code&gt; 循环来做，使用 &lt;code&gt;List&lt;/code&gt; 来存储结果，代码如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static BigDecimal listCalcFib(Integer i) {         if (i &amp;lt; 3) {             return new BigDecimal(1);         }         List&amp;lt;BigDecimal&amp;gt; result = Lists.newArrayListWithExpectedSize(i - 1);         result.add(new BigDecimal(1));         result.add(new BigDecimal(1));          for (int j = 2; j &amp;lt; i; j++) {             result.add(result.get(j - 2).add(result.get(j - 1)));         }         return result.get(i - 1);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;此时可以求出第 &lt;code&gt;25w&lt;/code&gt; 个斐波那契数需要 &lt;code&gt;4327ms&lt;/code&gt; 时间，再求第 &lt;code&gt;30w&lt;/code&gt; 个的时候报了堆溢出，明显内存不够了 &lt;code&gt;java.lang.OutOfMemoryError: Java heap space&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;进一步想把 &lt;code&gt;List&lt;/code&gt; 换成 &lt;code&gt;Array&lt;/code&gt; 是不是能提升求解效率，代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static BigDecimal arrayCalcFib(Integer i) {         if (i &amp;lt; 3) {             return new BigDecimal(1);         }         BigDecimal [] result = new BigDecimal[i];         result[0] = new BigDecimal(1);         result[1] = new BigDecimal(1);          for (int j = 2; j &amp;lt; i; j++) {             result[j] = result[j - 2].add(result[j - 1]);         }         return result[i - 1];     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;发现当求解较小斐波那契数比如第 &lt;code&gt;1000&lt;/code&gt; 个，&lt;code&gt;List&lt;/code&gt;需要花费 &lt;code&gt;12ms&lt;/code&gt;，而数组只要 &lt;code&gt;1ms&lt;/code&gt;,但是发现当求很大，比如第 &lt;code&gt;20w&lt;/code&gt; 个斐波那契数，第一次求解竟然&lt;code&gt;Array&lt;/code&gt; 比 &lt;code&gt;List&lt;/code&gt; 更慢。但是再求解一次，&lt;code&gt;List&lt;/code&gt; 花费 &lt;code&gt;2822ms&lt;/code&gt;，&lt;code&gt;Array&lt;/code&gt; 花费 &lt;code&gt;2075ms&lt;/code&gt;。猜想应该是大量申请内存导致内存扩张花费了不少时间。&lt;/p&gt; &lt;p&gt;仔细一想我并需要存储 &lt;code&gt;i-n&lt;/code&gt; 之间的所有结果，只需要求解第 &lt;code&gt;n&lt;/code&gt; 个，那是不是 3 个局部变量就能搞定呢？答案值肯定的，代码如下:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static BigDecimal tmpVariableCalcFib(Integer i) {         if (i &amp;lt; 3) {             return new BigDecimal(1);         }         BigDecimal  result = new BigDecimal(0);         BigDecimal first = new BigDecimal(1);         BigDecimal second = new BigDecimal(1);          for (int j = 2; j &amp;lt; i; j++) {             result = first.add(second);             first = second;             second = result;         }         return result;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果是第 &lt;code&gt;100w&lt;/code&gt; 个也可以求出，时间花了 &lt;code&gt;21653ms&lt;/code&gt;。而求第 &lt;code&gt;20w&lt;/code&gt; 只需要花 &lt;code&gt;1348ms&lt;/code&gt;,相比 &lt;code&gt;Array&lt;/code&gt; 的 &lt;code&gt;2075ms&lt;/code&gt; 快了不少。到这个时候花费的大部分时间应该都是循环中的大数加法，如果不使用文件系统缓存的话，应该很难再大幅提高了，这个时候我不禁想换其他语言试试，比如 &lt;code&gt;golang&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-golang"&gt;func calcFib(i int) *big.Int {  if i &amp;lt; 3{   return big.NewInt(1)  }  result,first,second := big.NewInt(0),big.NewInt(1),big.NewInt(1)  for j := 2; j &amp;lt; i;j++{   result = first.Add(first,second)   first = second   second = result  }  return result } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果只花了 &lt;code&gt;150.907815ms&lt;/code&gt; 简直亮瞎狗眼有木有！！相比 &lt;code&gt;Java&lt;/code&gt; 最快的成绩 &lt;code&gt;1348ms&lt;/code&gt; 提高了将近一个数量级。再来算一下 &lt;code&gt;100w&lt;/code&gt; 个斐波那契数，相比 &lt;code&gt;Java&lt;/code&gt; 的 &lt;code&gt;21894ms&lt;/code&gt;，&lt;code&gt;golang&lt;/code&gt; 只花了 &lt;code&gt;4191ms&lt;/code&gt; 也是一个数量级的差距。随手计算了一下第 &lt;code&gt;200w&lt;/code&gt; 个斐波那契数 &lt;code&gt;golang&lt;/code&gt; 花了 &lt;code&gt;17063ms&lt;/code&gt;，大家猜第 &lt;code&gt;200w&lt;/code&gt; 斐波那契数有多长，简单数了一下 &lt;code&gt;417975&lt;/code&gt; 位长的数字。语言差距有如此之大吗？反正远远超出了我的预想。单单从这方面看，&lt;code&gt;golang&lt;/code&gt; 确实有点牛逼呀！💯&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 08 Nov 2018 12:53:00 GMT</pubDate>
    </item>
    <item>
      <title>金陵之秋</title>
      <link>https://www.zhangaoo.com/article/31</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/7c4nkodafigbnpju23d5g6oo7b.jpg" alt="一叶知秋" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/uif5jbgjoagu7o426p10rbki7n.jpg" alt="秋意紫峰" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/3608uunp5ei8roh0qvn3nttef6.jpg" alt="枫叶" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/394ocf3lu8hnfpqrppd3s7dlr4.jpg" alt="林荫大道" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/pe7e0ogah2j0rqg06e4gd11ev3.jpg" alt="行人" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/rq5q4hhg5oimkrh3b7ku7red3e.jpg" alt="旷野" /&gt; &lt;/br&gt; &lt;img src="https://www.zhangaoo.com/upload/2018/10/u6k95mugbejbsoerfdls6v15gm.jpg" alt="明城墙" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sun, 28 Oct 2018 10:47:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇三之类库</title>
      <link>https://www.zhangaoo.com/article/30</link>
      <content:encoded>&lt;h1&gt;第4章 类库&lt;/h1&gt; &lt;p&gt;接下来将详细阐述另一个重要方面:如何使用 Lambda 表达式。即使不需要编写像 &lt;code&gt;Stream&lt;/code&gt; 这样重度使用函数式编程风格的类库，学会如 何使用 Lambda 表达式也是非常重要的。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Java 8&lt;/code&gt; 中的另一个变化是引入了默认方法和接口的静态方法，它改变了人们认识类库的方 式，接口中的方法也可以包含代码体了。&lt;/p&gt; &lt;p&gt;本章还对前 3 章疏漏的知识点进行补充，比如，Lambda 表达式方法重载的工作原理、基 本类型的使用方法等&lt;/p&gt; &lt;h2&gt;4.1 在代码中使用Lambda表达式&lt;/h2&gt; &lt;p&gt;我们来看一个日志系统中的具体案例。在 &lt;code&gt;slf4j&lt;/code&gt; 和 &lt;code&gt;log4j&lt;/code&gt; 等几种常用的日志系统中，有 一些记录日志的方法，当日志级别不低于某个固定级别时就会开始记录日志。如此一来， 在日志框架中设置类似&lt;code&gt;void debug(String message)&lt;/code&gt;这样的方法，当级别为&lt;code&gt;debug&lt;/code&gt;时，它 们就开始记录日志消息。&lt;/p&gt; &lt;p&gt;问题在于，频繁计算消息是否应该记录日志会对系统性能产生影响。程序员通过显式调用 &lt;code&gt;isDebugEnabled&lt;/code&gt; 方法来优化系统性能。即使直接调用 &lt;code&gt;debug&lt;/code&gt; 方法能省去记录文本信息，也仍然需要调用 &lt;code&gt;expensiveOperation&lt;/code&gt; 方法，并且需要将执行结果和已有字符串连接起来，因此，使用 &lt;code&gt;if&lt;/code&gt; 语句显式判断，可以让程序跑得更快。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 isDebugEnabled 方法降低日志性能开销&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Logger logger = new Logger();  if (logger.isDebugEnabled()) {          logger.debug(&amp;quot;Look at this: &amp;quot; + expensiveOperation());      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里我们想做的是传入一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，生成一条用作日志信息的字符串。只有日志 级别在调试或以上级别时，才会执行该 &lt;code&gt;Lambda&lt;/code&gt; 表达式。使用这个方式重写上面的代码。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式简化日志代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Logger logger = new Logger();     logger.debug(() -&amp;gt; &amp;quot;Look at this: &amp;quot; + expensiveOperation()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;那么在 &lt;code&gt;Logger&lt;/code&gt; 类中该方法是如何实现的呢?从类库的角度看，我们可以使用内置的 &lt;code&gt;Supplier&lt;/code&gt; 函数接口，它只有一个 &lt;code&gt;get&lt;/code&gt; 方法。然后通过调用 isDebugEnabled 判断是否需要记录日志，是否需要调用 &lt;code&gt;get&lt;/code&gt; 方法，如果需要，就调用 &lt;code&gt;get&lt;/code&gt; 方法并将结果传给 &lt;code&gt;debug&lt;/code&gt; 方法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;启用 &lt;code&gt;Lambda&lt;/code&gt; 表达式实现的日志记录器&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void debug(Supplier&amp;lt;String&amp;gt; message) {      if (isDebugEnabled()) {              debug(message.get());     } } //调用如下 debug(()-&amp;gt; &amp;quot;Log information&amp;quot;) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;调用 &lt;code&gt;get()&lt;/code&gt; 方法，相当于调用传入的 &lt;code&gt;Lambda&lt;/code&gt; 表达式。这种方式也能和匿名内部类一起工作，如果用户暂时无法升级到 &lt;code&gt;Java 8&lt;/code&gt;，这种方式可以实现向后兼容。值得注意的是，不同的函数接口有不同的方法。如果使用 &lt;code&gt;Predicate&lt;/code&gt;，就应该调用 &lt;code&gt;test&lt;/code&gt; 方法，如果使用 &lt;code&gt;Function&lt;/code&gt;，就应该调用 &lt;code&gt;apply&lt;/code&gt; 方法。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;思考：没感觉换成Lambda表达式带来了其他优势，似乎还没GET到作者的意图，难道就是增加了一个方法，简化了几行代码？&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;4.2 基本类型&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;Java&lt;/code&gt; 中，有一些相伴的类型，比如 &lt;code&gt;int&lt;/code&gt; 和 &lt;code&gt;Integer&lt;/code&gt;—— 前者是基本类型，后者是装箱类型。基本类型内建在语言和运行环境中，是基本的程序构 建模块;而装箱类型属于普通的 &lt;code&gt;Java&lt;/code&gt; 类，只不过是对基本类型的一种封装。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Java&lt;/code&gt; 的泛型是基于对泛型参数类型的擦除——换句话说，假设它是 &lt;code&gt;Object&lt;/code&gt; 对象的实例—— 因此只有装箱类型才能作为泛型参数。这就解释了为什么在 &lt;code&gt;Java&lt;/code&gt; 中想要一个包含整型值的 列表 &lt;code&gt;List&amp;lt;int&amp;gt;&lt;/code&gt;，实际上得到的却是一个包含整型对象的列表 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;关于&lt;code&gt;Java&lt;/code&gt;泛型类型擦除的补充 &lt;code&gt;Java&lt;/code&gt; 的泛型在编译器有效，在运行期被删除，也就是说所有泛型参数类型在编译后都会被清除掉，看下面一个列子，代码如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class Foo {       public void listMethod(List&amp;lt;String&amp;gt; stringList){       }       public void listMethod(List&amp;lt;Integer&amp;gt; intList) {       }   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;编译上面的代码报错如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//此错误的意思是说listMethod(List&amp;lt;String&amp;gt;) 方法在编译时擦除类型后的方法是listMethod(List&amp;lt;E&amp;gt;)，它与另外一个方法重复，也就是方法签名重复。 Method listMethod(List&amp;lt;String&amp;gt;) has the same erasure listMethod(List&amp;lt;E&amp;gt;) as another method in type Foo &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;反编译之后的方法代码如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void listMethod(List list)  {  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从上面代码可以看出 &lt;code&gt;Java&lt;/code&gt; 编译后的字节码中已经没有泛型的任何信息，在编译后所有的泛型类型都会做相应的转化，转化如下：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;List&lt;String&gt;、List&lt;T&gt; 擦除后的类型为 List。&lt;/li&gt; &lt;li&gt;List&lt;String&gt;[]、List&lt;T&gt;[] 擦除后的类型为 List[]。&lt;/li&gt; &lt;li&gt;List&amp;lt;? extends E&amp;gt;、List&amp;lt;? super E&amp;gt; 擦除后的类型为 List&lt;E&gt;。&lt;/li&gt; &lt;li&gt;List&amp;lt;T extends Serialzable &amp;amp; Cloneable&amp;gt; 擦除后类型为 List&lt;Serializable&gt;。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;回归正题&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;由于装箱类型是对象，因此在内存中存在额外开销。比如，整型在内存中占用 &lt;code&gt;4&lt;/code&gt; 字节，整型对象却要占用 &lt;code&gt;16&lt;/code&gt; 字节。这一情况在数组上更加严重，整型数组中的每个元素 只占用基本类型的内存，而整型对象数组中，每个元素都是内存中的一个指针，指向 &lt;code&gt;Java&lt;/code&gt; 堆中的某个对象。在最坏的情况下，同样大小的数组，&lt;code&gt;Integer[]&lt;/code&gt; 要比 &lt;code&gt;int[]&lt;/code&gt; 多占用 &lt;code&gt;6&lt;/code&gt; 倍内存。&lt;/p&gt; &lt;p&gt;将基本类型转换为装箱类型，称为装箱，反之则称为拆箱，两者都需要额外的计算开销。 对于需要大量数值运算的算法来说，装箱和拆箱的计算开销，以及装箱类型占用的额外内 存，会明显减缓程序的运行速度。&lt;/p&gt; &lt;p&gt;为了减小这些性能开销，&lt;code&gt;Stream&lt;/code&gt; 类的某些方法对基本类型和装箱类型做了区分。下图所示的高阶函数&lt;code&gt;mapToLong&lt;/code&gt;和其他类似函数即为该方面的一个尝试。在&lt;code&gt;Java 8&lt;/code&gt;中，仅对整型、 长整型和双浮点型做了特殊处理，因为它们在数值计算中用得最多，特殊处理后的系统性 能提升效果最明显。&lt;code&gt;mapToLong&lt;/code&gt;参数接受如下&lt;code&gt;Lambda&lt;/code&gt;表达式：&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/4jcmmdt3iuh4qoiut45hrdnpm2.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;对基本类型做特殊处理的方法在命名上有明确的规范。如果方法返回类型为基本类型，则 在基本类型前加 &lt;code&gt;To&lt;/code&gt;，如上图中的&lt;code&gt;ToLongFunction&lt;/code&gt;。如果参数是基本类型，则不加前缀只需类型名即可，如下图中的 &lt;code&gt;LongFunction&lt;/code&gt;。如果高阶函数使用基本类型，则在操作后加 后缀 &lt;code&gt;To&lt;/code&gt; 再加基本类型，如 &lt;code&gt;mapToLong&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/6m55aaimmsjp0os1rsq16hj1fa.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;这些基本类型都有与之对应的 &lt;code&gt;Stream&lt;/code&gt;，以基本类型名为前缀，如 &lt;code&gt;LongStream&lt;/code&gt;。事实上， &lt;code&gt;mapToLong&lt;/code&gt; 方法返回的不是一个一般的 &lt;code&gt;Stream&lt;/code&gt;，而是一个特殊处理的 &lt;code&gt;Stream&lt;/code&gt;。在这个特 殊的 &lt;code&gt;Stream&lt;/code&gt; 中，&lt;code&gt;map&lt;/code&gt; 方法的实现方式也不同，它接受一个 &lt;code&gt;LongUnaryOperator&lt;/code&gt; 函数，将 一个长整型值映射成另一个长整型值，如下图所示。通过一些高阶函数装箱方法，如 &lt;code&gt;mapToObj&lt;/code&gt;，也可以从一个基本类型的 &lt;code&gt;Stream&lt;/code&gt; 得到一个装箱后的 &lt;code&gt;Stream&lt;/code&gt;，如 &lt;code&gt;Stream&amp;lt;Long&amp;gt;&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/322vdqqpcsirlq360jmgcqlkcc.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;如有可能，应尽可能多地使用对基本类型做过特殊处理的方法，进而改善性能。这些特殊的&lt;code&gt;Stream&lt;/code&gt;还提供额外的方法，避免重复实现一些通用的方法，让代码更能体现出数值计算 的意图。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 summaryStatistics 方法统计曲目长度&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Album album = new Album(Sets.newHashSet(             new Track(&amp;quot;wwwww&amp;quot;, 120, &amp;quot;wrwerwer&amp;quot;),             new Track(&amp;quot;sdsds&amp;quot;, 30, &amp;quot;sfsdfsdfsdf&amp;quot;),             new Track(&amp;quot;wqeqweqwe&amp;quot;, 120, &amp;quot;sasdasd&amp;quot;),             new Track(&amp;quot;asdasd&amp;quot;, 120, &amp;quot;123qeew&amp;quot;)));     IntSummaryStatistics trackStatistic = album.getTracksStream()             .mapToInt(track -&amp;gt; track.getLength())             .summaryStatistics();      System.out.printf(&amp;quot;Max:%d,Min:%d,Avg:%f,Sum:%d&amp;quot;,trackStatistic.getMax(),trackStatistic.getMin(),trackStatistic.getAverage(),trackStatistic.getSum()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;无需手动计算这些信息，这里使用对基 本类型进行特殊处理的方法&lt;code&gt;mapToInt&lt;/code&gt;，将每首曲目映射为曲目长度。因为该方法返回一个 &lt;code&gt;IntStream&lt;/code&gt;对象，它包含一个 &lt;code&gt;summaryStatistics&lt;/code&gt; 方法，这个方法能计算出各种各样的统计 值，如 &lt;code&gt;IntStream&lt;/code&gt; 对象内所有元素中的最小值、最大值、平均值以及数值总和。&lt;/p&gt; &lt;p&gt;这些统计值在所有特殊处理的 &lt;code&gt;Stream&lt;/code&gt;，如 &lt;code&gt;DoubleStream、LongStream&lt;/code&gt; 中都可以得出。如无 需全部的统计值，也可分别调用 &lt;code&gt;min、max、average&lt;/code&gt; 或 &lt;code&gt;sum&lt;/code&gt; 方法获得单个的统计值，同样， 三种基本类型对应的特殊 &lt;code&gt;Stream&lt;/code&gt; 也都包含这些方法。&lt;/p&gt; &lt;h2&gt;4.3 重载解析&lt;/h2&gt; &lt;p&gt;在 &lt;code&gt;Java&lt;/code&gt; 中可以重载方法，造成多个方法有相同的方法名，但签名确不一样。这在推断参数 类型时会带来问题，因为系统可能会推断出多种类型。这时，&lt;code&gt;javac&lt;/code&gt; 会挑出最具体的类型。 如下面的例子输出 &lt;code&gt;String&lt;/code&gt;，而不是 &lt;code&gt;Object&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;overloadedMethod(&amp;quot;abc&amp;quot;);  private void overloadedMethod(Object o) {      System.out.print(&amp;quot;Object&amp;quot;); }  private void overloadedMethod(String s) {      System.out.print(&amp;quot;String&amp;quot;); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;BinaryOperator&lt;/code&gt; 是一种特殊的 &lt;code&gt;BiFunction&lt;/code&gt; 类型，参数的类型和返回值的类型相同。比如， 两个整数相加就是一个 &lt;code&gt;BinaryOperator&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;Lambda 表达式的类型就是对应的函数接口类型，因此，将 Lambda 表达式作为参数 传递时，情况也依然如此。操作时可以重载一个方法，分别接受 BinaryOperator 和该接口的一个子类作为参数。调用这些方法时，Java 推导出的 Lambda 表达式的类型正 是最具体的函数接口的类型。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface IntegerBiFunction extends BinaryOperator&amp;lt;Integer&amp;gt; {}  private void overloadedMethod(BinaryOperator&amp;lt;Integer&amp;gt; Lambda) {     System.out.print(&amp;quot;BinaryOperator&amp;quot;); }  private void overloadedMethod(IntegerBiFunction Lambda) {     System.out.print(&amp;quot;IntegerBinaryOperator&amp;quot;); }  @Test public void overloadMethodTest() {     overloadedMethod((x, y) -&amp;gt; x + y); } //结果输出 IntegerBinaryOperator，这里IntegerBiFunction接口继承了BinaryOperator&amp;lt;Integer&amp;gt;，因此IntegerBiFunction更具体 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当然，同时存在多个重载方法时，哪个是“最具体的类型”可能并不明确。重载方法导致的编译错误&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private interface IntPredicate {      public boolean test(int value); } private void overloadedMethod(Predicate&amp;lt;Integer&amp;gt; predicate) {      System.out.print(&amp;quot;Predicate&amp;quot;); } private void overloadedMethod(IntPredicate predicate) {      System.out.print(&amp;quot;IntPredicate&amp;quot;); } //编译报错 overloadedMethod((x) -&amp;gt; true); //强制指定类型 overloadedMethod((IntPredicate)(x) -&amp;gt; true);  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;传入 &lt;code&gt;overloadedMethod&lt;/code&gt; 方法的 &lt;code&gt;Lambda&lt;/code&gt; 表达式和两个函数接口 &lt;code&gt;Predicate&lt;/code&gt;、&lt;code&gt;IntPredicate&lt;/code&gt; 在 类型上都是匹配的。在这段代码块中，两种情况都定义了相应的重载方法，这时，&lt;code&gt;javac&lt;/code&gt; 就无法编译，在错误报告中显示 &lt;code&gt;Lambda&lt;/code&gt; 表达式被模糊调用。&lt;code&gt;IntPredicate&lt;/code&gt; 没有继承 &lt;code&gt;Predicate&lt;/code&gt;，因此编译器无法推断出哪个类型更具体。&lt;/p&gt; &lt;p&gt;将 &lt;code&gt;Lambda&lt;/code&gt; 表达式强制转换为 &lt;code&gt;IntPredicate&lt;/code&gt; 或 &lt;code&gt;Predicate&amp;lt;Integer&amp;gt;&lt;/code&gt; 类型可以解决这个问 题，至于转换为哪种类型则取决于要调用哪个函数接口。当然，如果以前你曾自行设计过 类库，就可以将其视为“代码异味”，不该再重载，而应当开始重新命名重载方法。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式作为参数时，其类型由它的目标类型推导得出，推导过程遵循 如下规则:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;如果只有一个可能的目标类型，由相应函数接口里的参数类型推导得出;&lt;/li&gt; &lt;li&gt;如果有多个可能的目标类型，由最具体的类型推导得出;&lt;/li&gt; &lt;li&gt;如果有多个可能的目标类型且最具体的类型不明确，则需人为指定类型。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;4.4 @FunctionalInterface&lt;/h2&gt; &lt;p&gt;前面已讨论过函数接口定义的标准，但未提及 &lt;code&gt;@FunctionalInterface&lt;/code&gt; 注释。事实上，每个用作函数接口的接口都应该添加这个注释。&lt;/p&gt; &lt;p&gt;这究竟是什么意思呢? &lt;code&gt;Java&lt;/code&gt; 中有一些接口，虽然只含一个方法，但并不是为了使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式来实现的。比如，有些对象内部可能保存着某种状态，使用带有一个方法 的接口可能纯属巧合。&lt;code&gt;java.lang.Comparable&lt;/code&gt; 和 &lt;code&gt;java.io.Closeable&lt;/code&gt; 就属于这样的情况。&lt;/p&gt; &lt;p&gt;如果一个类是可比较的，就意味着在该类的实例之间存在某种顺序，比如字符串中的字母 顺序。人们通常不会认为函数是可比较的，如果一个东西既没有属性也没有状态，拿什么 比较呢?&lt;/p&gt; &lt;p&gt;一个可关闭的对象必须持有某种打开的资源，比如一个需要关闭的文件句柄。同样，该接口也不能是一个纯函数，因为关闭资源是更改状态的另一种形式。&lt;/p&gt; &lt;p&gt;和 &lt;code&gt;Closeable&lt;/code&gt; 和 &lt;code&gt;Comparable&lt;/code&gt; 接口不同，为了提高 &lt;code&gt;Stream&lt;/code&gt; 对象可操作性而引入的各种新接口，都需要有 &lt;code&gt;Lambda&lt;/code&gt; 表达式可以实现它。它们存在的意义在于将代码块作为数据打包起来。因此，它们都添加了 &lt;code&gt;@FunctionalInterface&lt;/code&gt; 注释。&lt;/p&gt; &lt;p&gt;该注释会强制 &lt;code&gt;javac&lt;/code&gt; 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举 类型、类或另一个注释，或者接口包含不止一个抽象方法，&lt;code&gt;javac&lt;/code&gt; 就会报错。重构代码时， 使用它能很容易发现问题。&lt;/p&gt; &lt;h2&gt;4.5 二进制接口的兼容性&lt;/h2&gt; &lt;p&gt;如第3章开篇所言，&lt;code&gt;Java 8&lt;/code&gt;中对&lt;code&gt;API&lt;/code&gt;最大的改变在于集合类。虽然Java在持续演进，但它 一直在保持着向后二进制兼容。具体来说，使用&lt;code&gt;Java 1&lt;/code&gt;到&lt;code&gt;Java 7&lt;/code&gt;编译的类库或应用，可以 直接在&lt;code&gt;Java 8&lt;/code&gt;上运行。&lt;/p&gt; &lt;p&gt;当然，错误也难免会时有发生，但和其他编程平台相比，二进制兼容性一直被视为 &lt;code&gt;Java&lt;/code&gt; 的关键优势所在。除非引入新的关键字，如 enum，达成源代码向后兼容也不是没有可能实 现。可以保证，只要是 &lt;code&gt;Java 1&lt;/code&gt; 到 &lt;code&gt;Java 7&lt;/code&gt; 写出的代码，在 &lt;code&gt;Java 8&lt;/code&gt; 中依然可以编译通过。&lt;/p&gt; &lt;p&gt;事实上，修改了像集合类这样的核心类库之后，这一保证也很难实现。我们可以用具体的 例子作为思考练习。&lt;code&gt;Java 8&lt;/code&gt;中为&lt;code&gt;Collection&lt;/code&gt;接口增加了&lt;code&gt;stream&lt;/code&gt;方法，这意味着所有实现了 &lt;code&gt;Collection&lt;/code&gt; 接口的类都必须增加这个新方法。对核心类库里的类来说，实现这个新方法(比如为 &lt;code&gt;ArrayList&lt;/code&gt; 增加新的 &lt;code&gt;stream&lt;/code&gt; 方法)就能就能使问题迎刃而解。&lt;/p&gt; &lt;p&gt;缺憾在于，这个修改依然打破了二进制兼容性，在 &lt;code&gt;JDK&lt;/code&gt; 之外实现 &lt;code&gt;Collection&lt;/code&gt; 接口的类， 例如&lt;code&gt;MyCustomList&lt;/code&gt;，也仍然需要实现新增的&lt;code&gt;stream&lt;/code&gt;方法。这个&lt;code&gt;MyCustomList&lt;/code&gt;在&lt;code&gt;Java 8&lt;/code&gt;中 无法通过编译，即使已有一个编译好的版本，在 &lt;code&gt;JVM&lt;/code&gt; 加载 &lt;code&gt;MyCustomList&lt;/code&gt; 类时，类加载器仍然会引发异常。&lt;/p&gt; &lt;p&gt;这是所有使用第三方集合类库的梦魇，要避免这个糟糕情况，则需要在&lt;code&gt;Java 8&lt;/code&gt;中添加新的 语言特性:默认方法&lt;/p&gt; &lt;h2&gt;4.6&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Collection&lt;/code&gt; 接口中增加了新的 &lt;code&gt;stream&lt;/code&gt; 方法，如何能让 &lt;code&gt;MyCustomList&lt;/code&gt; 类在不知道该方法的情况下通过编译?&lt;code&gt;Java 8&lt;/code&gt;通过如下方法解决该问题:&lt;code&gt;Collection&lt;/code&gt;接口告诉它所有的子类: “如果你没有实现 &lt;code&gt;stream&lt;/code&gt; 方法，就使用我的吧。”接口中这样的方法叫作默认方法，在任何接口中，无论函数接口还是非函数接口，都可以使用该方法。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Iterable&lt;/code&gt; 接口中也新增了一个默认方法:&lt;code&gt;forEach&lt;/code&gt;，该方法功能和 &lt;code&gt;for&lt;/code&gt; 循环类似，但是允许用户使用一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式作为循环体。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;默认方法示例:&lt;code&gt;forEach&lt;/code&gt; 实现方式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    default void forEach(Consumer&amp;lt;? super T&amp;gt; action) {         Objects.requireNonNull(action);         for (T t : this) {             action.accept(t);         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果已经习惯了通过调用接口方法来使用 &lt;code&gt;Lambda&lt;/code&gt; 表达式的方式，那么这个例子理解起来就相当简单。它使用一个常规的 &lt;code&gt;for&lt;/code&gt; 循环遍历 &lt;code&gt;Iterable&lt;/code&gt; 对象，然后对每个值调用 &lt;code&gt;accept&lt;/code&gt; 方法。&lt;/p&gt; &lt;p&gt;既然如此简单，为何还要单独提出来呢?重点就在于代码段前面的新关键字 &lt;code&gt;default&lt;/code&gt;。这 个关键字告诉 &lt;code&gt;javac&lt;/code&gt; 用户真正需要的是为接口添加一个新方法。除了添加了一个新的关键字，默认方法在继承规则上和普通方法也略有区别。&lt;/p&gt; &lt;p&gt;和类不同，接口没有成员变量，因此默认方法只能通过调用子类的方法来修改子类本身， 避免了对子类的实现做出各种假设。&lt;/p&gt; &lt;h2&gt;默认方法和子类&lt;/h2&gt; &lt;p&gt;默认方法的重写规则也有一些微妙之处。从最简单的情况开始来看:没有重写。在下面的例子中，&lt;code&gt;Parent&lt;/code&gt; 接口定义了一个默认方法 &lt;code&gt;welcome&lt;/code&gt;，调用该方法时，发送一条信息。&lt;code&gt;ParentImpl&lt;/code&gt; 类没有实现 &lt;code&gt;welcome&lt;/code&gt; 方法，因此它自然继承了该默认方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public interface Parent {         void message(String body);          default void welcome() {             message(&amp;quot;Parents:Hi!&amp;quot;);         }         String getLastMessage();     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;在下面例子中，我们调用默认方法，可以看到断言正确。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void parentDefaultUsed() {         Parent parent = new ParentImpl();         parent.welcome();         assertEquals(&amp;quot;Parent: Hi!&amp;quot;, parent.getLastMessage());     } //解析：实现类ParentImpl并未实现welcome()接口，而是调用接口中的默认方法 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这时可新建一个接口 &lt;code&gt;Child&lt;/code&gt;，继承自 &lt;code&gt;Parent&lt;/code&gt; 接口，代码如下所示。&lt;code&gt;Child&lt;/code&gt; 接口实现了自己的默认 &lt;code&gt;welcome&lt;/code&gt; 方法，凭直觉判断可知，该方法重写了 &lt;code&gt;Parent&lt;/code&gt; 的方法。同样在这个例子中，&lt;code&gt;ChildImpl&lt;/code&gt; 类不会实现 &lt;code&gt;welcome&lt;/code&gt; 方法，因此它自然也继承了接口的默认方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//Child 继承了 Parent 但是重写了welcome默认方法     public interface Child extends Parent{         @Override         default void welcome(){             message(&amp;quot;Child: Hi!&amp;quot;);         }     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;调用 &lt;code&gt;Child&lt;/code&gt; 接口的客户代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void childOverrideDefault() {         Child child = new ChildImpl();         child.welcome();         assertEquals(&amp;quot;Child: Hi!&amp;quot;, child.getLastMessage());     }     //最后输出的字符串自然是 &amp;quot;Child: Hi!&amp;quot;。 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;类继承体系如下图&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/6u0koqvjlgjr1qpjeahegou10t.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;现在默认方法成了虚方法——和静态方法刚好相反。任何时候，一旦与类中定义的方法产生冲突，都要优先选择类中定义的方法。例如下展示了这种情况，最终调用的 是 &lt;code&gt;OverridingParent&lt;/code&gt; 的，而不是 &lt;code&gt;Parent&lt;/code&gt; 的 &lt;code&gt;welcome&lt;/code&gt;方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;// 重写 welcome 默认实现的父类     public class OverridingParent extends ParentImpl {         @Override         public void welcome() {             message(&amp;quot;Class Parent: Hi!&amp;quot;);         }     } // 调用的是类中的具体方法，而不是默认方法     @Test     public void concreteBeatsDefault() {         Parent parent = new OverridingParent();         parent.welcome();         assertEquals(&amp;quot;Class Parent: Hi!&amp;quot;, parent.getLastMessage());     }     &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;下面的代码展示了另一种情况，或许不认为类中重写的方法能够覆盖默认方法。&lt;code&gt;OverridingChild&lt;/code&gt; 本身并没有任何操作，只是继承了 &lt;code&gt;Child&lt;/code&gt; 和 &lt;code&gt;OverridingParent&lt;/code&gt; 中的 &lt;code&gt;welcome&lt;/code&gt; 方法。最后，调 用的是 &lt;code&gt;OverridingParent&lt;/code&gt; 中的 &lt;code&gt;welcome&lt;/code&gt; 方法，而不是 &lt;code&gt;Child&lt;/code&gt; 接口中定义的默认方法，原因在于，与接口中定义的默认方法相比，类中重写的方法更具体&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class OverridingChild extends OverridingParent implements Child {     }      @Test     public void concreteBeatsCloserDefault() {         Child child = new OverridingChild();         child.welcome();         assertEquals(&amp;quot;Class Parent: Hi!&amp;quot;, child.getLastMessage());     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简言之，类中重写的方法胜出。这样的设计主要是由增加默认方法的目的决定的，增加默认方法主要是为了在接口上向后兼容。让类中重写方法的优先级高于默认方法能简化很多继承问题。&lt;/p&gt; &lt;p&gt;假设已实现了一个定制的列表 &lt;code&gt;MyCustomList&lt;/code&gt;，该类中有一个 &lt;code&gt;addAll&lt;/code&gt; 方法，如果新的 &lt;code&gt;List&lt;/code&gt; 接口也增加了一个默认方法 &lt;code&gt;addAll&lt;/code&gt;，该方法将对列表的操作代理到 &lt;code&gt;add&lt;/code&gt; 方法。如果类中重写的方法没有默认方法的优先级高，那么就会破坏已有的实现。&lt;/p&gt; &lt;h2&gt;4.7 多重继承&lt;/h2&gt; &lt;p&gt;接口允许多重继承，因此有可能碰到两个接口包含签名相同的默认方法的情况。比如下面代码中，接口 &lt;code&gt;Carriage&lt;/code&gt; 和 &lt;code&gt;Jukebox&lt;/code&gt; 都有一个默认方法 &lt;code&gt;rock&lt;/code&gt;，虽然各有各的用途。类 &lt;code&gt;MusicalCarriage&lt;/code&gt; 同时实现了接口 &lt;code&gt;Jukebox&lt;/code&gt; 和 &lt;code&gt;Carriage&lt;/code&gt;，它到底继承 了哪个接口的 &lt;code&gt;rock&lt;/code&gt; 方法呢?&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public interface Jukebox {         default String rock() {             return &amp;quot;... all over the world!&amp;quot;;         }     }      public interface Carriage {         public default String rock() {             return &amp;quot;... from side to side&amp;quot;;         }     }      public class MusicalCarriage implements Carriage, Jukebox {     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;此时，&lt;code&gt;javac&lt;/code&gt; 并不明确应该继承哪个接口中的方法，因此编译器会报错:&lt;code&gt;class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox&lt;/code&gt;。当然，在类 中实现 &lt;code&gt;rock&lt;/code&gt; 方法就能解决这个问题&lt;/p&gt; &lt;ul&gt; &lt;li&gt;实现 rock 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class MusicalCarriage implements Carriage, Jukebox {         @Override         public String rock() {             return Carriage.super.rock();         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;该例中使用了增强的 &lt;code&gt;super&lt;/code&gt; 语法，用来指明使用接口 &lt;code&gt;Carriage&lt;/code&gt; 中定义的默认方法。此前，使用 &lt;code&gt;super&lt;/code&gt; 关键字是指向父类，现在使用类似 &lt;code&gt;InterfaceName.super&lt;/code&gt; 这样的语法指的是继承自父接口的方法。&lt;/p&gt; &lt;h2&gt;三定律&lt;/h2&gt; &lt;p&gt;如果对默认方法的工作原理，特别是在多重继承下的行为还没有把握，如下三条简单的定 律可以帮助大家:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;类胜于接口。如果在继承链中有方法体或抽象的方法声明，那么就可以忽略接口中定义的方法。&lt;/li&gt; &lt;li&gt;子类胜于父类。如果一个接口继承了另一个接口，且两个接口都定义了一个默认方法， 那么子类中定义的方法胜出。&lt;/li&gt; &lt;li&gt;没有规则三。如果上面两条规则不适用，子类要么需要实现该方法，要么将该方法声明为抽象方法。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;其中第一条规则是为了让代码向后兼容。&lt;/p&gt; &lt;h2&gt;4.8 权衡&lt;/h2&gt; &lt;p&gt;在接口中定义方法的诸多变化引发了一系列问题，既然可用代码主体定义方法，那&lt;code&gt;Java 8&lt;/code&gt; 中的接口还是旧有版本中界定的代码吗?现在的接口提供了某种形式上的多重继承功能， 然而多重继承在以前饱受诟病，&lt;code&gt;Java&lt;/code&gt; 因此舍弃了该语言特性，这也正是 &lt;code&gt;Java&lt;/code&gt; 在易用性方面优于 &lt;code&gt;C++&lt;/code&gt; 的原因之一。&lt;/p&gt; &lt;p&gt;语言特性的利弊也在不断演化。很多人认为多重继承的问题在于对象状态的继承，而不是代码块的继承，默认方法避免了状态的继承，也因此避免了 &lt;code&gt;C++&lt;/code&gt; 中多重继承的最大缺点。&lt;/p&gt; &lt;p&gt;突破语言上的局限性吸引着无数优秀的程序员不断尝试。现在已有一些博客文章，阐述在 &lt;code&gt;Java 8&lt;/code&gt; 中实现完全的多重继承做出的尝试，包括状态的继承和默认方法。尝试突破 &lt;code&gt;Java 8&lt;/code&gt; 这些有意为之的语言限制时，却往往又掉进 &lt;code&gt;C++&lt;/code&gt; 的旧有陷阱之中。&lt;/p&gt; &lt;p&gt;接口和抽象类之间还是存在明显的区别。接口允许多重继承，却没有成员变量;抽象类可以继承成员变量，却不能多重继承。在对问题域建模时，需要根据具体情况进行权衡，而在以前的 &lt;code&gt;Java&lt;/code&gt; 中可能并不需要这样。&lt;/p&gt; &lt;h2&gt;4.9 接口的静态方法&lt;/h2&gt; &lt;p&gt;前面已多次出现过 &lt;code&gt;Stream.of&lt;/code&gt; 方法的调用，接下来将对其进行详细介绍。&lt;code&gt;Stream&lt;/code&gt; 是个接口， &lt;code&gt;Stream.of&lt;/code&gt; 是接口的静态方法。这也是 &lt;code&gt;Java 8&lt;/code&gt; 中添加的一个新的语言特性，旨在帮助编写类库的开发人员，但对于日常应用程序的开发人员也同样适用。&lt;/p&gt; &lt;p&gt;人们在编程过程中积累了这样一条经验，那就是一个包含很多静态方法的类。有时，类是 一个放置工具方法的好地方，比如 &lt;code&gt;Java 7&lt;/code&gt; 中引入的 &lt;code&gt;Objects&lt;/code&gt; 类，就包含了很多工具方法， 这些方法不是具体属于某个类的。&lt;/p&gt; &lt;p&gt;当然，如果一个方法有充分的语义原因和某个概念相关，那么就应该将该方法和相关的类或接口放在一起，而不是放到另一个工具类中。这有助于更好地组织代码，阅读代码的人也更容易找到相关方法。&lt;/p&gt; &lt;p&gt;比如，如果想创建一个由简单值组成的 &lt;code&gt;Stream&lt;/code&gt;，自然希望 &lt;code&gt;Stream&lt;/code&gt; 中能有一个这样的方法。 这在以前很难达成，引入重接口的 &lt;code&gt;Stream&lt;/code&gt; 对象，最后促使 &lt;code&gt;Java&lt;/code&gt; 为接口加入了静态方法。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Stream 和其他几个子类还包含另外几个静态方法。特别是 range 和 iterate 方法提供了产生 Stream 的其他方式。&lt;/strong&gt; &lt;strong&gt;静态方法，只能通过接口名调用，不可以通过实现类的类名或者实现类的对象调用。default方法，只能通过接口实现类的对象来调用。&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;4.10 Optional&lt;/h2&gt; &lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; 方法的一个重点尚未提及: &lt;code&gt;reduce&lt;/code&gt; 方法有两种形式，一种如前面出现的需要有一个初始值，另一种变式则不需要有初始值。没有初始值的情况下，&lt;code&gt;reduce&lt;/code&gt; 的第一步使用 &lt;code&gt;Stream&lt;/code&gt; 中的前两个元素。有时，&lt;code&gt;reduce&lt;/code&gt; 操作不存在有意义的初始值，这样做就是有意义的，此时，&lt;code&gt;reduce&lt;/code&gt; 方法返回一个 &lt;code&gt;Optional&lt;/code&gt; 对象。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Optional&lt;/code&gt; 是为核心类库新设计的一个数据类型，用来替换 &lt;code&gt;null&lt;/code&gt; 值。人们对原有的 &lt;code&gt;null&lt;/code&gt; 值有很多抱怨，甚至连发明这一概念的&lt;code&gt;Tony Hoare&lt;/code&gt;也是如此，他曾说这是自己的一个“价值连城的错误”。作为一名有影响力的计算机科学家就是这样:虽然连一毛钱也见不到，却也可以犯一个“价值连城的错误”。&lt;/p&gt; &lt;p&gt;人们常常使用 &lt;code&gt;null&lt;/code&gt; 值表示值不存在，&lt;code&gt;Optional&lt;/code&gt; 对象能更好地表达这个概念。使用 &lt;code&gt;null&lt;/code&gt; 代表值不存在的最大问题在于 &lt;code&gt;NullPointerException&lt;/code&gt;。一旦引用一个存储 &lt;code&gt;null&lt;/code&gt; 值的变量，程序会立即崩溃。使用 &lt;code&gt;Optional&lt;/code&gt; 对象有两个目的:首先，&lt;code&gt;Optional&lt;/code&gt; 对象鼓励程序员适时检查变量是否为空，以避免代码缺陷;其次，它将一个类的 &lt;code&gt;API&lt;/code&gt; 中可能为空的值文档化，这比阅读实现代码要简单很多。&lt;/p&gt; &lt;p&gt;下面我们举例说明 &lt;code&gt;Optional&lt;/code&gt; 对象的 &lt;code&gt;API&lt;/code&gt;，从而切身体会一下它的使用方法。使用工厂方法 &lt;code&gt;of&lt;/code&gt;，可以从某个值创建出一个 &lt;code&gt;Optional&lt;/code&gt; 对象。&lt;code&gt;Optional&lt;/code&gt; 对象相当于值的容器，而该值可以 通过 &lt;code&gt;get&lt;/code&gt; 方法提取。如下代码所示。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Optional&amp;lt;String&amp;gt; a = Optional.of(&amp;quot;a&amp;quot;);     assertEquals(&amp;quot;a&amp;quot;,a.get()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;Optional&lt;/code&gt; 对象也可能为空，因此还有一个对应的工厂方法 &lt;code&gt;empty&lt;/code&gt;，另外一个工厂方法 &lt;code&gt;ofNullable&lt;/code&gt; 则可将一个空值转换成 &lt;code&gt;Optional&lt;/code&gt; 对象。如下代码展示了这两个方法，同时展示了第三个方法 &lt;code&gt;isPresent&lt;/code&gt; 的用法(该方法表示一个 &lt;code&gt;Optional&lt;/code&gt; 对象里是否有值)。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Optional&amp;lt;String&amp;gt; a = Optional.of(&amp;quot;a&amp;quot;);     Optional emptyOptional = Optional.empty();     Optional alsoEmpty = Optional.ofNullable(null);     assertFalse(emptyOptional.isPresent());     assertTrue(a.isPresent()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用 &lt;code&gt;Optional&lt;/code&gt; 对象的方式之一是在调用 &lt;code&gt;get()&lt;/code&gt; 方法前，先使用 &lt;code&gt;isPresent&lt;/code&gt; 检查 &lt;code&gt;Optional&lt;/code&gt; 对象是否有值。使用 &lt;code&gt;orElse&lt;/code&gt; 方法则更简洁，当 &lt;code&gt;Optional&lt;/code&gt; 对象为空时，该方法提供了一个备选值。如果计算备选值在计算上太过繁琐，即可使用 &lt;code&gt;orElseGet&lt;/code&gt; 方法。该方法接受一个 &lt;code&gt;Supplier&lt;/code&gt; 对象，只有在 &lt;code&gt;Optional&lt;/code&gt; 对象真正为空时才会调用。如下代码展示了这两个方法。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;orElse&lt;/code&gt; 和 &lt;code&gt;orElseGet&lt;/code&gt; 方法&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    assertEquals(&amp;quot;b&amp;quot;,emptyOptional.orElse(&amp;quot;b&amp;quot;));     assertEquals(&amp;quot;c&amp;quot;,emptyOptional.orElseGet(() -&amp;gt; &amp;quot;c&amp;quot;)); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;orElseGet&lt;/code&gt;用在需复杂计算的情况，因为可以传入一个&lt;code&gt;Supplier Lambda&lt;/code&gt;表达式&lt;/p&gt; &lt;p&gt;&lt;code&gt;Optional&lt;/code&gt; 对象不仅可以用于新的 &lt;code&gt;Java 8 API&lt;/code&gt;，也可用于具体领域类中，和普通的类别无二致。当试图避免空值相关的缺陷，如未捕获的异常时，可以考虑一下是否可使用 &lt;code&gt;Optional&lt;/code&gt; 对象。&lt;/p&gt; &lt;h2&gt;4.11 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;使用为基本类型定制的&lt;code&gt;Lambda&lt;/code&gt;表达式和&lt;code&gt;Stream&lt;/code&gt;，如&lt;code&gt;IntStream&lt;/code&gt;可以显著提升系统性能。&lt;/li&gt; &lt;li&gt;默认方法是指接口中定义的包含方法体的方法，方法名有&lt;code&gt;default&lt;/code&gt;关键字做前缀。&lt;/li&gt; &lt;li&gt;在一个值可能为空的建模情况下，使用&lt;code&gt;Optional&lt;/code&gt;对象能替代使用&lt;code&gt;null&lt;/code&gt;值。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;4.12 练习&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;在下面的 &lt;code&gt;Performance&lt;/code&gt; 接口基础上，添加 &lt;code&gt;getAllMusicians&lt;/code&gt; 方法，该方法返回包含所有艺术家名字的 &lt;code&gt;Stream&lt;/code&gt;，如果对象是乐队，则返回每个乐队成员的名字。例如，如果 &lt;code&gt;getMusicians&lt;/code&gt; 方法返回甲壳虫乐队，则 &lt;code&gt;getAllMusicians&lt;/code&gt; 方法返回乐队名和乐队成员， 如约翰 · 列侬、保罗 · 麦卡特尼等。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public interface Performance {         String getName();          Stream&amp;lt;Artist&amp;gt; getMusicians();          default Stream&amp;lt;Artist&amp;gt; getAllMusicians(){             return  getMusicians().flatMap(artist -&amp;gt; concat(Stream.of(artist), artist.getMembers()));         }      }  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;一个 &lt;code&gt;Artist&lt;/code&gt; 对象拆成两个流，一个是&lt;code&gt;Artist&lt;/code&gt;自身，一个是成员变量 &lt;code&gt;List&amp;lt;Artist&amp;gt; members&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;flatMap&lt;/code&gt; 方法可用 &lt;code&gt;Stream&lt;/code&gt; 替换值，然后将多个 &lt;code&gt;Stream&lt;/code&gt; 连接成一个 &lt;code&gt;Stream&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;java.util.stream.Stream.concat&lt;/code&gt;：the concatenation of the two input streams（把两个输入流连接级联在一起）&lt;/li&gt; &lt;/ul&gt; &lt;ol start="2"&gt; &lt;li&gt;根据前面描述的重载解析规则，能否重写默认方法中的 &lt;code&gt;equals&lt;/code&gt; 或 &lt;code&gt;hashCode&lt;/code&gt; 方法?&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;下面是官方的回答，理解的意思就是不需要重载&lt;/li&gt; &lt;/ul&gt; &lt;blockquote&gt; &lt;p&gt;No - they are defined on java.lang.Object, and 'class always wins.'&lt;/p&gt; &lt;/blockquote&gt; &lt;ol start="3"&gt; &lt;li&gt;如面的代码所示的 &lt;code&gt;Artists&lt;/code&gt; 类表示了一组艺术家，重构该类，使得 &lt;code&gt;getArtist&lt;/code&gt; 方法返回一 个 &lt;code&gt;Optional&amp;lt;Artist&amp;gt;&lt;/code&gt; 对象。如果索引在有效范围内，返回对应的元素，否则返回一个空 &lt;code&gt;Optional&lt;/code&gt; 对象。此外，还需重构 &lt;code&gt;getArtistName&lt;/code&gt; 方法，保持相同的行为。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;重构前&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class Artists {         private List&amp;lt;Artist&amp;gt; artists;          public Artists(List&amp;lt;Artist&amp;gt; artists) {             this.artists = artists;         }          public Artist getArtist(int index) {             if (index &amp;lt; 0 || index &amp;gt;= artists.size()) {                 indexException(index);             }             return artists.get(index);         }          private void indexException(int index) {             throw new IllegalArgumentException(index +                     &amp;quot;doesn't correspond to an Artist&amp;quot;);         }          public String getArtistName(int index) {             try {                 Artist artist = getArtist(index);                 return artist.getName();             } catch (IllegalArgumentException e) {                 return &amp;quot;unknown&amp;quot;;             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;重构后&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public class Artists {         private List&amp;lt;Artist&amp;gt; artists;          public Artists(List&amp;lt;Artist&amp;gt; artists) {             this.artists = artists;         }          public Optional&amp;lt;Artist&amp;gt; getArtist(int index) {             if (index &amp;lt; 0 || index &amp;gt;= artists.size()) {                 return Optional.empty();             }             return Optional.of(artists.get(index));         }          private void indexException(int index) {             throw new IllegalArgumentException(index +                     &amp;quot;doesn't correspond to an Artist&amp;quot;);         }          public String getArtistName(int index) {             if (getArtist(index).isPresent()) {                 Artist artist = getArtist(index).get();                 return artist.getName();             }             return &amp;quot;unknown&amp;quot;;         }     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;参考答案（&lt;code&gt;getArtistName&lt;/code&gt;可以写的更简洁）&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public String getArtistName(int index) {         Optional&amp;lt;Artist&amp;gt; artist = getArtist(index);         return artist.map(Artist::getName)                      .orElse(&amp;quot;unknown&amp;quot;);     } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;4.13 开放练习&lt;/h2&gt; &lt;p&gt;审阅工作代码库或熟悉的开源项目代码，找出哪些只包含静态方法的类适合用包含静态方法的接口替代。如有可能，和同事一起讨论，看他们是否赞同你找出的结果。&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 25 Oct 2018 15:51:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇二之流</title>
      <link>https://www.zhangaoo.com/article/lambda-stream</link>
      <content:encoded>&lt;h1&gt;第三章 流&lt;/h1&gt; &lt;p&gt;Java 8 对核心类库的改进主要包括集合类的 API 和新引入的流 (Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作。&lt;/p&gt; &lt;h2&gt;3.1 从外部迭代到内部迭代&lt;/h2&gt; &lt;p&gt;传统使用 &lt;code&gt;for&lt;/code&gt; 循环计算来自伦敦的艺术家人数&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;int count = 0; for (Artist artist : allArtists) {     if (artist.isFrom(&amp;quot;London&amp;quot;)) {          count++;     }  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这样的操作可行，但存在几个问题:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;需要写很多样板代码&lt;/li&gt; &lt;li&gt;将 &lt;code&gt;for&lt;/code&gt; 循环改造成并行方式运行也很麻烦&lt;/li&gt; &lt;li&gt;无法流畅传达程序员的意图,单一的 for 循环，问题不大，多重嵌套循环的大代码库时，负担就重了。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;for 循环其实是一个封装了迭代的语法糖，首先调用 iterator 方法，产生一个新的 Iterator 对象，进而控制整 个迭代过程，这就是&lt;code&gt;外部迭代&lt;/code&gt;，可简单理解为需要手动在业务代码中写迭代过程。它从本质上来讲是一种串行化操作。总体来看，使用 for 循环会将行为和方法混为一谈。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/7a1e355m38h12rg99uchap1quj.png" alt="alt" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;int count = 0; Iterator&amp;lt;Artist&amp;gt; iterator = allArtists.iterator();  while(iterator.hasNext()) {     Artist artist = iterator.next();      if (artist.isFrom(&amp;quot;London&amp;quot;)) {         count++;      } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用&lt;code&gt;内部迭代&lt;/code&gt;计算来自伦敦的艺术家人数，内部迭代可简单理解为迭代过程是在函数库内部。&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/1cem50iu46iefo1diid5h66cc2.png" alt="alt" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//该方法不是返回一个控制迭代的 Iterator 对象，而是返回内部迭代中的相应接口:Stream。 long count = allArtists.stream()                        .filter(artist -&amp;gt; artist.isFrom(&amp;quot;London&amp;quot;))                        .count(); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;3.2 实现机制&lt;/h2&gt; &lt;h3&gt;及早求值方法与惰性求值方法&lt;/h3&gt; &lt;p&gt;如下代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;allArtists.stream()           .filter(artist -&amp;gt; {             System.out.println(artist.getName());             return artist.isFrom(&amp;quot;London&amp;quot;); }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这行代码并未做什么实际性的工作，&lt;code&gt;filter&lt;/code&gt; 只刻画出了 &lt;code&gt;Stream&lt;/code&gt;，但没有产生新的集合。像 &lt;code&gt;filter&lt;/code&gt; 这样只描述 &lt;code&gt;Stream&lt;/code&gt;，最终不产生新集合的方法叫作 &lt;strong&gt;惰性求值方法&lt;/strong&gt;;而像 &lt;code&gt;count&lt;/code&gt; 这样 最终会从 &lt;code&gt;Stream&lt;/code&gt; 产生值的方法叫作 &lt;strong&gt;及早求值方法&lt;/strong&gt;。运行这段代码，程序不会输出任何信息&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;long count = allArtists.stream()                        .filter(artist -&amp;gt; {                         System.out.println(artist.getName());                         return artist.isFrom(&amp;quot;London&amp;quot;); })                        .count(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;运行上述程序，命令行里输出复合条件的值。&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 &lt;code&gt;Stream&lt;/code&gt;， 那么是惰性求值;如果返回值是另一个值或为空，那么就是及早求值。使用这些操作的理 想方式就是形成一个惰性求值的链，最后用一个及早求值的操作返回想要的结果，这正是 它的合理之处。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置，&lt;strong&gt;最后调 用一个 &lt;code&gt;build&lt;/code&gt; 方法，这时，对象才被真正创建&lt;/strong&gt;。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;为什么要区分惰性求值和及早求值? 只有在对需要什么样的结果和操作有了更多了解之后，才能更有效率地进行计算。例如，如果要找出大于 &lt;code&gt;10&lt;/code&gt; 的第一个数 字，那么并不需要和所有元素去做比较，只要找出第一个匹配的元素就够了。这也意味着 可以在集合类上级联多种操作，但迭代只需一次。&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;常用的流操作&lt;/h2&gt; &lt;h3&gt;3.3.1 collect(toList())&lt;/h3&gt; &lt;p&gt;&lt;code&gt;collect(toList())&lt;/code&gt; 方法由 &lt;code&gt;Stream&lt;/code&gt; 里的值生成一个列表，是一个及早求值操作。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;String&amp;gt; collected = Stream.of(&amp;quot;a&amp;quot;,&amp;quot;b&amp;quot;,&amp;quot;c&amp;quot;)                                 .collect(Collectors.toList());     assertEquals(Arrays.asList(&amp;quot;a&amp;quot;,&amp;quot;b&amp;quot;,&amp;quot;c&amp;quot;),collected); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;由于很多 &lt;code&gt;Stream&lt;/code&gt; 操作都是惰性求值，因此调用 &lt;code&gt;Stream&lt;/code&gt; 上一系列方法之后，还需要最后再 调用一个类似 &lt;code&gt;collect&lt;/code&gt; 的 &lt;strong&gt;及早求值方法&lt;/strong&gt;。&lt;/p&gt; &lt;h3&gt;3.3.2 map&lt;/h3&gt; &lt;p&gt;如果有一个函数可以将一种类型的值转换成另外一种类型，&lt;code&gt;map&lt;/code&gt; 操作就可以 使用该函数，将一个流中的值转换成一个新的流。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;for&lt;/code&gt; 循环将字符串转换为大写&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;String&amp;gt; collected = new ArrayList&amp;lt;&amp;gt;(); for (String string : asList(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;hello&amp;quot;)) {           String uppercaseString = string.toUpperCase();           collected.add(uppercaseString);       } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;map&lt;/code&gt; 操作将字符串转换为大写形式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;String&amp;gt; collected = Stream.of(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;hello&amp;quot;)                         .map(string -&amp;gt; string.toUpperCase())                           .collect(toList()); assertEquals(Arrays.asList(&amp;quot;A&amp;quot;,&amp;quot;B&amp;quot;,&amp;quot;HELLO&amp;quot;),collected);                         &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;传给&lt;code&gt;map&lt;/code&gt;的lambda表达式必须是&lt;code&gt;Function&lt;/code&gt;的一个实例，参数和返回值不用是同一种类型。&lt;/p&gt; &lt;h3&gt;3.3.3 filter&lt;/h3&gt; &lt;p&gt;上面的例子已经可以看到，&lt;code&gt;filter&lt;/code&gt;是用来过滤几个元素的，&lt;code&gt;filter&lt;/code&gt;返回的是&lt;code&gt;stream&lt;/code&gt;，因此也是 &lt;strong&gt;惰性求值方法&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;过滤包含指定子串的字符串&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;String&amp;gt; collected = Stream.of(&amp;quot;a&amp;quot;,&amp;quot;b&amp;quot;,&amp;quot;hello&amp;quot;,&amp;quot;asdabc&amp;quot;,&amp;quot;ab&amp;quot;)         .filter(str -&amp;gt; str.contains(&amp;quot;ab&amp;quot;))         .collect(Collectors.toList()); assertEquals(Arrays.asList(&amp;quot;asdabc&amp;quot;,&amp;quot;ab&amp;quot;),collected); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;filter&lt;/code&gt;接受一个函数作为参数，该函数必须是&lt;code&gt;Predicate&lt;/code&gt;的一个实例，因此返回值必须是&lt;code&gt;true&lt;/code&gt;或&lt;code&gt;false&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;3.3.4 flatMap&lt;/h3&gt; &lt;p&gt;&lt;code&gt;flatMap&lt;/code&gt; 方法可用 &lt;code&gt;Stream&lt;/code&gt; 替换值，然后将多个 &lt;code&gt;Stream&lt;/code&gt; 连接成一个 &lt;code&gt;Stream&lt;/code&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;包含多个列表的 &lt;code&gt;Stream&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;Integer&amp;gt; together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))         .flatMap(numbers -&amp;gt; numbers.stream())         .collect(Collectors.toList()); assertEquals(Arrays.asList(1, 2, 3, 4), together); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;调用 &lt;code&gt;stream&lt;/code&gt; 方法，将每个列表转换成 &lt;code&gt;Stream&lt;/code&gt; 对象，其余部分由 &lt;code&gt;flatMap&lt;/code&gt; 方法处理。 &lt;code&gt;flatMap&lt;/code&gt; 方法的相关函数接口和 &lt;code&gt;map&lt;/code&gt; 方法的一样，都是 &lt;code&gt;Function&lt;/code&gt; 接口，只是方法的返回值 限定为 &lt;code&gt;Stream&lt;/code&gt; 类型罢了&lt;/p&gt; &lt;h3&gt;3.3.5 max和min&lt;/h3&gt; &lt;p&gt;使用 &lt;code&gt;Stream&lt;/code&gt; 查找长度最短的字符串&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    String min = Stream.of(&amp;quot;123&amp;quot;, &amp;quot;4567&amp;quot;,&amp;quot;89101112&amp;quot;)             .min(Comparator.comparing(str -&amp;gt; str.length()))             .get();     System.out.println(min); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查找 &lt;code&gt;Stream&lt;/code&gt; 中的最大或最小元素，首先要考虑的是用什么作为排序的指标，这里使用字符串长度作为排序的指标。为了让&lt;code&gt;Stream&lt;/code&gt;对象按照曲目长度进行排序，需要传给它一个&lt;code&gt;Comparator&lt;/code&gt;对象。&lt;code&gt;Java 8&lt;/code&gt; 提 供了一个新的静态方法&lt;code&gt;comparing&lt;/code&gt;，使用它可以方便地实现一个比较器。放在以前，我们 需要比较两个对象的某项属性的值，现在只需要提供一个存取方法就够了&lt;/p&gt; &lt;h3&gt;3.3.6 通用模式&lt;/h3&gt; &lt;p&gt;上面找最小值的如果不使用Lambda表达式，最一般的for循环遍历代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;String&amp;gt; strs = Arrays.asList(&amp;quot;123&amp;quot;, &amp;quot;4567&amp;quot;,&amp;quot;89101112&amp;quot;);     String minStr = strs.get(0);     for(String str : strs){         if(str.length() &amp;lt; minStr.length()){             minStr = str;         }     }     assertEquals(strs.get(0),minStr); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上这种模式总结一下，可称为&lt;code&gt;reduce&lt;/code&gt;模式，更一般的代码形式如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Object accumulator = initialValue;      for(Object element : collection) {         accumulator = combine(accumulator, element);     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;首先赋给 &lt;code&gt;accumulator&lt;/code&gt; 一个初始值:&lt;code&gt;initialValue&lt;/code&gt;，然后在循环体中，通过调用 &lt;code&gt;combine&lt;/code&gt; 函数，拿 &lt;code&gt;accumulator&lt;/code&gt; 和集合中的每一个元素做运算，再将运算结果赋给 &lt;code&gt;accumulator&lt;/code&gt;，最后 &lt;code&gt;accumulator&lt;/code&gt; 的值就是想要的结果。&lt;/p&gt; &lt;h3&gt;3.3.7 reduce&lt;/h3&gt; &lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; 操作可以实现从一组值中生成一个值。在上述例子中用到的 &lt;code&gt;count&lt;/code&gt;、&lt;code&gt;min&lt;/code&gt; 和 &lt;code&gt;max&lt;/code&gt; 方 法，因为常用而被纳入标准库中。事实上，这些方法都是 &lt;code&gt;reduce&lt;/code&gt; 操作。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用 &lt;code&gt;reduce&lt;/code&gt; 求和&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;int sum = Stream.of(1,2,3,4,5,6)                 .reduce(0,(acc,element)-&amp;gt;acc+element); assertEquals(21,sum);                 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式的返回值是最新的 &lt;code&gt;acc&lt;/code&gt;，是上一轮 &lt;code&gt;acc&lt;/code&gt; 的值和当前元素相加的结果。&lt;code&gt;reducer&lt;/code&gt; 的类型是第 2 章已介绍过的 &lt;code&gt;BinaryOperator&lt;/code&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;展开 reduce 操作&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;BinaryOperator&amp;lt;Integer&amp;gt; accumulator = (acc, element) -&amp;gt; acc + element;  int count = accumulator.apply(                      accumulator.apply(                          accumulator.apply(0, 1),                     2),                  3); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;BinaryOperator&lt;/code&gt;是&lt;code&gt;Function interface&lt;/code&gt;（包含唯一接口&lt;code&gt;apply&lt;/code&gt;），Lambda表达式实际上就是实现了&lt;code&gt;apply&lt;/code&gt;函数，因此我们也可以显示调用&lt;code&gt;apply&lt;/code&gt;方法。&lt;/p&gt; &lt;h3&gt;3.3.8 整合操作&lt;/h3&gt; &lt;p&gt;Stream 接口的方法如此之多，有时会让人难以选择，本节将举例说明如何将问题分解为简单的 &lt;code&gt;Stream&lt;/code&gt; 操作。&lt;/p&gt; &lt;p&gt;第一个要解决的问题是，找出某张专辑上所有乐队的国籍。艺术家列表里既有个人，也有 乐队。利用一点领域知识，假定一般乐队名以定冠词 &lt;code&gt;The&lt;/code&gt; 开头。当然这不是绝对的，但也差不多。 首先， 可将这个问题分解为如下几个步骤。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;找出专辑上的所有表演者。&lt;/li&gt; &lt;li&gt;分辨出哪些表演者是乐队。&lt;/li&gt; &lt;li&gt;找出每个乐队的国籍。&lt;/li&gt; &lt;li&gt;将找出的国籍放入一个集合。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    List&amp;lt;Artist&amp;gt; artists = Arrays.asList(new Artist(&amp;quot;aert&amp;quot;, &amp;quot;China&amp;quot;), new Artist(&amp;quot;adasda&amp;quot;, &amp;quot;USA&amp;quot;),                                             new Artist(&amp;quot;qweqvsf&amp;quot;, &amp;quot;Jap&amp;quot;), new Artist(&amp;quot;The askjkj&amp;quot;, &amp;quot;England&amp;quot;));     List&amp;lt;String&amp;gt; nations = artists.stream()                             .filter(str -&amp;gt; str.getName().startsWith(&amp;quot;The&amp;quot;))                             .map(art -&amp;gt; art.getFrom())                             .collect(Collectors.toList());     assertEquals(Arrays.asList(&amp;quot;England&amp;quot;),nations); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个例子将 &lt;code&gt;Stream&lt;/code&gt; 的链式操作展现得淋漓尽致，调用 &lt;code&gt;filter&lt;/code&gt; 和 &lt;code&gt;map&lt;/code&gt; 方法都 返回 &lt;code&gt;Stream&lt;/code&gt; 对象，因此都属于惰性求值，而 &lt;code&gt;collect&lt;/code&gt; 方法属于及早求值。&lt;code&gt;map&lt;/code&gt; 方法接受一 个 &lt;code&gt;Lambda&lt;/code&gt; 表达式，使用该 &lt;code&gt;Lambda&lt;/code&gt; 表达式对 &lt;code&gt;Stream&lt;/code&gt; 上的每个元素做映射，形成一个新的 &lt;code&gt;Stream&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;3.4 重构遗留代码&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;本节将举例说明如何将一段使用循环进行集合操作的 代码，重构成基于 &lt;code&gt;Stream&lt;/code&gt; 的操作。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;假定选定一组专辑，找出其中所有长度大于 1 分钟的曲目名称。下面是遗留代码，首先 初始化一个 Set 对象，用来保存找到的曲目名称。然后使用 for 循环遍历所有专辑，每次 循环中再使用一个 for 循环遍历每张专辑上的每首曲目，检查其长度是否大于 60 秒，如 果是，则将该曲目名称加入 Set 对象。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Set&amp;lt;String&amp;gt; findLongTracks(List&amp;lt;Album&amp;gt; albums) {         Set&amp;lt;String&amp;gt; trackNames = new HashSet&amp;lt;&amp;gt;();         for (Album album : albums) {             for (Track track : album.getTracks()) {                 if (track.getLength() &amp;gt; 60) {                     String name = track.getSongName();                     trackNames.add(name);                 }             }         }         return trackNames;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用流来重构该段代码的方式很多，下面介绍的只是其 中一种。事实上，对 &lt;code&gt;Stream API&lt;/code&gt; 越熟悉，就越不需要细分步骤。之所以在示例中一步一步地重构，完全是出于帮助大家学习的目的，在工作中无需这样做。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;第一步要修改的是 &lt;code&gt;for&lt;/code&gt; 循环。首先使用 &lt;code&gt;Stream&lt;/code&gt; 的 &lt;code&gt;forEach&lt;/code&gt; 方法替换掉 &lt;code&gt;for&lt;/code&gt; 循环，但还是暂时保留原来循环体中的代码，这是在重构时非常方便的一个技巧。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Set&amp;lt;String&amp;gt; findLongTracksStep1(List&amp;lt;Album&amp;gt; albums) {         Set&amp;lt;String&amp;gt; trackNames = new HashSet&amp;lt;&amp;gt;();         albums.stream()                 .forEach(album -&amp;gt; {                     album.getTracks()                             .forEach(track -&amp;gt; {                                 if (track.getLength() &amp;gt; 60) {                                     trackNames.add(track.getSongName());                                 }                             });                 });         return trackNames;     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;重构的第二步:找出长度大于 1 分钟的曲目&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Set&amp;lt;String&amp;gt; findLongTracksStep2(List&amp;lt;Album&amp;gt; albums) {         Set&amp;lt;String&amp;gt; trackNames = new HashSet&amp;lt;&amp;gt;();         albums.stream()                 .forEach(album -&amp;gt; {                     album.getTracksStream()                             .filter(track -&amp;gt; track.getLength() &amp;gt; 60)                             .map(track -&amp;gt; track.getSongName())                             .forEach(name -&amp;gt; trackNames.add(name));                 });         return trackNames;     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;重构外层循环&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Set&amp;lt;String&amp;gt; findLongTracksStep3(List&amp;lt;Album&amp;gt; albums) {         Set&amp;lt;String&amp;gt; trackNames = new HashSet&amp;lt;&amp;gt;();         albums.stream()                 .flatMap(album -&amp;gt; album.getTracksStream())                 .filter(track -&amp;gt; track.getLength() &amp;gt; 60)                 .map(track -&amp;gt; track.getSongName())                 .forEach(name -&amp;gt; trackNames.add(name));          return trackNames;     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上面的代码中使用一组简洁的方法调用替换掉两个嵌套的 &lt;code&gt;for&lt;/code&gt; 循环，看起来清晰很多。然 而至此并未结束，仍需手动创建一个 &lt;code&gt;Set&lt;/code&gt; 对象并将元素加入其中，但我们希望看到的是整 个计算任务由一连串的 &lt;code&gt;Stream&lt;/code&gt; 操作完成。&lt;/p&gt; &lt;ol start="4"&gt; &lt;li&gt;重构手动创建一个 &lt;code&gt;Set&lt;/code&gt; 对象并将元素&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Set&amp;lt;String&amp;gt; findLongTracksStep4(List&amp;lt;Album&amp;gt; albums) {         return                 albums.stream()                         .flatMap(album -&amp;gt; album.getTracksStream())                         .filter(track -&amp;gt; track.getLength() &amp;gt; 60)                         .map(track -&amp;gt; track.getSongName())                         .collect(Collectors.toSet());      } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简而言之，选取一段遗留代码进行重构，转换成使用流风格的代码。最初只是简单地使用 流，但没有引入任何有用的流操作。随后通过一系列重构，最终使代码更符合使用流的风 格。在上述步骤中我们没有提到一个重点，即编写示例代码的每一步都要进行单元测试，保证代码能够正常工作。重构遗留代码时，这样做很有帮助。&lt;/p&gt; &lt;h2&gt;3.5 多次调用流操作&lt;/h2&gt; &lt;p&gt;用户也可以选择每一步强制对函数求值，而不是将所有的方法调用链接在一起，但是，最 好不要如此操作。展示了如何用如上述不建议的编码风格来找出专辑上所有演出乐 队的国籍。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;误用 Stream 的例子&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;Artist&amp;gt; musicians = album.getMusicians()                                    .collect(toList()); List&amp;lt;Artist&amp;gt; bands = musicians.stream()                                    .filter(artist -&amp;gt; artist.getName().startsWith(&amp;quot;The&amp;quot;))                                    .collect(toList()); Set&amp;lt;String&amp;gt; origins = bands.stream()                                 .map(artist -&amp;gt; artist.getNationality())                                 .collect(toSet());                                                                       &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;符合 Stream 使用习惯的链式调用&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Set&amp;lt;String&amp;gt; origins = album.getMusicians()                                 .filter(artist -&amp;gt; artist.getName().startsWith(&amp;quot;The&amp;quot;))                                 .map(artist -&amp;gt; artist.getNationality())                                 .collect(toSet()); &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;第一个例子和流的链式调用相比有如下缺点&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;代码可读性差，样板代码太多，隐藏了真正的业务逻辑;&lt;/li&gt; &lt;li&gt;效率差，每一步都要对流及早求值，生成新的集合;&lt;/li&gt; &lt;li&gt;代码充斥一堆垃圾变量，它们只用来保存中间结果，除此之外毫无用处;&lt;/li&gt; &lt;li&gt;难于自动并行化处理。&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;3.6 高阶函数&lt;/h2&gt; &lt;p&gt;高阶函数是指接受另外一个函数作为参数，或返回一个函数的函数。高阶函数不难辨认:看函数签名就够了。如果函数的参数列表里包含函数接口，或该函数返回一个函数接口，那么该函数就是高阶函数。&lt;/p&gt; &lt;p&gt;map 是一个高阶函数，因为它的 mapper 参数是一个函数。事实上，本章介绍的 Stream 接口 中几乎所有的函数都是高阶函数。之前的排序例子中还用到了 comparing 函数，它接受一 个函数作为参数，获取相应的值，同时返回一个 Comparator。Comparator 可能会被误认为 是一个对象，但它有且只有一个抽象方法，所以实际上是一个函数接口。&lt;/p&gt; &lt;p&gt;事实上，可以大胆断言，Comparator 实际上应该是个函数，但是那时的 Java 只有对象，因 此才造出了一个类，一个匿名类。成为对象实属巧合，函数接口向正确的方向迈出了一步。&lt;/p&gt; &lt;h2&gt;3.7 正确使用Lambda表达式&lt;/h2&gt; &lt;p&gt;本章介绍的概念能够帮助用户写出更简单的代码，因为这些概念描述了数据上的操作，明确了要达成什么转化(理解为大部分操作都可以转化成接口函数，比如：&lt;code&gt;Predicate&lt;/code&gt;、&lt;code&gt;Function&lt;/code&gt;、&lt;code&gt;Consume&lt;/code&gt;等等)，而不是说明如何转化。这种方式写出的代码，潜在的缺陷更少，更直接地表达了程序员的意图。&lt;/p&gt; &lt;p&gt;没有副作用的函数不会改变程序或外界的状态。本书中的第一个 &lt;code&gt;Lambda&lt;/code&gt; 表达式示例是有副作用的，它向控制台输出了信息——一个可观测到的副作用。下面的代码有没有副作用?&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private ActionEvent lastEvent; private void registerHandler() {      button.addActionListener((ActionEvent event) -&amp;gt; {         this.lastEvent = event;     }); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上代码有副作用，只要是给类的成员变量赋值就肯定会改变类的状态，那么就一定有副作用。&lt;/p&gt; &lt;p&gt;无论何时，将 &lt;code&gt;Lambda&lt;/code&gt; 表达式传给 &lt;code&gt;Stream&lt;/code&gt; 上的高阶函数，都应该尽量避免副作用。唯一的 例外是 &lt;code&gt;forEach&lt;/code&gt; 方法，它是一个终结方法。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;这节理解的不是很透彻，需要后续重点关注一下&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;3.8 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;内部迭代将更多控制权交给了集合类。&lt;/li&gt; &lt;li&gt;和&lt;code&gt;Iterator&lt;/code&gt;类似，&lt;code&gt;Stream&lt;/code&gt;是一种内部迭代方式。&lt;/li&gt; &lt;li&gt;将&lt;code&gt;Lambda&lt;/code&gt;表达式和&lt;code&gt;Stream&lt;/code&gt;上的方法结合起来，可以完成很多常见的集合操作。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;3.9 练习&lt;/h2&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;常用流操作。实现如下函数: a. 编写一个求和函数，计算流中所有数之和。例如，int addUp(Stream&lt;Integer&gt; numbers);&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static int addUp(Stream&amp;lt;Integer&amp;gt; numbers) {     return numbers.reduce(0, (acc, num) -&amp;gt; acc + num); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;b. 编写一个函数，接受艺术家列表作为参数，返回一个字符串列表，其中包含艺术家的姓名和国籍;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static List&amp;lt;String&amp;gt; getArtistNameAndNation(List&amp;lt;Artist&amp;gt; artists){     return artists.stream()             .flatMap(artist -&amp;gt; Stream.of(artist.getName(),artist.getFrom()))             .collect(Collectors.toList());  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这个题目刚开始没理解对直接把国籍生成的List追加到了名称的结果List，这里主要还是&lt;code&gt;flatMap&lt;/code&gt;的用法 &lt;strong&gt;可用 &lt;code&gt;Stream&lt;/code&gt; 替换值，然后将多个 &lt;code&gt;Stream&lt;/code&gt; 连接成一个 &lt;code&gt;Stream&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;c. 编写一个函数，接受专辑列表作为参数，返回一个由最多包含 3 首歌曲的专辑组成的列表。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static List&amp;lt;Album&amp;gt; filterAlbum(List&amp;lt;Album&amp;gt; albums){     return albums.stream()             .filter(album -&amp;gt; album.getTracks().size() &amp;lt;= 3)             .collect(Collectors.toList()); } &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;迭代。修改如下代码，将外部迭代转换成内部迭代:&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;int totalMembers = 0; for (Artist artist : artists) {    Stream&amp;lt;Artist&amp;gt; members = artist.getMembers();    totalMembers += members.count(); }  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;修改后代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;artists.stream()        .flatMap(artist -&amp;gt; artist.getMembers())        .reduce(0,(acc,members) -&amp;gt; member.count()) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt; &lt;p&gt;求值。根据 &lt;code&gt;Stream&lt;/code&gt; 方法的签名，判断其是惰性求值还是及早求值。 a. boolean anyMatch(Predicate&amp;lt;? super T&amp;gt; predicate);//及早求值 b. Stream&lt;T&gt; limit(long maxSize);//惰性求值&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;高阶函数。下面的 &lt;code&gt;Stream&lt;/code&gt; 函数是高阶函数吗?为什么? a. boolean anyMatch(Predicate&amp;lt;? super T&amp;gt; predicate); //高阶函数，因为参数传递是一组操作（函数） b. Stream&lt;T&gt; limit(long maxSize);//非高阶函数，返回值或参数非一组操作&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;纯函数。下面的 Lambda 表达式有无副作用，或者说它们是否更改了程序状态?&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    x-&amp;gt;x+1//无副作用，未改变程序的状态 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;示例代码如下所示:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    AtomicInteger count = new AtomicInteger(0);      List&amp;lt;String&amp;gt; origins = album.musicians()     .forEach(musician -&amp;gt; count.incAndGet();)  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;a. 上述示例代码中传入 &lt;code&gt;forEach&lt;/code&gt; 方法的 &lt;code&gt;Lambda&lt;/code&gt; 表达式。//它是一个终结方法&lt;/p&gt; &lt;ol start="6"&gt; &lt;li&gt;计算一个字符串中小写字母的个数(提示:参阅 String 对象的 chars 方法)。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static long countLowercase(String str){         return str.chars()                 .filter(ch -&amp;gt; ch &amp;gt;= 'a' &amp;amp;&amp;amp; ch &amp;lt;= 'z')                 .count();     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="7"&gt; &lt;li&gt;在一个字符串列表中，找出包含最多小写字母的字符串。对于空列表，返回&lt;code&gt;Optional&amp;lt;String&amp;gt;&lt;/code&gt;对象。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public static Optional&amp;lt;String&amp;gt; mostUpcaseStr(List&amp;lt;String&amp;gt; strs) {         return strs.stream()                 .max(Comparator.comparing(str -&amp;gt; str.chars()                         .filter(ch -&amp;gt; ch &amp;gt;= 'a' &amp;amp;&amp;amp; ch &amp;lt;= 'z')                         .count()));     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;思路是利用&lt;code&gt;max&lt;/code&gt;方法，需要自定义比较函数&lt;/p&gt; &lt;h2&gt;3.10 进阶练习&lt;/h2&gt; &lt;p&gt;简单看了一下题目，感觉完全没思路（-__-!!），看了一下系统的默认实现，也是没怎么看懂，只能留着过几天再来看一下&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;只用 &lt;code&gt;reduce&lt;/code&gt; 和 &lt;code&gt;Lambda&lt;/code&gt; 表达式写出实现 &lt;code&gt;Stream&lt;/code&gt; 上的 &lt;code&gt;map&lt;/code&gt; 操作的代码，如果不想返回 &lt;code&gt;Stream&lt;/code&gt;，可以返回一个 &lt;code&gt;List&lt;/code&gt;。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;只用 &lt;code&gt;reduce&lt;/code&gt; 和 &lt;code&gt;Lambda&lt;/code&gt; 表达式写出实现 &lt;code&gt;Stream&lt;/code&gt; 上的 &lt;code&gt;filter&lt;/code&gt; 操作的代码，如果不想返回 &lt;code&gt;Stream&lt;/code&gt;，可以返回一个 &lt;code&gt;List&lt;/code&gt;。&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt;</content:encoded>
      <pubDate>Sat, 13 Oct 2018 08:15:00 GMT</pubDate>
    </item>
    <item>
      <title>SpringCloud篇三之Ribbon-Retry</title>
      <link>https://www.zhangaoo.com/article/ribbon-retry</link>
      <content:encoded>&lt;h1&gt;背景&lt;/h1&gt; &lt;p&gt;微服务通常都要求高可靠性，光靠前面介绍的&lt;a href="https://www.zhangaoo.com/article/ribbon#directory092092130743566310" target="_blank"&gt;Load Balance Rule&lt;/a&gt;还不能完全解决我们的问题。如下图：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;当请求还在&lt;code&gt;1&lt;/code&gt;处时，此时如果&lt;code&gt;Service3&lt;/code&gt;服务不可用了，&lt;code&gt;Gateway&lt;/code&gt;中的&lt;code&gt;Ribbon&lt;/code&gt; 如果使用的&lt;code&gt;BestAvailableRule&lt;/code&gt;，&lt;code&gt;AvailabilityFilteringRule&lt;/code&gt;等&lt;code&gt;Load Balance Rule&lt;/code&gt;的话可以剔除不可用的&lt;code&gt;Service3&lt;/code&gt;服务，这种情况服务不会出错。&lt;/li&gt; &lt;li&gt;但是当请求已经经由&lt;code&gt;Gateway&lt;/code&gt;转发已经到达了&lt;code&gt;4&lt;/code&gt;处时，如果只配置&lt;code&gt;Load Balance Rule&lt;/code&gt;的话已经无能为力了，因为&lt;code&gt;Load Balance Rule&lt;/code&gt;只是做&lt;code&gt;Server&lt;/code&gt;的选择，此时已经选定&lt;code&gt;Service3&lt;/code&gt;，这种情况就会导致本次调用出错。&lt;/li&gt; &lt;li&gt;如果我们配置使用了&lt;code&gt;Ribbon&lt;/code&gt;的&lt;code&gt;Retry&lt;/code&gt;策略的话，即使请求已经到达&lt;code&gt;4&lt;/code&gt;处，并且本次会请求失败，但是在请求失败后可以根据策略进行一次或多次 &lt;code&gt;Retry&lt;/code&gt; ，尝试的机制则可以最大限度的规避这种特殊情况的服务不可用用的情况&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/10/tjho2efet4g40o38toat0d2s5i.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h1&gt;配置使用Retry策略&lt;/h1&gt; &lt;p&gt;基本架构如下：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;一个 &lt;code&gt;Eureka&lt;/code&gt; (非集群模式，集群模式配置有差异)&lt;/li&gt; &lt;li&gt;一个网关（&lt;code&gt;Gateway&lt;/code&gt;）反向代理服务，ribbon做&lt;code&gt;LB Rule&lt;/code&gt;，&lt;code&gt;Retry&lt;/code&gt;模块做失败请求&lt;code&gt;Retry&lt;/code&gt;;网关从&lt;code&gt;Eureka&lt;/code&gt;发现服务并反向代理；&lt;/li&gt; &lt;li&gt;三个服务节点，把服务注册到&lt;code&gt;Eureka&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;配置注意点&lt;/h1&gt; &lt;h2&gt;Eureka&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 7001  eureka:   instance:     hostname: localhost   client:     registerWithEureka: false # 不把自己注册到服务注册中心     fetchRegistry: false #不从服务注册中心取服务，因为是单节点的Eureka，因此只提供给服务注册和网关取服务     service-url:        defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/   server:     enable-self-preservation: false #在调试时关闭eureka注册中心的保护机制  &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;服务模块&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   application:     name: service-provide #服务的名称，如果是一个服务集群，则此名字必须一样 server:   host: 127.0.0.1   port: 0 eureka:   client:     service-url:       defaultZone: http://127.0.0.1:7001/eureka/   instance:     instance-id: user-service1 #相同的服务，本字段也必须唯一，也就是一个同一个服务集群本字段也不能相同 &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;网关模块&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 8080  spring:   application:     name: zuul-gateway eureka:    instance:      hostname: localhost    client:      register-with-eureka: false      fetch-registry: true      service-url:        defaultZone: http://${eureka.instance.hostname}:7001/eureka/ zuul:   routes:     skytsdb:       path: /**       serviceId: service-provide       retryable: true  #负载均衡规则，根据ServiceID设置，重点就是这几行配置 service-provide:   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule     MaxAutoRetries: 0           # Max number of retries on the same server (excluding the first try)     MaxAutoRetriesNextServer: 3 # Max number of next servers to retry (excluding the first server)     OkToRetryOnAllOperations: true &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;注意POM需&lt;code&gt;retry&lt;/code&gt;模块依赖 不在该模块正常请求也不会报错，但当有请求失败的时候不会进行尝试会直接报错&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;dependency&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.retry&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-retry&amp;lt;/artifactId&amp;gt;     &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;完整github代码&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://github.com/zealzhangz/SpringCloud-Ribbon-Retry" target="_blank"&gt;github代码&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 11 Oct 2018 11:24:00 GMT</pubDate>
    </item>
    <item>
      <title>Java8函数式编程篇一</title>
      <link>https://www.zhangaoo.com/article/java8-function-interface</link>
      <content:encoded>&lt;p&gt;&lt;img src="http://img.zhangaoo.com/20203411635-011.png" alt="20203411635-011" /&gt;&lt;/p&gt; &lt;h1&gt;第一章&lt;/h1&gt; &lt;h2&gt;1.1 修改Java的目的&lt;/h2&gt; &lt;p&gt;&lt;code&gt;java.util.concurrent&lt;/code&gt;的不足，让代码在多核 &lt;code&gt;CPU&lt;/code&gt; 上高效运行。为了编写这类处理批量数据的并行类库，需要在语言层面上修改现有的&lt;code&gt;Java&lt;/code&gt;:增加 &lt;code&gt;Lambda&lt;/code&gt; 表达式。&lt;/p&gt; &lt;p&gt;面向对象编程是对数据进 行抽象，而函数式编程是对行为进行抽象。&lt;/p&gt; &lt;p&gt;Java 8还让集合类可以拥有一些额外的方法:default方法。程序员在维护自己的类库时， 可以使用这些方法。&lt;/p&gt; &lt;h2&gt;1.2 什么是函数式编程&lt;/h2&gt; &lt;p&gt;其核心是:在思考问题时，使用不可变值和函 数，函数对一个值进行处理，映射成另一个值。&lt;/p&gt; &lt;h1&gt;第二章&lt;/h1&gt; &lt;h2&gt;2.1 Lambda表达式&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;一个&lt;code&gt;Swing Button&lt;/code&gt;点击事件处理的例子：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;button.addActionListener(new ActionListener() {      public void actionPerformed(ActionEvent event) {              System.out.println(&amp;quot;button clicked&amp;quot;);          } }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;ActionListener&lt;/code&gt;是动作监听接口，动作处理需实现接口中的&lt;code&gt;actionPerformed&lt;/code&gt;方法，一般的做法如上直接&lt;code&gt;new&lt;/code&gt;一个匿名类，然后直接实现&lt;code&gt;actionPerformed&lt;/code&gt;方法。设计匿名内部类的目的，就是为了方便 &lt;code&gt;Java&lt;/code&gt; 程序员将代码作为数据传递。&lt;/p&gt; &lt;p&gt;上述代码有两个比较明显的问题：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;4&lt;/code&gt;行冗繁的样板代码。&lt;/li&gt; &lt;li&gt;这些代码还相当难读，因为它没有清楚地表达程 序员的意图。我们不想传入对象，只想传入&lt;code&gt;行为&lt;/code&gt;。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Lambda&lt;/code&gt;表达式解决以上两个问题&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;button.addActionListener(event -&amp;gt; System.out.println(&amp;quot;button clicked&amp;quot;)); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2.2 辨别&lt;code&gt;Lambda&lt;/code&gt;表达式&lt;/h2&gt; &lt;h3&gt;2.2.1 Lambda 表达式不包含参数&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式不包含参数，使用空括号 &lt;code&gt;()&lt;/code&gt; 表示没有参数。该 &lt;code&gt;Lambda&lt;/code&gt; 表达式 实现了 &lt;code&gt;Runnable&lt;/code&gt; 接口，该接口也只有一个 &lt;code&gt;run&lt;/code&gt; 方法，没有参数，且返回类型为 &lt;code&gt;void&lt;/code&gt;。注意这里有无参数取决于对应接口中方法是否有返回值。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Runnable noArguments = () -&amp;gt; System.out.println(&amp;quot;Hello World&amp;quot;); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2.2.2 包含一个参数&lt;/h3&gt; &lt;p&gt;中所示的 &lt;code&gt;Lambda&lt;/code&gt; 表达式包含且只包含一个参数，可省略参数的括号，这和例 &lt;code&gt;2-2&lt;/code&gt; 中的 形式一样。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;button.addActionListener(event -&amp;gt; System.out.println(&amp;quot;button clicked&amp;quot;)); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2.2.2 Lambda代码块&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式的主体不仅可以是一个表达式，而且也可以是一段代码块，使用大括号 &lt;code&gt;({})&lt;/code&gt;将代码块括起来&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Runnable multiStatement = () -&amp;gt; {     System.out.print(&amp;quot;Hello&amp;quot;);     System.out.println(&amp;quot; World&amp;quot;); };  ExecutorService executorService = new ThreadPoolExecutor(2, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue&amp;lt;Runnable&amp;gt;()); executorService.execute(multiStatement); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2.2.3 Lambda 表达式包含多个参数&lt;/h3&gt; &lt;p&gt;这行代码并不是将两个数字相加，而是创建了一个函数，用来计算 两个数字相加的结果。变量 add 的类型是 BinaryOperator&lt;Long&gt;，它不是两个数字的和， 而是将两个数字相加的那行代码。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;BinaryOperator&amp;lt;Long&amp;gt; add = (x, y) -&amp;gt; x + y; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;所有 &lt;code&gt;Lambda&lt;/code&gt; 表达式中的参数类型都是由编译器推断得出的。这当然不错， 但有时最好也可以显式声明参数类型，此时就需要使用小括号将参数括起来，多个参数的 情况也是如此。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;BinaryOperator&amp;lt;Long&amp;gt; addExplicit = (Long x, Long y) -&amp;gt; x + y; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2.3 引用值，而不是变量&lt;/h2&gt; &lt;p&gt;在匿名类内引用外部的变量，在&lt;code&gt;Java7&lt;/code&gt;中必须将变量显示声明为&lt;code&gt;final&lt;/code&gt;，&lt;code&gt;Java8&lt;/code&gt;中可以省略&lt;code&gt;final&lt;/code&gt;(&lt;code&gt;Java 8&lt;/code&gt;虽然放松了这一限制，可以引用非&lt;code&gt;final&lt;/code&gt;变量，但是该变量在既成事实上必须是 &lt;code&gt;final&lt;/code&gt;，虽然无需将变量声明为final，但在Lambda表达式中，也无法用作非终态变量。如 果坚持用作非终态变量，编译器就会报错。 )，但是变量的还是&lt;code&gt;final&lt;/code&gt; 类型，不能被赋值。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;final String name = getUserName();  button.addActionListener(new ActionListener({     public void actionPerformed(ActionEvent event) {          System.out.println(&amp;quot;hi &amp;quot; + name);     }  }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;Lambda&lt;/code&gt;表达式也是一样，既成事实上的 final 是指只能给该变量赋值一次。换句话说，Lambda 表达式引用的是值， 而不是变量。在例 2-6 中，name 就是一个既成事实上的 final 变量。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;String name = getUserName(); button.addActionListener(event -&amp;gt; System.out.println(&amp;quot;hi &amp;quot; + name)); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果你试图给该变量多次赋值，然后在 Lambda 表达式中引用它，编译器就会报错。无法通过编译，并显示出错信息:local variables referenced from a Lambda expression must be final or effectively final1。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    //无法编译通过      String name = getUserName();      name = formatUserName(name);      button.addActionListener(event -&amp;gt; System.out.println(&amp;quot;hi &amp;quot; + name)); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意：在匿名类或&lt;code&gt;Lambda&lt;/code&gt;表达式中&lt;code&gt;Java8&lt;/code&gt;中虽然可以省略&lt;code&gt;final&lt;/code&gt;关键词，但是该变量已经成为既成事实上的 &lt;code&gt;final&lt;/code&gt; 变量，尝试给他赋值的话编译也会报错。&lt;/p&gt; &lt;h2&gt;2.4 函数接口&lt;/h2&gt; &lt;p&gt;&lt;strong&gt;函数接口是只有一个抽象方法的接口，用作 Lambda 表达式的类型。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;如果一个接口中有多个抽象方法就不是函数接口，就不能用作 &lt;code&gt;Lambda&lt;/code&gt; 表达式。多个抽象方法情况编译会直接报如下错误信息：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Error:(373, 59) java: The target type of this expression must be a functional interface &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;ActionListener&lt;/code&gt;就是函数接口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface ActionListener extends EventListener {     public void actionPerformed(ActionEvent e); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;函数接口，接口中单一方法的命名并不重要，只要方法签名和 &lt;code&gt;Lambda&lt;/code&gt; 表达式的类 型匹配即可。可在函数接口中为参数起一个有意义的名字，增加代码易读性，便于更透彻 地理解参数的用途。&lt;/p&gt; &lt;p&gt;这里的函数接口接受一个 &lt;code&gt;ActionEvent&lt;/code&gt; 类型的参数，返回空(&lt;code&gt;void&lt;/code&gt;)，但函数接口还可有其他形式。例如，函数接口可以接受两个参数，并返回一个值，还可以使用泛型，这完全取 决于你要干什么。&lt;/p&gt; &lt;p&gt;以后我将使用图形来表示不同类型的函数接口。指向函数接口的箭头表示参数，如果箭头 从函数接口射出，则表示方法的返回类型。&lt;code&gt;ActionListener&lt;/code&gt; 的函数接口如下图所示：&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/09/o1vd65dpe4h9mqo1o2u11s9bcv.jpeg" alt="alt" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Java中重要的函数接口&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;可简单理解为可以把lambda表达式，一组行为（函数接口的实现）传递给方法。以前想传递函数(行为)， 必须先将函数封装成对象的方法。然后传递改对象。lambda表达式则可以直接传递 函数（行为）。&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th align="left"&gt;接口&lt;/th&gt;&lt;th align="left"&gt;参数&lt;/th&gt;&lt;th align="left"&gt;返回类型&lt;/th&gt;&lt;th align="left"&gt;说明&lt;/th&gt;&lt;th align="left"&gt;示例&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td align="left"&gt;Predicate&lt;T&gt;&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;boolean&lt;/td&gt;&lt;td align="left"&gt;通过Lambda实现该接口中的test方法，返回一个布尔值，用作判断用&lt;/td&gt;&lt;td align="left"&gt;Predicate&lt;Integer&gt; boolValue = x -&amp;gt; x &amp;gt; 5;&lt;/br&gt;System.out.println(boolValue.test(1));&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;Consumer&lt;T&gt;&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;void&lt;/td&gt;&lt;td align="left"&gt;通过Lambda实现该接口中的accept方法，不返回值，用于执行一些操作&lt;/td&gt;&lt;td align="left"&gt;Consumer&lt;Integer&gt; consumer = x -&amp;gt; System.out.println(x);&lt;/br&gt;consumer.accept(123);&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;Function&amp;lt;T,R&amp;gt;&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;R&lt;/td&gt;&lt;td align="left"&gt;接受一个输入值T，处理后返回R类型数据&lt;/td&gt;&lt;td align="left"&gt;Function&amp;lt;Integer,Integer&amp;gt; function = t -&amp;gt; t+2;&lt;/br&gt;System.out.println(function.apply(4));&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;Supplier&lt;T&gt;&lt;/td&gt;&lt;td align="left"&gt;None&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;类似于工厂方法，返回一个T类型的变量&lt;/td&gt;&lt;td align="left"&gt;Supplier&lt;Integer&gt; supplier = () -&amp;gt; 2;&lt;/br&gt;Integer i = supplier.get();&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;UnaryOperator&lt;T&gt;&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;继承自接口&lt;code&gt;Function&lt;/code&gt;，感觉和&lt;code&gt;Function&lt;/code&gt;类似没看出什么差别&lt;/td&gt;&lt;td align="left"&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="left"&gt;BinaryOperator&lt;T&gt;&lt;/td&gt;&lt;td align="left"&gt;(T, T)&lt;/td&gt;&lt;td align="left"&gt;T&lt;/td&gt;&lt;td align="left"&gt;作用于于两个同类型操作符的操作，并且返回了操作符同类型的结果&lt;/td&gt;&lt;td align="left"&gt;BinaryOperator&lt;Integer&gt; binaryOperator = (x,y) -&amp;gt; x*y;&lt;/br&gt;binaryOperator.apply(2,3);&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;ul&gt; &lt;li&gt;Consumer补充列子&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public class ConsumerTest {     public static void main(String[] args) {         Consumer&amp;lt;Integer&amp;gt; consumer = (x) -&amp;gt; {             int num = x * 2;             System.out.println(num);         };         Consumer&amp;lt;Integer&amp;gt; consumer1 = (x) -&amp;gt; {             int num = x * 3;             System.out.println(num);         };         consumer.andThen(consumer1).accept(10);     } } /** 结果 20 30 **/ &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;UnaryOperator&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    UnaryOperator&amp;lt;Integer&amp;gt; unaryOperator = x -&amp;gt; x + 1;     Integer i = unaryOperator.apply(10);     System.out.println(i);     System.out.println(UnaryOperator.identity().apply(5));     System.out.println(UnaryOperator.identity().apply(&amp;quot;1234567890&amp;quot;));      /***     结果：     11     5     1234567890     **/     &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2.5 类型推断&lt;/h2&gt; &lt;p&gt;Lambda表达式中的类型推断，实际上是Java 7就引入的目标类型推断的扩展。Java 7 中的菱形操作符，它可使 javac 推断出泛型参数的类型：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Map&amp;lt;String, Integer&amp;gt; diamondWordCounts = new HashMap&amp;lt;&amp;gt;(); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果将构造函数直接传递给一个方法，也可根据方法签名来推断类型：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;//在Java7中不能通过编译 private void useHashmap(Map&amp;lt;String, String&amp;gt; values); useHashmap(new HashMap&amp;lt;&amp;gt;()); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Java 8 更进一步，可省略 &lt;code&gt;Lambda&lt;/code&gt; 表达 式中的所有参数类型。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Predicate——用来判断 真假的函数接口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Predicate&amp;lt;Integer&amp;gt; atLeast5 = x -&amp;gt; x &amp;gt; 5; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;接口&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Predicate&amp;lt;T&amp;gt; {      boolean test(T t); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;接口图示&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/09/7pps3e4guggskru5fju9sv03na.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;code&gt;Predicate&lt;/code&gt; 接口图示，接受一个对象，返回一个布尔值&lt;/p&gt; &lt;h2&gt;2.6 要点回顾&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式是一个匿名方法，将行为像数据一样进行传递。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Lambda&lt;/code&gt; 表达式的常见结构:&lt;code&gt;BinaryOperator&amp;lt;Integer&amp;gt; add = (x, y) → x + y&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;函数接口指仅具有单个抽象方法的接口，用来表示Lambda表达式的类型。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;2.7 练习&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;请看 Function 函数接口并回答下列问题。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Function&amp;lt;T, R&amp;gt; {      R apply(T t); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;a. 请画出该函数接口的图示。 &lt;img src="https://www.zhangaoo.com/upload/2018/09/u92ieabbssh28p3h2vcmfro8lg.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;b. 若要编写一个计算器程序，你会使用该接口表示什么样的 Lambda 表达式?&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Function&amp;lt;Double,Double&amp;gt; calc = t -&amp;gt; t * 2.0;  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;c. 下列哪些 &lt;code&gt;Lambda&lt;/code&gt; 表达式有效实现了 Function&amp;lt;Long,Long&amp;gt; ?&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;x-&amp;gt;x+1; //OK (x,y)-&amp;gt;x+1; //NG x-&amp;gt;x==1;//NG &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;&lt;code&gt;ThreadLocal Lambda&lt;/code&gt;表达式。&lt;code&gt;Java&lt;/code&gt;有一个&lt;code&gt;ThreadLocal&lt;/code&gt;类，作为容器保存了当前线程里局部变量的值。&lt;code&gt;Java 8&lt;/code&gt;为该类新加了一个工厂方法，接受一个&lt;code&gt;Lambda&lt;/code&gt;表达式，并产生 一个新的 &lt;code&gt;ThreadLocal&lt;/code&gt; 对象，而不用使用继承，语法上更加简洁。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;a. 在 &lt;code&gt;Javadoc&lt;/code&gt; 或集成开发环境(IDE)里找出该方法。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * Represents a supplier of results.  *  * &amp;lt;p&amp;gt;There is no requirement that a new or distinct result be returned each  * time the supplier is invoked.  *  * &amp;lt;p&amp;gt;This is a &amp;lt;a href=&amp;quot;package-summary.html&amp;quot;&amp;gt;functional interface&amp;lt;/a&amp;gt;  * whose functional method is {@link #get()}.  *  * @param &amp;lt;T&amp;gt; the type of results supplied by this supplier  *  * @since 1.8  */ @FunctionalInterface public interface Supplier&amp;lt;T&amp;gt; {      /**      * Gets a result.      *      * @return a result      */     T get(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;b. DateFormatter 类是非线程安全的。使用构造函数创建一个线程安全的 &lt;code&gt;SimpleDateFormat&lt;/code&gt;对象，并输出日期，如“01-Jan-1970”。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    Supplier&amp;lt;ThreadLocal&amp;gt; threadLocal = () -&amp;gt; ThreadLocal.withInitial(() -&amp;gt; new SimpleDateFormat(&amp;quot;dd-MMM-yyyy&amp;quot;));     SimpleDateFormat df = (SimpleDateFormat)threadLocal.get().get();     System.out.println(df.format(new Date())); &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;类型推断规则。下面是将 Lambda 表达式作为参数传递给函数的一些例子。javac 能正确推断出 Lambda 表达式中参数的类型吗?换句话说，程序能编译吗? a. Runnable helloWorld = () -&amp;gt; System.out.println(&amp;quot;hello world&amp;quot;);//OK b. 使用 Lambda 表达式实现 ActionListener 接口://OK&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;JButton button = new JButton();      button.addActionListener(event -&amp;gt; System.out.println(event.getActionCommand())); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;c. 以如下方式重载 check 方法后，还能正确推断出 check(x -&amp;gt; x &amp;gt; 5) 的类型吗? &lt;strong&gt;不能正确推导，有歧义，因为输入的值都是Integer，返回都是boolean&lt;/strong&gt;&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;No - the lambda expression could be inferred as IntPred or Predicate&lt;Integer&gt; so the overload is ambiguous.&lt;/p&gt; &lt;/blockquote&gt; &lt;pre&gt;&lt;code class="language-java"&gt;interface IntPred { boolean test(Integer value); } boolean check(Predicate&amp;lt;Integer&amp;gt; predicate); boolean check(IntPred predicate); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;写成如下形式就可以推导类型：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface IntPred {     boolean test(Double value);     public boolean check(IntPred intPred, Double i){         return intPred.test(i);     }     public boolean check(Predicate&amp;lt;Integer&amp;gt; predicate,Integer i){         return predicate.test(i);     }      //执行     System.out.println(check(x -&amp;gt; x &amp;gt; 5, 6.0));     System.out.println(check(x -&amp;gt; x &amp;gt; 10, 6)); } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Sat, 29 Sep 2018 12:43:00 GMT</pubDate>
    </item>
    <item>
      <title>SpringCloud篇四之Feign</title>
      <link>https://www.zhangaoo.com/article/feign</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/09/1kon36hfssincoqibh75cemjul.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h1&gt;Feign是什么？&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Feign&lt;/code&gt;是一个申明性&lt;code&gt;REST&lt;/code&gt;客户端&lt;/li&gt; &lt;li&gt;&lt;code&gt;Feign&lt;/code&gt;使得&lt;code&gt;Web&lt;/code&gt;服务客户端的写入更加方便 要使用&lt;code&gt;Feign&lt;/code&gt;创建一个界面并对其进行注释。它具有可插入注释支持，包括&lt;code&gt;Feign&lt;/code&gt;注释和&lt;code&gt;JAX-RS&lt;/code&gt;注释。&lt;code&gt;Feign&lt;/code&gt;还支持可插拔编码器和解码器。&lt;code&gt;Spring Cloud&lt;/code&gt;增加了对&lt;code&gt;Spring MVC&lt;/code&gt;注释的支持，并使用&lt;code&gt;Spring Web&lt;/code&gt;中默认使用的&lt;code&gt;HttpMessageConverters&lt;/code&gt;。&lt;code&gt;Spring Cloud&lt;/code&gt;集成&lt;code&gt;Ribbon&lt;/code&gt;和&lt;code&gt;Eureka&lt;/code&gt;以在使用&lt;code&gt;Feign&lt;/code&gt;时提供负载均衡的&lt;code&gt;http&lt;/code&gt;客户端。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Feign是一个声明式的Web服务客户端，使得编写Web服务客户端变得非常容易，只需要创建一个接口，然后在上面添加注解即可&lt;/strong&gt;&lt;/p&gt; &lt;h1&gt;Feign能干什么？&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Feign&lt;/code&gt;旨在使编写&lt;code&gt;Java Http&lt;/code&gt;客户端变得更容易。&lt;/li&gt; &lt;li&gt;前面在使用&lt;code&gt;Ribbon+RestTemplate&lt;/code&gt;时，利用&lt;code&gt;RestTemplate&lt;/code&gt;对&lt;code&gt;HTTP&lt;/code&gt;请求的封装处理，形成了一套模版化的调用方法。但是在实际开发中，由于对服务依赖的调用可能不止一处，往往一个接口会被多处调用，所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。可以发现&lt;code&gt;RestTemplate&lt;/code&gt;的调用方法如果对于一个服务在多个微服务间调用时代码十分累赘。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    // RestTemplate 调用方法     private static final String REST_URL_PREFIX = &amp;quot;http://MICROSERVICECLOUD-USER&amp;quot;;     @Autowired     private RestTemplate restTemplate;     @RequestMapping(&amp;quot;/add&amp;quot;)     public String add(User user){         return restTemplate.postForObject(REST_URL_PREFIX + &amp;quot;/user/add&amp;quot;,user,String.class);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Feign&lt;/code&gt;在此基础上做了进一步封装，由他来帮助我们定义和实现依赖服务接口的定义。在&lt;code&gt;Feign&lt;/code&gt;的实现下，我们只需创建一个接口并使用注解的方式来配置它(以前是&lt;code&gt;Dao&lt;/code&gt;接口上面标注&lt;code&gt;Mapper&lt;/code&gt;注解,现在是一个微服务接口上面标注一个&lt;code&gt;Feign&lt;/code&gt;注解即可)，即可完成对服务提供方的接口绑定，简化了使用&lt;code&gt;Spring cloud Ribbon&lt;/code&gt;时，自动封装服务调用客户端的开发量。&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;使用Feign调用微服务&lt;/h1&gt; &lt;h2&gt;构建工程&lt;/h2&gt; &lt;p&gt;延续前面的&lt;code&gt;User&lt;/code&gt;的例子，模仿&lt;code&gt;microservice-cloud-consumer-user-8002&lt;/code&gt;构建工程&lt;code&gt;microservice-cloud-consumer-user-feign-8003&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;依赖引入&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;!-- feign相关 --&amp;gt;     &amp;lt;dependency&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-cloud-starter-feign&amp;lt;/artifactId&amp;gt;     &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;配置文件&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 8003 eureka:   client:     register-with-eureka: false     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #注意这里使用ribbon实现负载均衡使用的是服务的名字，在eureka管理界面可以看到，Feign在定义接口的时候也会指定服务 MICROSERVICECLOUD-USER:   ribbon: #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Feign定义接口&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @version Version: 1.0  * @date DateTime: 2018/09/12 19:47:00&amp;lt;br/&amp;gt;  */ //向哪个微服务进行面向接口feign的编码 @FeignClient(value = &amp;quot;MICROSERVICECLOUD-USER&amp;quot;) public interface UserService {      @RequestMapping(value = &amp;quot;/user/add&amp;quot;,method = RequestMethod.POST)     String add(User user);      @RequestMapping(value=&amp;quot;/user/get/{name}&amp;quot;,method=RequestMethod.GET)     User get(@PathVariable(&amp;quot;name&amp;quot;) String name);      @RequestMapping(value=&amp;quot;/user/list&amp;quot;,method=RequestMethod.GET)     List&amp;lt;User&amp;gt; list();      @RequestMapping(value = &amp;quot;/user/discovery&amp;quot;,method = RequestMethod.GET)     List&amp;lt;ServiceInstance&amp;gt; discover(); } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;启动类配置@EnableFeignClients&lt;/h3&gt; &lt;p&gt;启动类不要忘记配置&lt;code&gt;@EnableFeignClients&lt;/code&gt;注解，否则启动会报错。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@SpringBootApplication @EnableEurekaClient @EnableFeignClients public class UserConsumer8003App {     public static void main(String[] args) {         SpringApplication.run(UserConsumer8003App.class,args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;创建Controller调用Feign服务&lt;/h3&gt; &lt;p&gt;可以看到定义好接口后我们就能调用，而不用使用&lt;code&gt;RestTemplate&lt;/code&gt;还需要各种拼接，&lt;code&gt;Feign&lt;/code&gt;调用也非常简洁明了。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt; /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 20:57:00&amp;lt;br/&amp;gt;  */ @RestController @RequestMapping(&amp;quot;/consumer/user&amp;quot;) public class UserControllerConsumer {     @Autowired     private UserService userService;      @RequestMapping(&amp;quot;/add&amp;quot;)     public String add(User user){         return userService.add(user);     }     @RequestMapping(&amp;quot;/get/{name}&amp;quot;)     public User get(@PathVariable(&amp;quot;name&amp;quot;) String name){         return userService.get(name);     }     @RequestMapping(&amp;quot;/list&amp;quot;)     public List&amp;lt;User&amp;gt; list(){         return userService.list();     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;ribbon负载均衡&lt;/h3&gt; &lt;p&gt;这里需要注意的是&lt;code&gt;Ribbon&lt;/code&gt;负载均衡可以直接在配置文件中配置，可以使用系统提供的7中负载均衡算法，也可以自定义。自定义负载均衡规则可参考上篇。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;#注意这里使用ribbon实现负载均衡使用的是服务的名字，在eureka管理界面可以看到，Feign在定义接口的时候也会指定服务 MICROSERVICECLOUD-USER:   ribbon: #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;完成以上代码后成功运行后就能正常访问接口了。&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 15 Sep 2018 06:42:13 GMT</pubDate>
    </item>
    <item>
      <title>SpringCloud篇三之Ribbon</title>
      <link>https://www.zhangaoo.com/article/ribbon</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://i.ytimg.com/vi/20oWFLtr6NM/maxresdefault.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h1&gt;Ribbon 是什么？&lt;/h1&gt; &lt;p&gt;&lt;code&gt;Ribbon&lt;/code&gt;是基于&lt;code&gt;Netflix Ribbon&lt;/code&gt;实现的一套客户端负载均衡的工具,它可以很好地控制HTTP和TCP客户端的行为。&lt;a href="https://github.com/Netflix/ribbon/wiki/Getting-Started" target="_blank"&gt;相关资料&lt;/a&gt;&lt;/p&gt; &lt;p&gt;简单的说，&lt;code&gt;Ribbon&lt;/code&gt;是&lt;code&gt;Netflix&lt;/code&gt;发布的开源项目，主要功能是提供客户端的软件负载均衡算法，将&lt;code&gt;Netflix&lt;/code&gt;的中间层服务连接在一起。&lt;code&gt;Ribbon&lt;/code&gt;客户端组件提供一系列完善的配置项如连接超时，重试等。&lt;/p&gt; &lt;p&gt;在配置文件中列出&lt;code&gt;Load Balancer&lt;/code&gt;（简称&lt;code&gt;LB&lt;/code&gt;）后面所有的机器，&lt;code&gt;Ribbon&lt;/code&gt;会自动的帮助你基于某种规则（如简单轮询，随机连接等）去连接这些机器。我们也很容易使用&lt;code&gt;Ribbon&lt;/code&gt;实现自定义的负载均衡算法。&lt;/p&gt; &lt;h1&gt;Ribbon能干什么？&lt;/h1&gt; &lt;p&gt;&lt;code&gt;LB&lt;/code&gt;，即负载均衡(&lt;code&gt;Load Balance&lt;/code&gt;)，在微服务或分布式集群中经常用的一种应用。 负载均衡简单的说就是将用户的请求平摊的分配到多个服务上，从而达到系统的&lt;code&gt;HA&lt;/code&gt;(&lt;code&gt;High Available&lt;/code&gt;)。&lt;/p&gt; &lt;p&gt;常见的负载均衡有软件&lt;code&gt;Nginx&lt;/code&gt;，&lt;code&gt;LVS&lt;/code&gt;，硬件&lt;code&gt;F5&lt;/code&gt;等。相应的在中间件，例如：&lt;code&gt;dubbo&lt;/code&gt;和&lt;code&gt;SpringCloud&lt;/code&gt;中均给我们提供了负载均衡，&lt;code&gt;SpringCloud&lt;/code&gt;的负载均衡算法可以自定义。&lt;/p&gt; &lt;h2&gt;集中式LB&lt;/h2&gt; &lt;p&gt;即在服务的消费方和提供方之间使用独立的&lt;code&gt;LB&lt;/code&gt;设施(可以是硬件，如&lt;code&gt;F5&lt;/code&gt;, 也可以是软件，如&lt;code&gt;nginx&lt;/code&gt;), 由该设施负责把访问请求通过某种策略转发至服务的提供方。&lt;/p&gt; &lt;h2&gt;进程内LB&lt;/h2&gt; &lt;p&gt;将&lt;code&gt;LB&lt;/code&gt;逻辑集成到消费方，消费方从服务注册中心获知有哪些地址可用，然后自己再从这些地址中选择出一个合适的服务器。&lt;code&gt;Ribbon&lt;/code&gt;就属于进程内&lt;code&gt;LB&lt;/code&gt;，它只是一个类库，集成于消费方进程，消费方通过它来获取到服务提供方的地址。&lt;/p&gt; &lt;h1&gt;Ribbon初步的配置&lt;/h1&gt; &lt;ol&gt; &lt;li&gt;给&lt;code&gt;microservice-cloud-consumer-user-8002&lt;/code&gt;消费者模块添加如下依赖&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;dependency&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-cloud-starter-eureka&amp;lt;/artifactId&amp;gt;     &amp;lt;/dependency&amp;gt;     &amp;lt;dependency&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-cloud-starter-ribbon&amp;lt;/artifactId&amp;gt;     &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;修改&lt;code&gt;application.yml&lt;/code&gt; 追加&lt;code&gt;eureka&lt;/code&gt;的服务注册地址&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   client:     register-with-eureka: false     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;因为消费者只需从集群&lt;code&gt;Eureka&lt;/code&gt;获取服务提供者的信息，本身并不需要注册到注册中心因此&lt;code&gt;register-with-eureka: false&lt;/code&gt;&lt;/p&gt; &lt;ol start="3"&gt; &lt;li&gt;对&lt;code&gt;ConfigBean&lt;/code&gt;类进行新注解&lt;code&gt;@LoadBalanced&lt;/code&gt;获得&lt;code&gt;Rest&lt;/code&gt;时加入&lt;code&gt;Ribbon&lt;/code&gt;的配置。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @version Version: 1.0  * @date DateTime: 2018/08/15 20:54:00&amp;lt;br/&amp;gt;  */ @Configuration public class ConfigBean {     /**      * RestTemplate提供了多种便捷访问远程Http服务的方法，      * 是一种简单便捷的访问restful服务模板类，是Spring提供的用于访问Rest服务的客户端模板工具集。      */     @Bean     @LoadBalanced //要求客户端通过Rest去访问微服务的时候自带负载均衡     public RestTemplate getRestTemplate(){         return new RestTemplate();     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;主启动类&lt;code&gt;UserConsumer8002App&lt;/code&gt;添加&lt;code&gt;@EnableEurekaClient&lt;/code&gt;注解。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@SpringBootApplication @EnableEurekaClient public class UserConsumer8002App {     public static void main(String[] args) {         SpringApplication.run(UserConsumer8002App.class,args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;修改&lt;code&gt;UserControllerConsumer&lt;/code&gt;客户端访问类。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@RestController @RequestMapping(&amp;quot;/consumer/user&amp;quot;) public class UserControllerConsumer { //    private static final String REST_URL_PREFIX = &amp;quot;http://localhost:8001&amp;quot;;      private static final String REST_URL_PREFIX = &amp;quot;http://MICROSERVICECLOUD-USER&amp;quot;;      @Autowired     private RestTemplate restTemplate;      @RequestMapping(&amp;quot;/add&amp;quot;)     public String add(User user){         return restTemplate.postForObject(REST_URL_PREFIX + &amp;quot;/user/add&amp;quot;,user,String.class);     }     @RequestMapping(&amp;quot;/get/{name}&amp;quot;)     public User get(@PathVariable(&amp;quot;name&amp;quot;) String name){         return restTemplate.getForObject(REST_URL_PREFIX + &amp;quot;/user/get/&amp;quot; + name,User.class);     }     @RequestMapping(&amp;quot;/list&amp;quot;)     public List&amp;lt;User&amp;gt; list(){         return restTemplate.getForObject(REST_URL_PREFIX + &amp;quot;/user/list&amp;quot;,List.class);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用如下图展示的服务&lt;code&gt;Application Name&lt;/code&gt;作为服务提供者的地址，这样可配置一个服提供者集群，通过&lt;code&gt;Ribbon&lt;/code&gt;访问服务实现负载均衡。 &lt;img src="https://www.zhangaoo.com/upload/2018/08/su9sjddtcqgmhqo642evr6m0on.png" alt="alt" /&gt;&lt;/li&gt; &lt;/ul&gt; &lt;ol start="6"&gt; &lt;li&gt;测试 依次启动三个&lt;code&gt;Eureka&lt;/code&gt;集群服务，然后分别启动服务提供者和服务消费者，然后在浏览器访问接口&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;http://127.0.0.1:8002/consumer/user/list # 可得到返回JSON结果 [     {         &amp;quot;id&amp;quot;: 1,         &amp;quot;name&amp;quot;: &amp;quot;zhangsan&amp;quot;,         &amp;quot;age&amp;quot;: 18,         &amp;quot;gender&amp;quot;: 1,         &amp;quot;dbSource&amp;quot;: &amp;quot;user_db&amp;quot;     },     ... ] &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;后台也发现相关日志&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;2018-08-28 21:01:01.585  INFO 18615 --- [qtp754617275-20] c.netflix.loadbalancer.BaseLoadBalancer  : Client: MICROSERVICECLOUD-USER instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=MICROSERVICECLOUD-USER,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null 2018-08-28 21:01:01.593  INFO 18615 --- [qtp754617275-20] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater ...... 2018-08-28 21:01:01.613  INFO 18615 --- [qtp754617275-20] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client MICROSERVICECLOUD-USER initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=MICROSERVICECLOUD-USER,current list of Servers=[127.0.0.1:8001],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;] },Server stats: [[Server:127.0.0.1:8001; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0] ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@4ae2c843 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;小结：Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号。&lt;/strong&gt;&lt;/p&gt; &lt;h1&gt;Ribbon负载均衡&lt;/h1&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/osugljo7luhd7of9ijob5bv02u.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;因为上面的例子只有一个服务提供者并不能很好的表达&lt;code&gt;Ribbon&lt;/code&gt;的负载均衡，下面我们深入的了解一下负载均衡极其策略&lt;/p&gt; &lt;p&gt;Ribbon在工作时分成两步&lt;/p&gt; &lt;ul&gt; &lt;li&gt;第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server&lt;/li&gt; &lt;li&gt;第二步再根据用户指定的策略，在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略：比如轮询、随机和根据响应时间加权。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;添加服务提供者&lt;/h2&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;在总父工程下创建&lt;code&gt;microservice-cloud-provider-user-8011&lt;/code&gt;的&lt;code&gt;maven Module&lt;/code&gt;配置参照&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;同1一样的步骤创建&lt;code&gt;microservice-cloud-provider-user-8021&lt;/code&gt;模块，注意更改配置的端口号&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;创建模块对应的DB&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;分别创建两个&lt;code&gt;DB&lt;/code&gt;，&lt;code&gt;user_db2、user_db3&lt;/code&gt;分别对应&lt;code&gt;microservice-cloud-provider-user-8011&lt;/code&gt;和&lt;code&gt;microservice-cloud-provider-user-8021&lt;/code&gt;模块&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;启动并验证&lt;/h2&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;先启动&lt;code&gt;Eureka&lt;/code&gt;集群模块，然后启动3个服务提供者模块，然后启动服务消费者模块&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;访问服务消费者端口验证&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;# 在浏览器方位以下接口，获取名为zhangsan用户信息 http://127.0.0.1:8002/consumer/user/get/zhangsan &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;第一次返回结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;id&amp;quot;: 1,     &amp;quot;name&amp;quot;: &amp;quot;zhangsan&amp;quot;,     &amp;quot;age&amp;quot;: 18,     &amp;quot;gender&amp;quot;: 1,     &amp;quot;dbSource&amp;quot;: &amp;quot;user_db3&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;第二次返回结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;id&amp;quot;: 1,     &amp;quot;name&amp;quot;: &amp;quot;zhangsan&amp;quot;,     &amp;quot;age&amp;quot;: 18,     &amp;quot;gender&amp;quot;: 1,     &amp;quot;dbSource&amp;quot;: &amp;quot;user_db2&amp;quot; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;由上结果可知，两次返回的&lt;code&gt;dbSource&lt;/code&gt;值不一样，证明两次走的是不同服务提供者的接口，默认的负载均衡策略为轮询。 访问&lt;code&gt;http://127.0.0.1:7001&lt;/code&gt;可以看到&lt;code&gt;MICROSERVICECLOUD-USER&lt;/code&gt;微服务下面挂着三个实例&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/psv557ehumjempa14ukh015vvu.png" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;：&lt;code&gt;Ribbon&lt;/code&gt;其实就是一个软负载均衡的客户端组件，他可以和其他所需请求的客户端结合使用，和&lt;code&gt;eureka&lt;/code&gt;结合只是其中的一个实例。&lt;/p&gt; &lt;h1&gt;Ribbon核心组件之IRule&lt;/h1&gt; &lt;p&gt;&lt;code&gt;SpringCloud&lt;/code&gt;结合&lt;code&gt;Ribbon&lt;/code&gt;，它默认出厂自带了7种算法。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;RoundRobinRule&lt;/code&gt; 轮询&lt;/li&gt; &lt;li&gt;&lt;code&gt;RandomRule&lt;/code&gt; 随机&lt;/li&gt; &lt;li&gt;&lt;code&gt;AvailabilityFilteringRule&lt;/code&gt; 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务，还有并发的连接数量超过阈值的服务，然后对剩余的服务列表按照轮询策略进行访问。&lt;/li&gt; &lt;li&gt;&lt;code&gt;WeightedResponseTimeRule&lt;/code&gt; 根据平均响应时间计算所有服务的权重，响应时间越快服务权重越大被选中的概率越高。刚启动时如果统计信息不足，则使用&lt;code&gt;RoundRobinRule&lt;/code&gt;策略，等统计信息足够，会切换到&lt;code&gt;WeightedResponseTimeRule&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;RetryRule&lt;/code&gt; 先按照&lt;code&gt;RoundRobinRule&lt;/code&gt;的策略获取服务，如果获取服务失败则在指定时间内会进行重试，获取可用的服务。&lt;/li&gt; &lt;li&gt;&lt;code&gt;BestAvailableRule&lt;/code&gt; 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务，然后选择一个并发量最小的服务。&lt;/li&gt; &lt;li&gt;&lt;code&gt;ZoneAvoidanceRule&lt;/code&gt; 默认规则,复合判断server所在区域的性能和server的可用性选择服务器。&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;换成随机的负载均衡算法（RandomRule）算法&lt;/h2&gt; &lt;p&gt;我们在&lt;code&gt;microservice-cloud-consumer-user-8002&lt;/code&gt;加入以下配置&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Bean     public IRule myRule(){         return new RandomRule();     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;同样的更改为以上&lt;code&gt;AvailabilityFilteringRule&lt;/code&gt; 、&lt;code&gt;WeightedResponseTimeRule&lt;/code&gt;算法也是一样的。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Ribbon自定义算法&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;如果上面的7种算法，都不够出来业务逻辑，那么可以来自定义算法。&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;1.修改&lt;code&gt;microservice-cloud-consumer-user-8002&lt;/code&gt;的主启动类，在主启动类上添加一个注解&lt;code&gt;@RibbonClient()&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@SpringBootApplication @EnableEurekaClient /*在启动该微服务的时候就能去加载我们的自定义Ribbon配置类，从而使配置生效。 并且官方文档明确给出了警告：     这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下，     否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享，也就是说     我们达不到特殊化定制的目的了。*/ //那么解决方案是重新在java包下再建一个包名，并把MySelfRule类放入该包内。 @RibbonClient(name=&amp;quot;MICROSERVICECLOUD-USER&amp;quot;,configuration=MyselfRule.class) public class UserConsumer8002App {     public static void main(String[] args) {         SpringApplication.run(UserConsumer8002App.class,args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;编写MyselfRule自定义配置类的：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.myrule;  import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;  /**  * @version Version: 1.0  * @date DateTime: 2018/09/08 14:56:00&amp;lt;br/&amp;gt;  */ @Configuration public class MyselfRule{     @Bean     public IRule myRule() {         //Ribbon默认是轮询，我自定义为随机         return new RandomRule();      } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;深度自定义负载均衡规则&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;这里说的深度自定义就是继承虚类&lt;code&gt;AbstractLoadBalancerRule&lt;/code&gt;实现&lt;code&gt;IRule&lt;/code&gt;接口，就和系统实现&lt;code&gt;RoundRobinRule&lt;/code&gt;、&lt;code&gt;RandomRule&lt;/code&gt;负载均衡规则一样的实现&lt;/li&gt; &lt;li&gt;现在我们实现一个，随机选择一个&lt;code&gt;Server&lt;/code&gt;，并且该&lt;code&gt;Server&lt;/code&gt;调用&lt;code&gt;5&lt;/code&gt;次然后再随机选择下一个&lt;code&gt;Server&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.myrule;  import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server;  import java.util.List; import java.util.concurrent.ThreadLocalRandom;  /**  * @version Version: 1.0  * @date DateTime: 2018/09/08 16:07:00&amp;lt;br/&amp;gt;  */ public class MyselfRandomRule extends AbstractLoadBalancerRule {     //总共被调用的次数，目前要求每台被调用5次     private int total = 0;     //当前提供服务的机器号     private int currentIndex = 0;     public Server choose(ILoadBalancer lb, Object key) {         //ILoadBalancer哪一种负载均衡算法如果等于null的话就返回null，那么自然而然，它肯定会加载一种算法，所以它不会变成null。         if (lb == null) {             return null;         }         //现在还不知道是哪个算法来响应server         Server server = null;         //如果说server等于null，那么就看线程是否中断了，如果被中断的话就返回null         while (server == null) {             if (Thread.interrupted()) {                 return null;             } //upList的意思就是现在活着的可以对外提供的机器，然后.get()方法通过 //int index = rand.nextInt(serverCount); 那么就是随机到几就返回第几的值             List&amp;lt;Server&amp;gt; upList = lb.getReachableServers();             List&amp;lt;Server&amp;gt; allList = lb.getAllServers(); //如果serverCount目前有三台，那么就不等于0，那么就是flase。             int serverCount = allList.size();             if (serverCount == 0) {                 /*                  * No servers. End regardless of pass, because subsequent passes                  * only get more restrictive.                  */                 return null;             } //这个的意思就是说如果serverCount有三台，那么index就得到了从下标0和1和2数组 // int index = rand.nextInt(serverCount); // server = upList.get(index); //当第一次total &amp;lt; 5的时候 //当第二次total &amp;lt; 5的时候 //当第五次total &amp;lt; 5的时候（那么第五次就不小于5），那么if(total &amp;lt; 5)这段里面的代码就不执行了             if (total &amp;lt; 5) { //那么第一次的server是0号机 //那么第二次的server也是0号机                 server = upList.get(currentIndex); //第一次的总的计数次数是加一个1 //第二次的总的计数次数是再加一个1                 total++; //当第五次total &amp;lt; 5的时候就走else             } else { //那么total等于0                 total = 0; //而currentIndex就加一个1                 currentIndex++; //那么1大于等于upList.size()，目前假设有三台机器，那么1就不大于等于upList.size() //那么现在就是给0号机给1号机进行服务了，以此类推。。 //但是如果currentIndex等于下标3的时候并且&amp;gt;= upList.size()，但我们按照数组下标来算的话只                //有0和1和2的下标，那么当currentIndex等于下标3的时候这样就是超过第三台了，那么                         //currentIndex就重新等于0。以此类推。。                 if (currentIndex &amp;gt;= upList.size()) {                     currentIndex = 0;                 }             } //如果这个server等于null，那么线程中断一会，下一轮继续             if (server == null) {                 /*                  * The only time this should happen is if the server list were                  * somehow trimmed. This is a transient condition. Retry after                  * yielding.                  */                  Thread.yield();                 continue;             } //如果活着好好的，那么就返回server回去             if (server.isAlive()) {                 return (server);             } // Shouldn't actually happen.. but must be transient or a bug.             server = null;             Thread.yield();         } //返回对应该响应服务是8001，还是8002还是8003         return server;     }     protected int chooseRandomInt(int serverCount) {         return ThreadLocalRandom.current().nextInt(serverCount);     }      @Override     public Server choose(Object key) {         return choose(getLoadBalancer(), key);     }      @Override     public void initWithNiwsConfig(IClientConfig clientConfig) {     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;配置使用该随机规则&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.myrule;  import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;  /**  * @version Version: 1.0  * @date DateTime: 2018/09/08 14:56:00&amp;lt;br/&amp;gt;  */ @Configuration public class MyselfRule{     @Bean     public IRule myRule() {         //Ribbon默认是轮询，我自定义为随机 //        return new RandomRule();         //深度自定义的随机规则，每个Server调用5次         return new MyselfRandomRule();      } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;调用接口测试发现每个Server节点调用5次后换另外一个节点&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sun, 09 Sep 2018 09:06:36 GMT</pubDate>
    </item>
    <item>
      <title>使用JMeter进行性能测试</title>
      <link>https://www.zhangaoo.com/article/jmeter-start</link>
      <content:encoded>&lt;h1&gt;使用JMeter做性能测试&lt;/h1&gt; &lt;p&gt;&lt;img src="https://i0.wp.com/www.daqaa.com/wp-content/uploads/2016/09/jmeter_square.png?w=256&amp;amp;ssl=1" alt="alt" /&gt; 最近需要使用&lt;code&gt;JMeter&lt;/code&gt;来做接口性能测试工具，简单记录一下试用方法&lt;/p&gt; &lt;h1&gt;下载安装&lt;/h1&gt; &lt;p&gt;由于是&lt;code&gt;Java&lt;/code&gt;程序，只需加载官网的二进制包到本地，在装好&lt;code&gt;Java&lt;/code&gt;的前提下就可以运行了。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#下载地址 https://jmeter.apache.org/download_jmeter.cgi  #运行路径 apache-jmeter-4.0/bin/jmeter.sh &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;具体使用&lt;/h1&gt; &lt;p&gt;现在以测试一个&lt;code&gt;Restful API&lt;/code&gt;的&lt;code&gt;QPS&lt;/code&gt;为例子，接口如下用户名为&lt;code&gt;zhangsan&lt;/code&gt;用户信息&lt;/p&gt; &lt;pre&gt;&lt;code&gt;http://127.0.0.1:8002/consumer/user/get/zhangsan &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;新建线程组&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;右击&lt;code&gt;测试计划&lt;/code&gt;-&amp;gt;&lt;code&gt;添加&lt;/code&gt;-&amp;gt;&lt;code&gt;Threads(Users)&lt;/code&gt;-&amp;gt;&lt;code&gt;线程组&lt;/code&gt;&lt;/li&gt; &lt;li&gt;配置线程组如下图，这里我们线程数设置为&lt;code&gt;5&lt;/code&gt;勾选&lt;code&gt;调度器&lt;/code&gt;然后持续时间设置为&lt;code&gt;60&lt;/code&gt;，其实就是模拟&lt;code&gt;5&lt;/code&gt;个用户持续跑&lt;code&gt;60&lt;/code&gt;秒钟&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/rrjg47tkmmhcco0ijj21aoc262.png" alt="alt" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;线程数：起多少个线程，或者说模拟多少个用户&lt;/li&gt; &lt;li&gt;Ramp-up Period：JMeter花多长时间启动所有的线程。假设值是100，线程数是10，则每隔10秒会启动一个线程&lt;/li&gt; &lt;li&gt;循环次数：Number of times to perform the test case. Alternatively, &amp;quot;forever&amp;quot; can be selected causing the test to run until manually stopped&lt;/li&gt; &lt;li&gt;其他参数具体可点击&lt;code&gt;GUI&lt;/code&gt;界面的帮助按钮进行了解&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;添加HTTP采样器&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;右击上一步创建的线程组，选择&lt;code&gt;添加&lt;/code&gt;-&amp;gt;&lt;code&gt;Sample&lt;/code&gt;-&amp;gt;&lt;code&gt;HTTP请求&lt;/code&gt;&lt;/li&gt; &lt;li&gt;依次设置协议、服务器名称或IP、端口号、HTTP请求方法、路径&lt;/li&gt; &lt;li&gt;如果请求参数在Body里面可以写死请求的Body，也可以通过变量在每个请求动态生成&lt;code&gt;Body&lt;/code&gt;这种方法后面会做介绍&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/7updgi4o4qj0aqa9irpd5hih5v.png" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;设置请求头&lt;/h2&gt; &lt;p&gt;如果需要请求头的按需添加&lt;/p&gt; &lt;ol&gt; &lt;li&gt;右击上一步创建的线程组，选择&lt;code&gt;添加&lt;/code&gt;-&amp;gt;&lt;code&gt;配置原件&lt;/code&gt;-&amp;gt;&lt;code&gt;HTTP信息头管理器&lt;/code&gt;&lt;/li&gt; &lt;li&gt;点击下方中部的&lt;code&gt;添加&lt;/code&gt;按钮分别写入&lt;code&gt;Content-Type&lt;/code&gt;，值为&lt;code&gt;application/json&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;添加测试报告查看组件&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;右击上一步创建的线程组，选择&lt;code&gt;添加&lt;/code&gt;-&amp;gt;&lt;code&gt;监听器&lt;/code&gt;-&amp;gt;&lt;code&gt;查看结果树&lt;/code&gt;、聚合报告、图形结果，可根据需求添加&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;使用BeanShell动态定定义请求参数&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;右击上一步创建的线程组，选择&lt;code&gt;添加&lt;/code&gt;-&amp;gt;&lt;code&gt;前置处理器&lt;/code&gt;-&amp;gt;&lt;code&gt;BeanShell PreProcessor&lt;/code&gt;&lt;/li&gt; &lt;li&gt;在&lt;code&gt;Parameters&lt;/code&gt;定义参数，这里定义的变量只能识别为字符串，比如这里定义了三个用户名字用逗号分隔开，后面再取到参数后再使用&lt;code&gt;split&lt;/code&gt;方法提取每个用户名。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/vpeinuie5gh3jqrs7jihpj095k.png" alt="alt" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;String []names = Parameters.split(&amp;quot;,&amp;quot;);  //vars.getIteration()获取当前操作的次数 int randomNameIndex = vars.getIteration() % names.length; //这里把值put到名为vars的Map中，在其他地方使用${queryName}则可以取到该值 vars.put(&amp;quot;queryName&amp;quot;, names[randomNameIndex]); &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;其他代码参考&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;import org.apache.commons.lang3.RandomStringUtils;  StringBuilder result = new StringBuilder(); String newline = System.getProperty(&amp;quot;line.separator&amp;quot;); int max = Integer.parseInt(Parameters); Random random = new Random();  StringBuilder data = new StringBuilder(); for(int i = 0; i &amp;lt; max; i++){  data.append(&amp;quot;testTable,metric=wind.UC_ResetAlarms&amp;quot;);  data.append(RandomStringUtils.randomNumeric(1)); // data.append(RandomStringUtils.randomAlphanumeric(6));  data.append(&amp;quot;,machineId=&amp;quot;);  data.append(RandomStringUtils.randomAlphanumeric(6));  data.append(&amp;quot; value=&amp;quot;);  data.append(RandomStringUtils.randomNumeric(2));  data.append(&amp;quot;.&amp;quot;);  data.append(RandomStringUtils.randomNumeric(2));  data.append(newline); }  vars.put(&amp;quot;myData&amp;quot;, data.toString());  &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;result.append(&amp;quot;{&amp;quot;); result.append(&amp;quot;&amp;quot;productIds&amp;quot; : [&amp;quot;); result.append(newline); for (int i = 1; i &amp;lt; max; i++) {     result.append(&amp;quot;&amp;quot;&amp;quot;).append(random.nextInt()).append(&amp;quot;&amp;quot;,&amp;quot;);     result.append(newline); } result.append(&amp;quot;]&amp;quot;); result.append(newline); result.append(&amp;quot;}&amp;quot;);  vars.put(&amp;quot;json&amp;quot;, result.toString());  &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;内置函数&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;__RandomString The RandomString function returns a random String of length using characters in chars to use.&lt;/li&gt; &lt;li&gt;Examples:&lt;/li&gt; &lt;/ul&gt; &lt;blockquote&gt; &lt;p&gt;will return a random string of 5 characters which can be readable or not&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;返回5个字符的随机字符串，因为未指定字符，可能会产生不可读的字符&lt;/p&gt; &lt;pre&gt;&lt;code&gt;${__RandomString(5)} &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;will return a random string of 10 characters picked from abcdefg set, like cdbgdbeebd or adbfeggfad, … 从给定的字符串中产生长度为10的随机字符串&lt;/p&gt; &lt;/blockquote&gt; &lt;pre&gt;&lt;code&gt;${__RandomString(10,abcdefg)} &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;will return a random string of 6 characters picked from a12zeczclk set and store the result in MYVAR, MYVAR will contain string like 2z22ak or z11kce, …&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;从给定的字符创中生成随机字符串，并把值存储到变量MYVAR&lt;/p&gt; &lt;pre&gt;&lt;code&gt;${__RandomString(6,a12zeczclk, MYVAR)} &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;注意&lt;/h2&gt; &lt;pre&gt;&lt;code&gt;================================================================================ Don't use GUI mode for load testing !, only for Test creation and Test debugging. For load testing, use NON GUI Mode:    jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] &amp;amp; increase Java Heap to meet your test requirements:    Modify current env variable HEAP=&amp;quot;-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m&amp;quot; in the jmeter batch file Check : https://jmeter.apache.org/usermanual/best-practices.html ================================================================================ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;对性能测试的时候一定不要使用图形界面，要使用命令行模式&lt;/p&gt; &lt;pre&gt;&lt;code&gt;jmeter -n -t /Users/zealzhangz/Documents/dev/company/sky-test/jmeter/TSDB-插入数据测试.jmx -l /Users/zealzhangz/Documents/dev/company/sky-test/jmeter/result/`date &amp;quot;+%Y%m%d&amp;quot;`/`date &amp;quot;+%Y%m%d%H%m%S&amp;quot;.txt` -e -o /Users/zealzhangz/Documents/dev/company/sky-test/jmeter/result/`date &amp;quot;+%Y%m%d&amp;quot;`/report &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;使用分布式Jmeter做性能测试&lt;/h1&gt; &lt;p&gt;我们在在做性能测试时发现单个&lt;code&gt;Jmeter&lt;/code&gt;的能力有限，即使启更多的线程也不能线性的增强&lt;code&gt;Jmeter&lt;/code&gt;的能力。这种情况就需要集群搭建一个能力更强的Jmeter集群。架构如下图：&lt;/p&gt; &lt;p&gt;&lt;img src="https://cdn2.hubspot.net/hubfs/208250/Blog_Images/disdock1.png" alt="alt" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;JMeter Client&lt;/code&gt;主要是调度的作用，把测试任务下发到各个真正执行的测试的&lt;code&gt;JMeter Server&lt;/code&gt;。然后汇集 测试结果，生成测试报告。&lt;/li&gt; &lt;li&gt;&lt;code&gt;JMeter Server&lt;/code&gt;实际完成测试的节点。&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;搭建分布式Jmeter步骤&lt;/h1&gt; &lt;h2&gt;首先启动Server节点&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;-Dserver_port=6099&lt;/code&gt;：与&lt;code&gt;JMeter Client&lt;/code&gt;通信的端口&lt;/li&gt; &lt;li&gt;&lt;code&gt;-Djava.rmi.server.hostname&lt;/code&gt;=10.115.0.224：与&lt;code&gt;JMeter Client&lt;/code&gt;通信的&lt;code&gt;hostname&lt;/code&gt;，一般就是 宿主机的&lt;code&gt;IP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;一个机器启多个节点时每个Jmeter对应不同端口号&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;nohup ./apache-jmeter-4.0/bin/jmeter-server -Dserver_port=6099 -Djava.rmi.server.hostname=10.115.0.224 &amp;gt; jmeter-6099.log  2&amp;gt;&amp;amp;1 &amp;amp; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：当宿主机配置较高的情况可在宿主机启动多个Server节点，实验表明一个宿主机启动 多个Jmeter性能比一个Jmeter（资源同比 增加）启更多线程性能更强。&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;配置并启动&lt;code&gt;JMeter Client&lt;/code&gt;&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;.jmx&lt;/code&gt;文件为在&lt;code&gt;JMeter GUI&lt;/code&gt;界面编写测试好的脚本文件&lt;/li&gt; &lt;li&gt;参数&lt;code&gt;-R&lt;/code&gt;后面接分布式节点&lt;code&gt;IP:port&lt;/code&gt;也就是 上面设置的&lt;code&gt;JMeter Server&lt;/code&gt;端口和&lt;code&gt;IP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;测试结果会存在result.txt文件， HTML报告存在指定的目录下面&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#!/usr/bin/env bash JMETER_HOME=/home/xxxxxx/apache-jmeter-4.0 baseDir=/home/xxxxxx/jmeter-shell result=$baseDir/`date &amp;quot;+%Y%m%d%H%M%S-standalone-skytsdb-226-27jmeter-6thread-100points&amp;quot;`  if [[ ! -d &amp;quot;${result}/report&amp;quot; ]] then     mkdir -p ${result}/report fi  $JMETER_HOME/bin/jmeter.sh -n -t $baseDir/Standalone-TSDB.jmx -R 10.115.0.223:1099,10.115.0.223:2099,10.115.0.223:3099,10.115.0.223:4099,10.115.0.223:5099,10.115.0.223:6099,10.115.0.223:7099,10.115.0.223:8099,10.115.0.223:9099,10.115.0.225:1099,10.115.0.225:2099,10.115.0.225:3099,10.115.0.225:4099,10.115.0.225:5099,10.115.0.225:6099,10.115.0.225:7099,10.115.0.225:8099,10.115.0.225:9099,10.115.0.224:1099,10.115.0.224:2099,10.115.0.224:3099,10.115.0.224:4099,10.115.0.224:5099,10.115.0.224:6099,10.115.0.224:7099,10.115.0.224:8099,10.115.0.224:9099 -l $result/result.txt  -e -o $result/report &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 30 Aug 2018 12:14:00 GMT</pubDate>
    </item>
    <item>
      <title>nmon性能测试工具的使用</title>
      <link>https://www.zhangaoo.com/article/nmon-tool</link>
      <content:encoded>&lt;h1&gt;nmon工具的简单使用&lt;/h1&gt; &lt;p&gt;最近在做&lt;code&gt;InfluxDB&lt;/code&gt;的性能测试，经同事推荐使用&lt;code&gt;nmon&lt;/code&gt;工具来记录机器资源的使用情况。包括常见的&lt;code&gt;CPU&lt;/code&gt;、内存、磁盘&lt;code&gt;IO&lt;/code&gt;、网络&lt;code&gt;IO&lt;/code&gt;等详细信息&lt;/p&gt; &lt;h1&gt;安装nmon&lt;/h1&gt; &lt;ul&gt; &lt;li&gt;或者下载rpm包安装&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;wget http://mirror.ghettoforge.org/distributions/gf/el/6/gf/x86_64/nmon-14i-1.gf.el6.x86_64.rpm rpm -ivh nmon-14i-1.gf.el6.x86_64.rpm &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;或者下载二进制文件&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;http://nmon.sourceforge.net/pmwiki.php?n=Site.Download &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;ubuntu的话可直接使用命令安装&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;apt-get install nmon &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;nmon的使用&lt;/h1&gt; &lt;ol&gt; &lt;li&gt;查看帮助&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;nmon -h &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;生成监控数据&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;# -f 表示生成的数据文件名中有时间; # -s 10 表示每 10 秒采集一次数据; # -c 60 表示采集 60 次，10*60=600 秒; $ nmon -f -s 1 -c 1900 #每一秒中采集一次，一共采集1900秒 &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;使用nmon_analyser分析数据&lt;/h1&gt; &lt;p&gt;&lt;code&gt;nmon_analyser&lt;/code&gt;工具可以吧数据制成报表，方便查看和分析。工具包含两个文件&lt;code&gt;NA_UserGuide v54.docx&lt;/code&gt;和&lt;code&gt;nmon analyser v54.xlsm&lt;/code&gt;，&lt;code&gt;Word&lt;/code&gt;文档为使用说明，&lt;code&gt;Excel&lt;/code&gt;就是我们的工具，点击加载数据&lt;code&gt;Analyser sheet&lt;/code&gt;中的&lt;code&gt;Analyze nmon data&lt;/code&gt;即可生成图标。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;下载地址&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;https://www.ibm.com/developerworks/community/wikis/home?lang=en#!/wiki/Power+Systems/page/nmon_analyser &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;关于生成的Excel图标所表示的含义具体可查看和工具一起的&lt;code&gt;NA_UserGuide v54.docx&lt;/code&gt;文档&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 30 Aug 2018 06:02:44 GMT</pubDate>
    </item>
    <item>
      <title>Docker入门篇一</title>
      <link>https://www.zhangaoo.com/article/docker-start</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://msdnshared.blob.core.windows.net/media/2017/10/docker.png" alt="alt" /&gt;&lt;/p&gt; &lt;h1&gt;什么是Docker?&lt;/h1&gt; &lt;p&gt;Docker是一个开源的引擎，可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署，包括&lt;code&gt;VMs（虚拟机）&lt;/code&gt;、&lt;code&gt;bare metal&lt;/code&gt;、&lt;code&gt;OpenStack&lt;/code&gt; 集群和其他的基础应用平台。&lt;/p&gt; &lt;h2&gt;Docker通常用于如下场景：&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;web应用的自动化打包和发布；&lt;/li&gt; &lt;li&gt;自动化测试和持续集成、发布；&lt;/li&gt; &lt;li&gt;在服务型环境中部署和调整数据库或其他的后台应用；&lt;/li&gt; &lt;li&gt;从头编译或者扩展现有的&lt;code&gt;OpenShift&lt;/code&gt;或&lt;code&gt;Cloud Foundry&lt;/code&gt;平台来搭建自己的&lt;code&gt;PaaS&lt;/code&gt;环境。&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;关于docker入门教程&lt;/h1&gt; &lt;h2&gt;准备开始&lt;/h2&gt; &lt;p&gt;&lt;code&gt;Docker&lt;/code&gt;系统有两个程序：&lt;code&gt;docker&lt;/code&gt;服务端和&lt;code&gt;docker&lt;/code&gt;客户端。其中&lt;code&gt;docker&lt;/code&gt;服务端是一个服务进程，管理着所有的容器。&lt;code&gt;docker&lt;/code&gt;客户端则扮演着&lt;code&gt;docker&lt;/code&gt;服务端的远程控制器，可以用来控制&lt;code&gt;docker&lt;/code&gt;的服务端进程。大部分情况下，&lt;code&gt;docker&lt;/code&gt;服务端和客户端运行在一台机器上。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;注册&lt;code&gt;docker&lt;/code&gt;账号&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;https://store.docker.com/signup?next=%2F%3Fref%3Dlogin &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;登录&lt;code&gt;docker&lt;/code&gt;账号下载docker.dmg进行安装地址如下&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;https://store.docker.com/editions/community/docker-ce-desktop-mac &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;安装完成后验证在终端敲入一下命令进行验证&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#查看版本号 docker version #查看其它 更多命令 docker &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;搜索可用docker镜像&lt;/h2&gt; &lt;p&gt;使用docker最简单的方式莫过于从现有的容器镜像开始。Docker官方网站专门有一个页面来存储所有可用的镜像，网址是：index.docker.io。你可以通过浏览这个网页来查找你想要使用的镜像，或者使用命令行的工具来检索。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;使用命令检索镜像&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;➜  ~ docker search influxdb NAME                 DESCRIPTION                                    STARS                OFFICIAL       AUTOMATED influxdb             InfluxDB is an open source time series datab…   536                 [OK]                 tutum/influxdb       InfluxDB image - listens in port 8083 (web) …   220                                     [OK] telegraf             Telegraf is an agent for collecting metrics …   176                 [OK]                 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;学会使用docker命令来下载镜像&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;docker pull influxdb &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;➜  ~ docker pull influxdb Using default tag: latest Error response from daemon: Get https://registry-1.docker.io/v2/library/influxdb/manifests/latest: unauthorized: incorrect username or password &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;出现以上错误信息的 大概率是没登录，需要在命令行中登录&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;docker login Authenticating with existing credentials... Stored credentials invalid or expired Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username (xxxxxx@gmail.com):  Password:  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;需要注意的是这里需要使用用户名登录，而不是邮箱登录。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;登录成功后再次尝试&lt;code&gt;pull&lt;/code&gt;镜像，如下&lt;code&gt;shell&lt;/code&gt;输出&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;➜  ~ docker pull influxdb Using default tag: latest latest: Pulling from library/influxdb 55cbf04beb70: Pull complete  1607093a898c: Pull complete  9a8ea045c926: Pull complete  4c8b66fe6495: Pull complete  9f3c67b9b082: Pull complete  864cc6881ca8: Pull complete  c1165c5c85e6: Pull complete  0b5bd48b7b2b: Pull complete  Digest: sha256:c9098612611038b6d0daddf1ed89d0144f41124b0feed765c0d31844e7f32e9f Status: Downloaded newer image for influxdb:latest &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;如果镜像有多个版本，可使用&lt;code&gt;tag&lt;/code&gt;进行区分&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;docker pull influxdb:1.6-data-alpine &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;启动一个镜像&lt;/h2&gt; &lt;p&gt;&lt;code&gt;docker&lt;/code&gt;容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源，包括文件系统、系统类库、&lt;code&gt;shell&lt;/code&gt; 环境等等。但这个沙盒默认是不会运行任何程序的。你需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程，所以当该进程结束的时候，容器也会完全的停止。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;这里我们以&lt;code&gt;InfluxDB&lt;/code&gt;作为实验对象，官网的&lt;a href="https://docs.docker.com/samples/library/influxdb/#using-this-image" target="_blank"&gt;参考教程&lt;/a&gt;&lt;/li&gt; &lt;li&gt;对于&lt;code&gt;docker&lt;/code&gt;我们需要了解几点，&lt;code&gt;docker&lt;/code&gt;内是不能保存的数据的，因为重启后数据都会丢失。因此对于数据库的数据文件或配置文件我们需要保存在在宿主机上。通过启动时的命令行参数可以执行位置&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;启动&lt;code&gt;InfluxDB&lt;/code&gt;镜像&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;$ docker run -p 8086:8086 -v /Users/xxxxxx/Documents/dev/company/docker/influxdb/data:/var/lib/influxdb influxdb &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;这里把宿主机目录&lt;code&gt;/Users/xxxxxx/Documents/dev/company/docker/influxdb/data&lt;/code&gt;挂载到镜像的&lt;code&gt;/var/lib/influxdb&lt;/code&gt;目录用来存储数据&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;也可以使用如下命令让&lt;code&gt;docker&lt;/code&gt;自己控制挂载点&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;$ docker run -p 8086:8086 -v influxdb:/var/lib/influxdb influxdb &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;向外暴露的端口 The following ports are important and are used by InfluxDB. &lt;ul&gt; &lt;li&gt;8086 HTTP API port&lt;/li&gt; &lt;li&gt;8083 Administrator interface port, if it is enabled&lt;/li&gt; &lt;li&gt;2003 Graphite support, if it is enabled&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;端口必须在启动命令中显示的加上不然默认是不会开放的的&lt;/li&gt; &lt;/ul&gt; &lt;ol start="3"&gt; &lt;li&gt; &lt;p&gt;添加配置文件 &lt;code&gt;InfluxDB&lt;/code&gt;既能使用命令行中的环境变量来启动&lt;code&gt;InfluxDB&lt;/code&gt;，也能通过挂载配置文件来启动&lt;/p&gt; &lt;ul&gt; &lt;li&gt;生成一个配置文件&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;    $ docker run --rm influxdb influxd config &amp;gt; influxdb.conf &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;启动参数添加配置文件&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;    docker run -d -p 8086:8086 -p 8083:8083 -m 8G \     -v /home/zhanga/influxdb/conf/influxdb.conf:/etc/influxdb/influxdb.conf:ro \     -v /home/zhanga/influxdb/data:/var/lib/influxdb \     -e INFLUXDB_ADMIN_ENABLED=true \     influxdb -config /etc/influxdb/influxdb.conf &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用命令行变量配置&lt;code&gt;InfluxDB&lt;/code&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;blockquote&gt; &lt;p&gt;For environment variables, the format is INFLUXDB_$SECTION_$NAME. All dashes (-) are replaced with underscores (_). If the variable isn’t in a section, then omit that part.&lt;/p&gt; &lt;/blockquote&gt; &lt;pre&gt;&lt;code&gt;INFLUXDB_REPORTING_DISABLED=true INFLUXDB_META_DIR=/path/to/metadir INFLUXDB_DATA_QUERY_LOG_ENABLED=false &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;配置&lt;code&gt;InfluxDB Administrator Interface&lt;/code&gt; The administrator interface is deprecated as of 1.1.0 and will be removed in 1.3.0. It is disabled by default. If needed, it can still be enabled by setting an environment variable like below:&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;docker run -p 8086:8086 -p 8083:8083 \     -e INFLUXDB_ADMIN_ENABLED=true \     influxdb &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;注意上面的&lt;code&gt;Administrator Interface&lt;/code&gt;并不是管理的&lt;code&gt;Dashboard&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;ol start="6"&gt; &lt;li&gt;HTTP API 可参考InfluxDB&lt;a href="https://docs.influxdata.com/influxdb/v1.6/guides/writing_data/" target="_blank"&gt;相关文档&lt;/a&gt; Creating a DB named mydb:&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;$ curl -i -XPOST http://localhost:8086/query --data-urlencode &amp;quot;q=CREATE DATABASE mydb&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Inserting into the DB: Write a point to the database mydb with a timestamp in seconds&lt;/p&gt; &lt;pre&gt;&lt;code&gt;$ curl -i -XPOST &amp;quot;http://localhost:8086/write?db=mydb&amp;amp;precision=s&amp;quot; --data-binary 'mymeas,mytag=1 myfield=90 1463683075'  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;插入成功返回结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;➜  curl -i -XPOST &amp;quot;http://localhost:8086/write?db=mydb&amp;amp;precision=s&amp;quot; --data-binary 'mymeas,mytag=1 myfield=90 1463683075' HTTP/1.1 204 No Content Content-Type: application/json Request-Id: e4c96103-a528-11e8-8003-000000000000 X-Influxdb-Build: OSS X-Influxdb-Version: 1.6.1 X-Request-Id: e4c96103-a528-11e8-8003-000000000000 Date: Tue, 21 Aug 2018 09:59:29 GMT &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Query Data: Query data with a SELECT statement&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;$ curl -G 'http://localhost:8086/query?db=mydb' --data-urlencode 'q=SELECT * FROM &amp;quot;mymeas&amp;quot;' curl -G 'http://192.168.20.232:8086/query?db=mydb' --data-urlencode 'q=SELECT * FROM &amp;quot;mymeas&amp;quot;' &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;返回结果&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;results&amp;quot;: [         {             &amp;quot;statement_id&amp;quot;: 0,             &amp;quot;series&amp;quot;: [                 {                     &amp;quot;name&amp;quot;: &amp;quot;mymeas&amp;quot;,                     &amp;quot;columns&amp;quot;: [                         &amp;quot;time&amp;quot;,                         &amp;quot;myfield&amp;quot;,                         &amp;quot;mytag&amp;quot;                     ],                     &amp;quot;values&amp;quot;: [                         [                             &amp;quot;2016-05-19T18:37:55Z&amp;quot;,                             90,                             &amp;quot;1&amp;quot;                         ],                         [                             &amp;quot;2016-05-19T18:37:56Z&amp;quot;,                             91,                             &amp;quot;2&amp;quot;                         ]                     ]                 }             ]         }     ] } &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;Docker 参数使用&lt;/h1&gt; &lt;h3&gt;docker ps&lt;/h3&gt; &lt;p&gt;查看当前运行的容器，作用类似于&lt;code&gt;Linux&lt;/code&gt;中的&lt;code&gt;ps&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;docker images&lt;/h3&gt; &lt;p&gt;查看当前已经下载的镜像&lt;/p&gt; &lt;h3&gt;docker run -d&lt;/h3&gt; &lt;p&gt;Run container in background and print container ID&lt;/p&gt; &lt;h3&gt;docker stop&lt;/h3&gt; &lt;p&gt;Stop one or more running containers&lt;/p&gt; &lt;pre&gt;&lt;code&gt;docker stop [OPTIONS] CONTAINER [CONTAINER...] &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;docker run -m&lt;/h3&gt; &lt;p&gt;Memory limit&lt;/p&gt; &lt;h3&gt;docker container run&lt;/h3&gt; &lt;p&gt;Run a command in a new container&lt;/p&gt; &lt;h3&gt;docker container stop&lt;/h3&gt; &lt;p&gt;Stop one or more running containers&lt;/p&gt; &lt;h3&gt;docker exec&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;连接到容器内的 终端&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;docker exec -it b00c6630464f /bin/bash   &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;-v参数&lt;/h3&gt; &lt;p&gt;把宿主机的目录挂载到&lt;code&gt;docker&lt;/code&gt;容器中，比如一些存储数据和配置文件的目录&lt;/p&gt; &lt;p&gt;&lt;strong&gt;注意：如果容器中什么都没运行，那么容器启动后会自动结束比如下面这个只含有java8的容器&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;docker run -it nimmis/java-centos:oracle-8-jre &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;--net=&amp;quot;host&amp;quot;&lt;/h3&gt; &lt;p&gt;配置让容器完全使用宿主机的网络，不安全，官方不推荐使用&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 25 Aug 2018 07:58:07 GMT</pubDate>
    </item>
    <item>
      <title>SpringCloud篇二之Eureka</title>
      <link>https://www.zhangaoo.com/article/springcloud-eureka</link>
      <content:encoded>&lt;h1&gt;SpringCloud Eureka是什么？&lt;/h1&gt; &lt;p&gt;&lt;code&gt;Eureka&lt;/code&gt; 英 &lt;code&gt;[juˈri:kə]&lt;/code&gt; 字面意思是发现、找到的意思。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Eureka&lt;/code&gt;是&lt;code&gt;Netflix&lt;/code&gt;开发的服务发现框架，本身是一个基于&lt;code&gt;REST&lt;/code&gt;的服务,主要用于定位运行在&lt;code&gt;AWS&lt;/code&gt;域中的中间层服务，以达到负载均衡和中间层服务故障转移的目的。&lt;code&gt;Spring Cloud&lt;/code&gt;将它集成在其子项目&lt;code&gt;spring-cloud-netflix&lt;/code&gt;中，以实现&lt;code&gt;Spring Cloud的&lt;/code&gt;服务发现功能。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Eureka&lt;/code&gt; 采用了 &lt;code&gt;C-S&lt;/code&gt; 的设计架构。&lt;code&gt;Eureka Server&lt;/code&gt; 作为服务注册功能的服务器，它是服务注册中心。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Eureka&lt;/code&gt; 和&lt;code&gt;Dubbo&lt;/code&gt;的架构对比图：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Eureka&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/78i5a1d760gvhpfo9ueu134mhr.jpg" alt="eureka" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;dubbo&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/raf7jopmfgjaiq4oca1dhhjequ.png" alt="dubbo" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;包含两个组件：&lt;code&gt;Eureka Server&lt;/code&gt;和&lt;code&gt;Eureka Client&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;Eureka Server&lt;/code&gt;提供服务注册服务&lt;/li&gt; &lt;li&gt;各个节点启动后，会在&lt;code&gt;Eureka Server&lt;/code&gt;中进行注册，这样&lt;code&gt;Eureka Server&lt;/code&gt;中的服务注册表中将会存储所有可用服务节点的信息，服务节点的信息可以在界面中直观的看到&lt;/li&gt; &lt;li&gt;&lt;code&gt;Eureka Client&lt;/code&gt;是一个&lt;code&gt;Java&lt;/code&gt;客户端，用于简化&lt;code&gt;Eureka Server&lt;/code&gt;的交互，客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后，将会向&lt;code&gt;Eureka Server&lt;/code&gt;发送心跳(默认周期为30秒)。如果&lt;code&gt;Eureka Server&lt;/code&gt;在多个心跳周期内没有接收到某个节点的心跳，&lt;code&gt;Eureka Server&lt;/code&gt;将会从服务注册表中把这个服务节点移除（默认90秒)。&lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;Eureka的三大角色&lt;/h1&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;Eureka Server&lt;/code&gt; 提供服务注册和发现。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Service Provider&lt;/code&gt;服务提供方将自身服务注册到&lt;code&gt;Eureka&lt;/code&gt;，从而使服务消费方能够找到。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Service Consume&lt;/code&gt;r服务消费方从&lt;code&gt;Eureka&lt;/code&gt;获取注册服务列表，从而能够消费服务。&lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;Eureka的构建和使用&lt;/h1&gt; &lt;h2&gt;创建Eureka注册中心模块&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;POM&lt;/code&gt;文件如下&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;parent&amp;gt;         &amp;lt;artifactId&amp;gt;user-management&amp;lt;/artifactId&amp;gt;         &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;         &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;/parent&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;artifactId&amp;gt;microservice-cloud-eureka-7001&amp;lt;/artifactId&amp;gt;      &amp;lt;dependencies&amp;gt;         &amp;lt;!--eureka-server服务端 --&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-cloud-starter-eureka-server&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;!-- 修改后立即生效，热部署 --&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;springloaded&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;入口代码&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/**  * @version Version: 1.0  * @date DateTime: 2018/08/18 23:42:00&amp;lt;br/&amp;gt;  */ @SpringBootApplication @EnableEurekaServer // EurekaServer服务器端启动类,接受其它微服务注册进来 public class EurekaServer7001App {     public static void main(String[] args) {         SpringApplication.run(EurekaServer7001App.class,args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;启动并验证启动成功&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;# 直接浏览器访问端口 http://127.0.0.1:7001 &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;将已有的服务注册到注册中心&lt;/h2&gt; &lt;p&gt;将&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;服务注册到注册中心&lt;/p&gt; &lt;ol&gt; &lt;li&gt;修改&lt;code&gt;POM&lt;/code&gt;文件添加依赖&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- 将微服务provider侧注册进eureka --&amp;gt;    &amp;lt;dependency&amp;gt;      &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;      &amp;lt;artifactId&amp;gt;spring-cloud-starter-eureka&amp;lt;/artifactId&amp;gt;    &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;修改配置文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   client: #客户端注册进eureka服务列表内     service-url:        defaultZone: http://localhost:7001/eureka &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;添加服务发现注解&lt;code&gt;@EnableEurekaClient&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz;  import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:44:00&amp;lt;br/&amp;gt;  */ @EnableEurekaClient //本服务启动后会自动注册进eureka服务中 @SpringBootApplication public class UserProvider8001App {     public static void main(String[] args) {         SpringApplication.run(UserProvider8001App.class,args);     } }  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;测试服务注册结果 直接在浏览器访问下面&lt;code&gt;url&lt;/code&gt;，确认服务正确注册&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;http://localhost:7001 &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;完善我们的微服务&lt;/h2&gt; &lt;p&gt;目前我们已经完成了注册功能，但是此时我们刷新一下http://localhost:7001/ 这个页面就会看到一段红色的字&lt;/p&gt; &lt;pre&gt;&lt;code&gt;EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;这个是&lt;code&gt;Eureka&lt;/code&gt;的自我保护的一个机制。 那么为什么&lt;code&gt;Eureka&lt;/code&gt;会有这种自我保护的机制呢，原因有以下几点？ &lt;ol&gt; &lt;li&gt;因为长时间没有另外的一些服务访问，也就是说没有心跳。&lt;/li&gt; &lt;li&gt;服务名没有变更，已经有的服务现在没了。 某时刻某一个微服务不可用了，&lt;code&gt;eureka&lt;/code&gt;不会立刻清理，依旧会对该微服务的信息进行保存。那么我们怎么解决呢？&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;采用&lt;code&gt;actuator&lt;/code&gt;与注册微服务信息进行完善 &lt;code&gt;actuator&lt;/code&gt;是什么？&lt;/li&gt; &lt;li&gt;&lt;code&gt;actuator&lt;/code&gt;是&lt;code&gt;SpringBoot&lt;/code&gt;里面主要用来主管和监控和配置&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;我们先来完善我们的微服务再来解决这种Eureka自我保护的机制&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;修改主机名 在&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;的&lt;code&gt;application.yml&lt;/code&gt;文件里面最后一行添加如下配置：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:     instance:         instance-id: microservicecloud-user-8001         prefer-ip-address: true                                 #访问路径可以显示IP地址 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;微服务info内容详细信息&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;修改&lt;code&gt;microservice-cloud-provider-user-8001 POM&lt;/code&gt;文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- actuator监控信息完善 --&amp;gt;    &amp;lt;dependency&amp;gt;      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;      &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;    &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;修改总工程pom文件添加构建&lt;code&gt;build&lt;/code&gt;信息 添加内容：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;build&amp;gt;    &amp;lt;finalName&amp;gt;micro-service-cloud&amp;lt;/finalName&amp;gt;    &amp;lt;resources&amp;gt;      &amp;lt;resource&amp;gt;        &amp;lt;directory&amp;gt;src/main/resources&amp;lt;/directory&amp;gt;        &amp;lt;filtering&amp;gt;true&amp;lt;/filtering&amp;gt;      &amp;lt;/resource&amp;gt;    &amp;lt;/resources&amp;gt;    &amp;lt;plugins&amp;gt;      &amp;lt;plugin&amp;gt;        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;        &amp;lt;artifactId&amp;gt;maven-resources-plugin&amp;lt;/artifactId&amp;gt;        &amp;lt;configuration&amp;gt;          &amp;lt;delimiters&amp;gt;           &amp;lt;delimit&amp;gt;@&amp;lt;/delimit&amp;gt;&amp;lt;!--注意不要使用$，原因是${}会被maven处理--&amp;gt;          &amp;lt;/delimiters&amp;gt;        &amp;lt;/configuration&amp;gt;      &amp;lt;/plugin&amp;gt;    &amp;lt;/plugins&amp;gt;   &amp;lt;/build&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;再修改&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;工程下的&lt;code&gt;application.yml&lt;/code&gt;文件，来添加我们这个微服务的一些描述&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;info:   app.name: zhanga-micro-service-cloud   company.name: www.zhangaoo.com                           #我的博客地址   build.artifactId: @project.artifactId@                   #注意不要使用$，原因是${}会被maven处理   build.version: @project.version@ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;访问对应的接口&lt;code&gt;http://192.168.50.95:8001/info&lt;/code&gt;，就会返回以下&lt;code&gt;JSON&lt;/code&gt;结果&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;app&amp;quot;: {         &amp;quot;name&amp;quot;: &amp;quot;zhanga-micro-service-cloud&amp;quot;     },     &amp;quot;company&amp;quot;: {         &amp;quot;name&amp;quot;: &amp;quot;www.zhangaoo.com&amp;quot;     },     &amp;quot;build&amp;quot;: {         &amp;quot;artifactId&amp;quot;: &amp;quot;microservicecloud-provider-user-8001&amp;quot;,         &amp;quot;version&amp;quot;: &amp;quot;1.0-SNAPSHOT&amp;quot;     } } &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;Eureka的自我保护机制介绍&lt;/h2&gt; &lt;p&gt;默认情况下，如果&lt;code&gt;Eureka Server&lt;/code&gt;在一定时间内没有接收到某个微服务实例的心跳，&lt;code&gt;Eureka Server&lt;/code&gt;将会注销该实例（默认&lt;code&gt;90&lt;/code&gt;秒）。但是当网络分区故障发生时，微服务与&lt;code&gt;Eureka Server&lt;/code&gt;之间无法正常通信，以上行为可能变得非常危险了——因为微服务本身其实是健康的，此时本不应该注销这个微服务。&lt;code&gt;Eureka&lt;/code&gt;通过“自我保护模式”来解决这个问题——当&lt;code&gt;Eureka Server&lt;/code&gt;节点在短时间内丢失过多客户端时（可能发生了网络分区故障），那么这个节点就会进入自我保护模式。一旦进入该模式，&lt;code&gt;Eureka Server&lt;/code&gt;就会保护服务注册表中的信息，不再删除服务注册表中的数据（也就是不会注销任何微服务）。当网络故障恢复后，该&lt;code&gt;Eureka Server&lt;/code&gt;节点会自动退出自我保护模式。&lt;/p&gt; &lt;p&gt;到的心跳数重新恢复到阈值以上时，该&lt;code&gt;Eureka Server&lt;/code&gt;节点就会自动退出自我保护模式。它的设计哲学就是宁可保留错误的服务注册信息，也不盲目注销任何可能健康的服务实例。一句话讲解：好死不如赖活着。&lt;/p&gt; &lt;p&gt;综上，自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务（健康的微服务和不健康的微服务都会保留），也不盲目注销任何健康的微服务。使用自我保护模式，可以让&lt;code&gt;Eureka&lt;/code&gt;集群更加的健壮、稳定。&lt;/p&gt; &lt;p&gt;在&lt;code&gt;Spring Cloud&lt;/code&gt;中，可以使用&lt;code&gt;eureka.server.enable-self-preservation = false&lt;/code&gt; 禁用自我保护模式。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:    enable-self-preservation: false &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是&lt;code&gt;eureka&lt;/code&gt;自我保护模式希望大家在没有其他特殊业务需求的话就不要去禁用它的自我保护模式&lt;/p&gt; &lt;h2&gt;Eure的服务发现&lt;/h2&gt; &lt;p&gt;对于注册进&lt;code&gt;eureka&lt;/code&gt;里面的微服务，可以通过服务发现来获得该服务的信息&lt;/p&gt; &lt;ol&gt; &lt;li&gt;在UserController中加入下面代码&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Autowired     private DiscoveryClient discoveryClient;      @RequestMapping(value = &amp;quot;/discovery&amp;quot;,method = RequestMethod.GET)     public Object discover(){         List&amp;lt;String&amp;gt; list = discoveryClient.getServices();         log.info(&amp;quot;list:&amp;quot; + JSONObject.toJSONString(list));         List&amp;lt;ServiceInstance&amp;gt; srvList = discoveryClient.getInstances(&amp;quot;MICROSERVICECLOUD-USER&amp;quot;);         for (ServiceInstance element : srvList) {             //然后打印你指定要找的微服务的ID和主机和端口以及访问地址等信息             log.info(element.getServiceId() + &amp;quot;\t&amp;quot; + element.getHost() + &amp;quot;\t&amp;quot; + element.getPort() + &amp;quot;\t&amp;quot; + element.getUri());         }         return this.discoveryClient;     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;测试接口结果 访问接口&lt;code&gt;http://127.0.0.1:8001/user/discovery&lt;/code&gt;结果如下&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;localServiceInstance&amp;quot;: {         &amp;quot;host&amp;quot;: &amp;quot;192.168.50.95&amp;quot;,         &amp;quot;port&amp;quot;: 8001,         &amp;quot;serviceId&amp;quot;: &amp;quot;microservicecloud-user&amp;quot;,         &amp;quot;metadata&amp;quot;: {},         &amp;quot;uri&amp;quot;: &amp;quot;http://192.168.50.95:8001&amp;quot;,         &amp;quot;secure&amp;quot;: false     },     &amp;quot;services&amp;quot;: [         &amp;quot;microservicecloud-user&amp;quot;     ] } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;给服务定义静态&lt;code&gt;metadata&lt;/code&gt;  在上面的JSON返回结果中&lt;code&gt;metadata&lt;/code&gt;为空，我们可以通过配置文件定义静态&lt;code&gt;metadata&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   instance:     metadata-map:       fixed-s1: &amp;quot;value_1&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;再次请求返回结果如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;services&amp;quot;: [         &amp;quot;microservicecloud-eureka&amp;quot;     ],     &amp;quot;localServiceInstance&amp;quot;: {         &amp;quot;host&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,         &amp;quot;port&amp;quot;: 8001,         &amp;quot;uri&amp;quot;: &amp;quot;http://127.0.0.1:8001&amp;quot;,         &amp;quot;serviceId&amp;quot;: &amp;quot;microservicecloud-user&amp;quot;,         &amp;quot;metadata&amp;quot;: {             &amp;quot;fixed-s1&amp;quot;: &amp;quot;value_1&amp;quot;         },         &amp;quot;secure&amp;quot;: false     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;Dynamic Service Metadata&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;可在代码中动态定义&lt;code&gt;metadata&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Component public class DynamicMetadataReporter {     @Autowired     private ApplicationInfoManager applicationInfoManager;      @PostConstruct     public void init() {         Map&amp;lt;String, String&amp;gt; map = applicationInfoManager.getInfo().getMetadata();         map.put(&amp;quot;dynamic-s1&amp;quot;, &amp;quot;value_2&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;[     {         &amp;quot;host&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,         &amp;quot;port&amp;quot;: 8001,         &amp;quot;uri&amp;quot;: &amp;quot;http://127.0.0.1:8001&amp;quot;,         &amp;quot;serviceId&amp;quot;: &amp;quot;MICROSERVICECLOUD-USER&amp;quot;,         &amp;quot;metadata&amp;quot;: {             &amp;quot;dynamic-s1&amp;quot;: &amp;quot;value_2&amp;quot;,             &amp;quot;fixed-s1&amp;quot;: &amp;quot;value_1&amp;quot;         },         ....     }     ] &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;Eureka集群配置&lt;/h1&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;按照&lt;code&gt;microservice-cloud-eureka-7001&lt;/code&gt;配置分别在创建&lt;code&gt;microservice-cloud-eureka-7002&lt;/code&gt;和&lt;code&gt;microservice-cloud-eureka-7003&lt;/code&gt;两个模块&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;修改3台&lt;code&gt;eureka&lt;/code&gt;服务器的&lt;code&gt;application.yml&lt;/code&gt;配置&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;7001的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 7001 eureka:   instance:     hostname: localhost #eureka服务端的实例名称   client:     register-with-eureka: false #false表示不向注册中心注册自己。     fetch-registry: false #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       # 单机环境设置为 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/       defaultZone: http://localhost:7002/eureka/,http://localhost:7003/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;7002的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 7002 eureka:   instance:     hostname: localhost #eureka服务端的实例名称   client:     register-with-eureka: false #false表示不向注册中心注册自己。     fetch-registry: false #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       defaultZone: http://localhost:7001/eureka/,http://localhost:7003/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;7003的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 7003 eureka:   instance:     hostname: localhost #eureka服务端的实例名称   client:     register-with-eureka: false #false表示不向注册中心注册自己。     fetch-registry: false #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;修改&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;配置文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   client:     service-url:       defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/   instance:     instance-id: microservicecloud-user-8001     prefer-ip-address: true         #访问路径可以显示IP地址     ip-address:  127.0.0.1 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;按上述配置配置以后发现会有下面问题&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Eureka Server间无法同步数据，具体表现是每个Eureka Server上的续约数都不一样，同时在General Info标签下别的Eureka Server显示为”unavailable-replicas”。&lt;/li&gt; &lt;li&gt;DS Replicas标签下面也不显示任何东西&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;针对上面的两个问题，需要修改两点&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;集群默认使用hostname，因此需要在hosts文件映射一下&lt;/li&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;集群各个&lt;code&gt;Eureka Server&lt;/code&gt;需要互相发现，因此需要设置&lt;code&gt;register-with-eureka: true&lt;/code&gt;和&lt;code&gt;fetch-registry: true&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   instance:     hostname: localhost #eureka服务端的实例名称   client:     register-with-eureka: true      fetch-registry: true  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;重新配置如下&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;hosts配置配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;127.0.0.1  eureka7001.com 127.0.0.1  eureka7002.com 127.0.0.1  eureka7003.com &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;7001的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   application:     name: microservicecloud-eureka server:   port: 7001 eureka:   instance:     hostname: eureka7001.com #eureka服务端的实例名称 #    prefer-ip-address: true #    ip-address: 127.0.0.1   client:     register-with-eureka: true #false表示不向注册中心注册自己。     fetch-registry: true #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       # 单机环境设置为 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/       defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;7002的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   application:     name: microservicecloud-eureka server:   port: 7002 eureka:   instance:     hostname: eureka7002.com #eureka服务端的实例名称 #    prefer-ip-address: true #    ip-address: 127.0.0.1   client:     register-with-eureka: true #false表示不向注册中心注册自己。     fetch-registry: true #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;7003的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   application:     name: microservicecloud-eureka server:   port: 7003 eureka:   instance:     hostname: eureka7003.com #eureka服务端的实例名称 #    prefer-ip-address: true #    ip-address: 127.0.0.1   client:     register-with-eureka: true #false表示不向注册中心注册自己。     fetch-registry: true #false表示自己端就是注册中心，我的职责就是维护服务实例，并不需要去检索服务     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/        #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;microservice-cloud-provider-user-8001&lt;/code&gt;的配置&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   client:     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/   instance:     instance-id: microservicecloud-user-8001     prefer-ip-address: true         #访问路径可以显示IP地址     ip-address:  127.0.0.1 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;注意：因为开启了&lt;code&gt;register-with-eureka: true&lt;/code&gt;和&lt;code&gt;fetch-registry: true&lt;/code&gt;，如果其中一个&lt;code&gt;eureka server&lt;/code&gt;连接不上的话就会异常，默认每10秒钟会重试连接一次。因此这个异常在&lt;code&gt;eureka&lt;/code&gt;集群中有节点不可用的话是正常的。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;一个完整的截图如下，集群中的每个&lt;code&gt;eureka&lt;/code&gt;都会同步集群中的信息 &lt;img src="https://www.zhangaoo.com/upload/2018/08/sj4kmecvu0i7ip73bblno3ogcm.png" alt="alt" /&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h1&gt;Eureka相比Zookeeper有哪些优势呢？&lt;/h1&gt; &lt;blockquote&gt; &lt;p&gt;著名的&lt;code&gt;CAP&lt;/code&gt;理论指出，一个分布式系统不可能同时满足&lt;code&gt;C&lt;/code&gt;(一致性)、&lt;code&gt;A&lt;/code&gt;(可用性)和&lt;code&gt;P&lt;/code&gt;(分区容错性)。由于分区容错性&lt;code&gt;P&lt;/code&gt;在是分布式系统中必须要保证的，因此我们只能在&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;C&lt;/code&gt;之间进行权衡。&lt;/p&gt; &lt;/blockquote&gt; &lt;ul&gt; &lt;li&gt;Zookeeper保证的是CP。&lt;/li&gt; &lt;li&gt;Eureka则是AP。&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;Zookeeper&lt;/code&gt;保证&lt;code&gt;CP&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;当向注册中心查询服务列表时，我们可以容忍注册中心返回的是几分钟以前的注册信息，但不能接受服务直接&lt;code&gt;down&lt;/code&gt;掉不可用。也就是说，服务注册功能对可用性的要求要高于一致性。但是&lt;code&gt;zk&lt;/code&gt;会出现这样一种情况，当&lt;code&gt;master&lt;/code&gt;节点因为网络故障与其他节点失去联系时，剩余节点会重新进行&lt;code&gt;leader&lt;/code&gt;选举。问题在于，选举&lt;code&gt;leader&lt;/code&gt;的时间太长，30 ~ 120s, 且选举期间整个&lt;code&gt;zk&lt;/code&gt;集群都是不可用的，这就导致在选举期间注册服务瘫痪。在云部署的环境下，因网络问题使得&lt;code&gt;zk&lt;/code&gt;集群失去&lt;code&gt;master&lt;/code&gt;节点是较大概率会发生的事，虽然服务能够最终恢复，但是漫长的选举时间导致的注册长期不可用是不能容忍的。&lt;/p&gt; &lt;ol start="2"&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;保证&lt;code&gt;AP&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;code&gt;Eureka&lt;/code&gt;看明白了这一点，因此在设计时就优先保证可用性。&lt;code&gt;Eureka&lt;/code&gt;各个节点都是平等的，几个节点挂掉不会影响正常节点的工作，剩余的节点依然可以提供注册和查询服务。而&lt;code&gt;Eureka&lt;/code&gt;的客户端在向某个&lt;code&gt;Eureka&lt;/code&gt;注册或时如果发现连接失败，则会自动切换至其它节点，只要有一台Eureka还在，就能保证注册服务可用(保证可用性)，只不过查到的信息可能不是最新的(不保证强一致性)。除此之外，&lt;code&gt;Eureka&lt;/code&gt;还有一种自我保护机制，如果在15分钟内超过85%的节点都没有正常的心跳，那么&lt;code&gt;Eureka&lt;/code&gt;就认为客户端与注册中心出现了网络故障，此时会出现以下几种情况：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;不再从注册列表中移除因为长时间没收到心跳而应该过期的服务&lt;/li&gt; &lt;li&gt;&lt;code&gt;Eureka&lt;/code&gt;仍然能够接受新服务的注册和查询请求，但是不会被同步到其它节点上(即保证当前节点依然可用)&lt;/li&gt; &lt;li&gt;当网络稳定时，当前实例新的注册信息会被同步到其它节点中&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;因此， &lt;code&gt;Eureka&lt;/code&gt;可以很好的应对因网络故障导致部分节点失去联系的情况，而不会像&lt;code&gt;zookeeper&lt;/code&gt;那样使整个注册服务瘫痪&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 25 Aug 2018 06:32:02 GMT</pubDate>
    </item>
    <item>
      <title>使用CountDownLatch做线程同步</title>
      <link>https://www.zhangaoo.com/article/countdownlatch</link>
      <content:encoded>&lt;h1&gt;CountDownLatch&lt;/h1&gt; &lt;p&gt;源码的说明如下&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.&lt;/p&gt; &lt;/blockquote&gt; &lt;blockquote&gt; &lt;p&gt;CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后，再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后，计数器的值就会减一。当计数器的值为0时，表示所有的线程都已经完成了任务，然后在CountDownLatch上等待的线程就可以恢复执行任务。&lt;/p&gt; &lt;/blockquote&gt; &lt;h1&gt;CountDownLatch的用法&lt;/h1&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;用法一：&lt;code&gt;CountDownLatch&lt;/code&gt;典型用法某一线程在开始运行前等待n个线程执行完毕。将&lt;code&gt;CountDownLatch&lt;/code&gt;的计数器初始化为&lt;code&gt;n new CountDownLatch(n)&lt;/code&gt; ，每当一个任务线程执行完毕，就将计数器减&lt;code&gt;1 countdownlatch.countDown()&lt;/code&gt;，当计数器的值变为&lt;code&gt;0&lt;/code&gt;时，在&lt;code&gt;CountDownLatch上 await()&lt;/code&gt; 的线程就会被唤醒。一个典型应用场景就是启动一个服务时，主线程需要等待多个组件加载完毕，之后再继续执行。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;用法二：实现多个线程开始执行任务的最大并行性。注意是并行性，不是并发，强调的是多个线程在某一时刻同时开始执行。类似于赛跑，将多个线程放到起点，等待发令枪响，然后同时开跑。做法是初始化一个共享的&lt;code&gt;CountDownLatch(1)&lt;/code&gt;，将其计数器初始化为&lt;code&gt;1&lt;/code&gt;，多个线程在开始执行任务前首先 &lt;code&gt;coundownlatch.await()&lt;/code&gt;，当主线程调用 &lt;code&gt;countDown()&lt;/code&gt; 时，计数器变为0，多个线程同时被唤醒。&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;CountDownLatch的不足&lt;/h1&gt; &lt;p&gt;&lt;code&gt;CountDownLatch&lt;/code&gt;是一次性的，计数器的值只能在构造方法中初始化一次，之后没有任何机制再次对其设置值，当&lt;code&gt;CountDownLatch&lt;/code&gt;使用完毕后，它不能再次被使用。&lt;/p&gt; &lt;h1&gt;主要的几个方法&lt;/h1&gt; &lt;p&gt;1.构造器&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public CountDownLatch(int count) {  };  //参数count为计数值 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;2.其他相关方法&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起，它会等待直到count值为0才继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()类似，只不过等待一定的时间后count值还没变为0的话就会继续执行 public void countDown() { };  //将count值减1 &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;使用示例&lt;/h1&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void testCountDownLatch() {         CountDownLatch latch = new CountDownLatch(2);         ExecutorService executorService = new ThreadPoolExecutor(2, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue&amp;lt;Runnable&amp;gt;());         executorService.execute(new Runnable() {             @Override             public void run() {                 try {                     System.out.println(&amp;quot;Sub thread 1: &amp;quot; + Thread.currentThread().getName() + &amp;quot; running ... &amp;quot;);                     Thread.sleep(4000);                     System.out.println(&amp;quot;Sub thread 1: &amp;quot; + Thread.currentThread().getName() + &amp;quot; completed&amp;quot;);                 } catch (InterruptedException e) {                     e.printStackTrace();                 } finally {                     latch.countDown();                 }             }         });          executorService.execute(new Runnable() {             @Override             public void run() {                 try {                     System.out.println(&amp;quot;Sub thread 2: &amp;quot; + Thread.currentThread().getName() + &amp;quot; running ... &amp;quot;);                     Thread.sleep(1000);                     System.out.println(&amp;quot;Sub thread 2: &amp;quot; + Thread.currentThread().getName() + &amp;quot; completed&amp;quot;);                 } catch (InterruptedException e) {                     e.printStackTrace();                 } finally {                     latch.countDown();                 }             }         });          try {             System.out.println(&amp;quot;Waiting two sub thread ...&amp;quot;);             latch.await();             System.out.println(&amp;quot;Sub thread all donne&amp;quot;);             System.out.println(&amp;quot;Continuing main thread&amp;quot;);         } catch (InterruptedException e) {             e.printStackTrace();         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;结果&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;Waiting two sub thread ... Sub thread 2: pool-1-thread-2 running ...  Sub thread 2: pool-1-thread-2 completed Sub thread 1: pool-1-thread-1 completed Sub thread all donne Continuing main thread &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;简要说明：主线程会等待两个子线程执行结束，然后再 执行。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Fri, 17 Aug 2018 10:47:25 GMT</pubDate>
    </item>
    <item>
      <title>SpringCloud入门篇一</title>
      <link>https://www.zhangaoo.com/article/springcloud-first</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/uafqarops8ig3o7e4r24saanl6.jpeg" alt="alt" /&gt;&lt;/p&gt; &lt;h1&gt;SpringCloud是什么？&lt;/h1&gt; &lt;p&gt;&lt;code&gt;SpringCloud&lt;/code&gt;是一个基于&lt;code&gt;SpringBoot&lt;/code&gt;实现的微服务架构开发工具。 它为微服务架构中涉及的配置管理、 服务治理、 断路器、 智能路由、 微代理、 控制总线、 全局锁、 决策竞选、 分布式会话和集群状态管理等操作提供了一 种简单的开发方式。总结为以下两点：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;SpringCloud&lt;/code&gt;是分布式一站式的解决方案。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SpringCloud&lt;/code&gt;是微服务技术的一种落地的体现和实现。&lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;SpringCloud和SpringBoot的区别和关系？&lt;/h1&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;SpringBoot&lt;/code&gt;专注于快速方便的开发单个个体微服务。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SpringCloud&lt;/code&gt;是关注全局的微服务协调整理治理框架以及一整套的落地解决方案，它将&lt;code&gt;SpringBoot&lt;/code&gt;开发的一个个单体微服务整合并管理起来，为各个微服务之间提供：配置管理，服务发现，断路器，路由，微代理，事件总线等的集成服务。&lt;/li&gt; &lt;li&gt;&lt;code&gt;SpringBoot&lt;/code&gt;可以离开&lt;code&gt;SpringCloud&lt;/code&gt;独立使用，但是&lt;code&gt;SpringCloud&lt;/code&gt;离不开&lt;code&gt;SpringBoot&lt;/code&gt;，属于依赖的关系。&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;总结：SpringBoot专注于快速，方便的开发单个微服务个体，SpringCloud关注全局的服务治理框架。&lt;/strong&gt;&lt;/p&gt; &lt;h1&gt;SpringCloud和Dubbo区别和对比&lt;/h1&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/u4mq9uctpcjc2o2uheeol8fjbv.png" alt="alt" /&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;SpringCloud功能比Dubbo更加强大，涵盖面更广，并且作为Spring的拳头项目，它能够与Spring Framework，SpringBoot，Spring Data等其他Spring项目完美整合，这些对于微服务而言是至关重要的。&lt;/li&gt; &lt;li&gt;而使用Dubbo构建的微服务架构就像组装电脑，各环节我们的选择自由度很高，但是最终很可能因为一条内存质量不行就点不亮了，而SpringCloud就像品牌机，在Spring Source的整合下，做了大量的兼容性测试，保证了机器拥有更高的稳定性，但是如果要在使用非原装组件外的东西，就需要对其基础有足够的了解。&lt;/li&gt; &lt;li&gt;那么最大的区别在于：SpringCloud抛弃了Dubbo的RPC通信，采用的是基于HTTP的REST方式。&lt;/li&gt; &lt;/ol&gt; &lt;h1&gt;Rest微服务项目实战&lt;/h1&gt; &lt;p&gt;就简单做一个用户管理的服务吧，Consumer消费者（Client）通过REST调用Provider提供者（Server）提供的服务&lt;/p&gt; &lt;h2&gt;创建Maven项目作为父项目&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;POM.xml&lt;/code&gt;配置如下，注意父项目打包类型&lt;code&gt;&amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;     &amp;lt;artifactId&amp;gt;user-management&amp;lt;/artifactId&amp;gt;     &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;      &amp;lt;modules&amp;gt;         &amp;lt;module&amp;gt;dto&amp;lt;/module&amp;gt;         &amp;lt;module&amp;gt;microservice-cloud-provider-user-8001&amp;lt;/module&amp;gt;         &amp;lt;module&amp;gt;microservice-cloud-consumer-user-8002&amp;lt;/module&amp;gt;     &amp;lt;/modules&amp;gt;      &amp;lt;properties&amp;gt;         &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;         &amp;lt;junit.version&amp;gt;4.12&amp;lt;/junit.version&amp;gt;         &amp;lt;log4j.version&amp;gt;1.2.17&amp;lt;/log4j.version&amp;gt;         &amp;lt;lombok.version&amp;gt;1.16.18&amp;lt;/lombok.version&amp;gt;     &amp;lt;/properties&amp;gt;      &amp;lt;parent&amp;gt;         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;         &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;         &amp;lt;version&amp;gt;1.5.10.RELEASE&amp;lt;/version&amp;gt;     &amp;lt;/parent&amp;gt;      &amp;lt;dependencyManagement&amp;gt;         &amp;lt;dependencies&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;Dalston.SR1&amp;lt;/version&amp;gt;                 &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;                 &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;5.0.4&amp;lt;/version&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;1.0.31&amp;lt;/version&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;1.3.0&amp;lt;/version&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;logback-core&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;1.2.3&amp;lt;/version&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;${junit.version}&amp;lt;/version&amp;gt;                 &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;             &amp;lt;/dependency&amp;gt;             &amp;lt;dependency&amp;gt;                 &amp;lt;groupId&amp;gt;log4j&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;log4j&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;${log4j.version}&amp;lt;/version&amp;gt;             &amp;lt;/dependency&amp;gt;         &amp;lt;/dependencies&amp;gt;     &amp;lt;/dependencyManagement&amp;gt;     &amp;lt;build&amp;gt;         &amp;lt;plugins&amp;gt;             &amp;lt;plugin&amp;gt;                 &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;                 &amp;lt;inherited&amp;gt;true&amp;lt;/inherited&amp;gt;                 &amp;lt;configuration&amp;gt;                     &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;                     &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;                 &amp;lt;/configuration&amp;gt;             &amp;lt;/plugin&amp;gt;         &amp;lt;/plugins&amp;gt;     &amp;lt;/build&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;创建第一个子模块&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;DTO&lt;/code&gt;数据传输对象模块，该模块定义&lt;code&gt;PO VO&lt;/code&gt;等，&lt;code&gt;POM&lt;/code&gt;文件如下&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;parent&amp;gt;         &amp;lt;artifactId&amp;gt;user-management&amp;lt;/artifactId&amp;gt;         &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;         &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;/parent&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;artifactId&amp;gt;dto&amp;lt;/artifactId&amp;gt;     &amp;lt;dependencies&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;该模块的目录结构如下&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;. ├── main │   ├── java │   │   └── com │   │       └── zealzhangz │   │           └── dto │   │               ├── po │   │               │   └── User.java │   │               └── vo │   └── resources └── test     └── java &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;&lt;code&gt;User.java&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.dto.po;  import lombok.Data; import lombok.NoArgsConstructor;  /**  * @author Created by xxxx.&amp;lt;br/&amp;gt;  * @version Version: 1.0  */ //set设置值/get获取值注解 @Data //无参数构造注解 @NoArgsConstructor public class User {     private Integer id;      private String name;      private Short age;      private Byte gender;      /**      * 来自那个数据库，因为微服务架构可以一个服务对应一个数据库，同一个信息被存储到不同数据库。      */     private String dbSource; }  &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;创建第二个模块--用户服务提供者&lt;/h2&gt; &lt;p&gt;主要提供用户信息的&lt;code&gt;CRUD&lt;/code&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;POM&lt;/code&gt;文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;parent&amp;gt;         &amp;lt;artifactId&amp;gt;user-management&amp;lt;/artifactId&amp;gt;         &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;         &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;/parent&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;artifactId&amp;gt;microservicecloud-provider-user-8001&amp;lt;/artifactId&amp;gt;      &amp;lt;dependencies&amp;gt;         &amp;lt;!-- 引入自己定义的api通用包，可以使用User用户Entity --&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;dto&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;${project.version}&amp;lt;/version&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;!-- actuator监控信息完善 --&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;logback-core&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;spring-boot-starter-jetty&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;!-- 修改后立即生效，热部署 --&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;springloaded&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;                 &amp;lt;dependency&amp;gt;                     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;                     &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;                 &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;代码 结构&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;. ├── main │   ├── java │   │   └── com │   │       └── zealzhangz │   │           ├── UserProvider8001App.java │   │           ├── controller │   │           │   └── UserController.java │   │           ├── dao │   │           │   └── UserDao.java │   │           └── service │   │               ├── UserService.java │   │               └── impl │   │                   └── UserServiceImpl.java │   └── resources │       ├── application.yml │       └── mybatis │           ├── mapper │           │   └── UserMapper.xml │           └── mybatis.cfg.xml └── test     └── java &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;创建配置文件 主要配置服务端口，数据源，数据库连接池等信息&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 8001 mybatis:   config-location: classpath:mybatis/mybatis.cfg.xml        # mybatis配置文件所在路径   type-aliases-package:   mapper-locations:   - classpath:mybatis/mapper/**/*.xml                       # mapper映射文件 spring:   application:     name: microservicecloud-user   datasource:     driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包     url: jdbc:mysql://localhost:3306/user_db                # 数据库名称     username: root     password: root     druid:       initial-size: 1                                       # 数据库连接池的最小维持连接数       max-active: 3       min-idle: 1                                           # 初始化连接数       max-wait: 10       test-while-idle: true       validation-query: SELECT 1 FROM DUAL eureka:   client:     service-url:       defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/   instance:     instance-id: microservicecloud-user-8001     prefer-ip-address: true         #访问路径可以显示IP地址     ip-address:  127.0.0.1     metadata-map:       fixed-s1: &amp;quot;value_1&amp;quot; info:   app.name: zhanga-micro-service-cloud   company.name: www.zhangaoo.com                           #我的博客地址   build.artifactId: @project.artifactId@   build.version: @project.version@ &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;&lt;code&gt;mybatis.cfg.xml mybatis&lt;/code&gt;配置文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt; &amp;lt;!DOCTYPE configuration         PUBLIC &amp;quot;-//mybatis.org//DTD Config 3.0//EN&amp;quot;         &amp;quot;http://mybatis.org/dtd/mybatis-3-config.dtd&amp;quot;&amp;gt; &amp;lt;configuration&amp;gt;      &amp;lt;settings&amp;gt;         &amp;lt;setting name=&amp;quot;cacheEnabled&amp;quot; value=&amp;quot;true&amp;quot; /&amp;gt;&amp;lt;!-- 二级缓存开启 --&amp;gt;     &amp;lt;/settings&amp;gt; &amp;lt;/configuration&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;&lt;code&gt;UserMapper&lt;/code&gt;对应&lt;code&gt;XML&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt; &amp;lt;!DOCTYPE mapper PUBLIC &amp;quot;-//mybatis.org//DTD Mapper 3.0//EN&amp;quot;         &amp;quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&amp;quot;&amp;gt;  &amp;lt;mapper namespace=&amp;quot;com.zealzhangz.dao.UserDao&amp;quot;&amp;gt;     &amp;lt;resultMap id=&amp;quot;user&amp;quot; type=&amp;quot;com.zealzhangz.dto.po.User&amp;quot;&amp;gt;         &amp;lt;result column=&amp;quot;db_source&amp;quot; jdbcType=&amp;quot;VARCHAR&amp;quot; property=&amp;quot;dbSource&amp;quot; /&amp;gt;     &amp;lt;/resultMap&amp;gt;     &amp;lt;select id=&amp;quot;findUserByName&amp;quot; resultMap=&amp;quot;user&amp;quot; parameterType=&amp;quot;com.zealzhangz.dto.po.User&amp;quot;&amp;gt;         select id,name,age,gender,db_source from user where name=#{name};     &amp;lt;/select&amp;gt;     &amp;lt;select id=&amp;quot;getAll&amp;quot; resultMap=&amp;quot;user&amp;quot;&amp;gt;         select id,name,age,gender,db_source from user;     &amp;lt;/select&amp;gt;     &amp;lt;insert id=&amp;quot;addUser&amp;quot; parameterType=&amp;quot;com.zealzhangz.dto.po.User&amp;quot;&amp;gt;         INSERT INTO user(name,age,gender,db_source) VALUES(#{name},#{age},#{gender},#{dbSource});     &amp;lt;/insert&amp;gt; &amp;lt;/mapper&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="6"&gt; &lt;li&gt;&lt;code&gt;mysql&lt;/code&gt;数据库脚本&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;DROP DATABASE IF EXISTS user_db; CREATE DATABASE user_db CHARACTER SET UTF8; USE user_db;  CREATE TABLE user (   id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,   name VARCHAR(60) NOT NULL  DEFAULT '' COMMENT '姓名',   age TINYINT NOT NULL DEFAULT 0 COMMENT '年龄',   gender TINYINT NOT NULL DEFAULT 0  COMMENT '男：1，女：0',   db_source VARCHAR(60) NOT NULL DEFAULT 'db1' COMMENT '来自那个数据库,因为微服务架构可以一个服务对应一个数据库'  );  INSERT INTO user(name,age,gender,db_source) VALUES('zhangsan',18,1,DATABASE()); INSERT INTO user(name,age,gender,db_source) VALUES('lisi',19,0,DATABASE()); INSERT INTO user(name,age,gender,db_source) VALUES('wangwu',20,1,DATABASE()); INSERT INTO user(name,age,gender,db_source) VALUES('zengliu',21,0,DATABASE()); INSERT INTO user(name,age,gender,db_source) VALUES('chengli',22,1,DATABASE()); &lt;/code&gt;&lt;/pre&gt; &lt;ol start="7"&gt; &lt;li&gt;UserDao.java&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.dao;  import com.zealzhangz.dto.po.User; import org.apache.ibatis.annotations.Mapper;  import java.util.List;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:22:00&amp;lt;br/&amp;gt;  */ @Mapper public interface UserDao {     /**      * add a user to db      * @param user      */     void addUser(User user);      /**      * find a user by user name      * @param user      * @return      */     User findUserByName(User user);      /**      * get all user      * @return      */     List&amp;lt;User&amp;gt; getAll(); } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="8"&gt; &lt;li&gt;UserService.java&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.service;  import com.zealzhangz.dto.po.User;  import java.util.List;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:32:00&amp;lt;br/&amp;gt;  */ public interface UserService {     /**      * add a user to db      * @param user      */     void addUser(User user);      /**      * find a user by user name      * @param user      * @return      */     User findUserByName(User user);      /**      * get all user      * @return      */     List&amp;lt;User&amp;gt; getAll(); } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="9"&gt; &lt;li&gt;UserServiceImpl.java&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.service.impl;  import com.zealzhangz.dao.UserDao; import com.zealzhangz.dto.po.User; import com.zealzhangz.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;  import java.util.List;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:33:00&amp;lt;br/&amp;gt;  */ @Service public class UserServiceImpl implements UserService{     @Autowired     private UserDao userDao;      @Override     public void addUser(User user){         userDao.addUser(user);     }      @Override     public User findUserByName(User user){         return userDao.findUserByName(user);     }      @Override     public List&amp;lt;User&amp;gt; getAll(){         return userDao.getAll();     } }  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="10"&gt; &lt;li&gt;UserController.java&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.controller;  import com.zealzhangz.dto.po.User; import com.zealzhangz.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*;  import java.util.List;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:39:00&amp;lt;br/&amp;gt;  */ @RestController @RequestMapping(&amp;quot;/user&amp;quot;) public class UserController {     @Autowired     private UserService userService;      @RequestMapping(value=&amp;quot;/add&amp;quot;,method= RequestMethod.POST)     public String addUser(@RequestBody User user){         userService.addUser(user);         return &amp;quot;SUCCESS&amp;quot;;     }      @RequestMapping(value=&amp;quot;/get/{name}&amp;quot;,method=RequestMethod.GET)     public User get(@PathVariable(&amp;quot;name&amp;quot;) String name) {         User user = new User();         user.setName(name);         return userService.findUserByName(user);     }      @RequestMapping(value=&amp;quot;/list&amp;quot;,method=RequestMethod.GET)     public List&amp;lt;User&amp;gt; list() {         return userService.getAll();     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="11"&gt; &lt;li&gt;最后添加启动入口&lt;code&gt;UserProvider8001App.java&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz;  import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 00:44:00&amp;lt;br/&amp;gt;  */ @SpringBootApplication public class UserProvider8001App {     public static void main(String[] args) {         SpringApplication.run(UserProvider8001App.class,args);     } }  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="12"&gt; &lt;li&gt;运行访问接口&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;http://127.0.0.1:8001/user/list &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-json"&gt;[     {         &amp;quot;id&amp;quot;: 1,         &amp;quot;name&amp;quot;: &amp;quot;zhangsan&amp;quot;,         &amp;quot;age&amp;quot;: 18,         &amp;quot;gender&amp;quot;: 1,         &amp;quot;dbSource&amp;quot;: &amp;quot;user_db&amp;quot;     },     {         &amp;quot;id&amp;quot;: 2,         &amp;quot;name&amp;quot;: &amp;quot;lisi&amp;quot;,         &amp;quot;age&amp;quot;: 19,         &amp;quot;gender&amp;quot;: 0,         &amp;quot;dbSource&amp;quot;: &amp;quot;user_db&amp;quot;     } ] &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;创建第三个模块--用户服务消费者&lt;/h2&gt; &lt;p&gt;该模块直接调用用户服务提供者的服务&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;POM.xml&lt;/code&gt;文件&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt; &amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;          xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;          xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;     &amp;lt;parent&amp;gt;         &amp;lt;artifactId&amp;gt;user-management&amp;lt;/artifactId&amp;gt;         &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;         &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     &amp;lt;/parent&amp;gt;     &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;      &amp;lt;artifactId&amp;gt;microservice-cloud-consumer-user-8002&amp;lt;/artifactId&amp;gt;      &amp;lt;dependencies&amp;gt;         &amp;lt;!-- 引入自己定义的api通用包，可以使用User用户Entity --&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;com.zealzhangz&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;dto&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;${project.version}&amp;lt;/version&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-starter-jetty&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;!-- 修改后立即生效，热部署 --&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;springloaded&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;         &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt; &amp;lt;/project&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;代码结构目录&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;. ├── main │   ├── java │   │   └── com │   │       └── zealzhangz │   │           ├── UserConsumer8002App.java │   │           ├── config │   │           │   └── ConfigBean.java │   │           └── controller │   │               └── UserControlerConsumer.java │   └── resources │       └── application.yml └── test     └── java &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;&lt;code&gt;application.yml&lt;/code&gt;配置文件 主要定义服务端口&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;server:   port: 8002 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;创建一个ConfigBean类&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.config;  import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 20:54:00&amp;lt;br/&amp;gt;  */ @Configuration public class ConfigBean {     /**      * RestTemplate提供了多种便捷访问远程Http服务的方法，      * 是一种简单便捷的访问restful服务模板类，是Spring提供的用于访问Rest服务的客户端模板工具集。      */     @Bean     public RestTemplate getRestTemplate(){         return new RestTemplate();     } }  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;UserControllerConsumer.java&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz.controller;  import com.zealzhangz.dto.po.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;  import java.util.List;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 20:57:00&amp;lt;br/&amp;gt;  */ @RestController @RequestMapping(&amp;quot;/consumer/user&amp;quot;) public class UserControllerConsumer {     private static final String REST_URL_PREFIX = &amp;quot;http://localhost:8001&amp;quot;;      @Autowired     private RestTemplate restTemplate;      @RequestMapping(&amp;quot;/add&amp;quot;)     public String add(User user){         return restTemplate.postForObject(REST_URL_PREFIX + &amp;quot;/user/add&amp;quot;,user,String.class);     }     @RequestMapping(&amp;quot;/get/{name}&amp;quot;)     public User get(@PathVariable(&amp;quot;name&amp;quot;) String name){         return restTemplate.getForObject(REST_URL_PREFIX + &amp;quot;/user/get/&amp;quot; + name,User.class);     }     @RequestMapping(&amp;quot;/list&amp;quot;)     public List&amp;lt;User&amp;gt; list(){         return restTemplate.getForObject(REST_URL_PREFIX + &amp;quot;/user/list&amp;quot;,List.class);     } }  &lt;/code&gt;&lt;/pre&gt; &lt;ol start="6"&gt; &lt;li&gt;创建启动入口类&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.zealzhangz;  import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;  /**  * @version Version: 1.0  * @date DateTime: 2018/08/15 21:17:00&amp;lt;br/&amp;gt;  */ @SpringBootApplication public class UserConsumer8002App {     public static void main(String[] args) {         SpringApplication.run(UserConsumer8002App.class,args);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="7"&gt; &lt;li&gt;访问接口&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;http://127.0.0.1:8002/consumer/user/list &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-json"&gt;[     {         &amp;quot;id&amp;quot;: 1,         &amp;quot;name&amp;quot;: &amp;quot;zhangsan&amp;quot;,         &amp;quot;age&amp;quot;: 18,         &amp;quot;gender&amp;quot;: 1,         &amp;quot;dbSource&amp;quot;: &amp;quot;user_db&amp;quot;     },     {         &amp;quot;id&amp;quot;: 2,         &amp;quot;name&amp;quot;: &amp;quot;lisi&amp;quot;,         &amp;quot;age&amp;quot;: 19,         &amp;quot;gender&amp;quot;: 0,         &amp;quot;dbSource&amp;quot;: &amp;quot;user_db&amp;quot;     } ] &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;总结&lt;/h1&gt; &lt;p&gt;经过上面简单的几个步骤就建立了一个完整微服务示例，后面还会集成更多的&lt;code&gt;SpringCloud&lt;/code&gt;组件，让我们的系统更加健壮&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 15 Aug 2018 16:06:45 GMT</pubDate>
    </item>
    <item>
      <title>认识时序数据库OpenTSDB</title>
      <link>https://www.zhangaoo.com/article/opentsdb</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/3feo76cftig32r3s8k6cmfm4u3.png" alt="alt" /&gt; 继昨天介绍了Hbase，今天顺便也介绍一下OpenTSDB。&lt;/p&gt; &lt;h2&gt;什么是 OpenTSDB&lt;/h2&gt; &lt;p&gt;OpenTSDB ，可以认为是一个时系列数据（库），它基于HBase存储数据，充分发挥了HBase的分布式列存储特性，支持数百万每秒的读写，它的特点就是容易扩展，灵活的tag机制。&lt;/p&gt; &lt;h2&gt;架构简介&lt;/h2&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/5pma80rq5ghqop6getv3nipkiq.png" alt="alt" /&gt; 其最主要的部件就是TSD了，这是接收数据并存储到HBase处理的核心所在。而带有C（collector）标志的Server，则是数据采集源，将数据发给 TSD服务。 每个TSD都是独立的服务，没有leader。&lt;/p&gt; &lt;h2&gt;安装 OpenTSDB&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;Linux操作系统&lt;/li&gt; &lt;li&gt;JRE 1.6 or later&lt;/li&gt; &lt;li&gt;HBase 0.92 or later&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;安装GnuPlot&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;# ubuntu 环境安装gunplot sudo apt-get install gunplot # centos 安装 gunplot sudo yum install -y gunplot &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;安装Hbase&lt;/h3&gt; &lt;p&gt;&lt;a href="https://www.zhangaoo.com/article/habase" target="_blank"&gt;参考安装Hbase&lt;/a&gt;&lt;/p&gt; &lt;h3&gt;安装 OpenTSDB&lt;/h3&gt; &lt;p&gt;如果build失败，那肯定是缺少Make或者Autotools等东西，用包管理器安装即可&lt;/p&gt; &lt;pre&gt;&lt;code&gt;$ git clone git://github.com/OpenTSDB/opentsdb.git $ cd opentsdb $ ./build.sh &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;创建表OpenTSDB所需要的表结构：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;env COMPRESSION=NONE ./src/create_table.sh &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看habse shell相关的表创建成功&lt;/p&gt; &lt;pre&gt;&lt;code&gt;&amp;gt; list TABLE tsdb tsdb-meta tsdb-tree tsdb-uid 4 row(s) in 0.0160 seconds &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;表创建之后，即可启动tsd服务，只需要运行如下命令：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#!/usr/bin/env bash source /home/aozhang/Documents/company/env/java-1.8-env staticroot=/home/aozhang/Documents/company/app/opentsdb/build/staticroot cachedir=/home/aozhang/Documents/company/app/opentsdb/build/cachedir /home/aozhang/Documents/company/app/opentsdb/build/tsdb tsd --port=4242 --staticroot=$staticroot --cachedir=$cachedir --zkquorum=192.168.31.111 --auto-metric &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意执行上面脚本时，日志读写的目录可能会没权限，如果遇到加上&lt;code&gt;sudo&lt;/code&gt;执行，&lt;code&gt;zkquorum&lt;/code&gt;指定&lt;code&gt;zookeeper&lt;/code&gt;服务地址，默认端口&lt;code&gt;2181&lt;/code&gt;，&lt;code&gt;--auto-metric&lt;/code&gt;表明如果插入的&lt;code&gt;Data points&lt;/code&gt;的&lt;code&gt;metrics&lt;/code&gt;不存在就自动创建&lt;/p&gt; &lt;h2&gt;保存数据到OpenTSDB&lt;/h2&gt; &lt;p&gt;最简单的保存数据方式就是使用telnet&lt;/p&gt; &lt;pre&gt;&lt;code&gt;$ telnet 192.168.31.111 4242  put sys.cpu.user 1436333416 23 host=web01 user=10001 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;通过http://192.168.31.111:4242 访问&lt;/p&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/us4rrg8ersit9r7kq1b7ftrtvk.png" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;OpenTSDB中的数据存储结构&lt;/h2&gt; &lt;p&gt;我们来看看 OpenTSDB 的重要概念uid，先从HBase中存储的数据开始吧，我们来看一下它都有哪些表，以及这些表都是干什么的。&lt;/p&gt; &lt;h3&gt;tsdb：存储数据点&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;hbase(main):003:0&amp;gt; scan 'tsdb' ROW                                                 COLUMN+CELL     \x00\x00\x03U\x9C\xAEP\x00\x00\x01\x00\x00\x01\x00 column=t:q\x80, timestamp=1533643344526, value=\x17  \x00\x03\x00\x00\x05                                               1 row(s) in 0.0100 seconds hbase(main):004:0&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看出，该表只有一条数据，我们先不管rowid，只来看看列，只有一列，值为0x17，即十进制23，即该metric的值。 左面的row key则是 OpenTSDB 的特点之一，其规则为：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;metric + timestamp + tagk1 + tagv1… + tagkN + tagvN &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上属性值均为对应名称的uid。&lt;/p&gt; &lt;p&gt;我们上面添加的metric为：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;sys.cpu.user 1436333416 23 host=web01 user=10001 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;一共涉及到5个uid，即名为sys.cpu.user的metric，以及host和user两个tagk及其值web01和10001。 上面数据的row key为：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;\x00\x00\x03U\x9C\xAEP\x00\x00\x01\x00\x00\x01\x00\x00\x03\x00\x00\x05 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;具体这个row key是怎么算出来的，我们来看看tsdb-uid表。&lt;/p&gt; &lt;h3&gt;tsdb-uid：存储name和uid的映射关系&lt;/h3&gt; &lt;p&gt;tsdb-uid用来保存名字和UID（metric，tagk，tagv）之间互相映射的关系，都是成组出现的，即给定一个name和uid，会保存（name,uid）和（uid,name）两条记录。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;hbase(main):004:0&amp;gt; scan 'tsdb-uid' \x00\x00\x01                                       column=name:tagv, timestamp=1533527119974, value=web01  \x00\x00\x01                                       column=name:tagk, timestamp=1533527119948, value=host \x00\x00\x03                                       column=name:metrics, timestamp=1533643343251, value=sys.cpu.user \x00\x00\x03                                       column=name:tagk, timestamp=1533643343270, value=user \x00\x00\x05                                       column=name:tagv, timestamp=1533643343283, value=10001  10001                                              column=id:tagv, timestamp=1533643343286, value=\x00\x00\x05 host                                               column=id:tagk, timestamp=1533527119952, value=\x00\x00\x01 sys.cpu.user                                       column=id:metrics, timestamp=1533643343255, value=\x00\x00\x03 user                                               column=id:tagk, timestamp=1533643343273, value=\x00\x00\x03  web01                                              column=id:tagv, timestamp=1533527119980, value=\x00\x00\x01 &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;\x00\x00\x03  + U +  \x9C\xAE + P +      \x00\x00\x01           +  \x00\x00\x03 + \x00\x00\x05 sys.cpu.user       1436333416           host    =      web01          user         10001 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看出，这和我们前面说到的row key的构成方式是吻合的。 需要着重说明的是时间戳的存储方式。虽然我们指定的时间是以秒为单位的，但是，row key中用到的却是以一小时为单位的，即：1436333416 – 1436333416 % 3600 = 1436331600 。 1436331600转换为16进制，即0x55 0x9c 0xae 0x50，而0x55即大写字母U，0x50为大写字母P，这就是4个字节的时间戳存储方式。相信下面这张图能帮助各位更好理解这个意思，即一小时只有一个row key，每秒钟的数据都会存为一列，大大提高查询的速度。&lt;/p&gt; &lt;p&gt;&lt;img src="http://77gaj2.com1.z0.glb.clouddn.com/2015/07/09/opentsdb/row-key-storage.png/zoom1" alt="alt" /&gt;&lt;/p&gt; &lt;p&gt;重要：我们看到，上面的metric也好，tagk或者tagv也好，uid只有3个字节，这是 OpenTSDB 的默认配置，三个字节，应该能表示1600多万的不同数据，这对metric名或者tagk来说足够长了，对tagv来说就不一定了，比如tagv是ip地址的话，或者电话号码，那么这个字段就不够长了，这时可以通过修改源代码来重新编译 OpenTSDB 就可以了，同时要注意的是，重编以后，老数据就不能直接使用了，需要导出后重新导入。&lt;/p&gt; &lt;h3&gt;tsdb-meta：元数据表&lt;/h3&gt; &lt;p&gt;我们再看下第三个表tsdb-meta，这是用来存储时间序列索引和元数据的表。这也是一个可选特性，默认是不开启的，可以通过配置文件来启用该特性，这里不做特殊介绍了。&lt;/p&gt; &lt;h4&gt;tsdb-tree：树形表&lt;/h4&gt; &lt;p&gt;第4个表是tsdb-tree，用来以树状层次关系来表示metric的结构，只有在配置文件开启该特性后，才会使用此表，这里我们不介绍了，可以自己尝试&lt;/p&gt; &lt;h2&gt;通过HTTP接口保存数据&lt;/h2&gt; &lt;p&gt;假设我们有如下数据，保存为文件mysql.json：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;[     {         &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,         &amp;quot;timestamp&amp;quot;: 1435716527,         &amp;quot;value&amp;quot;: 1234,         &amp;quot;tags&amp;quot;: {            &amp;quot;host&amp;quot;: &amp;quot;web01&amp;quot;,            &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;         }     },     {         &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,         &amp;quot;timestamp&amp;quot;: 1435716529,         &amp;quot;value&amp;quot;: 2345,         &amp;quot;tags&amp;quot;: {            &amp;quot;host&amp;quot;: &amp;quot;web01&amp;quot;,            &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;         }     },     {         &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,         &amp;quot;timestamp&amp;quot;: 1435716627,         &amp;quot;value&amp;quot;: 3456,         &amp;quot;tags&amp;quot;: {            &amp;quot;host&amp;quot;: &amp;quot;web02&amp;quot;,            &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;         }     },     {         &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,         &amp;quot;timestamp&amp;quot;: 1435716727,         &amp;quot;value&amp;quot;: 6789,         &amp;quot;tags&amp;quot;: {            &amp;quot;host&amp;quot;: &amp;quot;web01&amp;quot;,            &amp;quot;dc&amp;quot;: &amp;quot;tianjin&amp;quot;         }     } ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之后执行如下命令：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; http://192.168.31.111:4242/api/put -d @data-points1.json &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看数据 http://192.168.31.111:4242/#start=2015/07/01-10:07:04&amp;amp;end=2015/07/01-10:20:01&amp;amp;m=sum:opentsdb.test&amp;amp;o=&amp;amp;m=sum:mysql.innodb.row_lock_time&amp;amp;o=&amp;amp;yrange=%5B0:%5D&amp;amp;key=top%20right%20box&amp;amp;wxh=1420x564&amp;amp;style=linespoint&lt;/p&gt; &lt;h2&gt;查询数据&lt;/h2&gt; &lt;p&gt;查询数据可以使用query接口，它既可以使用get的query string方式，也可以使用post方式以JSON格式指定查询条件，这里我们以后者为例，对刚才保存的数据进行说明。&lt;/p&gt; &lt;p&gt;首先，保存如下内容为search.json：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-json"&gt;{     &amp;quot;start&amp;quot;: 1435716527,     &amp;quot;queries&amp;quot;: [         {             &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,             &amp;quot;aggregator&amp;quot;: &amp;quot;avg&amp;quot;,             &amp;quot;tags&amp;quot;: {                 &amp;quot;host&amp;quot;: &amp;quot;*&amp;quot;,                 &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;             }         }     ] } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行如下命令进行查询：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;$ curl -s -X POST -H &amp;quot;Content-Type: application/json&amp;quot; http://localhost:4242/api/query -d @search.json | jq . [   {     &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,     &amp;quot;tags&amp;quot;: {       &amp;quot;host&amp;quot;: &amp;quot;web01&amp;quot;,       &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;     },     &amp;quot;aggregateTags&amp;quot;: [],     &amp;quot;dps&amp;quot;: {       &amp;quot;1435716527&amp;quot;: 1234,       &amp;quot;1435716529&amp;quot;: 2345     }   },   {     &amp;quot;metric&amp;quot;: &amp;quot;mysql.innodb.row_lock_time&amp;quot;,     &amp;quot;tags&amp;quot;: {       &amp;quot;host&amp;quot;: &amp;quot;web02&amp;quot;,       &amp;quot;dc&amp;quot;: &amp;quot;beijing&amp;quot;     },     &amp;quot;aggregateTags&amp;quot;: [],     &amp;quot;dps&amp;quot;: {       &amp;quot;1435716627&amp;quot;: 3456     }   } ] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可以看出，我们保存了dc=tianjin的数据，但是并没有在此查询中返回，这是因为，我们指定了dc=beijing这一条件。&lt;/p&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;p&gt;可以看出来， OpenTSDB 还是非常容易上手的，尤其是单机版，安装也很简单。有HBase作为后盾，查询起来也非常快，很多大公司，类似雅虎等，也都在用此软件。但是，大规模用起来，多个TDB以及多存储节点等，应该都需要专业、细心的运维工作了。&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 07 Aug 2018 16:01:05 GMT</pubDate>
    </item>
    <item>
      <title>Hbase的安装及简单使用</title>
      <link>https://www.zhangaoo.com/article/habase</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/08/tj62a1g1c2igfra55g2ledh455.jpg" alt="alt" /&gt; 最近在学习使用Hbase，从安装到使用简单操作了一遍。安装Hbase的前提是已经安装配置好了Hadoop和zookeeper。这里就不在 细讲了，后面可能会在补充一下。&lt;/p&gt; &lt;h3&gt;安装Hbase&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;下载Hbase&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;wget http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.6.1-bin.tar.gz tar -xzvf hbase-1.2.6.1-bin.tar.gz &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;在单机模式下配置HBase&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;在hbase-env.sh配置JAVA_HOME环境变量&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;cd /home/aozhang/Documents/company/app/hbase-1.2.6.1/conf vim hbase-env.sh export JAVA_HOME=/home/aozhang/Documents/company/app/jdk1.8.0_181 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;Edit conf/hbase-site.xml&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;   &amp;lt;property&amp;gt;     &amp;lt;name&amp;gt;hbase.rootdir&amp;lt;/name&amp;gt;     &amp;lt;value&amp;gt;file:///home/aozhang/Documents/company/data/hbase&amp;lt;/value&amp;gt;   &amp;lt;/property&amp;gt;   &amp;lt;property&amp;gt;     &amp;lt;name&amp;gt;hbase.zookeeper.property.dataDir&amp;lt;/name&amp;gt;     &amp;lt;value&amp;gt;/home/aozhang/Documents/company/data/hbase/zookeeper&amp;lt;/value&amp;gt;   &amp;lt;/property&amp;gt;   &amp;lt;property&amp;gt;     &amp;lt;name&amp;gt;hbase.unsafe.stream.capability.enforce&amp;lt;/name&amp;gt;     &amp;lt;value&amp;gt;false&amp;lt;/value&amp;gt;     &amp;lt;description&amp;gt;       Controls whether HBase will check for stream capabilities (hflush/hsync).        Disable this if you intend to run on LocalFileSystem, denoted by a rootdir       with the 'file://' scheme, but be mindful of the NOTE below.        WARNING: Setting this to false blinds you to potential data loss and       inconsistent system state in the event of process and/or node failures. If       HBase is complaining of an inability to use hsync or hflush it's most       likely not a false positive.     &amp;lt;/description&amp;gt;   &amp;lt;/property&amp;gt; &amp;lt;/configuration&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;启动HBASE&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;cd /home/aozhang/Documents/company/app/hbase-1.2.6.1/bin ./start-hbase.sh &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;访问页面是否正常&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;http://aozhang-latitude-5290:16010/master-status&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;ubuntu16搭建OpenTSDB中的大坑hosts文件&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;ubuntu hosts文件中默认hostname映射是127.0.1.1，这会导致OpenTSDB通过hostname访问服务是报错&lt;/li&gt; &lt;li&gt;错误映射&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;127.0.1.1 aozhang-Latitude-5290 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;正确映射(直接使用局域网IP映射)&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;192.168.31.111 aozhang-Latitude-5290 &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;hbase server 断电后发现Master启动不了&lt;/h4&gt; &lt;p&gt;查看master相关日志，删除日志中报错数据&lt;/p&gt; &lt;pre&gt;&lt;code&gt;cd ../data/hbase/WALs/ rm -rf aozhang-latitude-5290,16201,1532424355724-splitting &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase导入创建表脚本报错：Compression algorithm 'lzo' previously failed test.&lt;/h4&gt; &lt;pre&gt;&lt;code&gt;Type &amp;quot;exit&amp;lt;RETURN&amp;gt;&amp;quot; to leave the HBase Shell Version 1.2.6.1, rUnknown, Sun Jun  3 23:19:26 CDT 2018  create 'tsdb-uid',   {NAME =&amp;gt; 'id', COMPRESSION =&amp;gt; 'LZO', BLOOMFILTER =&amp;gt; 'ROW'},   {NAME =&amp;gt; 'name', COMPRESSION =&amp;gt; 'LZO', BLOOMFILTER =&amp;gt; 'ROW'}  ERROR: org.apache.hadoop.hbase.DoNotRetryIOException: java.lang.RuntimeException: java.lang.ClassNotFoundException: com.hadoop.compression.lzo.LzoCodec Set hbase.table.sanity.checks to false at conf or table descriptor if you want to bypass sanity checks  at org.apache.hadoop.hbase.master.HMaster.warnOrThrowExceptionForFailure(HMaster.java:1754) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;配置hbase-site.xml参数：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;property&amp;gt;     &amp;lt;name&amp;gt;hbase.table.sanity.checks&amp;lt;/name&amp;gt;     &amp;lt;value&amp;gt;false&amp;lt;/value&amp;gt;   &amp;lt;/property&amp;gt;   &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;为OpenTSDB导入脚本（需导入代码版本对应的脚本）&lt;/h4&gt; &lt;pre&gt;&lt;code&gt;/home/aozhang/Documents/company/app/hbase-1.2.6.1/create_table.sh &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;启动OpenTSDB报错&lt;/h4&gt; &lt;pre&gt;&lt;code&gt;2018-07-27 21:00:10.616  WARN 3973 --- [e I/O Worker #1] org.hbase.async.HBaseClient              : Probe Exists(table=&amp;quot;tsdb-uid&amp;quot;, key=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 65, 115, 121, 110, 99, 72, 66, 97, 115, 101, 126, 112, 114, 111, 98, 101, 126, 60, 59, 95, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 65, 115, 121, 110, 99, 72, 66, 97, 115, 101, 126, 112, 114, 111, 98, 101, 126, 60, 59, 95, 60], family=null, qualifiers=null, attempt=0, region=null) failed  org.hbase.async.NonRecoverableException: Too many attempts: Exists(table=&amp;quot;tsdb-uid&amp;quot;, key=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 65, 115, 121, 110, 99, 72, 66, 97, 115, 101, 126, 112, 114, 111, 98, 101, 126, 60, 59, 95, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 65, 115, 121, 110, 99, 72, 66, 97, 115, 101, 126, 112, 114, 111, 98, 101, 126, 60, 59, 95, 60], family=null, qualifiers=null, attempt=11, region=null) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;把之前的4张表删除后，用&lt;code&gt;opentsdb-2.3.1&lt;/code&gt;下脚本重建了一下表就OK了，删除表的实现先disable再删除，发现这边的删除是有顺序的，要先删除&lt;code&gt;tsdb&lt;/code&gt;再删&lt;code&gt;tsdb-uid&lt;/code&gt;再删&lt;code&gt;tsdb-tree&lt;/code&gt;再删&lt;code&gt;tsdb-meta&lt;/code&gt;&lt;/li&gt; &lt;li&gt;出错的原因基本可确定是脚本的版本和代码版本不一致&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):001:0&amp;gt; list TABLE                                           tsdb                                                         tsdb-meta                                                             tsdb-tree                                                            tsdb-uid  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;重建命令&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;    cd /home/aozhang/Documents/company/app/opentsdb-2.3.1     env COMPRESSION=NONE HBASE_HOME=/home/aozhang/Documents/company/app/hbase-1.2.6.1 ./src/create_table.sh &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HBase是什么?&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;HBase是建立在Hadoop文件系统之上的分布式面向列的数据库。它是一个开源项目，是横向扩展的。&lt;/li&gt; &lt;li&gt;HBase是一个数据模型，类似于谷歌的大表设计，可以提供快速随机访问海量结构化数据。它利用了Hadoop的文件系统（HDFS）提供的容错能力。&lt;/li&gt; &lt;li&gt;它是Hadoop的生态系统，提供对数据的随机实时读/写访问，是Hadoop文件系统的一部分。&lt;/li&gt; &lt;li&gt;人们可以直接或通过HBase的存储HDFS数据。使用HBase在HDFS读取消费/随机访问数据。 HBase在Hadoop的文件系统之上，并提供了读写访问。 &lt;img src="https://www.zhangaoo.com/upload/2018/07/63bnrt395qjeiq2q8jqn5j5die.png" alt="alt" /&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;HBase的存储机制&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;HBase是一个面向列的数据库，在表中它由行排序。表模式定义只能列族，也就是键值对。一个表有多个列族以及每一个列族可以有任意数量的列。后续列的值连续地存储在磁盘上。表中的每个单元格值都具有时间戳&lt;/li&gt; &lt;li&gt;表是行的集合。&lt;/li&gt; &lt;li&gt;行是列族的集合。&lt;/li&gt; &lt;li&gt;列族是列的集合。&lt;/li&gt; &lt;li&gt;列是键值对的集合。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Hbase操作&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;启动 HBase Shell&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;/home/aozhang/Documents/company/app/hbase-1.2.6.1/bin/hbase shell #查看所有表 hbase(main):001:0&amp;gt; list TABLE   tsdb   tsdb-meta  tsdb-tree  tsdb-uid  4 row(s) in 0.0300 seconds =&amp;gt; [&amp;quot;tsdb&amp;quot;, &amp;quot;tsdb-meta&amp;quot;, &amp;quot;tsdb-tree&amp;quot;, &amp;quot;tsdb-uid&amp;quot;] # 提供HBase的状态，例如，服务器的数量。 hbase(main):002:0&amp;gt; status 1 active master, 0 backup masters, 1 servers, 0 dead, 6.0000 average load &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;一系列命令&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;list :查看有哪些表 status:命令返回包括在系统上运行的服务器的细节和系统的状态 version:该命令返回HBase系统使用的版本 table_help:此命令将引导如何使用表引用的命令。下面给出的是使用这个命令的语法。 whoami:该命令返回HBase用户详细信息。如果执行这个命令，返回当前HBase用户 &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase在表中操作的命令&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;create: 创建一个表。&lt;/li&gt; &lt;li&gt;ist: 列出HBase的所有表。&lt;/li&gt; &lt;li&gt;disable: 禁用表。&lt;/li&gt; &lt;li&gt;is_disabled: 验证表是否被禁用。&lt;/li&gt; &lt;li&gt;enable: 启用一个表。&lt;/li&gt; &lt;li&gt;is_enabled: 验证表是否已启用。&lt;/li&gt; &lt;li&gt;describe: 提供了一个表的描述。&lt;/li&gt; &lt;li&gt;alter: 改变一个表。&lt;/li&gt; &lt;li&gt;exists: 验证表是否存在。&lt;/li&gt; &lt;li&gt;drop: 从HBase中删除表。&lt;/li&gt; &lt;li&gt;drop_all: 丢弃在命令中给出匹配“regex”的表。&lt;/li&gt; &lt;li&gt;Java Admin API: 在此之前所有的上述命令，Java提供了一个通过API编程来管理实现DDL功能。在这个org.apache.hadoop.hbase.client包中有HBaseAdmin和HTableDescriptor 这两个重要的类提供DDL功能。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;数据操纵语言&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;put: 把指定列在指定的行中单元格的值在一个特定的表。&lt;/li&gt; &lt;li&gt;get: 取行或单元格的内容。&lt;/li&gt; &lt;li&gt;delete: 删除表中的单元格值。&lt;/li&gt; &lt;li&gt;deleteall: 删除给定行的所有单元格。&lt;/li&gt; &lt;li&gt;scan: 扫描并返回表数据。&lt;/li&gt; &lt;li&gt;count: 计数并返回表中的行的数目。&lt;/li&gt; &lt;li&gt;truncate: 禁用，删除和重新创建一个指定的表。&lt;/li&gt; &lt;li&gt;在此之前所有上述命令，Java提供了一个客户端API来实现DML功能，CRUD（创建检索更新删除）操作更多的是通过编程，在org.apache.hadoop.hbase.client包下。 在此包HTable 的 Put和Get是重要的类。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;HBaseAdmin类&lt;/h4&gt; &lt;p&gt;HBaseAdmin是一个类表示管理。这个类属于org.apache.hadoop.hbase.client包。使用这个类，可以执行管理员任务。使用Connection.getAdmin()方法来获取管理员的实例。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;创建一个新的表&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;void createTable(HTableDescriptor desc) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;创建一个新表使用一组初始指定的分割键限定空区域&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;void createTable(HTableDescriptor desc, byte[][] splitKeys) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;从表中删除列&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;void deleteColumn(byte[] tableName, String columnName) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;删除表中的列&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;void deleteColumn(String tableName, String columnName) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;删除表&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;void deleteTable(String tableName) &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;Descriptor类&lt;/h4&gt; &lt;p&gt;这个类包含一个HBase表，如详细信息：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;所有列族的描述，&lt;/li&gt; &lt;li&gt;如果表是目录表&lt;/li&gt; &lt;li&gt;如果表是只读的&lt;/li&gt; &lt;li&gt;存储的最大尺寸&lt;/li&gt; &lt;li&gt;当区域分割发生&lt;/li&gt; &lt;li&gt;与之相关联的协同处理器等&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;构造函数,构造一个表描述符指定TableName对象。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;HTableDescriptor(TableName name) &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;将列家族给定的描述符&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;HTableDescriptor addFamily(HColumnDescriptor family) &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase创建表&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;可以使用命令创建一个表，在这里必须指定表名和列族名。在HBase shell中创建表的语法如下所示。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;create ‘&amp;lt;table name&amp;gt;’,’&amp;lt;column family&amp;gt;’  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;示例&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):002:0&amp;gt; create 'emp','personal data','professional data' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;它有两个列族：“personal data”和“professional data”。&lt;/li&gt; &lt;/ul&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th align="center"&gt;Row key&lt;/th&gt;&lt;th align="center"&gt;personal data&lt;/th&gt;&lt;th align="right"&gt;professional data&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td align="center"&gt;&lt;/td&gt;&lt;td align="center"&gt;&lt;/td&gt;&lt;td align="right"&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td align="center"&gt;&lt;/td&gt;&lt;td align="center"&gt;&lt;/td&gt;&lt;td align="right"&gt;&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;h4&gt;HBase删除表&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;删除前必须见禁用表&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;disable_all &amp;quot;tsdb&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;删除&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;drop_all &amp;quot;tsdb&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;启用表&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;enable &amp;quot;emp&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;扫描验证表&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;scan 'emp' &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;查看表是否被启用&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;is_enabled 'emp' &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HBase表描述和修改&lt;/h3&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; describe 'table name' &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;修改 alter用于更改现有表的命令。使用此命令可以更改列族的单元，设定最大数量和删除表范围运算符，并从表中删除列家族。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;更改列族单元格的最大数目&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; alter 't1', NAME =&amp;gt; 'f1', VERSIONS =&amp;gt; 5 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;在下面的例子中，单元的最大数目设置为5。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):009:0&amp;gt; alter  'emp', NAME =&amp;gt; 'personal data', VERSIONS =&amp;gt; 5 Updating all regions with the new schema... 1/1 regions updated. Done. Unknown argument ignored: VERSIONS Updating all regions with the new schema... 1/1 regions updated. Done. 0 row(s) in 3.8320 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;表范围运算符 使用alter，可以设置和删除表范围，运算符，如MAX_FILESIZE，READONLY，MEMSTORE_FLUSHSIZE，DEFERRED_LOG_FLUSH等。&lt;/li&gt; &lt;li&gt;设置只读 下面给出的是语法，是用以设置表为只读。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt;alter 't1', READONLY(option) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在下面的例子中，我们已经设置表emp为只读。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;hbase(main):015:0&amp;gt; alter 'emp', READONLY Updating all regions with the new schema... 1/1 regions updated. Done. 0 row(s) in 1.9000 seconds hbase(main):016:0&amp;gt;  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;删除表范围运算符 也可以删除表范围运算。下面给出的是语法，从emp表中删除“MAX_FILESIZE”。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; alter 'emp', METHOD =&amp;gt; 'table_att_unset', NAME =&amp;gt; 'MAX_FILESIZE' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;删除列族 使用alter，也可以删除列族。下面给出的是使用alter删除列族的语法。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; alter 'emp', 'delete' =&amp;gt; 'personal data' &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase Table Exists&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;可以使用exists命令验证表的存在&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;hbase(main):021:0&amp;gt; exists 'emp' Table emp does exist  0 row(s) in 0.0110 seconds &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase删除表&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;用drop命令可以删除表。在删除一个表之前必须先将其禁用。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;hbase(main):018:0&amp;gt; disable 'emp' 0 row(s) in 1.4580 seconds   hbase(main):019:0&amp;gt; drop 'emp' 0 row(s) in 0.3060 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用exists 命令验证表是否被删除。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):020:0&amp;gt; exists 'emp' Table emp does not exist  0 row(s) in 0.0730 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;drop_all 这个命令是用来在给出删除匹配“regex”表。它的语法如下：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; drop_all 't.*' &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：要删除表，则必须先将其禁用。&lt;/strong&gt;&lt;/p&gt; &lt;h4&gt;HBase关闭&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;exit,可以通过键入exit命令退出shell。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;hbase(main):021:0&amp;gt; exit &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;停止HBase&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;要停止HBase，浏览进入到HBase主文件夹，然后键入以下命令。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;./bin/stop-hbase.sh &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase创建数据&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;put 命令&lt;/li&gt; &lt;li&gt;add() - Put类的方法&lt;/li&gt; &lt;li&gt;put() - HTable 类的方法.&lt;/li&gt; &lt;li&gt;在HBase中创建下表 &lt;img src="https://www.zhangaoo.com/upload/2018/08/6tgf2as1kgj5upr6grb3bnvtst.png" alt="alt" /&gt;&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;创建表emp，如果存在删除重建&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;hbase(main):023:0&amp;gt; disable 'emp' 0 row(s) in 2.2420 seconds  hbase(main):024:0&amp;gt; drop 'emp'  0 row(s) in 1.2430 seconds  hbase(main):026:0&amp;gt; create 'emp', 'personal data', 'professional data' 0 row(s) in 1.2390 seconds  =&amp;gt; Hbase::Table - emp &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;使用put命令插入数据，它的语法如下：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;put '&amp;lt;table name&amp;gt;','row1','&amp;lt;colfamily:colname&amp;gt;','&amp;lt;value&amp;gt;' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;插入第一行&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):028:0&amp;gt; put 'emp','1','personal data:name','raju' 0 row(s) in 0.0660 seconds hbase(main):032:0&amp;gt; put 'emp','1','personal data:city','hyderabad' 0 row(s) in 0.0060 seconds hbase(main):033:0&amp;gt; put 'emp','1','professional data:designation','manager' 0 row(s) in 0.0070 seconds hbase(main):034:0&amp;gt; put 'emp','1','professional data:salary','50000' 0 row(s) in 0.0050 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;查看已经插入的第一行数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):035:0&amp;gt; scan 'emp' ROW                                      COLUMN+CELL  1                                       column=personal data:city, timestamp=1533194702585, value=hyderabad   1                                       column=personal data:name, timestamp=1533194618563, value=raju   1                                       column=professional data:designation, timestamp=1533194765859, value=manager   1                                       column=professional data:salary, timestamp=1533194786444, value=50000  1 row(s) in 0.0090 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用Java API 插入数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void insertRowData() {         /**          * insert data          **/         Table table = null;         try {             Connection connection = ConnectionFactory.createConnection(configuration);             TableName tableName = TableName.valueOf(&amp;quot;emp&amp;quot;);             table = connection.getTable(tableName);             Put put = new Put(Bytes.toBytes(&amp;quot;row2&amp;quot;));             put.addColumn(Bytes.toBytes(&amp;quot;personal data&amp;quot;), Bytes.toBytes(&amp;quot;city&amp;quot;), Bytes.toBytes(&amp;quot;ravi&amp;quot;));             put.addColumn(Bytes.toBytes(&amp;quot;personal data&amp;quot;), Bytes.toBytes(&amp;quot;name&amp;quot;), Bytes.toBytes(&amp;quot;chengnai&amp;quot;));             put.addColumn(Bytes.toBytes(&amp;quot;professional data&amp;quot;), Bytes.toBytes(&amp;quot;designation&amp;quot;), Bytes.toBytes(&amp;quot;sr.engineer&amp;quot;));             put.addColumn(Bytes.toBytes(&amp;quot;professional data&amp;quot;), Bytes.toBytes(&amp;quot;salary&amp;quot;), Bytes.toBytes(&amp;quot;30,000&amp;quot;));             table.put(put);         } catch (MasterNotRunningException e) {             e.printStackTrace();         } catch (ZooKeeperConnectionException e) {             e.printStackTrace();         } catch (IOException e) {             e.printStackTrace();         } finally {             try {                 if (table != null) {                     table.close();                 }             } catch (IOException e) {                 e.printStackTrace();             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase更新数据&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;可以使用put命令更新现有的单元格值。按照下面的语法，并注明新值，如下图所示。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;put 'table name','row','Column family:column name','new value' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;假设HBase中有一个表emp拥有下列数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):038:0&amp;gt; scan 'emp' ROW                                      COLUMN+CELL   1                                       column=personal data:city, timestamp=1533194702585, value=hyderabad  1                                       column=personal data:name, timestamp=1533194618563, value=raju    1                                       column=professional data:designation, timestamp=1533194765859, value=manager  1                                       column=professional data:salary, timestamp=1533194786444, value=50000   row2                                    column=personal data:city, timestamp=1533195621053, value=ravi   row2                                    column=personal data:name, timestamp=1533195621053, value=chengnai  row2                                    column=professional data:designation, timestamp=1533195621053, value=sr.engineer   row2                                    column=professional data:salary, timestamp=1533195621053, value=30,000  2 row(s) in 0.0140 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;以下命令将更新名为'raju'员工的城市值为'Delhi'。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):039:0&amp;gt; put 'emp','1','personal data:city','Delhi' 0 row(s) in 0.0070 seconds &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;hbase(main):040:0&amp;gt; scan 'emp' ROW                                      COLUMN+CELL                                                                                                           1                                       column=personal data:city, timestamp=1533196196912, value=Delhi  1                                       column=personal data:name, timestamp=1533194618563, value=raju   1                                       column=professional data:designation, timestamp=1533194765859, value=manager   1                                       column=professional data:salary, timestamp=1533194786444, value=50000    row2                                    column=personal data:city, timestamp=1533195621053, value=ravi    row2                                    column=personal data:name, timestamp=1533195621053, value=chengnai  row2                                    column=professional data:designation, timestamp=1533195621053, value=sr.engineer   row2                                    column=professional data:salary, timestamp=1533195621053, value=30,000  2 row(s) in 0.0150 seconds &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase读取数据&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;get命令和HTable类的get()方法用于从HBase表中读取数据。使用 get 命令，可以同时获取一行数据。它的语法如下&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;get '&amp;lt;table name&amp;gt;','row1' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;扫描emp表的第一行。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):043:0&amp;gt; get 'emp' , '1' COLUMN                                   CELL   personal data:city                      timestamp=1533196196912, value=Delhi  personal data:name                      timestamp=1533194618563, value=raju   professional data:designation           timestamp=1533194765859, value=manager  professional data:salary                timestamp=1533194786444, value=50000  4 row(s) in 0.0170 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;读取指定列,使用get方法读取指定列。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt;get 'table name', ‘rowid’, {COLUMN =&amp;gt; ‘column family:column name ’} &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;是用于读取HBase表中的特定列&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;get 'emp', '1', {COLUMN=&amp;gt;'personal data:name'} &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;hbase(main):045:0&amp;gt; get 'emp', 'row2', {COLUMN=&amp;gt;'personal data:name'} COLUMN                                   CELL  personal data:name                      timestamp=1533195621053, value=chengnai  1 row(s) in 0.0030 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用Java API读取数据&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public void getData() {         /**          * get data          **/         Table table = null;         try {             Connection connection = ConnectionFactory.createConnection(configuration);             TableName tableName = TableName.valueOf(&amp;quot;emp&amp;quot;);             table = connection.getTable(tableName);             Get get = new Get(Bytes.toBytes(&amp;quot;row2&amp;quot;));             Result result = table.get(get);             NavigableMap&amp;lt;byte[], NavigableMap&amp;lt;byte[], NavigableMap&amp;lt;Long, byte[]&amp;gt;&amp;gt;&amp;gt; navigableMap = result.getMap();             for (Map.Entry&amp;lt;byte[], NavigableMap&amp;lt;byte[], NavigableMap&amp;lt;Long, byte[]&amp;gt;&amp;gt;&amp;gt; entry : navigableMap.entrySet()) {                 System.out.println(&amp;quot;columnFamily:&amp;quot; + Bytes.toString(entry.getKey()));                 NavigableMap&amp;lt;byte[], NavigableMap&amp;lt;Long, byte[]&amp;gt;&amp;gt; map = entry.getValue();                 for (Map.Entry&amp;lt;byte[], NavigableMap&amp;lt;Long, byte[]&amp;gt;&amp;gt; en : map.entrySet()) {                     System.out.print(Bytes.toString(en.getKey()) + &amp;quot;##&amp;quot;);                     NavigableMap&amp;lt;Long, byte[]&amp;gt; nm = en.getValue();                     for (Map.Entry&amp;lt;Long, byte[]&amp;gt; me : nm.entrySet()) {                         System.out.println(&amp;quot;column key:&amp;quot; + me.getKey() + &amp;quot; value:&amp;quot; + Bytes.toString(me.getValue()));                     }                 }             }         } catch (MasterNotRunningException e) {             e.printStackTrace();         } catch (ZooKeeperConnectionException e) {             e.printStackTrace();         } catch (IOException e) {             e.printStackTrace();         } finally {             try {                 if (table != null) {                     table.close();                 }             } catch (IOException e) {                 e.printStackTrace();             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;HBase删除数据&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;从表删除特定单元格,使用 delete 命令，可以在一个表中删除特定单元格。 delete 命令的语法如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;delete '&amp;lt;table name&amp;gt;', '&amp;lt;row&amp;gt;', '&amp;lt;column name &amp;gt;', '&amp;lt;time stamp&amp;gt;' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;下面是一个删除特定单元格和例子。在这里，我们删除salary&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):048:0&amp;gt; delete 'emp', '1', 'professional data:salary',1533194786444 0 row(s) in 0.0260 seconds  hbase(main):049:0&amp;gt; scan 'emp' ROW                                      COLUMN+CELL   1                                       column=personal data:city, timestamp=1533196196912, value=Delhi  1                                       column=personal data:name, timestamp=1533194618563, value=raju   1                                       column=professional data:designation, timestamp=1533194765859, value=manager   row2                                    column=personal data:city, timestamp=1533196466515, value=ShangHai  row2                                    column=personal data:name, timestamp=1533195621053, value=chengnai  row2                                    column=professional data:designation, timestamp=1533195621053, value=sr.engineer  row2                                    column=professional data:salary, timestamp=1533195621053, value=30,000  2 row(s) in 0.0230 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;删除表的所有单元格,使用“deleteall”命令，可以删除一行中所有单元格。下面给出是 deleteall 命令的语法。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):050:0&amp;gt; deleteall 'emp','1' 0 row(s) in 0.0240 seconds &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用Java API删除&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void deleteData(){         /**          * delete data          **/         Table table = null;         try {             Connection connection = ConnectionFactory.createConnection(configuration);             TableName tableName = TableName.valueOf(&amp;quot;emp&amp;quot;);             table = connection.getTable(tableName);             Delete delete = new Delete(Bytes.toBytes(&amp;quot;1&amp;quot;));             //删除列簇里面的指定列，不要被名字迷惑，这里就是删除接口，应该要理解成add要删除的列到Delete的对象里             delete.addColumn(Bytes.toBytes(&amp;quot;professional data&amp;quot;), Bytes.toBytes(&amp;quot;designation&amp;quot;));             //删除整个列簇             delete.addFamily(Bytes.toBytes(&amp;quot;professional data&amp;quot;)); //            table.delete(delete);         } catch (MasterNotRunningException e) {             e.printStackTrace();         } catch (ZooKeeperConnectionException e) {             e.printStackTrace();         } catch (IOException e) {             e.printStackTrace();         } finally {             try {                 if (table != null) {                     table.close();                 }             } catch (IOException e) {                 e.printStackTrace();             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HBase扫描&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;scan 命令用于查看HTable数据。使用 scan 命令可以得到表中的数据。它的语法如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;scan '&amp;lt;table name&amp;gt;' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用Java API扫描Hbase&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Test     public void scanHbase(){         /**          * delete data          **/         Table table = null;         ResultScanner scanner = null;         try {             Connection connection = ConnectionFactory.createConnection(configuration);             TableName tableName = TableName.valueOf(&amp;quot;emp&amp;quot;);             table = connection.getTable(tableName);             // Instantiating the Scan class             Scan scan = new Scan();             scan.addColumn(Bytes.toBytes(&amp;quot;personal data&amp;quot;), Bytes.toBytes(&amp;quot;city&amp;quot;));             scan.addColumn(Bytes.toBytes(&amp;quot;personal data&amp;quot;), Bytes.toBytes(&amp;quot;name&amp;quot;));             // Getting the scan result             scanner = table.getScanner(scan);             // Reading values from scan result             for (Result result = scanner.next(); result != null; result = scanner.next()){                 System.out.println(&amp;quot;Found row : &amp;quot; + result);             }         } catch (MasterNotRunningException e) {             e.printStackTrace();         } catch (ZooKeeperConnectionException e) {             e.printStackTrace();         } catch (IOException e) {             e.printStackTrace();         } finally {             try {                 if(scanner != null){                     scanner.close();                 }                 if (table != null) {                     table.close();                 }             } catch (IOException e) {                 e.printStackTrace();             }         }     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HBase计数和截断&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;可以使用count命令计算表的行数量。它的语法如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;count '&amp;lt;table name&amp;gt;' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;truncate,此命令将禁止删除并重新创建一个表。truncate 的语法如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; truncate 'table name' &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;HBase安全&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;grant,授予特定的权限，如读，写，执行和管理表给定一个特定的用户&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; grant &amp;lt;user&amp;gt; &amp;lt;permissions&amp;gt; [&amp;lt;table&amp;gt; [&amp;lt;column family&amp;gt; [&amp;lt;column; qualifier&amp;gt;]] &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;1. R - 代表读取权限 2. W - 代表写权限  3. X - 代表执行权限 4. C - 代表创建权限 5. A - 代表管理权限 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;下面给出是为用户“Tutorialspoint'授予所有权限的例子。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;我们可以从RWXCA组，其中给予零个或多个特权给用户&lt;/p&gt; &lt;pre&gt;&lt;code&gt;hbase(main):018:0&amp;gt; grant 'Tutorialspoint', 'RWXCA' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;revoke,用于撤销用户访问表的权限&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt; revoke &amp;lt;user&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;下面的代码撤消名为“Tutorialspoint”用户的所有权限。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase(main):006:0&amp;gt; revoke 'Tutorialspoint' &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;user_permission用于列出特定表的所有权限&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;hbase&amp;gt;user_permission ‘tablename’ &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Mon, 06 Aug 2018 16:01:30 GMT</pubDate>
    </item>
    <item>
      <title>Java线程池的使用</title>
      <link>https://www.zhangaoo.com/article/thread</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/07/paso3fspkihhnrftacqt4eihjt.png" alt="alt" /&gt;&lt;/p&gt; &lt;h3&gt;使用线程池的原则&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;根据阿里Java开发手册指导建议直接使用以下代码创建线程池，这样可以做到对线程使用的资源做到心中有数，而不会出现因线程创建过多而产生OOM时的不知所措。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public ThreadPoolExecutor(int corePoolSize,                             int maximumPoolSize,                             long keepAliveTime,                             TimeUnit unit,                             BlockingQueue&amp;lt;Runnable&amp;gt; workQueue) &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;corePoolSize&lt;/code&gt;：核心线程数，默认情况下核心线程会一直存活，即使处于闲置状态也不会受存&lt;code&gt;keepAliveTime&lt;/code&gt;限制。除非将&lt;code&gt;allowCoreThreadTimeOut&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;&lt;code&gt;maximumPoolSize&lt;/code&gt;:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的&lt;code&gt;LinkedBlockingDeque&lt;/code&gt;时，这个值无效。&lt;/li&gt; &lt;li&gt;&lt;code&gt;keepAliveTime&lt;/code&gt;:非核心线程的闲置超时时间，超过这个时间就会被回收。&lt;/li&gt; &lt;li&gt;&lt;code&gt;unit&lt;/code&gt;:指定&lt;code&gt;keepAliveTime&lt;/code&gt;的单位，如&lt;code&gt;TimeUnit.SECONDS&lt;/code&gt;。当将&lt;code&gt;allowCoreThreadTimeOut&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;时对&lt;code&gt;corePoolSize&lt;/code&gt;生效。&lt;/li&gt; &lt;li&gt;&lt;code&gt;workQueue&lt;/code&gt;:线程池中的任务队列.常用的有三种队列，&lt;code&gt;SynchronousQueue&lt;/code&gt;,&lt;code&gt;LinkedBlockingDeque&lt;/code&gt;,&lt;code&gt;ArrayBlockingQueue&lt;/code&gt;。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;可执行定时任务的线程池&lt;/h3&gt; &lt;h3&gt;包含两种使用方法&lt;code&gt;scheduleWithFixedDelay&lt;/code&gt;和&lt;code&gt;scheduleAtFixedRate&lt;/code&gt;&lt;/h3&gt; &lt;h4&gt;&lt;code&gt;scheduleWithFixedDelay&lt;/code&gt;本次任务执行结束后等待指定延时再执行下次任务，下一次任务受本次计划执行时间的影响。延时计时从本次任务结束开始。&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public ScheduledFuture&amp;lt;?&amp;gt; scheduleWithFixedDelay(Runnable command,                                                      long initialDelay,                                                      long delay,                                                      TimeUnit unit); &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;command:he task to execute&lt;/li&gt; &lt;li&gt;initialDelay:the time to delay first execution&lt;/li&gt; &lt;li&gt;delay:本次任务结束后延时时间&lt;/li&gt; &lt;li&gt;unit:延时时间单位&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;t1 start    t1 end/delay start    delay end/t2 start  ↓            ↓                     ↓ |____________|_____________________|__________ &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    try{             ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);             scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {                 @Override                 public void run() {                     try{                         System.out.println(Thread.currentThread().getName()+&amp;quot;:task1:&amp;quot;+ DateTime.now().toString(&amp;quot;yyyy-MM-dd hh🇲🇲ss&amp;quot;));                         Thread.sleep(3000);                     } catch (InterruptedException e){                         e.printStackTrace();                     }                  }             }, 0, 3, TimeUnit.SECONDS);             System.in.read();         } catch (IOException e){             e.printStackTrace();     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;小结：对于scheduleWithFixedDelay下次任务需等待t1执行时间+delay时间,以上任务每隔6秒执行一次&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;&lt;code&gt;scheduleAtFixedRate&lt;/code&gt;延时计时从本次任务开始就计时，如果&lt;code&gt;delay&lt;/code&gt;大于任务执行时间则在&lt;code&gt;delay&lt;/code&gt;时间后执行下次任务，如果&lt;code&gt;delay&lt;/code&gt;小于任务执行时间。则在本次任务结束后开始下次人去&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public ScheduledFuture&amp;lt;?&amp;gt; scheduleAtFixedRate(Runnable command,                                                   long initialDelay,                                                   long period,                                                   TimeUnit unit); &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;command:he task to execute&lt;/li&gt; &lt;li&gt;initialDelay:the time to delay first execution&lt;/li&gt; &lt;li&gt;delay:从本次任务开始的延时时间&lt;/li&gt; &lt;li&gt;unit:延时时间单位&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;t1 start/delay start   delay end    t1 end/t2 start/delay start ↓                          ↓                ↓ |__________________________|________________|______________________ &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;        try{             ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);             scheduledExecutorService.scheduleAtFixedRate(new Runnable() {                 @Override                 public void run() {                     try{                         System.out.println(Thread.currentThread().getName()+&amp;quot;:task2:&amp;quot;+ DateTime.now().toString(&amp;quot;yyyy-MM-dd hh🇲🇲ss&amp;quot;));                         Thread.sleep(3000);                     } catch (InterruptedException e){                         e.printStackTrace();                     }                  }             }, 0, 1, TimeUnit.SECONDS);             System.in.read();         } catch (IOException e){             e.printStackTrace();         } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;小结：对于&lt;code&gt;scheduleAtFixedRate&lt;/code&gt; &lt;code&gt;delay&lt;/code&gt;开始时间就是任务其实时间，因此下次任务的开始时间是max{延时时间,任务执行时间}（两个时间取大者），上面代码执行间隔时间是3秒&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sat, 28 Jul 2018 07:30:30 GMT</pubDate>
    </item>
    <item>
      <title>重温Spring in action3篇二AOP</title>
      <link>https://www.zhangaoo.com/article/spring-in-action-2-aop</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/07/39ft3sdvqoileqgd787inkl1o4.png" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;面向切面的&lt;code&gt;Spring&lt;/code&gt;&lt;/h2&gt; &lt;h3&gt;&lt;code&gt;AOP&lt;/code&gt;术语&lt;/h3&gt; &lt;h4&gt;通知(&lt;code&gt;Advice&lt;/code&gt;):简单理解为是一段增强代码，由切面添加到特定连接点的功能代码&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Spring&lt;/code&gt;有5种类型的通知 &lt;ol&gt; &lt;li&gt;&lt;code&gt;Before&lt;/code&gt;--在方法调用之前调用通知&lt;/li&gt; &lt;li&gt;&lt;code&gt;After&lt;/code&gt;--在方法完成后调用通知，无论方法是否执行成功&lt;/li&gt; &lt;li&gt;&lt;code&gt;After-returning&lt;/code&gt;--在方法成功执行后调用通知&lt;/li&gt; &lt;li&gt;&lt;code&gt;After-throwing&lt;/code&gt;--在抛出异常后调用通知&lt;/li&gt; &lt;li&gt;&lt;code&gt;Around&lt;/code&gt;--通知包裹了被通知的方法，在被通知方法调用前后调用后执行自定义的行为&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;连接点(&lt;code&gt;Joinpoint&lt;/code&gt;)&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;需要被增强方法的执行点&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;切点(&lt;code&gt;Pointcut&lt;/code&gt;)&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;用&lt;code&gt;AspectJ&lt;/code&gt;来描述规则，这个规则用来匹配连接点(&lt;code&gt;Joinpoint&lt;/code&gt;),给满足规则的连接点增加增强代码&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;连接点(&lt;code&gt;Joinpoint&lt;/code&gt;)和切点(&lt;code&gt;Pointcut&lt;/code&gt;)关系&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;在&lt;code&gt;Spring AOP&lt;/code&gt;中所有的方法执行都是连接点，而切入点是一个描述信息用于修饰连接点，通过切入点的描述我们可以确定哪些连接点可以织入增强代码。增强代码在连接点执行，而切点规定了哪些连接点可以执行哪些增强代码。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;织入&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;将切面应用到目标对象来创建新代理对象的过程&lt;/li&gt; &lt;li&gt;织入的时机 &lt;ol&gt; &lt;li&gt;编译期：切面在目标类编译时被织入&lt;/li&gt; &lt;li&gt;类加载期：切面在目标类加载到JVM时被织入。&lt;/li&gt; &lt;li&gt;运行期：切面在应用运到某个时刻被织入。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Spring对AOP的支持&lt;/h3&gt; &lt;h4&gt;常见的AOP框架&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;AspectJ&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;JBoss AOP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;Srping AOP&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;h4&gt;&lt;code&gt;Spring&lt;/code&gt; 提供了4种&lt;code&gt;AOP&lt;/code&gt;支持&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;基于代理的经典&lt;code&gt;AOP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;@AspectJ&lt;/code&gt;注解驱动的切面&lt;/li&gt; &lt;li&gt;纯&lt;code&gt;POJO&lt;/code&gt;切面&lt;/li&gt; &lt;li&gt;注入式&lt;code&gt;AspectJ&lt;/code&gt;切面&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;前三种都是&lt;code&gt;Spring&lt;/code&gt;基于代理的&lt;code&gt;AOP&lt;/code&gt;变体，因此&lt;code&gt;Spring&lt;/code&gt;对&lt;code&gt;AOP&lt;/code&gt;的支持局限于方法的拦截，如果需要对构造器和属性的拦截，那么应该考虑在&lt;code&gt;AspectJ&lt;/code&gt;中实现切面。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;&lt;code&gt;Spring&lt;/code&gt;代理的几个特点&lt;/h4&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;Spring&lt;/code&gt;通知是&lt;code&gt;Java&lt;/code&gt;编写的，&lt;code&gt;AspectJ&lt;/code&gt;与之相反。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Spring&lt;/code&gt;在运行期间通知对象，&lt;code&gt;Spring&lt;/code&gt;在运行期间将切面织入到&lt;code&gt;Spring&lt;/code&gt;管理的&lt;code&gt;Bean&lt;/code&gt;中，因为运行时才床架代理对象，所以我们不需要特殊的编译器来织入&lt;code&gt;AOP&lt;/code&gt;的切面。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Spring&lt;/code&gt;只支持方法连接点，因为&lt;code&gt;Spring&lt;/code&gt;是基于动态代理。&lt;code&gt;AspectJ&lt;/code&gt;、&lt;code&gt;Jboos&lt;/code&gt;等&lt;code&gt;AOP&lt;/code&gt;框架还支持更细粒度的拦截，比如：字段和构造器的拦截。如果方法拦截不能满足我们的要求时可以通过&lt;code&gt;AspectJ&lt;/code&gt;配合来满足我们的要求。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;使用切点选择连接点&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;切点用于准确定位该在什么地方应用切面通知。切点和通知都是切面的最基本元素。在&lt;code&gt;Spring AOP&lt;/code&gt;中需要使用&lt;code&gt;AspectJ&lt;/code&gt;切点表达式来定义切点。&lt;/li&gt; &lt;li&gt;&lt;code&gt;Spring AOP&lt;/code&gt;所支持的&lt;code&gt;AspectJ&lt;/code&gt;切点指示器： &lt;ol&gt; &lt;li&gt;&lt;code&gt;arg()&lt;/code&gt;:限制连接点匹配参数为指定类型的执行方法&lt;/li&gt; &lt;li&gt;&lt;code&gt;@args()&lt;/code&gt;:限制连接点匹配参数由指定注解标注的执行方法&lt;/li&gt; &lt;li&gt;&lt;code&gt;execution()&lt;/code&gt;:用于匹配是连接点的执行方法&lt;/li&gt; &lt;li&gt;&lt;code&gt;this()&lt;/code&gt;:限制连接点匹配AOP代理的Bean引用为指定类型的类&lt;/li&gt; &lt;li&gt;&lt;code&gt;target()&lt;/code&gt;:限制连接点匹配的对象为指定类型的类&lt;/li&gt; &lt;li&gt;&lt;code&gt;@target()&lt;/code&gt;:限制连接点匹配特定的执行对象，这些对象对应了类要具备指定类型的注解&lt;/li&gt; &lt;li&gt;&lt;code&gt;within()&lt;/code&gt;:限制连接点匹配指定的类型&lt;/li&gt; &lt;li&gt;&lt;code&gt;@within()&lt;/code&gt;:限制连接点匹配指定注解所标注的类型（当使用Spring AOP时，方法定义在有指定注解所标注的类里）&lt;/li&gt; &lt;li&gt;&lt;code&gt;@annotation&lt;/code&gt;:限制匹配带有指定注解连接点&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;如果使用&lt;code&gt;AspectJ&lt;/code&gt;除上面以外的指示器将会抛出异常&lt;/li&gt; &lt;li&gt;只有&lt;code&gt;execution()&lt;/code&gt;指示器是唯一的执行匹配，其他指示器都是用于限制匹配的，这说明&lt;code&gt;execution()&lt;/code&gt;是我们在编写切点定义时最主要的指示器，在此基础上其他指示器用来限制所匹配的切点。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;编写切点&lt;/h3&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;例如：假设在执行&lt;code&gt;play()&lt;/code&gt;方法前执行通知&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;execution(* com.zealzhangz.common.play(..)) &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;上面的*表示任意返回类型&lt;/li&gt; &lt;li&gt;..表示任意参数&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用&lt;code&gt;within&lt;/code&gt;来限制匹配&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;execution(* com.zealzhangz.common.play(..)) and within(com.zealzhangz.common.*) &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;此时限制必须在&lt;code&gt;com.zealzhangz.commo&lt;/code&gt;包中。&lt;/li&gt; &lt;li&gt;类似的可以使用&lt;code&gt;or&lt;/code&gt;、&lt;code&gt;not&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;Spring的Bean()指示器&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;除了上面说的指示器，Spring2.5还引入了一个新的Bean指示器，该指示器允许我们使用Bean的ID来标识bean，例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;execution(* com.zealzhangz.common.play(..)) and bean(piano) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;或者排除指定名称的bean&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;execution(* com.zealzhangz.common.play(..)) and !bean(piano)   &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;在XML中声明切面&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;AOP配置元素 &lt;ol&gt; &lt;li&gt;&lt;a href="aop:advisor" target="_blank"&gt;aop:advisor&lt;/a&gt;:定义&lt;code&gt;AOP&lt;/code&gt;通知器。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:after" target="_blank"&gt;aop:after&lt;/a&gt;:定义&lt;code&gt;AOP&lt;/code&gt;后置通知（不管被通知的方法是否执行成功）。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:after-returning" target="_blank"&gt;aop:after-returning&lt;/a&gt;:定义&lt;code&gt;AOP after-returning&lt;/code&gt;(在方法成功执行后调用通知)。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:throwing" target="_blank"&gt;aop:throwing&lt;/a&gt;:定义&lt;code&gt;AOP after-throwing&lt;/code&gt;(在抛出异常后调用通知)。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:around" target="_blank"&gt;aop:around&lt;/a&gt;:定义环绕通知。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:aspect" target="_blank"&gt;aop:aspect&lt;/a&gt;:定义切面。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:aspecj:autoproxy" target="_blank"&gt;aop:aspecj:autoproxy&lt;/a&gt;:启用注解&lt;code&gt;@AspectJ&lt;/code&gt;驱动的切面。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:before" target="_blank"&gt;aop:before&lt;/a&gt;:定义&lt;code&gt;AOP&lt;/code&gt;前置通知。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:config" target="_blank"&gt;aop:config&lt;/a&gt;:顶层&lt;code&gt;AOP&lt;/code&gt;配置元素，大多数&lt;code&gt;&amp;lt;aop:*&amp;gt;&lt;/code&gt;必须包含在&lt;a href="aop:config" target="_blank"&gt;aop:config&lt;/a&gt;中。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:declare-parents" target="_blank"&gt;aop:declare-parents&lt;/a&gt;:为被通知的对象引入而外的接口，并透明的实现。&lt;/li&gt; &lt;li&gt;&lt;a href="aop:pointcut" target="_blank"&gt;aop:pointcut&lt;/a&gt;:定义切点。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;声明前置和后置通知&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;把&lt;code&gt;audience&lt;/code&gt;（观众变成一个切面）&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;   &amp;lt;aop:config&amp;gt;        &amp;lt;aop:aspect ref=&amp;quot;audience&amp;quot;&amp;gt;            &amp;lt;!--前置通知，表演之前观众就坐--&amp;gt;            &amp;lt;aop:before method=&amp;quot;takeSeats&amp;quot; pointcut=&amp;quot;execution(* com.zealzhangz.common.Instrument.play(..))&amp;quot;/&amp;gt;            &amp;lt;!--前置通知，表演之前观众关闭手机--&amp;gt;            &amp;lt;aop:before method=&amp;quot;takeOffCellphones&amp;quot; pointcut=&amp;quot;execution(* com.zealzhangz.common.Instrument.play(..))&amp;quot;/&amp;gt;            &amp;lt;!--成功表演观众鼓掌--&amp;gt;            &amp;lt;aop:after-returning method=&amp;quot;applaud&amp;quot; pointcut=&amp;quot;execution(* com.zealzhangz.common.Instrument.play())&amp;quot;/&amp;gt;            &amp;lt;!--表演失败，观众要求退钱--&amp;gt;            &amp;lt;aop:after-throwing method=&amp;quot;demandRefund&amp;quot; pointcut=&amp;quot;execution(* com.zealzhangz.common.Instrument.play()))&amp;quot;/&amp;gt;        &amp;lt;/aop:aspect&amp;gt;    &amp;lt;/aop:config&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;观察上面的切面，发现相同的切点我们重复定义了4次，这违反了&lt;code&gt;DRY&lt;/code&gt;原则，为了避免上述重复代码我们可以简化如下&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;   &amp;lt;aop:config&amp;gt;        &amp;lt;aop:aspect ref=&amp;quot;audience&amp;quot;&amp;gt;            &amp;lt;aop:pointcut id=&amp;quot;player&amp;quot; expression=&amp;quot;execution(* com.zealzhangz.common.Instrument.play(..))&amp;quot;/&amp;gt;            &amp;lt;!--前置通知，表演之前观众就坐--&amp;gt;            &amp;lt;aop:before method=&amp;quot;takeSeats&amp;quot;  pointcut-ref=&amp;quot;player&amp;quot;/&amp;gt;            &amp;lt;!--前置通知，表演之前观众关闭手机--&amp;gt;            &amp;lt;aop:before method=&amp;quot;takeOffCellphones&amp;quot; pointcut-ref=&amp;quot;player&amp;quot;/&amp;gt;            &amp;lt;!--成功表演观众鼓掌--&amp;gt;            &amp;lt;aop:after-returning method=&amp;quot;applaud&amp;quot; pointcut-ref=&amp;quot;player&amp;quot;/&amp;gt;            &amp;lt;!--表演失败，观众要求退钱--&amp;gt;            &amp;lt;aop:after-throwing method=&amp;quot;demandRefund&amp;quot; pointcut-ref=&amp;quot;player&amp;quot;/&amp;gt;        &amp;lt;/aop:aspect&amp;gt;    &amp;lt;/aop:config&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;声明环绕通知&lt;/h3&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;环绕通知相比前置、后置通知的有优点，假设有场景需要计算前置通知与后置通知代码执行的时间，那不得不在切面类中声明一个字段保存起始时间在后置通知中使用结束时间减去字段中的起始时间。如果这么实现的话至少存在以下两个问题。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;因为切面类也是&lt;code&gt;Bean&lt;/code&gt;，&lt;code&gt;Bean&lt;/code&gt;是以单列的形式存在。那么存在字段的话会产生线程安全问题&lt;/li&gt; &lt;li&gt;即便不存在线程安全的问题，代码的实现逻辑也相对复杂&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;这种情景环绕通知能很好的解决这个问题，如下代码：&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;   &amp;lt;aop:config&amp;gt;        &amp;lt;aop:aspect ref=&amp;quot;audience&amp;quot;&amp;gt;            &amp;lt;aop:pointcut id=&amp;quot;player&amp;quot; expression=&amp;quot;execution(* com.zealzhangz.common.Instrument.play(..))&amp;quot;/&amp;gt;            &amp;lt;!--环绕通知--&amp;gt;            &amp;lt;aop:around method=&amp;quot;watchPerformance&amp;quot; pointcut-ref=&amp;quot;player&amp;quot;/&amp;gt;        &amp;lt;/aop:aspect&amp;gt;    &amp;lt;/aop:config&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    public void watchPerformance(ProceedingJoinPoint joinPoint){         try{             System.out.println(&amp;quot;The audience taking their seat.&amp;quot;);             System.out.println(&amp;quot;The audience take off their cellphones&amp;quot;);             long start = System.currentTimeMillis();              joinPoint.proceed();              long end = System.currentTimeMillis();             System.out.println(&amp;quot;CLAP CLAP CLAP CLAP&amp;quot;);             System.out.println(&amp;quot;The performance took &amp;quot; + (end - start) + &amp;quot; milliseconds&amp;quot;);          } catch (Throwable t){             System.out.println(&amp;quot;Boo! We want our money back&amp;quot;);         }     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意:&lt;/strong&gt; 以上环绕通知方法包含了入参&lt;code&gt;ProceedingJoinPoint&lt;/code&gt;这里是必须的，&lt;code&gt;joinPoint.proceed();&lt;/code&gt;这行代码是必须的，&lt;/p&gt; &lt;h3&gt;为通知传递参数&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;有时候通知不仅仅是对方法的简单包装，还需要校验传递给方法的参数值。&lt;/li&gt; &lt;li&gt;例如&lt;code&gt;MindReader&lt;/code&gt;有两个接口分别是截听志愿者的思想、获取志愿者的思想。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface MindReader {     /**      * 截听志愿者的思想      * @param thoughts      */     void interceptThoughts(String thoughts);      /**      * 获取志愿者的思想      * @return      */     String getThoughts(); } //预言家`Magician`是MindReader的具体实现 public class Magician implements MindReader {      private String thoughts;      @Override     public void interceptThoughts(String thoughts){         System.out.println(&amp;quot;Intercepting volunteer's thoughts&amp;quot;);         this.thoughts = thoughts;     }      @Override     public String  getThoughts(){         return this.thoughts;     } } //现在为读心者定义一个读心对象Thinker public interface Thinker {     /**      * 思考者      * @param thoughts      */     void thinkOfSomething(String thoughts); }  //一个思考着具体实现志愿者 public class Volunteer implements Thinker {     private String thoughts;     @Override     public void thinkOfSomething(String thoughts){         this.thoughts = thoughts;     }      public String getThoughts(){         return thoughts;     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;定义切面读取志愿者的思想&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;magician&amp;quot; class=&amp;quot;com.zealzhangz.pojo.Magician&amp;quot;/&amp;gt;     &amp;lt;bean id=&amp;quot;volunteer&amp;quot; class=&amp;quot;com.zealzhangz.pojo.Volunteer&amp;quot;/&amp;gt;          &amp;lt;aop:config&amp;gt;         &amp;lt;aop:aspect ref=&amp;quot;magician&amp;quot;&amp;gt;             &amp;lt;aop:pointcut id=&amp;quot;thinking&amp;quot;                           expression=&amp;quot;execution(* com.zealzhangz.common.Thinker.thinkOfSomething(String)) and args(thoughts)&amp;quot;/&amp;gt;             &amp;lt;aop:before pointcut-ref=&amp;quot;thinking&amp;quot;                         method=&amp;quot;interceptThoughts&amp;quot;                         arg-names=&amp;quot;thoughts&amp;quot;/&amp;gt;         &amp;lt;/aop:aspect&amp;gt;     &amp;lt;/aop:config&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;Magician超感官知觉的关键之处在于在于切点定义&lt;code&gt;&amp;lt;aop:before&amp;gt;&lt;/code&gt;和&lt;code&gt;arg-names&lt;/code&gt;属性，切点标识了&lt;code&gt;Thinker&lt;/code&gt;的方法&lt;code&gt;thinkOfSomething()&lt;/code&gt;指定了String参数,然后在&lt;code&gt;args&lt;/code&gt;参数中标识了将&lt;code&gt;thoughts&lt;/code&gt;作为参数。&lt;code&gt;&amp;lt;aop:before&amp;gt;&lt;/code&gt;中也引用了参数&lt;code&gt;thoughts&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;通过切面引入新功能（为Bean添加新方法）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;因为Java是静态语言，一旦编译完成就很难再为类添加新功能了。&lt;/li&gt; &lt;li&gt;直接上代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;aop:config&amp;gt;         &amp;lt;aop:aspect&amp;gt;             &amp;lt;aop:declare-parents types-matching=&amp;quot;com.zealzhangz.common.Instrument+&amp;quot;                                  implement-interface=&amp;quot;com.zealzhangz.common.Contestant&amp;quot;                                  default-impl=&amp;quot;com.zealzhangz.pojo.GraciousContestant&amp;quot;/&amp;gt;         &amp;lt;/aop:aspect&amp;gt;     &amp;lt;/aop:config&amp;gt;      &amp;lt;!--或者可以把实现直接引用默认的bean--&amp;gt;     &amp;lt;bean id=&amp;quot;graciousContestant&amp;quot; class=&amp;quot;com.zealzhangz.pojo.GraciousContestant&amp;quot;/&amp;gt;     &amp;lt;aop:config&amp;gt;         &amp;lt;aop:aspect&amp;gt;             &amp;lt;aop:declare-parents types-matching=&amp;quot;com.zealzhangz.common.Instrument+&amp;quot;                                  implement-interface=&amp;quot;com.zealzhangz.common.Contestant&amp;quot;                                  delegate-ref=&amp;quot;graciousContestant&amp;quot;/&amp;gt;         &amp;lt;/aop:aspect&amp;gt;     &amp;lt;/aop:config&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;types-matching&lt;/code&gt;匹配实现了&lt;code&gt;com.zealzhangz.common.Instrument&lt;/code&gt;接口的所有bean&lt;/li&gt; &lt;li&gt;&lt;code&gt;implement-interface&lt;/code&gt;对所有匹配的Bean添加实现接口&lt;code&gt;com.zealzhangz.common.Contestant&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;default-impl&lt;/code&gt;接口的默认实现为&lt;code&gt;com.zealzhangz.pojo.GraciousContestant&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;注解切面&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;我们以上的切面都是通过在XML中定义实现的，对于不喜欢XML配置的程序员来说，可直接在Java中使用注解实现切面的功能。&lt;/li&gt; &lt;li&gt;直接上代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Athlete {     /**      * 具体运动      */     void running(); } public class Swimmer implements Athlete {     @Override     public void running(){         System.out.println(&amp;quot;I'm swimming&amp;quot;);     } } @Aspect @Component public class AthleteAop {     /**      * 定义切点      */     @Pointcut(             &amp;quot;execution(* com.zealzhangz.common.Athlete.running(..))&amp;quot;)     public void sports(){      }     @Before(&amp;quot;sports()&amp;quot;)     public void readySports(){         System.out.println(&amp;quot;I'm going to ready to sports&amp;quot;);     }     @Before(&amp;quot;sports()&amp;quot;)     public void haveReady(){         System.out.println(&amp;quot;Prepare to end&amp;quot;);     }     @AfterReturning(&amp;quot;sports()&amp;quot;)     public void calculationResult(){         System.out.println(&amp;quot;Good job.&amp;quot;);     }     @AfterThrowing(&amp;quot;sports()&amp;quot;)     public void failed(){         System.out.println(&amp;quot;I screwed up&amp;quot;);     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;除了以上代码还需要在&lt;code&gt;Spring&lt;/code&gt;上下文中声明一个自动代理&lt;code&gt;Bean&lt;/code&gt;，该&lt;code&gt;Bean&lt;/code&gt;知道如何把&lt;code&gt;@AspectJ&lt;/code&gt;注解所标注的&lt;code&gt;Bean&lt;/code&gt;转变为代理通知，为此&lt;code&gt;Spring&lt;/code&gt;提供了&lt;code&gt;AnnotationAwareAspectJAutoProxyCreator&lt;/code&gt;的自动代理创建类，但是使用起来比较复杂，替代方案是使用一个&lt;code&gt;XML&lt;/code&gt;配置，配置如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;aop:aspectj-autoproxy/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：切面类&lt;code&gt;AthleteAop&lt;/code&gt;一定要注解&lt;code&gt;@Component&lt;/code&gt;再注解&lt;code&gt;@Aspect&lt;/code&gt;,&lt;code&gt;@Component&lt;/code&gt;是必须的且顺序不能变，否则切面不会生效。&lt;/p&gt; &lt;h3&gt;注解环绕通知&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;需使用注解&lt;code&gt;@Around&lt;/code&gt;，一个例子如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Aspect @Component public class AthleteAop {     /**      * 定义切点      */     @Pointcut(             &amp;quot;execution(* com.zealzhangz.common.Athlete.running(..))&amp;quot;)     public void sports(){      }     @Around(&amp;quot;sports()&amp;quot;)     public void watchingAthlete(ProceedingJoinPoint proceedingJoinPoint){         try{             System.out.println(&amp;quot;I'm going to ready to sports&amp;quot;);             System.out.println(&amp;quot;Prepare to end&amp;quot;);              proceedingJoinPoint.proceed();              System.out.println(&amp;quot;Good job.&amp;quot;);         }catch (Throwable t){             System.out.println(&amp;quot;I screwed up&amp;quot;);         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;传递参数给标注的通知&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;代码如下&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public interface Athlete {     /**      * 具体运动      * @param myDream      */     void running(String myDream); }  public class Swimmer implements Athlete {     @Override     public void running(String myDream){         System.out.println(&amp;quot;I'm swimming&amp;quot;);         System.out.println(myDream);     } }  @Aspect @Component public class AthleteAop {     @Pointcut(             &amp;quot;execution(* com.zealzhangz.common.Athlete.running(String)) &amp;amp;&amp;amp; args(myDream)&amp;quot;)     public void sports(String myDream){      }      @Before(&amp;quot;sports(myDream)&amp;quot;)     public void readySports(String myDream){         System.out.println(&amp;quot;I'm going to ready to sports&amp;quot;);         System.out.println(&amp;quot;Swimmer dream is &amp;quot; + myDream);     } }      @Test     public void TestSwim(){         Athlete swimmer = (Athlete)this.context.getBean(&amp;quot;swimmer&amp;quot;);         swimmer.running(&amp;quot;I want to win.&amp;quot;);     } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;注意切点定义时候的&lt;code&gt;args&lt;/code&gt;参数，以及前置通知注解中的参数&lt;code&gt;@Before(&amp;quot;sports(myDream)&amp;quot;)&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;标注引入（使用注解为Bean添加新方法）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;等价于&lt;a href="aop:declare-parents" target="_blank"&gt;aop:declare-parents&lt;/a&gt;的注解是@AspectJ的@DeclareParents&lt;/li&gt; &lt;li&gt;直接上代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Aspect @Component public class ContestantIntroducer {     @DeclareParents(value = &amp;quot;com.zealzhangz.common.Instrument+&amp;quot;,     defaultImpl = GraciousContestant.class)     public static Contestant contestant; } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;如上代码&lt;code&gt;ContestantIntroducer&lt;/code&gt;是一个切面，和之前创建切面所有不同的是该切面并没有前置、环绕、后置通知，它为实现了&lt;code&gt;Instrument&lt;/code&gt;接口的bean引入了&lt;code&gt;Contestant&lt;/code&gt;接口，&lt;code&gt;Contestant&lt;/code&gt;的实现为&lt;code&gt;GraciousContestant&lt;/code&gt;.&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code&gt;value&lt;/code&gt;属性等价于&lt;code&gt;&amp;lt;aop:declare-parents&amp;gt;&lt;/code&gt;的&lt;code&gt;ypes-matching&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;defaultImpl&lt;/code&gt;等价于&lt;code&gt;&amp;lt;aop:declare-parents&amp;gt;&lt;/code&gt;的&lt;code&gt;default-impl&lt;/code&gt;&lt;/li&gt; &lt;li&gt;由&lt;code&gt;@DeclareParents&lt;/code&gt;所标注的static属性指定了将被引入的接口&lt;/li&gt; &lt;li&gt;一个主意事项是&lt;code&gt;@DeclareParents&lt;/code&gt;并没有对应&lt;code&gt;&amp;lt;aop:declare-parents&amp;gt;&lt;/code&gt;的&lt;code&gt;delegate-ref&lt;/code&gt;，这是因为&lt;code&gt;@AspectJ&lt;/code&gt;并不是属于Spring的一个项目，因此它并不了解Spring的bean&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;我们需要把&lt;code&gt;ContestantIntroducer&lt;/code&gt;申明为Spring上下文中的一个，可以采用以下两个方法之一&lt;/p&gt; &lt;ol&gt; &lt;li&gt;在XML中配置为bean:&lt;code&gt;&amp;lt;bean class=&amp;quot;com.zealzhangz.pojo.ContestantIntroducer&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/li&gt; &lt;li&gt;直接使用&lt;code&gt;@Component&lt;/code&gt;注解，这种方式需要在XML中配置bean扫描范围&lt;code&gt;&amp;lt;context:component-scan base-package=&amp;quot;com.zealzhangz&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用注解的形式也必须记得在XML中引入&lt;code&gt;&amp;lt;aop:aspectj-autoproxy/&amp;gt;&lt;/code&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;注入AspectJ切面&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;上代码&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public aspect JudgeAspect {     public JudgeAspect(){}      //定义切点     pointcut sports():execution(* com.zealzhangz.common.Athlete.running(..));     //后置通知     after() returning():sports(){         System.out.println(criticismEngine.getCriticism());     }      private CriticismEngine criticismEngine;      public void setCriticismEngine(CriticismEngine criticismEngine) {         this.criticismEngine = criticismEngine;     } }  public interface CriticismEngine {      String getCriticism(); }  public class CriticismEngineImpl implements CriticismEngine{     private String[] criticismPool;     @Override     public String getCriticism(){         int i = (int)(Math.random()*this.criticismPool.length);         return this.criticismPool[i];     }     /**      * by spring inject      * @param criticismPool      */     public void setCriticismPool(String[] criticismPool) {         this.criticismPool = criticismPool;     } } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;简单的分析一下以上代码：&lt;code&gt;JudgeAspect&lt;/code&gt;为切面类，定义了切点和后置通知；&lt;code&gt;CriticismEngineImpl&lt;/code&gt;实现了接口&lt;code&gt;CriticismEngine&lt;/code&gt;，这个评价类会随机的获取评价信息。&lt;/li&gt; &lt;li&gt;&lt;code&gt;XML&lt;/code&gt;代码如下&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;criticismEngine&amp;quot; class=&amp;quot;com.zealzhangz.pojo.CriticismEngineImpl&amp;quot;&amp;gt;         &amp;lt;property name=&amp;quot;criticismPool&amp;quot;&amp;gt;             &amp;lt;list&amp;gt;                 &amp;lt;value&amp;gt;I'm not rude.&amp;lt;/value&amp;gt;                 &amp;lt;value&amp;gt;You are great.&amp;lt;/value&amp;gt;                 &amp;lt;value&amp;gt;I'm sorry.Too bad.&amp;lt;/value&amp;gt;                 &amp;lt;value&amp;gt;Are you serious.&amp;lt;/value&amp;gt;                 &amp;lt;value&amp;gt;Bye!Bye!&amp;lt;/value&amp;gt;             &amp;lt;/list&amp;gt;         &amp;lt;/property&amp;gt;     &amp;lt;/bean&amp;gt;     &amp;lt;bean class=&amp;quot;com.zealzhangz.pojo.JudgeAspect&amp;quot;           factory-method=&amp;quot;aspectOf&amp;quot;&amp;gt;         &amp;lt;property name=&amp;quot;criticismEngine&amp;quot; ref=&amp;quot;criticismEngine&amp;quot;/&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;切面类&lt;code&gt;JudgeAspect&lt;/code&gt;也被注册成了&lt;code&gt;bean&lt;/code&gt;，其中评价字段属性也由&lt;code&gt;bean&lt;/code&gt;装配。&lt;/li&gt; &lt;li&gt;&lt;code&gt;POM&lt;/code&gt;文件依赖&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;properties&amp;gt;         &amp;lt;spring.version&amp;gt;3.2.8.RELEASE&amp;lt;/spring.version&amp;gt;         &amp;lt;aspectj.version&amp;gt;1.8.9&amp;lt;/aspectj.version&amp;gt;         &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;         &amp;lt;java.source-target.version&amp;gt;1.7&amp;lt;/java.source-target.version&amp;gt;     &amp;lt;/properties&amp;gt;      &amp;lt;dependencies&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;${spring.version}&amp;lt;/version&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;aspectjweaver&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;${aspectj.version}&amp;lt;/version&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;aspectjrt&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;${aspectj.version}&amp;lt;/version&amp;gt;             &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;         &amp;lt;/dependency&amp;gt;         &amp;lt;dependency&amp;gt;             &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;             &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;             &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;         &amp;lt;/dependency&amp;gt;     &amp;lt;/dependencies&amp;gt;     &amp;lt;build&amp;gt;         &amp;lt;plugins&amp;gt;             &amp;lt;plugin&amp;gt;                 &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;                 &amp;lt;artifactId&amp;gt;aspectj-maven-plugin&amp;lt;/artifactId&amp;gt;                 &amp;lt;version&amp;gt;1.7&amp;lt;/version&amp;gt;                 &amp;lt;configuration&amp;gt;                     &amp;lt;showWeaveInfo&amp;gt;true&amp;lt;/showWeaveInfo&amp;gt;                     &amp;lt;source&amp;gt;${java.source-target.version}&amp;lt;/source&amp;gt;                     &amp;lt;target&amp;gt;${java.source-target.version}&amp;lt;/target&amp;gt;                     &amp;lt;Xlint&amp;gt;ignore&amp;lt;/Xlint&amp;gt;                     &amp;lt;complianceLevel&amp;gt;${java.source-target.version}&amp;lt;/complianceLevel&amp;gt;                     &amp;lt;encoding&amp;gt;UTF-8&amp;lt;/encoding&amp;gt;                     &amp;lt;verbose&amp;gt;true&amp;lt;/verbose&amp;gt;                 &amp;lt;/configuration&amp;gt;                 &amp;lt;executions&amp;gt;                     &amp;lt;execution&amp;gt;                         &amp;lt;!-- IMPORTANT --&amp;gt;                         &amp;lt;phase&amp;gt;process-sources&amp;lt;/phase&amp;gt;                         &amp;lt;goals&amp;gt;                             &amp;lt;goal&amp;gt;compile&amp;lt;/goal&amp;gt;                             &amp;lt;goal&amp;gt;test-compile&amp;lt;/goal&amp;gt;                         &amp;lt;/goals&amp;gt;                     &amp;lt;/execution&amp;gt;                 &amp;lt;/executions&amp;gt;                 &amp;lt;dependencies&amp;gt;                     &amp;lt;dependency&amp;gt;                         &amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;                         &amp;lt;artifactId&amp;gt;aspectjtools&amp;lt;/artifactId&amp;gt;                         &amp;lt;version&amp;gt;${aspectj.version}&amp;lt;/version&amp;gt;                     &amp;lt;/dependency&amp;gt;                 &amp;lt;/dependencies&amp;gt;             &amp;lt;/plugin&amp;gt;         &amp;lt;/plugins&amp;gt;     &amp;lt;/build&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 这里有个特别要注意的地方，编译aspectj语法的代码需要需要使用Ajc编译器，而不能使用javac编译器。否则会出现找不到类的异常提示&lt;code&gt;Caused by: java.lang.ClassNotFoundException: com.zealzhangz.pojo.JudgeAspect&lt;/code&gt;，如上&lt;code&gt;POM&lt;/code&gt;文件中的&lt;code&gt;&amp;lt;build&amp;gt;&lt;/code&gt;节点就定义了相关信息。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;总结：①虽然&lt;code&gt;Spring AOP&lt;/code&gt;能满足很多场景切面需求，但相比&lt;code&gt;AspectJ&lt;/code&gt;，&lt;code&gt;Spring AOP&lt;/code&gt;是一个功能比较弱的&lt;code&gt;AOP&lt;/code&gt;解决方案。&lt;code&gt;AspectJ&lt;/code&gt;提供了&lt;code&gt;Spring AOP&lt;/code&gt;所不能支持的许多类型的切点。②当&lt;code&gt;Spring AOP&lt;/code&gt;不能满足需求时，我们可以使用强大的&lt;code&gt;@AspectJ&lt;/code&gt;，对于这些场景我们可以使用&lt;code&gt;Spring&lt;/code&gt;为&lt;code&gt;AspectJ&lt;/code&gt;切面注入依赖。这些核心技术为创建松耦合的应用奠定了基础。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Sat, 07 Jul 2018 09:49:19 GMT</pubDate>
    </item>
    <item>
      <title>重温Spring in action3篇一依赖注入</title>
      <link>https://www.zhangaoo.com/article/spring-in-action3-1</link>
      <content:encoded>&lt;h2&gt;Spring基本知识&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;最近用到一些Spring的特性，发现好多知识点忘了，决定再重温一遍。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/06/2nuue6l6asikcpukqucs55jpk9.png" alt="alt" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Spring框架由6个定义明确的模块组成&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;Spring核心容器&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;是Spring最核心的部分，负责Spring应用中的Bean的创建、配置和管理在该模块中Spring的Bean工程提供了依赖注入。除了Bean工厂和应用上下文，该模块也提供了很多服务，比如：邮件、JNDI访问、EJB集成和调度。&lt;/li&gt; &lt;/ul&gt; &lt;ol start="2"&gt; &lt;li&gt;Spring的AOP模块&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;在该模块中Spring对面向切面提供了丰富的支持，该模块也是Spring应用系统开发切面的基础。&lt;/li&gt; &lt;/ul&gt; &lt;ol start="3"&gt; &lt;li&gt;数据集成与访问 -Spring的DAO和JDBC封装了不少模板代码，可使数据库操作代码简单明了还可避免数据库资源泄露等问题。&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;Spring提供了ORM模块，Spring没有尝试去创建自己的ORM解决方案，而是对许多流行的ORM模块进行了集成，包括Hibernate、Java Persisterncs API(JPA)、JDO\iBatis。&lt;/li&gt; &lt;li&gt;本模块也包含了JMS之上构建的Spring抽象层，使消息以异步的方式和其他应用集成。本模块还包含了对象映射到XML的特性，他最初是Spring Web Service项目的一部分。&lt;/li&gt; &lt;li&gt;除此之外本模块使用了Spring AOP为Spring应用中的对象提供事务管理服务。&lt;/li&gt; &lt;/ul&gt; &lt;ol start="4"&gt; &lt;li&gt;Web和远程调用多种流行的MVC&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;Spring集成了多种流行的MVC框架，但他的Web模块和远程调用模块自带了一个强大的MVC框架，有助于提升Web应用层技术的松耦合。&lt;/li&gt; &lt;li&gt;Spring远程调用服务集成了RMI、Hessian、Burlap、JAX-WS，同时Spring还自带了一个远程调用框架：HTTP invoker。&lt;/li&gt; &lt;/ul&gt; &lt;ol start="5"&gt; &lt;li&gt;Instrumentation模块&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;Instrumentation模块提供了为JVM添加代理（agent）的功能。具体来讲，它为Tomcat提供了一个织入代理，能够为Tomcat传递类文件，就像这些文件是被类加载器加载的一样。&lt;/li&gt; &lt;/ul&gt; &lt;ol start="6"&gt; &lt;li&gt;测试&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;该模块为JNDI、Servlet和portlet编写单元测试提供了一系列的模拟对象的实现。&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;Spring bean注入知识&lt;/h2&gt; &lt;h3&gt;注入bean的两种方式&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;构造器注入&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot;&amp;gt;         &amp;lt;constructor-arg name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;&amp;gt;         &amp;lt;constructor-arg name=&amp;quot;configuration&amp;quot; ref=&amp;quot;wxbConfiguration&amp;quot;&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;setter方法注入&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot;&amp;gt;         &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;         &amp;lt;property name=&amp;quot;configuration&amp;quot; ref=&amp;quot;wxbConfiguration&amp;quot;/&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;所有的Spring bean默认都是单列，如果需要每次获取bean都是新产生或者是唯一的话可以使用&lt;code&gt;scope=prototype&lt;/code&gt;例如：&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot; scope=&amp;quot;prototype&amp;quot;&amp;gt;         &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;         &amp;lt;property name=&amp;quot;configuration&amp;quot; ref=&amp;quot;wxbConfiguration&amp;quot;/&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;注入内部bean&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;如果有些bean是专用，不想和外界共享，此时可以注入内部bean，内部bean的最大缺点就是：不能被复用，仅适用于一次注入&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot; scope=&amp;quot;prototype&amp;quot;&amp;gt;         &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;         &amp;lt;property name=&amp;quot;configuration&amp;quot;&amp;gt;             &amp;lt;bean class=&amp;quot;com.air.tqb.utils.weixin.TestInnerBean&amp;quot;/&amp;gt;         &amp;lt;/property&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;使用&lt;code&gt;p&lt;/code&gt;标签简化&lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt;&lt;/h3&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot;         p:interfaceUrlPrefix=&amp;quot;${wxb.interface.url}&amp;quot;         p:configuration-ref=&amp;quot;wxbConfiguration&amp;quot;/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;装配集合&lt;/h3&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;如果对象字段使用的是Collections对象，那么种情况下List,Set,Map用法类似&lt;/p&gt; &lt;ol&gt; &lt;li&gt;装配List&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot; scope=&amp;quot;prototype&amp;quot;&amp;gt;       &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;       &amp;lt;property name=&amp;quot;configurations&amp;quot;&amp;gt;           &amp;lt;list&amp;gt;               &amp;lt;ref bean=&amp;quot;test1&amp;quot;&amp;gt;               &amp;lt;ref bean=&amp;quot;test2&amp;quot;&amp;gt;               &amp;lt;ref bean=&amp;quot;test3&amp;quot;&amp;gt;           &amp;lt;/list&amp;gt;       &amp;lt;/property&amp;gt;   &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;装配Map&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot; scope=&amp;quot;prototype&amp;quot;&amp;gt;     &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;     &amp;lt;property name=&amp;quot;configurations&amp;quot;&amp;gt;         &amp;lt;map&amp;gt;             &amp;lt;entry key=&amp;quot;TEST1&amp;quot; value-ref=&amp;quot;test1&amp;quot;/&amp;gt;             &amp;lt;entry key=&amp;quot;TEST2&amp;quot; value-ref=&amp;quot;test2&amp;quot;/&amp;gt;             &amp;lt;entry key=&amp;quot;TEST3&amp;quot; value-ref=&amp;quot;test3&amp;quot;/&amp;gt;         &amp;lt;/map&amp;gt;     &amp;lt;/property&amp;gt; &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;entry属性&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;  key:map中的key为String   ket-ref:map中的key为Spring上下文中其他bean的引用   value:map中的key为String   value-ref:map中的key为Spring上下文中其他bean的引用 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;装配Properties集合&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;如果key和value都是String类型，方式一可以使用Map进行装配，方式二也可使用&lt;code&gt;java.util.Properties&lt;/code&gt;进行装配&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;bean id=&amp;quot;weixinUtil&amp;quot; class=&amp;quot;com.air.tqb.utils.weixin.WeixinUtil&amp;quot; scope=&amp;quot;prototype&amp;quot;&amp;gt;       &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot; value=&amp;quot;${wxb.interface.url}&amp;quot;/&amp;gt;       &amp;lt;property name=&amp;quot;configurations&amp;quot;&amp;gt;           &amp;lt;props&amp;gt;               &amp;lt;prop key=&amp;quot;TEST1&amp;quot;&amp;gt;test1&amp;lt;/prop&amp;gt;               &amp;lt;prop key=&amp;quot;TEST2&amp;quot;&amp;gt;test2&amp;lt;/prop&amp;gt;               &amp;lt;prop key=&amp;quot;TEST3&amp;quot;&amp;gt;test3&amp;lt;/prop&amp;gt;           &amp;lt;/map&amp;gt;       &amp;lt;/props&amp;gt;   &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt; 5. 装配空值     ```xml   &amp;lt;property name=&amp;quot;interfaceUrlPrefix&amp;quot;&amp;gt;&amp;lt;null/&amp;gt;&amp;lt;/property&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="6"&gt; &lt;li&gt;使用SpEL表达式装配bean &lt;ul&gt; &lt;li&gt;装配字面值&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code&gt;&amp;lt;property name=&amp;quot;message&amp;quot; value=&amp;quot;This value is #{5}&amp;quot;/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;引用Bean、Properties和方法,这里使用SpEL把一个ID为&lt;code&gt;saxophone&lt;/code&gt;的bean装配到&lt;code&gt;instrument&lt;/code&gt;属性中&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;!--一般引用bean的方法--&amp;gt;   &amp;lt;property name=&amp;quot;instrument&amp;quot; ref=&amp;quot;saxophone&amp;quot;/&amp;gt;   &amp;lt;!--使用SpEL表达式引用--&amp;gt;   &amp;lt;property name=&amp;quot;instrument&amp;quot; value=&amp;quot;#{saxophone}&amp;quot;/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用SpEL引用Bean中的属性&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;bean id=&amp;quot;zhanga&amp;quot; class=&amp;quot;com.zealzhang.Test123&amp;quot;&amp;gt;       &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{kenny.song}&amp;quot;/&amp;gt;   &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;通过bean方法返回值装配其他bean的属性&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{songSelector.selectSong()}&amp;quot;&amp;gt;   &amp;lt;!--避免抛出空指针--&amp;gt;   &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{songSelector.selectSong()?.toUpperCase()}&amp;quot;&amp;gt;   &amp;lt;!--这里的？运算符会确保左边的值不null--&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;操作类,有很多情况需要访问类的静态变量，在SpEL中可使用&lt;code&gt;T()&lt;/code&gt;来访问&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;property name=&amp;quot;multiplier&amp;quot; value=&amp;quot;#{T(java.lang.Matn).PI}&amp;quot;&amp;gt;     &amp;lt;!--得到0-1之间的一个随机数--&amp;gt;     &amp;lt;property name=&amp;quot;randomNumber&amp;quot; value=&amp;quot;#{T(java.lang.Matn).random()}&amp;quot;&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;SpEL的其他用法 1. 算术运算：加、减、乘、除、取余、幂运算 2. 关系运算：&amp;lt;、&amp;gt;、==、&amp;lt;=、&amp;gt;=、lt、gt、eq、le、ge 3. 逻辑运算：and、or、not、| 4. 条件运算：?:、?: 5. 正则表达式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;!--一个条件运算的例子--&amp;gt;   &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{songSelector.selectSong() != null ?       songSelector.selectSong() : 'other'}&amp;quot;&amp;gt;   &amp;lt;!--和上面的例子等价--&amp;gt;   &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{songSelector.selectSong()?: 'other'}&amp;quot;&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;SpEL也可以筛选集合、访问集合成员、投影集合等&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;最小化Spring XML配置&lt;/h2&gt; &lt;h3&gt;自动装配Bean属性&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;4种类型的自动装配&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;byName:把与bean的属性具有相同的名称的bean自动装配到bean对应的属性中&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;       &amp;lt;!--正常装配--&amp;gt;        &amp;lt;bean id=&amp;quot;saxophone&amp;quot; class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt;         &amp;lt;bean id=&amp;quot;zhanga&amp;quot; class=&amp;quot;com.zealzhang.Test123&amp;quot;&amp;gt;            &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{kenny.song}&amp;quot;/&amp;gt;            &amp;lt;property name=&amp;quot;instrument&amp;quot; ref=&amp;quot;saxophone&amp;quot;/&amp;gt;        &amp;lt;/bean&amp;gt;         &amp;lt;!--自动装配--&amp;gt;        &amp;lt;bean id=&amp;quot;instrument&amp;quot; class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt;        &amp;lt;bean id=&amp;quot;zhanga&amp;quot; class=&amp;quot;com.zealzhang.Test123&amp;quot; autowire=&amp;quot;byName&amp;quot;&amp;gt;            &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{kenny.song}&amp;quot;/&amp;gt;        &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;byType自动装配:把与bean属性具有相同类型的的其他bean装配到bean的属性中&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;byType自动装配存在一个局限性，如果Spring找到多个bean匹配，Spring不会猜测哪个Bean最适合装配，而是选择抛出异常。&lt;/li&gt; &lt;li&gt;为避免因为使用byType自动装配带来的歧义，Spring提供了另外两种选择：①可以为自动装配标识一个首选Bean；②可以取消某个Bean自动装配候选资格；&lt;/li&gt; &lt;li&gt;可使用bean的&lt;code&gt;primary&lt;/code&gt;属性设置true标识为首选Bean，&lt;code&gt;primary&lt;/code&gt;默认为true，因此要设为非首选Bean需要显示设置&lt;code&gt;primary=&amp;quot;false&amp;quot;&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;bean id=&amp;quot;saxophone&amp;quot; primary=&amp;quot;false&amp;quot; class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;- 在自动装配时我们希望排除某些Bean可设置`autowire-candidate=&amp;quot;false&amp;quot;`  ```xml   &amp;lt;bean id=&amp;quot;saxophone&amp;quot; autowire-candidate=&amp;quot;false&amp;quot;  class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt; ``` &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt; &lt;p&gt;constructor自动装配：把与Bean的构造器入参相同的类型的掐Bean自动装配到Bean构造器的对应的入参中。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;constructor自动装配与byType自动装配有着相同的局限性当发现多个Bean匹配某个构造器的入参时Spring不会猜测哪个Bean更适合自动装配&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;bean id=&amp;quot;saxophone&amp;quot; autowire=&amp;quot;constructor&amp;quot;  class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt;  &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;autodetect装配：首先使用constructor自动装配。如果失败再次尝试byType自动装配&lt;/p&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;bean id=&amp;quot;saxophone&amp;quot; autowire=&amp;quot;autodetect&amp;quot;  class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;默认自动装配&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;我们所需要做的仅仅是在根元素&lt;beans&gt;增加一个&lt;code&gt;default-autowire=&amp;quot;byType&amp;quot;&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;     &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;      &amp;lt;beans xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;      xmlns:p=&amp;quot;http://www.springframework.org/schema/p&amp;quot; xmlns:context=&amp;quot;http://www.springframework.org/schema/context&amp;quot;      xmlns:aop=&amp;quot;http://www.springframework.org/schema/aop&amp;quot;      xmlns:tx=&amp;quot;http://www.springframework.org/schema/tx&amp;quot; xmlns=&amp;quot;http://www.springframework.org/schema/beans&amp;quot;      xsi:schemaLocation=&amp;quot;http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd      http://www.springframework.org/schema/context       http://www.springframework.org/schema/context/spring-context-3.0.xsd      http://www.springframework.org/schema/tx       http://www.springframework.org/schema/tx/spring-tx-3.0.xsd      http://www.springframework.org/schema/aop      http://www.springframework.org/schema/aop/spring-aop-3.0.xsd&amp;quot;      default-autowire=&amp;quot;byType&amp;quot;&amp;gt;       &amp;lt;/beans&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;默认情况下&lt;code&gt;default-autowire=&amp;quot;none&amp;quot;&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;混合使用自动装配和显示装配&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;对某个Bean选择了自动装配的同时也能显示的使用&lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt;装配想要装配的属性，这种情况显示装配优先级较高。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;bean id=&amp;quot;saxophone&amp;quot; class=&amp;quot;com.air.tqb.aop.MyInstrument&amp;quot;/&amp;gt;     &amp;lt;bean id=&amp;quot;zhanga&amp;quot; class=&amp;quot;com.zealzhang.Test123&amp;quot; autowire=&amp;quot;byName&amp;quot;&amp;gt;          &amp;lt;property name=&amp;quot;song&amp;quot; value=&amp;quot;#{kenny.song}&amp;quot;/&amp;gt;          &amp;lt;property name=&amp;quot;instrument&amp;quot; ref=&amp;quot;saxophone&amp;quot;/&amp;gt;     &amp;lt;/bean&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用注解装配&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;使用注解装配与在XML中使用autowire属性装配并没有太大区别&lt;/li&gt; &lt;li&gt;Spring容器默认是禁止注解装配的，我们需要显示的在xml中启用它，配置Spring的context命名空间&lt;code&gt;&amp;lt;context:annotation-config&amp;gt;&lt;/code&gt;，,最简单的启用方式如下：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;     &amp;lt;beans xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;         xmlns:p=&amp;quot;http://www.springframework.org/schema/p&amp;quot; xmlns:context=&amp;quot;http://www.springframework.org/schema/context&amp;quot;         xmlns:aop=&amp;quot;http://www.springframework.org/schema/aop&amp;quot;         xmlns:tx=&amp;quot;http://www.springframework.org/schema/tx&amp;quot; xmlns=&amp;quot;http://www.springframework.org/schema/beans&amp;quot;           xsi:schemaLocation=&amp;quot;http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context          http://www.springframework.org/schema/context/spring-context-3.0.xsd         http://www.springframework.org/schema/tx          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd         http://www.springframework.org/schema/aop         http://www.springframework.org/schema/aop/spring-aop-3.0.xsd&amp;quot;&amp;gt;         &amp;lt;context:annotation-config/&amp;gt;   &amp;lt;/beans&amp;gt;  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;使用@Autowired装配&lt;/li&gt; &lt;/ul&gt; &lt;ol&gt; &lt;li&gt;假设我们需要使用@Autowired让Spring自动装配Instrumentalist的instrument属性，则可对&lt;code&gt;setInstrument()&lt;/code&gt;方法进行标注&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;      //当Spring发现我们对setInstrument使用@Autowired注解时Spring就会对该方法执行byType自动装配       @Autowired       public void setInstrument(Instrument instrument) {           this.instrument = instrument;       } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;有趣的地方在于他不仅能装配&lt;code&gt;setter&lt;/code&gt;方法，还可标注需要自动装配Bean引用的任意方法：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;    @Autowired     public void hereYoueInstrument(Instrument instrument) {     this.instrument = instrument;     } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;注解甚至可以注解构造器&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;      @Autowired       public Instrumentalist(Instrument instrument) {           this.instrument = instrument;       } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;可直接标注属性甚至不会受限于&lt;code&gt;private&lt;/code&gt;关键词，并删除&lt;code&gt;setter&lt;/code&gt;方法，也是我们最常见的装配方法&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;      @Autowired       private Instrument instrument; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;使用&lt;code&gt;@Autowired&lt;/code&gt;自动装配时，需要注意以下两种情况&lt;/p&gt; &lt;ul&gt; &lt;li&gt;没有匹配的Bean&lt;/li&gt; &lt;li&gt;存在多个匹配的Bean&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;默认情况下&lt;code&gt;@Autowired&lt;/code&gt;具有强契约型，其所标注的参数或属性必须是可装配的，如果遇到不可装配的情况就会抛出令人厌烦的&lt;code&gt;NoUniqueBeanDefinitionException&lt;/code&gt;异常。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;如果属性不一定非要装配，null值也是可以接受的，这种情况下可以通过设置&lt;code&gt;@Autowired&lt;/code&gt;的&lt;code&gt;required&lt;/code&gt;属性为&lt;code&gt;false&lt;/code&gt;来配置自动装配是可选的。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;code&gt;@Autowired&lt;/code&gt;用在构造器装配时，如果有多个构造器，那么只能讲一个构造器的&lt;code&gt;required&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;其他构造器只能设为&lt;code&gt;false&lt;/code&gt;,标注多个构造器时Spring会从满足装配条件的构造器中选择入参最多的构造器。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;如果有多个Bean都完全满足装配条件可以使用&lt;code&gt;@Qualifier(&amp;quot;beanName&amp;quot;)&lt;/code&gt;限定器来指定想要装配的Bean。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用&lt;code&gt;@Inject&lt;/code&gt;实现基于标准的自动装配&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;@Inject&lt;/code&gt;是为了统一各种依赖注入框架的编程模型，JCP(Java Community Process)发布了依赖注入的规范，简称为JSR-330，更通常的叫法是 at inject。Spring 3开始已经兼容了该依赖注入模型。&lt;/li&gt; &lt;li&gt;&lt;code&gt;@Inject&lt;/code&gt;几乎可替代&lt;code&gt;@Autowired&lt;/code&gt;，但是&lt;code&gt;@Inject&lt;/code&gt;没有&lt;code&gt;require&lt;/code&gt;属性，因此&lt;code&gt;@Inject&lt;/code&gt;所标注的依赖关系必须存在，如果不存在就会抛出异常。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;   @Inject    private Instrument instrument; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;JSR-330还提供另一种技巧，注入一个Provider从Provider获取一个Bean&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;   class Car {       @Inject Car(Provider&amp;lt;Seat&amp;gt; seatProvider) {       Seat driver = seatProvider.get();       Seat passenger = seatProvider.get();       ...       }  } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;限&lt;code&gt;@Inject&lt;/code&gt;所注入的属性，&lt;code&gt;@Inject&lt;/code&gt;和&lt;code&gt;@Autowired&lt;/code&gt;有许多共同点，&lt;code&gt;@Inject&lt;/code&gt;中的&lt;code&gt;@Named(&amp;quot;beanName&amp;quot;)&lt;/code&gt;就类似于&lt;code&gt;@Autowired&lt;/code&gt;中的限定器&lt;code&gt;@Qualifier(&amp;quot;beanName&amp;quot;)&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;在注解注入中使用表达式&lt;/p&gt; &lt;ul&gt; &lt;li&gt;存在场景我们想要直接属性注入字面值，Spring3.0引入了 &lt;code&gt;@Value(&amp;quot;&amp;quot;)&lt;/code&gt;，例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;     @Value(&amp;quot;让我们荡起双桨&amp;quot;)      private String song; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;装配简单的值并不是&lt;code&gt;@Value(&amp;quot;&amp;quot;)&lt;/code&gt;所擅长的，借助SpEL&lt;code&gt;@Value&lt;/code&gt;变的更将的强大&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;   @Value(&amp;quot;#{systemProperties.myFavoriteSong}&amp;quot;)    private String song; &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;自动检测Bean&lt;/p&gt; &lt;ul&gt; &lt;li&gt;当在Spring配置文件中增加了&lt;code&gt;&amp;lt;context:annotation-config/&amp;gt;&lt;/code&gt;时有助于完全消除&lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;constructor-arg&amp;gt;&lt;/code&gt;元素，我们仍需要使用&lt;code&gt;&amp;lt;bean&amp;gt;&lt;/code&gt;显式的定义Bean。&lt;/li&gt; &lt;li&gt;针对以上的问题Spring还有l另一种技术，&lt;code&gt;&amp;lt;context:component-scan/&amp;gt;&lt;/code&gt;除了完成与&lt;code&gt;&amp;lt;context:annotation-config/&amp;gt;&lt;/code&gt;一样的工作外还允许Spring自动检测Bean和定义Bean。这意味着不使用&lt;code&gt;&amp;lt;bean&amp;gt;&lt;/code&gt;元素，&lt;code&gt;Spring&lt;/code&gt;中的大多数（或者所有）Bean能够实现定义和装配。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;   &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;    &amp;lt;beans xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;    xmlns:p=&amp;quot;http://www.springframework.org/schema/p&amp;quot; xmlns:context=&amp;quot;http://www.springframework.org/schema/context&amp;quot;    xmlns:aop=&amp;quot;http://www.springframework.org/schema/aop&amp;quot;    xmlns:tx=&amp;quot;http://www.springframework.org/schema/tx&amp;quot; xmlns=&amp;quot;http://www.springframework.org/schema/beans&amp;quot;           xsi:schemaLocation=&amp;quot;http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd    http://www.springframework.org/schema/context     http://www.springframework.org/schema/context/spring-context-3.0.xsd    http://www.springframework.org/schema/tx     http://www.springframework.org/schema/tx/spring-tx-3.0.xsd    http://www.springframework.org/schema/aop    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd&amp;quot;&amp;gt;    &amp;lt;context:annotation-config/&amp;gt;     &amp;lt;context:component-scan base-package=&amp;quot;com.air.tqb&amp;quot;&amp;gt;    &amp;lt;context:exclude-filter type=&amp;quot;annotation&amp;quot; expression=&amp;quot;org.springframework.stereotype.Controller&amp;quot;/&amp;gt;    &amp;lt;/context:component-scan&amp;gt;   &amp;lt;/beans&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;&amp;lt;context:component-scan&amp;gt;&lt;/code&gt;会扫描指的包及其子包，并且查找出能够注册为Spring Bean的类&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;code&gt;&amp;lt;context:component-scan&amp;gt;&lt;/code&gt;又是怎么知道哪些类能注册为Spring Bean的类呢？&lt;/p&gt; &lt;ul&gt; &lt;li&gt;默认情况下&lt;code&gt;&amp;lt;context:component-scan&amp;gt;&lt;/code&gt;查找使用g构造型(stereotype)注解所标注的类，这些注解如下： &lt;ol&gt; &lt;li&gt;&lt;code&gt;@Component&lt;/code&gt;:通用构造注解，标识该类为Spring组件&lt;/li&gt; &lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt;:标识将该类定义为SpringMVC controller。&lt;/li&gt; &lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt;:标识将该类定义为数据仓库&lt;/li&gt; &lt;li&gt;&lt;code&gt;@Service&lt;/code&gt;:标识将该类定义为服务&lt;/li&gt; &lt;li&gt;使用&lt;code&gt;@Component&lt;/code&gt;标注的自定义的任意注解&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;过滤组件扫描&lt;/p&gt; &lt;ul&gt; &lt;li&gt;我们可以增加一个包含过滤器来要求&lt;code&gt;&amp;lt;context:component-scan&amp;gt;&lt;/code&gt;自动注册我们指定的包或类，例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;  &amp;lt;context:component-scan base-package=&amp;quot;com.air.tqb&amp;quot;&amp;gt;   &amp;lt;context:include-filter type=&amp;quot;assignable&amp;quot; expression=&amp;quot;com.air.tqb.Instrument&amp;quot;/&amp;gt;   &amp;lt;/context:component-scan&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;以上配置自动注册Instrument的实现类。&lt;/li&gt; &lt;li&gt;除了使用&lt;code&gt;include-filter&lt;/code&gt;来制定扫描哪些类，还可使用&lt;code&gt;exclude-filter&lt;/code&gt;排除哪些类注册为Bean.&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;5中过滤器类型&lt;/p&gt; &lt;ol&gt; &lt;li&gt;annotation:过滤器扫描指定注解标注的那些&lt;/li&gt; &lt;li&gt;assignable:过滤器扫描派生与expression属性所指定类型的那些类&lt;/li&gt; &lt;li&gt;aspectj:过滤器扫描与expression属性所指定的AspectJ表达式匹配的类&lt;/li&gt; &lt;li&gt;custom:&lt;/li&gt; &lt;li&gt;regx:过滤器扫描类名与expression属性所指地正则表达式所匹配的类&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用Spring基于基于Java的配置&lt;/p&gt; &lt;ul&gt; &lt;li&gt;为了满足那些不喜欢使用XML配置的开发人员，可以直接使用Java代码配置Bean&lt;/li&gt; &lt;li&gt;即使可以不使用XML就能编写Spring的大多数配置，但任然需要少量XML来启用Java配置加上注解&lt;code&gt;@Configuration&lt;/code&gt;，如下：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;    &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;     &amp;lt;beans xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;     xmlns:p=&amp;quot;http://www.springframework.org/schema/p&amp;quot; xmlns:context=&amp;quot;http://www.springframework.org/schema/context&amp;quot;     xmlns:aop=&amp;quot;http://www.springframework.org/schema/aop&amp;quot;     xmlns:tx=&amp;quot;http://www.springframework.org/schema/tx&amp;quot; xmlns=&amp;quot;http://www.springframework.org/schema/beans&amp;quot;           xsi:schemaLocation=&amp;quot;http://www.springframework.org/schema/beans     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context      http://www.springframework.org/schema/context/spring-context-3.0.xsd     http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-3.0.xsd     http://www.springframework.org/schema/aop     http://www.springframework.org/schema/aop/spring-aop-3.0.xsd&amp;quot;&amp;gt;     &amp;lt;context:annotation-config/&amp;gt;      &amp;lt;context:component-scan base-package=&amp;quot;com.air.tqb&amp;quot;&amp;gt;     &amp;lt;/context:component-scan&amp;gt;     &amp;lt;/beans&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;以上配置会告知Spring在com.air.tqb包内查找&lt;code&gt;@Configuration&lt;/code&gt;注解所标注的所有类&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;使用&lt;code&gt;@Configuration&lt;/code&gt;定义一个配置类&lt;/p&gt; &lt;ul&gt; &lt;li&gt;使用&lt;code&gt;@Configuration&lt;/code&gt;注解的类就相当于XML配置文件中的&lt;code&gt;&amp;lt;beans&amp;gt;&lt;/code&gt;标签&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-java"&gt;     @Configuration      public class SpringConfiguration {      } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;声明一个简单的Bean&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;     @Configuration      public class SpringConfiguration {          @Bean          public Instrument piano(){              return new Piano();          }      } &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;以上代码在Spring应用上下文注册了一个ID为&lt;code&gt;piano&lt;/code&gt;Bean&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Fri, 29 Jun 2018 08:25:01 GMT</pubDate>
    </item>
    <item>
      <title>Go语言之方法和接口篇四</title>
      <link>https://www.zhangaoo.com/article/methods-interface</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/01/2ce1julifei0fpc5leiu8tnvr4.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;方法和接口&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;学习如何为类型定义方法；如何定义接口；以及如何将所有内容贯通起来。&lt;/li&gt; &lt;li&gt;本节课包含了方法和接口，可以用这种构造来定义对象及其行为。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;方法&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Go 没有类。不过你可以为结构体类型定义方法。&lt;/li&gt; &lt;li&gt;方法就是一类带特殊的 &lt;code&gt;接收者&lt;/code&gt; 参数的函数。&lt;/li&gt; &lt;li&gt;方法接收者在它自己的参数列表内，位于 &lt;code&gt;func&lt;/code&gt; 关键字和方法名之间。&lt;/li&gt; &lt;li&gt;在此例中， &lt;code&gt;Abs&lt;/code&gt; 方法拥有一个名为 &lt;code&gt;v&lt;/code&gt; ，类型为 &lt;code&gt;Vertex&lt;/code&gt; 的接收者。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type VertexTest struct {  X, Y float64 }  func (v VertexTest) Abs()float64  {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func main() {  v := VertexTest{3,4}  fmt.Println(v.Abs()) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;方法即函数&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;记住：方法只是个带接收者参数的函数。 代码：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;math&amp;quot;  &amp;quot;fmt&amp;quot; )  type VertexTest1 struct {  X, Y float64 }  func  Abs(v VertexTest1)float64  {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func main()  {  v := VertexTest1{3,4}  fmt.Println(Abs(v)) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;现在这个 Abs 的写法就是个正常的函数，功能并没有什么变化。&lt;/p&gt; &lt;h3&gt;方法（续）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;你也可以为非结构体类型声明方法。&lt;/li&gt; &lt;li&gt;在此例中，我们看到了一个带 Abs 方法的数值类型 MyFloat 。&lt;/li&gt; &lt;li&gt;你只能为在同一包内定义的类型的接收者声明方法， 而不能为其它包内定义的类型（包括 int 之类的内建类型）的接收者声明方法。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type MyFloat float64  func (f MyFloat)Abs()float64  {  if f &amp;lt; 0{   return float64(-f)  }  return float64(f) }  func main() {  f := MyFloat(-math.Sqrt2)  fmt.Println(f.Abs()) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：就是接收者的类型定义和方法声明必须在同一包内；不能为内建类型声明方法&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;指针接收者&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;你可以为指针接收者声明方法。&lt;/li&gt; &lt;li&gt;这意味着对于某类型 T ，接收者的类型可以用 *T 的文法。 （此外， T 不能是像 *int 这样的指针。）&lt;/li&gt; &lt;li&gt;例如，这里为 *Vertex 定义了 Scale 方法。&lt;/li&gt; &lt;li&gt;指针接收者的方法可以修改接收者指向的值（就像 Scale 在这做的）。 由于方法经常需要修改它的接收者，指针接收者比值接收者更常用。&lt;/li&gt; &lt;li&gt;试着移除第 16 行 Scale 函数声明中的 * ，观察此程序的行为如何变化。&lt;/li&gt; &lt;li&gt;若使用值接收者，那么 Scale 方法会对原始 Vertex 值的副本进行操作。 （对于函数的其它参数也是如此。） Scale 方法必须用指针接受者来更改 main 函数中声明的 Vertex 的值。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type VertexTest2 struct {  X,Y float64 }  func (v VertexTest2)Abs()float64  {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func (v *VertexTest2)Scale(f float64){  v.X = v.X * f  v.Y = v.Y * f }  func main() {  v := VertexTest2{3,4}  v.Scale(10)  fmt.Println(v) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：指针指向的是源对象，因此操作的是原对象，非指针操作的是原实例的拷贝&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;指针与函数&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;现在我们要把 Abs 和 Scale 方法重写为函数。&lt;/li&gt; &lt;li&gt;同样，我们先试着移除掉第 16 的 * 。 你能看出为什么程序的行为改变了吗？ 要怎样做才能让该示例升序通过编译？&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type VertexTest3 struct{  X,Y float64 }  func Abs(v VertexTest3) float64 {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func Scale(v * VertexTest3,f float64)  {  v.X = v.X * f  v.Y = v.Y * f }  func main() {  v := VertexTest3{3,4}  Scale(&amp;amp;v,10)  fmt.Println(Abs(v)) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：这里和上一小节实现的功能是一样的只不过上一小节是通过指针接受者，也就是结构体的方法来实现的，本小节是通过传递结构体指针或者引用给函数来实现的和&lt;code&gt;C++&lt;/code&gt;中的引用传递类似&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;方法与指针重定向&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;比较前两个程序，你大概会注意到带指针参数的函数必须接受一个指针：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var v Vertex ScaleFunc(v, 5)  // 编译错误！ ScaleFunc(&amp;amp;v, 5) // OK &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;而以指针为接收者的方法被调用时，接收者既能为值又能为指针：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var v Vertex v.Scale(5)  // OK p := &amp;amp;v p.Scale(10) // OK &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;对于语句 v.Scale(5) ，即便 v 是个值而非指针，带指针接收者的方法也能被直接调用。 也就是说，由于 Scale 方法有一个指针接收者，为方便起见，Go 会将语句 v.Scale(5) 解释为 (&amp;amp;v).Scale(5) 。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type VertexTest4 struct{  X,Y float64 }  func (v * VertexTest4)Scale(f float64)  {  v.Y = v.Y * f  v.X = v.X * f }  func ScaleFunc(v * VertexTest4,f float64)  {  v.X = v.X * f  v.Y = v.Y * f }   func main(){  v := VertexTest4{3,4}  v.Scale(2)  ScaleFunc(&amp;amp;v , 10)   p := &amp;amp;VertexTest4{6,8}  p.Scale(2)  ScaleFunc(p,10)   fmt.Println(v,p)  }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;方法与指针重定向（续）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;同样的事情也发生在相反的方向。&lt;/li&gt; &lt;li&gt;接受一个值作为参数的函数必须接受一个指定类型的值：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var v Vertex fmt.Println(AbsFunc(v))  // OK fmt.Println(AbsFunc(&amp;amp;v)) // 编译错误！ &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;而以值为接收者的方法被调用时，接收者既能为值又能为指针：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var v Vertex fmt.Println(v.Abs()) // OK p := &amp;amp;v fmt.Println(p.Abs()) // OK &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这种情况下，方法调用 p.Abs() 会被解释为 (*p).Abs() 。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;总结：调用方法时，调用者既可以为值也可以为引用或指针（此时引用或指针被go解释为值：p.Abs()-&amp;gt; (*p).Abs()）,方法参数为值时，只能是值不能是引用&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type VertexTest5 struct {  X,Y float64 }  func (v VertexTest5)Abs()float64  {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func AbsFunc(v VertexTest5)float64  {  return math.Sqrt(v.X*v.X + v.Y*v.Y) }  func main(){  v := VertexTest5{3,4}  fmt.Println(v.Abs())  fmt.Println(AbsFunc(v))   p := &amp;amp;VertexTest5{6,8}  fmt.Println(p.Abs())  fmt.Println(AbsFunc(*p)) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;选择值或指针作为接收者&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;使用指针接收者的原因有二： &lt;ol&gt; &lt;li&gt;首先，方法能够修改其接收者指向的值。&lt;/li&gt; &lt;li&gt;其次，这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时，这样做会更加高效。&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;在本例中， Scale 和 Abs 接收者的类型为 *Vertex ，即便 Abs 并不需要修改其接收者。&lt;/li&gt; &lt;li&gt;通常来说，所有给定类型的方法都应该有值或指针接收者，但并不应该二者混用。 （我们会在接下来几页中明白为什么。）&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type VertexTest6 struct{  X,Y float64 }  func (v *VertexTest6)Scale(f float64)  {  v.X = v.X * f  v.Y = v.Y * f }  func (v *VertexTest6)Abs()float64  {  return math.Sqrt(v.X*v.X+v.Y*v.Y) }  func main()  {  v := &amp;amp;VertexTest6{6,8}  fmt.Printf(&amp;quot;Before scaling: %+v, Abs: %v\n&amp;quot;, v, v.Abs())  v.Scale(10)  fmt.Printf(&amp;quot;After scaling: %+v, Abs: %v\n&amp;quot;, v, v.Abs()) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;接口&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;接口类型 是由一组方法签名定义的集合。&lt;/li&gt; &lt;li&gt;接口类型的值可以保存任何实现了这些方法的值。 &lt;strong&gt;注意：&lt;/strong&gt; 示例代码的 22 行存在一个错误。 由于 Abs 方法只为 *Vertex （指针类型）定义， 因此 Vertex （值类型）并未实现 Abser 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type Abser interface {  Abs()float64 }  type MyFloat1 float64  func (f MyFloat1)Abs()float64  {  if f &amp;lt; 0{   return float64(-f)  }  return float64(f) }  type VertexTest7 struct{  X,Y float64 }  func (v *VertexTest7)Abs()float64  {  return math.Sqrt(v.X*v.X+v.Y*v.Y) }  func main() {  var a Abser  f := MyFloat1(math.Sqrt2)  v := VertexTest7{3,4}   a = f //a MyFloat 实现了 Abser  a = &amp;amp;v //a *Vertex 实现了 Abser   // 下面一行，v 是一个 Vertex（而不是 *Vertex）  // 所以没有实现 Abser。  //a = v //放开改行注释会报编译错误，因为实现Abs是选择指针作为接受者，给a赋值时必须是指针或引用   fmt.Println(a.Abs()) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：实现接口的接受者是值还是指针，直接影响给接口变量赋值，接受者是指针必须赋值为引用，接受者是值赋值就是值&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;接口与隐式实现&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;类型通过实现一个接口的所有方法来实现该接口。 既然无需专门显式声明，也就没有“implements“关键字。&lt;/li&gt; &lt;li&gt;隐式接口从接口的实现中解耦了定义，这样接口的实现可以出现在任何包中，无需提前准备。&lt;/li&gt; &lt;li&gt;因此，也就无需在每一个实现上增加新的接口名称，这样同时也鼓励了明确的接口定义。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;接口值&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在内部，接口值可以看做包含值和具体类型的元组：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;(value, type) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;接口值保存了一个具体底层类型的具体值。&lt;/li&gt; &lt;li&gt;接口值调用方法时会执行其底层类型的同名方法。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  type I interface {  M() }  type T struct {  S string }  func (t *T)M()  {  fmt.Println(t.S) }  type F float64  func (f F) M()  {  fmt.Println(f) }  func main() {  var i I  i = &amp;amp;T{&amp;quot;Hello&amp;quot;}  describe(i)  i.M()   i = F(math.Pi)  describe(i)  i.M() }  func describe(i I)  {  fmt.Printf(&amp;quot;(%v, %T)\n&amp;quot;,i,i)   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;(&amp;amp;{Hello}, *main.T) Hello (3.141592653589793, main.F) 3.141592653589793 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;底层值为 nil 的接口值&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;即便接口内的具体值为 nil，方法仍然会被 nil 接收者调用。&lt;/li&gt; &lt;li&gt;在一些语言中，这会触发一个空指针异常，但在 Go 中通常会写一些方法来优雅地处理它（如本例中的 M 方法） &lt;em&gt;注意：&lt;/em&gt; 保存了 nil 具体值的接口其自身并不为 nil 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot; )  type I1 interface {  M() }  type T1 struct {  S string }  func (t *T1)M()  {  if t == nil{   fmt.Println(&amp;quot;&amp;lt;nil&amp;gt;&amp;quot;)   return  }  fmt.Println(t.S) }  func main() {  var i I1  var t *T1  i = t  describe1(i)  i.M()   i = &amp;amp;T1{&amp;quot;Hello&amp;quot;}  describe1(i)  i.M() }  func describe1(i I1)  {  fmt.Printf(&amp;quot;(%v, %T)\n&amp;quot;,i,i)  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;(&amp;lt;nil&amp;gt;, *main.T1) &amp;lt;nil&amp;gt; (&amp;amp;{Hello}, *main.T1) Hello &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;进一步理解指针传递者，指针传递者指向的类型拥有指针传递者定义处的方法，值传递者也是类似。&lt;/li&gt; &lt;li&gt;进一步理解接口，接口就是值和具体类型的元组，比如：(&amp;amp;{Hello}, *main.T1)&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;nil 接口值&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;nil 接口值既不保存值也不保存具体类型。&lt;/li&gt; &lt;li&gt;为 nil 接口调用方法会产生运行时错误(类似于其他语言的NPE(空指针错误))，因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type I2 interface {  M() }  func main() {  var i I2  describe2(i)  i.M()  }  func describe2(i I2) {  fmt.Printf(&amp;quot;(%v, %T)\n&amp;quot;, i, i) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;(&amp;lt;nil&amp;gt;, &amp;lt;nil&amp;gt;) panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x1095118]  goroutine 1 [running]: main.main()  /Users/zealzhangz/Documents/dev/p/go/src/hello-go/main/nil-interface-values.go:12 +0x38 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;空接口&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;指定了零个方法的接口值被称为 &lt;em&gt;空接口：&lt;/em&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;interface{} &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;空接口可保存任何类型的值。 （因为每个类型都至少实现了零个方法。）&lt;/li&gt; &lt;li&gt;空接口被用来处理未知类型的值。 例如，fmt.Print 可接受类型为 interface{} 的任意数量的参数。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  var i interface{}  describe3(i)   i = 42  describe3(i)   i = &amp;quot;Hello&amp;quot;  describe3(i) }  func describe3(i interface{}) {  fmt.Printf(&amp;quot;(%v, %T)\n&amp;quot;, i, i) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;(&amp;lt;nil&amp;gt;, &amp;lt;nil&amp;gt;) (42, int) (Hello, string) &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;类型断言&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;类型断言 提供了访问接口值底层具体值的方式。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;t := i.(T) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;该语句断言接口值 i 保存了具体类型 T ，并将其底层类型为 T 的值赋予变量 t 。&lt;/li&gt; &lt;li&gt;若 i 并未保存 T 类型的值，该语句就会触发一个恐慌。&lt;/li&gt; &lt;li&gt;为了 &lt;strong&gt;判断&lt;/strong&gt; 一个接口值是否保存了一个特定的类型， 类型断言可返回两个值：其底层值以及一个报告断言是否成功的布尔值。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;t, ok := i.(T) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;若 i 保存了一个 T ，那么 t 将会是其底层值，而 ok 为 true 。&lt;/li&gt; &lt;li&gt;否则， ok 将为 false 而 t 将为 T 类型的零值，程序并不会产生恐慌。&lt;/li&gt; &lt;li&gt;请注意这种语法和读取一个映射时的相同之处。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main   import &amp;quot;fmt&amp;quot;  func main() {  var i interface {} = &amp;quot;hello&amp;quot;   s := i.(string)  fmt.Println(s)   s,ok := i.(string)  fmt.Println(s,ok)   f,ok := i.(float64)  fmt.Println(f,ok)   f = i.(float64) //panic  fmt.Println(f)  } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;hello panic: interface conversion: interface {} is string, not float64 hello true 0 false &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;类型选择&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;类型选择&lt;/strong&gt; 是一种按顺序从几个类型断言中选择分支的结构。&lt;/li&gt; &lt;li&gt;类型选择与一般的 &lt;code&gt;switch&lt;/code&gt; 语句相似，不过类型选择中的 &lt;code&gt;case&lt;/code&gt; 为类型（而非值）， 它们针对给定接口值所存储的值的类型进行比较。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;switch v := i.(type) { case T:     // v 的类型为 T case S:     // v 的类型为 S default:     // 没有匹配，v 与 i 的类型相同 } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;类型选择中的声明与类型断言 &lt;code&gt;i.(T)&lt;/code&gt; 的语法相同，只是具体类型 &lt;code&gt;T&lt;/code&gt; 被替换成了关键字 &lt;code&gt;type&lt;/code&gt; 。&lt;/li&gt; &lt;li&gt;此选择语句判断接口值 i 保存的值类型是 T 还是 S 。 在 T 或 S 的情况下，变量 v 会分别按 T 或 S 类型保存 i 拥有的值。 在默认（即没有匹配）的情况下，变量 v 与 i 的接口类型和值相同。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func do(i interface{}) {  switch v := i.(type){  case int:   fmt.Printf(&amp;quot;Twice %v is %v\n&amp;quot;,v,v*2)  case string:   fmt.Printf(&amp;quot;%q is %v bytes long\n&amp;quot;,v,len(v))  default:   fmt.Printf(&amp;quot;I don't know about type %T!\n&amp;quot;, v)  } }  func main() {  do(21)  do(&amp;quot;hello&amp;quot;)  do(true) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Stringer&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;fmt 包中定义的 Stringer 是最普遍的接口之一。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;type Stringer interface {     String() string } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;Stringer 是一个可以用字符串描述自己的类型。fmt 包（还有很多包）都通过此接口来打印值。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type Person struct {  Age uint8  Name string }  func (p Person)string()string  {  return fmt.Sprintf(&amp;quot;%v (%v years)&amp;quot;,p.Name,p.Age) }  func main() {  p := Person{25,&amp;quot;San Zhang&amp;quot;}  fmt.Println(p.string()) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：这里实现&lt;code&gt;string()&lt;/code&gt;这个接口就相当于&lt;code&gt;Java&lt;/code&gt;中重写自己的&lt;code&gt;toString()&lt;/code&gt;方法&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;练习：Stringer&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。&lt;/li&gt; &lt;li&gt;例如，IPAddr{1, 2, 3, 4} 应当打印为 &amp;quot;1.2.3.4&amp;quot; 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type IPAddr [4]byte  // TODO: Add a &amp;quot;String() string&amp;quot; method to IPAddr. func (ip IPAddr)String()string  {  ipStr := &amp;quot;&amp;quot;  for i := 0;i &amp;lt; len(ip);i++{   ipStr += fmt.Sprintf(&amp;quot;%d&amp;quot;,ip[i]) + &amp;quot;.&amp;quot;  }  return ipStr[:len(ipStr)-1] }  func main() {  hosts := map[string]IPAddr{   &amp;quot;loopback&amp;quot;:  {127, 0, 0, 1},   &amp;quot;googleDNS&amp;quot;: {8, 8, 8, 8},  }  for name, ip := range hosts {   fmt.Printf(&amp;quot;%v: %v\n&amp;quot;, name, ip)  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：难点之一是byte转string，这里使用了fmt.Sprintf(&amp;quot;%d&amp;quot;,ip[i])，go还有专门的函数可以转换 &lt;code&gt;str1 := strconv.Itoa(i)&lt;/code&gt;，需要引入包&lt;code&gt;strconv&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;错误&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;Go&lt;/code&gt; 程序使用 &lt;code&gt;error&lt;/code&gt; 值来表示错误状态。&lt;/li&gt; &lt;li&gt;与 &lt;code&gt;fmt.Stringer&lt;/code&gt; 类似， &lt;code&gt;error&lt;/code&gt; 类型是一个内建接口：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;type error interface {     Error() string } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;（与 &lt;code&gt;fmt.Stringer&lt;/code&gt; 类似， &lt;code&gt;fmt&lt;/code&gt; 包在打印值时也会满足 &lt;code&gt;error&lt;/code&gt; 。）&lt;/li&gt; &lt;li&gt;通常函数会返回一个 &lt;code&gt;error&lt;/code&gt; 值，调用的它的代码应当判断这个错误是否等于 &lt;code&gt;nil&lt;/code&gt; 来进行错误处理。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;i, err := strconv.Atoi(&amp;quot;42&amp;quot;) if err != nil {     fmt.Printf(&amp;quot;couldn't convert number: %v\n&amp;quot;, err)     return } fmt.Println(&amp;quot;Converted integer:&amp;quot;, i) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;error&lt;/code&gt; 为 &lt;code&gt;nil&lt;/code&gt; 时表示成功；非 &lt;code&gt;nil&lt;/code&gt; 的 &lt;code&gt;error&lt;/code&gt; 表示失败。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;time&amp;quot; )  type MyError struct {  When time.Time  What string }  func (e *MyError)Error()string  {  return fmt.Sprintf(&amp;quot;at %v,%s&amp;quot;,e.When,e.What) }  func run()error  {  return &amp;amp;MyError{time.Now(),&amp;quot;it didn't work&amp;quot;} }  func main() {  if err := run();err != nil{   fmt.Println(err)  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：&lt;code&gt;run()&lt;/code&gt;返回的是引用而不是值，这跟实现接口&lt;code&gt;Erro()&lt;/code&gt;使用的是指针接受者是一致的&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;练习：错误&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;从之前的练习中复制 Sqrt 函数，修改它使其返回 error 值。&lt;/li&gt; &lt;li&gt;Sqrt 接受到一个负数时，应当返回一个非 nil 的错误值。复数同样也不被支持。&lt;/li&gt; &lt;li&gt;创建一个新的类型&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;type ErrNegativeSqrt float64 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;并为其实现&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;func (e ErrNegativeSqrt) Error() string &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;方法使其拥有 error 值，通过 ErrNegativeSqrt(-2).Error() 调用该方法应返回&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;&amp;quot;cannot Sqrt negative number: -2&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;注意：&lt;/em&gt; 在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题：fmt.Sprint(float64(e)) 。这是为什么呢？&lt;/p&gt; &lt;ul&gt; &lt;li&gt;修改 Sqrt 函数，使其接受一个负数时，返回 ErrNegativeSqrt 值。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type ErrNegativeSqrt float64  func (e ErrNegativeSqrt)Error()string  {  if float64(e) &amp;lt; 0{   return fmt.Sprintf(&amp;quot;cannot Sqrt negative number: %v&amp;quot;,float64(e))  }  return &amp;quot;&amp;quot; }  func Sqrt1(x float64)(float64, error){  z := float64(1)  for i := 0; i &amp;lt; 10;i++{   z = z - (z*z-x)/2*z  }  return z,ErrNegativeSqrt(x) }  func main() {  fmt.Println(Sqrt1(2))  fmt.Println(Sqrt1(-2)) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;Reader&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;io&lt;/code&gt; 包指定了 &lt;code&gt;io.Reader&lt;/code&gt; 接口， 它表示从数据流的末尾进行读取。&lt;/li&gt; &lt;li&gt;Go 标准库包含了该接口的许多实现， 包括文件、网络连接、压缩和加密等等。&lt;/li&gt; &lt;li&gt;io.Reader 接口有一个 Read 方法：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;func (T) Read(b []byte) (n int, err error) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;Read 用数据填充给定的字节切片并返回填充的字节数和错误值。 在遇到数据流的结尾时，它会返回一个 io.EOF 错误。&lt;/li&gt; &lt;li&gt;示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;io&amp;quot;  &amp;quot;strings&amp;quot; )  func main() {  r := strings.NewReader(&amp;quot;Hello, Reader!&amp;quot;)  b := make([]byte, 8)  for{   n,err := r.Read(b)   fmt.Printf(&amp;quot;n = %v err = %v b = %v\n&amp;quot;,n,err,b)   fmt.Printf(&amp;quot;b[:n] = %q\n&amp;quot;,b[:n])   if err == io.EOF{    break   }  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;练习：Reader&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;实现一个 Reader 类型，它产生一个 ASCII 字符 'A' 的无限流。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;golang.org/x/tour/reader&amp;quot;  type MyReader struct{}  // TODO: Add a Read([]byte) (int, error) method to MyReader.  func (r MyReader)Read(b []byte)(int,error)  {  for index,_ := range b{   b[index]='A'  }  return len(b),nil }  func main() {  reader.Validate(MyReader{}) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：仔细理解题意，这里的无限并不是真正的无限而是传入多大一个&lt;code&gt;b []byte&lt;/code&gt;数组来接受，就产生多大一个流&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;练习：rot13Reader&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;有种常见的模式是一个 &lt;code&gt;io.Reader&lt;/code&gt; 包装另一个 &lt;code&gt;io.Reader&lt;/code&gt; ，然后通过某种方式修改其数据流。&lt;/li&gt; &lt;li&gt;例如，&lt;code&gt;gzip.NewReader&lt;/code&gt; 函数接受一个 &lt;code&gt;io.Reader&lt;/code&gt; （已压缩的数据流）并返回一个同样实现了 &lt;code&gt;io.Reader&lt;/code&gt; 的 &lt;code&gt;*gzip.Reader&lt;/code&gt; （解压后的数据流）。&lt;/li&gt; &lt;li&gt;编写一个实现了 &lt;code&gt;io.Reader&lt;/code&gt; 并从另一个 &lt;code&gt;io.Reader&lt;/code&gt; 中读取数据的 &lt;code&gt;rot13Reader&lt;/code&gt; ， 通过应用 &lt;code&gt;rot13&lt;/code&gt; 代换密码对数据流进行修改。&lt;/li&gt; &lt;li&gt;&lt;code&gt;rot13Reader&lt;/code&gt; 类型已经提供。实现 Read 方法以满足 &lt;code&gt;io.Reader&lt;/code&gt; 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;io&amp;quot;  &amp;quot;os&amp;quot;  &amp;quot;strings&amp;quot; )  type rot13Reader struct {  r io.Reader }  func (ro *rot13Reader)Read(b []byte)(int,error)  {  n,err := ro.r.Read(b)  for i:=0;i &amp;lt; len(b);i++{   if (b[i] &amp;gt;= 'A' &amp;amp;&amp;amp; b[i] &amp;lt;= 'M') || (b[i] &amp;gt;= 'a' &amp;amp;&amp;amp; b[i] &amp;lt;= 'm') {    b[i] += 13   } else if  (b[i] &amp;gt;= 'N' &amp;amp;&amp;amp; b[i] &amp;lt;= 'Z') || (b[i] &amp;gt;= 'n' &amp;amp;&amp;amp; b[i] &amp;lt;= 'z'){    b[i] -= 13   }  }  return n,err }   func main() {  s := strings.NewReader(&amp;quot;Lbh penpxrq gur pbqr!&amp;quot;)  r := rot13Reader{s}  io.Copy(os.Stdout, &amp;amp;r) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;图像&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;image 包定义了 Image 接口：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package image  type Image interface {     ColorModel() color.Model     Bounds() Rectangle     At(x, y int) color.Color } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;注意：&lt;/em&gt; &lt;code&gt;Bounds&lt;/code&gt; 方法的返回值 &lt;code&gt;Rectangle&lt;/code&gt; 实际上是一个 &lt;code&gt;image.Rectangle&lt;/code&gt;， 它在 &lt;code&gt;image&lt;/code&gt; 包中声明。 （请参阅&lt;a href="https://go-zh.org/pkg/image/#Image" target="_blank"&gt;文档&lt;/a&gt;了解全部信息。）&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;color.Color&lt;/code&gt; 和 &lt;code&gt;color.Model&lt;/code&gt; 类型也是接口，但是通常因为直接使用预定义的实现&lt;/li&gt; &lt;li&gt;&lt;code&gt;image.RGBA&lt;/code&gt; 和 &lt;code&gt;image.RGBAModel&lt;/code&gt; 而被忽视了。这些接口和类型由&lt;code&gt;image/color&lt;/code&gt;包定义。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import(  &amp;quot;fmt&amp;quot;  &amp;quot;image&amp;quot; )  func main() {  m := image.NewRGBA(image.Rect(0,0,100,100))  fmt.Println(m.Bounds())  fmt.Println(m.At(0,0).RGBA()) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;练习：图像&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;还记得之前编写的图片生成器吗？我们再来编写另外一个，不过这次它将会返回一个&lt;code&gt;image.Image&lt;/code&gt;的实现而非一个数据切片。&lt;/li&gt; &lt;li&gt;定义你自己的 &lt;code&gt;Image&lt;/code&gt; 类型，实现必要的方法并调用 &lt;code&gt;pic.ShowImage&lt;/code&gt; 。&lt;/li&gt; &lt;li&gt;Bounds 应当返回一个 image.Rectangle ，例如 &lt;code&gt;image.Rect(0, 0, w, h)&lt;/code&gt; 。&lt;/li&gt; &lt;li&gt;ColorModel 应当返回 color.RGBAModel 。&lt;/li&gt; &lt;li&gt;At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 &lt;code&gt;color.RGBA{v, v, 255, 255}&lt;/code&gt; 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;golang.org/x/tour/pic&amp;quot;  &amp;quot;image&amp;quot;  &amp;quot;image/color&amp;quot; )  type Image struct{x, y, w, h int}  func (i Image)ColorModel()color.Model  {  return color.RGBAModel }  func (i Image)Bounds() image.Rectangle {  return image.Rect(i.x,i.y,i.w,i.h) } func (i Image)At(x,y int)color.Color  {  return color.RGBA{uint8(x), uint8(y), uint8(255), uint8(255)} }  func main() {  m := Image{0,0,200,200}  pic.ShowImage(m) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;__注意：__这里各个接口的具体实现直接调用库的实现&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 14 Feb 2018 13:53:08 GMT</pubDate>
    </item>
    <item>
      <title>Go语言复杂类型： struct、slice 和 map篇三</title>
      <link>https://www.zhangaoo.com/article/struct-slice-map</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/01/2ce1julifei0fpc5leiu8tnvr4.jpg" alt="gopher" /&gt;&lt;/p&gt; &lt;p&gt;##复杂类型： struct、slice 和 map 学习如何基于已有类型定义新的类型：本课涵盖了结构体、数组、slice 和 map。&lt;/p&gt; &lt;h3&gt;指针&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Go 具有指针。 指针保存了变量的内存地址。&lt;/li&gt; &lt;li&gt;类型 *T 是指向类型 T 的值的指针。其零值是 nil 。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;    var p *int &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;符号会生成一个指向其作用对象的指针。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;    i := 42     p = &amp;amp;i &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;*&lt;/code&gt; 符号表示指针指向的底层的值。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;fmt.Println(*p) //通过指针 p 读取 i *p = 32 //通过指针 p 设置 i &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;完整代码例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  i , j := 56, 128  p := &amp;amp;i    // point to i  fmt.Println(*p)  // read i through the pointer  *p =21    // set i through the pointer  fmt.Println(i)  // see the new value of i   p = &amp;amp;j    // point to j  *p = *p/24   // divide j through the pointer  fmt.Println(j)  // see the new value of j } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;这也就是通常所说的“间接引用”或“非直接引用”。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;与 &lt;code&gt;C&lt;/code&gt; 不同，&lt;code&gt;Go&lt;/code&gt; 没有指针运算。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;结构体&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;一个结构体（&lt;code&gt;struct&lt;/code&gt;）就是一个字段的集合。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type Coordinate struct {  x int  y int }  func main() {  fmt.Println(Coordinate{1,2}) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;而 &lt;code&gt;type&lt;/code&gt; 的含义跟其字面意思相符。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;结构体字段&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;结构体字段使用点号来访问。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  type CoordinateFiled struct {  X int  Y int }  func main() {  v := CoordinateFiled{1,6}  v.X =4  fmt.Println(v.X) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;结构体指针&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;结构体字段可以通过结构体指针来访问。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;../hello&amp;quot;  )  func main() {  v := hello.Coordinate{2, 6}  p := &amp;amp;v  p.X = 1e9   fmt.Println(v) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;通过指针间接的访问是透明的。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;结构体文法&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。&lt;/li&gt; &lt;li&gt;使用 Name: 语法可以仅列出部分字段。（字段名的顺序无关。）&lt;/li&gt; &lt;li&gt;特殊的前缀 &amp;amp; 返回一个指向结构体的指针。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;../hello&amp;quot;  )  var (  v1 = hello.Vertex{1,2}  v2 = hello.Vertex{X:1}  v3 = hello.Vertex{}  p = &amp;amp;hello.Vertex{1,2} )  func main() {  fmt.Println(v1,p,v2,v3) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;数组&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;类型 &lt;code&gt;[n]T&lt;/code&gt; 是一个有 &lt;code&gt;n&lt;/code&gt; 个类型为 &lt;code&gt;T&lt;/code&gt;的值的数组。&lt;/li&gt; &lt;li&gt;表达式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var a [10]int &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;定义变量 a 是一个有十个整数的数组。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;数组的长度是其类型的一部分，因此数组不能改变大小。 这看起来是一个制约，但是请不要担心； Go 提供了更加便利的方式来使用数组。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  var a[2] string  a[0] = &amp;quot;Hello&amp;quot;  a[1] = &amp;quot;World&amp;quot;   fmt.Println(a[0],a[1])  fmt.Println(a) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;slice&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;一个 slice 会指向一个序列的值，并且包含了长度信息。&lt;/li&gt; &lt;li&gt;[]T 是一个元素类型为 T 的 slice。&lt;/li&gt; &lt;li&gt;len(s) 返回 slice s 的长度。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  s := [] int{2,3,5,7,9,11}  fmt.Println(&amp;quot;s ==&amp;quot;,s)   for i := 0;i&amp;lt;len(s);i++{   fmt.Printf(&amp;quot;s[%d] == %d\n&amp;quot;,i,s[i])  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;slice 的 slice&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;slice 可以包含任意的类型，包括另一个 slice。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;strings&amp;quot; )  func main() {  game := [][]string{   []string {&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;},   []string {&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;},   []string {&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;,&amp;quot;-&amp;quot;},  }  // The players take turns.  game[0][0] = &amp;quot;X&amp;quot;  game[2][2] = &amp;quot;O&amp;quot;  game[2][0] = &amp;quot;X&amp;quot;  game[1][0] = &amp;quot;O&amp;quot;  game[0][2] = &amp;quot;X&amp;quot;   printBoard(game) }  func printBoard(s [][]string)  {  for i := 0;i &amp;lt; len(s);i++{   fmt.Printf(&amp;quot;%s\n&amp;quot;,strings.Join(s[i],&amp;quot; &amp;quot;))  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;对 slice 切片&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;slice 可以重新切片，创建一个新的 slice 值指向相同的数组。(跟python的切片类似) 表达式&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;s[lo:hi] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;表示从 lo 到 hi-1 的 slice 元素，含前端，不包含后端。因此&lt;/p&gt; &lt;pre&gt;&lt;code&gt;s[lo:lo] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;是空的&lt;/p&gt; &lt;pre&gt;&lt;code&gt;s[lo:lo+1] &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;有一个元素&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main()  {  s := []int{3,5,7,9,11,13,15,17}  fmt.Println(&amp;quot;s ==&amp;quot;,s)  fmt.Println(&amp;quot;s[1:4] ==&amp;quot;,s[1:4])   //省略下标代表从0开始  fmt.Println(&amp;quot;s[:4] ==&amp;quot;,s[:4])   //省略上标到len(s)结束  fmt.Println(&amp;quot;s[4:] ==&amp;quot;,s[4:]) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;构造 slice&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;slice 由函数 make 创建。这会分配一个全是零值的数组并且返回一个 slice 指向这个数组：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;a := make([]int, 5)  // len(a)=5 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;为了指定容量，可传递第三个参数到 make：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;b := make([]int, 0, 5) // len(b)=0, cap(b)=5  b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:]      // len(b)=4, cap(b)=4 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;完整代码例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  a := make([]int,5)  printSlice(&amp;quot;a&amp;quot;,a)   b := make([]int,0,5)  printSlice(&amp;quot;b&amp;quot;,b)   c := b[:3]  printSlice(&amp;quot;c&amp;quot;,c)   d := b[3:5]  printSlice(&amp;quot;d&amp;quot;,d) }  func printSlice(s string, x [] int) {  fmt.Printf(&amp;quot;%s len=%d cap=%d %v\n&amp;quot;,s,len(x),cap(x),x) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;nil slice&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;slice&lt;/code&gt; 的零值是 &lt;code&gt;nil&lt;/code&gt; 。&lt;/li&gt; &lt;li&gt;一个 &lt;code&gt;nil&lt;/code&gt; 的 &lt;code&gt;slice&lt;/code&gt; 的长度和容量是 0。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  var s []int   fmt.Println(s,len(s),cap(s))   if s == nil {  fmt.Println(&amp;quot;nil!&amp;quot;)   } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;向 slice 添加元素&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;向 &lt;code&gt;slice&lt;/code&gt; 的末尾添加元素是一种常见的操作，因此 &lt;code&gt;Go&lt;/code&gt; 提供了一个内建函数 &lt;code&gt;append&lt;/code&gt; 。 内建函数的文档对 &lt;a href="https://go-zh.org/pkg/builtin/#append" target="_blank"&gt;append&lt;/a&gt; 有详细介绍。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;func append(s []T, vs ...T) []T &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;append&lt;/code&gt; 的第一个参数 &lt;code&gt;s&lt;/code&gt; 是一个元素类型为 &lt;code&gt;T&lt;/code&gt; 的 &lt;code&gt;slice&lt;/code&gt; ，其余类型为 &lt;code&gt;T&lt;/code&gt; 的值将会附加到该 &lt;code&gt;slice&lt;/code&gt; 的末尾。&lt;/li&gt; &lt;li&gt;&lt;code&gt;append&lt;/code&gt; 的结果是一个包含原 &lt;code&gt;slice&lt;/code&gt; 所有元素加上新添加的元素的 &lt;code&gt;slice&lt;/code&gt;。&lt;/li&gt; &lt;li&gt;如果 &lt;code&gt;s&lt;/code&gt; 的底层数组太小，而不能容纳所有值时，会分配一个更大的数组。 返回的 &lt;code&gt;slice&lt;/code&gt; 会指向这个新分配的数组。 （了解更多关于 slice 的内容，参阅文章Go 切片：&lt;a href="https://blog.go-zh.org/go-slices-usage-and-internals" target="_blank"&gt;用法和本质&lt;/a&gt;。）&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;../hello&amp;quot;  )  func main() {  var a []int  hello.PrintSlice(&amp;quot;a&amp;quot;,a)   // append works on nil slices.  a = append(a,0)  hello.PrintSlice(&amp;quot;a&amp;quot;,a)   // the slice grows as needed.  a = append(a,1)  hello.PrintSlice(&amp;quot;a&amp;quot;,a)   // we can add more than one element at a time.  a = append(a,2,3,4,5,6,7,8,)  hello.PrintSlice(&amp;quot;a&amp;quot;,a) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;range&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;for&lt;/code&gt; 循环的 &lt;code&gt;range&lt;/code&gt;格式可以对 &lt;code&gt;slice&lt;/code&gt; 或者 &lt;code&gt;map&lt;/code&gt; 进行迭代循环。&lt;/li&gt; &lt;li&gt;当使用 for 循环遍历一个 slice 时，每次迭代 range 将返回两个值。 第一个是当前下标（序号），第二个是该下标所对应元素的一个拷贝。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  var pow1 = []int{1,2,4,8,16,32,64,128}  func main()  {  for i,v := range pow1{   fmt.Printf(&amp;quot;2**%d = %d\n&amp;quot;, i, v)  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;range（续）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;可以通过赋值给 _ 来忽略序号和值。&lt;/li&gt; &lt;li&gt;如果只需要索引值，去掉 “ , value ” 的部分即可。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  pow := make([]int,10)  for i := range pow{   pow[i] = 1&amp;lt;&amp;lt;uint(i)  }  for _,value := range pow{   fmt.Printf(&amp;quot;%d\n&amp;quot;, value)  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;练习：slice&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;实现 &lt;code&gt;Pic&lt;/code&gt; 。它返回一个长度为 &lt;code&gt;dy&lt;/code&gt; 的 &lt;code&gt;slice&lt;/code&gt;，其中每个元素是一个长度为 &lt;code&gt;dx&lt;/code&gt; 且元素类型为8位无符号整数的 &lt;code&gt;slice&lt;/code&gt;。当你运行这个程序时， 它会将每个整数作为对应像素的灰度值（好吧，其实是蓝度）并显示这个 &lt;code&gt;slice&lt;/code&gt; 所对应的图像。&lt;/li&gt; &lt;li&gt;计算每个像素的灰度值的方法由你决定；几个有意思的选择包括 (x+y)/2、x*y 和 x^y 。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;golang.org/x/tour/pic&amp;quot; )  func Pic(dx, dy int) [][]uint8 {  var array = [][]uint8{}  for i := 0;i &amp;lt; dy;i++{   array = append(array, make([]uint8,dx))  }  return array }  func main() {  pic.Show(Pic) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt; 1. （需要使用循环来分配 [][]uint8 中的每个 []uint8 。） 2. （使用 uint8(intValue) 来在类型之间进行转换。）&lt;/p&gt; &lt;h3&gt;map&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;map 映射键到值。&lt;/li&gt; &lt;li&gt;map 在使用之前必须用 make 来创建；值为 nil 的 map 是空的，并且不能对其赋值。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;hello-go/hello&amp;quot; ) var m map[string] hello.VertexLL  func main() {  m = make(map[string] hello.VertexLL)  m[&amp;quot;Bell Labs&amp;quot;] = hello.VertexLL{40.68433, -74.39967,}  fmt.Println(m[&amp;quot;Bell Labs&amp;quot;]) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;map 的文法&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;map 的文法跟结构体文法相似，不过必须有键名。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;hello-go/hello&amp;quot; )  var m1 = map[string]hello.VertexLL{  &amp;quot;Bell Labs&amp;quot;:{40.68433, -74.39967},  &amp;quot;Google&amp;quot;:hello.VertexLL{37.42202, -122.08408,}, }  func main() {  fmt.Println(m1) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;map 的文法（续）&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;若顶级类型只是一个类型名，你可以在文法的元素中省略它。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;hello-go/hello&amp;quot; )  var m2 = map[string]hello.VertexLL{  &amp;quot;Bell Labs&amp;quot;:{40.68433, -74.39967},  &amp;quot;Google&amp;quot;:{37.42202, -122.08408,}, }  func main() {  fmt.Println(m2) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;修改 map&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在 map m 中插入或修改一个元素：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;m[key] = elem &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;获得元素：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;elem = m[key] &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;删除元素：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;delete(m, key) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;通过双赋值检测某个键存在：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;elem, ok = m[key] &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;1. 如果 `key` 在 `m` 中， ok 为 true。否则， ok 为 false，并且 elem 是 map 的元素类型的零值。 2. 同样的，当从 map 中读取某个不存在的键时，结果是 map 的元素类型的零值。 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  m := make(map[string]int)  m[&amp;quot;Answer&amp;quot;] = 34  fmt.Println(&amp;quot;The value:&amp;quot;,m[&amp;quot;Answer&amp;quot;])   m[&amp;quot;Answer&amp;quot;] = 48  fmt.Println(&amp;quot;The value:&amp;quot;,m[&amp;quot;Answer&amp;quot;])   delete(m,&amp;quot;Answer&amp;quot;)  fmt.Println(&amp;quot;The value:&amp;quot;,m[&amp;quot;Answer&amp;quot;])   v,ok := m[&amp;quot;Answer&amp;quot;]  fmt.Println(&amp;quot;The value:&amp;quot;, v, &amp;quot;Present?&amp;quot;, ok)  } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;练习：map&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;实现 WordCount。它应当返回一个含有 s 中每个 “词” 个数的 map。函数 wc.Test 针对这个函数执行一个测试用例，并输出成功还是失败。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;提示：你会发现 &lt;a href="https://go-zh.org/pkg/strings/#Fields" target="_blank"&gt;strings.Fields&lt;/a&gt; 很有帮助。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;代码：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;golang.org/x/tour/wc&amp;quot;  &amp;quot;strings&amp;quot; )  func WordCount(s string) map[string]int {  strArray := strings.Fields(s)  count := make(map[string]int)  for i := 0;i &amp;lt; len(strArray); i++{   str := strArray[i]   _,ok := count[str]   if !ok{    count[str] = 1   } else {    count[str] += 1   }  }  return count }  func main() {  wc.Test(WordCount) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;函数值&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;函数也是值。他们可以像其他值一样传递，比如，函数值可以作为函数的参数或者返回值。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  func compute(fn func(float64,float64)float64)float64  {  return fn(3,4) }  func main() {  hypot := func(x,y float64)float64{   return math.Sqrt(x*x + y*y)  }  fmt.Println(hypot(6,8))  fmt.Println(compute(hypot))  fmt.Println(compute(math.Pow)) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;10 5 81 &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;函数的闭包&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Go 函数可以是一个闭包。闭包是一个函数值，它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值；换句话说这个函数被“绑定”在这个变量上。&lt;/li&gt; &lt;li&gt;例如，函数 adder 返回一个闭包。每个返回的闭包都被绑定到其各自的 sum 变量上。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  func adder()func(int)int {  sum := 0  return func(x int)int {   sum += x   return sum  } }  func main() {  pos,neg := adder(),adder()  for i:=0;i &amp;lt; 10;i++{   fmt.Println(    pos(i),    neg(-2 * i),   )  } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;输出结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;0 0 1 -2 3 -6 6 -12 10 -20 15 -30 21 -42 28 -56 36 -72 45 -90 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简单分析： 1. &lt;code&gt;adder()&lt;/code&gt;返回的是函数 2. 循环中多次调用&lt;code&gt;pos&lt;/code&gt;，可见&lt;code&gt;pos&lt;/code&gt;的sum相对&lt;code&gt;pos&lt;/code&gt;是全局的&lt;/p&gt; &lt;h3&gt;练习：斐波纳契闭包&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;现在来通过函数做些有趣的事情。&lt;/li&gt; &lt;li&gt;实现一个 fibonacci 函数，返回一个函数（一个闭包）可以返回连续的斐波纳契数。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;F(0)=0，F(1)=1, F(n)=F(n-1)+F(n-2)（n&amp;gt;=2，n∈N*） 1、1、2、3、5、8、13、21、34 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import &amp;quot;fmt&amp;quot;  // fibonacci 函数会返回一个返回 int 的函数。 func fibonacci() func() int {  i ,j := 0,1  return func() int {   tmp := i   i , j = j ,i + j   return tmp  } }  func main() {  f := fibonacci()  for i := 0; i &amp;lt; 10; i++ {   fmt.Println(f())  } } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Mon, 05 Feb 2018 15:35:24 GMT</pubDate>
    </item>
    <item>
      <title>Go语言流程控制篇二</title>
      <link>https://www.zhangaoo.com/article/go-condition-control</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/01/2ce1julifei0fpc5leiu8tnvr4.jpg" alt="gopher" /&gt;&lt;/p&gt; &lt;h2&gt;流程控制&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;主要包括以下语句 &lt;ol&gt; &lt;li&gt;for&lt;/li&gt; &lt;li&gt;if&lt;/li&gt; &lt;li&gt;else&lt;/li&gt; &lt;li&gt;switch&lt;/li&gt; &lt;li&gt;defer&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;for&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Go 只有一种循环结构—— for 循环。&lt;/li&gt; &lt;li&gt;基本的 for 循环包含三个由分号分开的组成部分： &lt;ol&gt; &lt;li&gt;初始化语句：在第一次循环执行前被执行&lt;/li&gt; &lt;li&gt;循环条件表达式：每轮迭代开始前被求值&lt;/li&gt; &lt;li&gt;后置语句：每轮迭代后被执行&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;例如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  sum := 0  for i := 0;i &amp;lt; 50;i++{   sum += i  }  fmt.Println(sum) }  &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;初始化语句一般是一个短变量声明，这里声明的变量仅在整个 for 循环语句可见。&lt;/li&gt; &lt;li&gt;如果条件表达式的值变为 false，那么迭代将终止。 &lt;strong&gt;注意：不像 C，Java，或者 Javascript 等其他语言，for 语句的三个组成部分并不需要用括号括起来，但循环体必须用 { } 括起来。&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;for(续)&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;循环初始化语句和后置语句都是可选的。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  sum := 1  for ;sum&amp;lt;1000;{   sum += sum  }  fmt.Println(sum) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;for 是 Go 的 “while”&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;基于此可以省略分号：C 的 while 在 Go 中叫做 for 。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  sum := 3   for sum &amp;lt; 1000{   sum += sum  }  fmt.Println(sum) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;死循环&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;如果省略了循环条件，循环就不会结束，因此可以用更简洁地形式表达死循环。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  func main() {  for{     } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;if&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;就像 for 循环一样，Go 的 if 语句也不要求用 ( ) 将条件括起来，同时， { } 还是必须有的。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot;  )  func sqrt(x float64) string{  if x &amp;lt; 0 {   return sqrt(-x) + &amp;quot;i&amp;quot;  }  return fmt.Sprint(math.Sqrt(x)) }  func main(){  fmt.Println(sqrt(2),sqrt(-4)) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;if 的便捷语句&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;跟 for 一样， if 语句可以在条件之前执行一个简单语句。&lt;/li&gt; &lt;li&gt;由这个语句定义的变量的作用域仅在 if 范围之内。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot;  )  func pow(x,n,limit float64) float64{  if v := math.Pow(x,n); v &amp;lt; limit{   return v  }  return limit }  func main() {  fmt.Println(   pow(3,2,10),   pow(3,4,10),  ) }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：这里的v作用范围只在&lt;code&gt;if&lt;/code&gt;范围之内&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;if 和 else&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在 &lt;code&gt;if&lt;/code&gt; 的便捷语句定义的变量同样可以在任何对应的  &lt;code&gt;else&lt;/code&gt; 块中使用。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math&amp;quot; )  func pow(x, n, limit float64)float64  {  if v := math.Pow(x,n); v &amp;lt; limit{   return v  } else{   fmt.Printf(&amp;quot;%g &amp;gt;= %g\n&amp;quot;,v,limit)  }  return limit }  func main() {  fmt.Println(   pow(3, 2, 10),   pow(3, 3, 20),  ) }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行结果：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;27 &amp;gt;= 20 9 20 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：（提示：两个 pow 调用都在 main 调用 fmt.Println 前执行完毕了。）&lt;/strong&gt;&lt;/p&gt; &lt;h4&gt;练习：循环和函数&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;牛顿法是通过选择一个初始点 z 然后重复这一过程求 Sqrt(x) 的近似值：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func Sqrt(x float64)float64{  z := float64(1)  for i := 0; i &amp;lt; 10;i++{   z = z - (z*z-x)/2*z  }  return z }  func main() {  fmt.Println(   Sqrt(2),  ) }  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;switch&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;除非以 fallthrough 语句结束，否则分支会自动终止。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;runtime&amp;quot; )  func main() {  fmt.Println(&amp;quot;Go runs on &amp;quot;)  switch os := runtime.GOOS; os{  case &amp;quot;darwin&amp;quot;:   fmt.Println(&amp;quot;OS X.&amp;quot;)  case &amp;quot;linux&amp;quot;:   fmt.Println(&amp;quot;Linux.&amp;quot;)  default:   fmt.Printf(&amp;quot;%s\n&amp;quot;,os)    } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;switch 的执行顺序&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;switch 的条件从上到下的执行，当匹配成功的时候停止。 例如&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;switch i { case 0: case f(): } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当 &lt;code&gt;i==0&lt;/code&gt; 时不会调用 &lt;code&gt;f&lt;/code&gt; 。&lt;/p&gt; &lt;pre&gt;&lt;code&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;runtime&amp;quot; )  func main() {  fmt.Println(&amp;quot;Go runs on &amp;quot;)  switch os := runtime.GOOS; os{  case &amp;quot;darwin&amp;quot;:   fmt.Println(&amp;quot;OS X.&amp;quot;)  case &amp;quot;linux&amp;quot;:   fmt.Println(&amp;quot;Linux.&amp;quot;)  default:   fmt.Printf(&amp;quot;%s\n&amp;quot;,os)     } }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：Go playground 中的时间总是从 2009-11-10 23:00:00 UTC 开始， 如何校验这个值作为一个练习留给读者完成。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;没有条件的 switch&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;没有条件的 &lt;code&gt;switch&lt;/code&gt; 同 &lt;code&gt;switch true&lt;/code&gt; 一样。&lt;/li&gt; &lt;li&gt;这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;time&amp;quot; )  func main() {  today := time.Now()  switch {  case today.Hour() &amp;lt; 12:   fmt.Println(&amp;quot;Good morning.&amp;quot;)  case today.Hour() &amp;lt; 17:   fmt.Println(&amp;quot;Good aftertoon.&amp;quot;)  default:   fmt.Println(&amp;quot;Good evening.&amp;quot;)  } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;defer&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;defer 语句会延迟函数的执行直到上层函数返回。&lt;/li&gt; &lt;li&gt;延迟调用的参数会立刻生成，但是在上层函数返回前函数都不会被调用。&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main            import &amp;quot;fmt&amp;quot;            func main() {       defer fmt.Println(&amp;quot;world&amp;quot;)       fmt.Println(&amp;quot;Hello&amp;quot;)      }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;__注意：简单的理解第二点，就上面的例子，也就是知道&lt;code&gt;main()&lt;/code&gt;返回前都不会调用&lt;code&gt;defer fmt.Println(&amp;quot;world&amp;quot;)&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;defer 栈&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;延迟的函数调用被压入一个栈中。当函数返回时， 会按照后进先出的顺序调用被延迟的函数调用。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  fmt.Println(&amp;quot;counting&amp;quot;)  for i:=0;i &amp;lt; 10;i++{   defer fmt.Println(i)  }  fmt.Println(&amp;quot;done&amp;quot;) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;counting done 9 8 7 6 5 4 3 2 1 0 &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Fri, 02 Feb 2018 15:26:32 GMT</pubDate>
    </item>
    <item>
      <title>Go语言基本常识篇一</title>
      <link>https://www.zhangaoo.com/article/go-basic</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/01/2ce1julifei0fpc5leiu8tnvr4.jpg" alt="Go" /&gt;&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;gopher [ˈgoʊfər] 是一种挖洞啮齿的动物，产于加拿大南部至巴拿马（Panama）地区。&lt;/p&gt; &lt;/blockquote&gt; &lt;h2&gt;Go基本知识&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;包&lt;/li&gt; &lt;li&gt;变量&lt;/li&gt; &lt;li&gt;函数&lt;/li&gt; &lt;li&gt;基本类型&lt;/li&gt; &lt;li&gt;类型转换&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;包&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;每个 Go 程序都是由包组成的。&lt;/li&gt; &lt;li&gt;程序运行的入口是包 main 。 例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math/rand&amp;quot; )  func main()  {  rand.Seed(2)  fmt.Println(&amp;quot;My favorite number is &amp;quot;,rand.Intn(10)) } &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;这个程序使用并导入了包 &amp;quot;fmt&amp;quot; 和 &amp;quot;math/rand&amp;quot; 。&lt;/li&gt; &lt;li&gt;按照惯例，包名与导入路径的最后一个目录一致。例如，&amp;quot;math/rand&amp;quot; 包由 package rand 语句开始。&lt;/li&gt; &lt;li&gt;&lt;code&gt;main&lt;/code&gt;函数必须在 &lt;code&gt;main package&lt;/code&gt;中才能运行，&lt;code&gt;main package&lt;/code&gt;定义的结构体等不能在&lt;code&gt;main package&lt;/code&gt;中共通使用 &lt;strong&gt;注意：这个程序的运行环境是确定性的，因此 rand.Intn 每次都会返回相同的数字。 （为了得到不同的随机数，需要提供一个随机数种子，参阅 rand.Seed。）, 也就是随机的种子才能得到随机数&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;包导入&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;上面代码用圆括号组合了导入，这是“打包”导入语句。&lt;/li&gt; &lt;li&gt;同样可以编写多个导入语句，例如：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;import &amp;quot;fmt&amp;quot; import &amp;quot;math/rand&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;不过使用打包的导入语句是更好的形式。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;导出名&lt;/h3&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;在 &lt;code&gt;Go&lt;/code&gt; 中，首字母大写的名称是被导出的，同样结构体的字段首字母大写对外才是可见的，否则外部不能引用（自己理解这里的导出的意思就是对外部包开放课访问的意思）。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;在导入包之后，你只能访问包所导出的名字，任何未导出的名字是不能被包外的代码访问的。 例如：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。&lt;/li&gt; &lt;li&gt;执行代码，注意编译器报的错误。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main      import (         &amp;quot;fmt&amp;quot;         &amp;quot;math&amp;quot;     )      func main() {         //fmt.Println(math.pi)         fmt.Println(math.Pi)     } &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;函数&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;函数可以没有参数或接受多个参数。&lt;/li&gt; &lt;li&gt;在这个例子中， add 接受两个 int 类型的参数。 &lt;strong&gt;注意类型在变量名 之后 。&lt;/strong&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import &amp;quot;fmt&amp;quot;          func add(x int, y int) int {         return x + y     }          func main() {         fmt.Println(add(42, 13))     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;函数（续）&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;当两个或多个连续的函数命名参数是同一类型，则除了最后一个类型之外，其他都可以省略。在这个例子中 ，&lt;code&gt;x int, y int&lt;/code&gt;被缩写为&lt;code&gt;x, y int&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;多值返回&lt;/h3&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;函数可以返回任意数量的返回值。&lt;br /&gt; 例如：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main      import &amp;quot;fmt&amp;quot;      func swap(x,y string)(string,string)  {      return y,x     }      func main()  {      a,b := swap(&amp;quot;1&amp;quot;,&amp;quot;2&amp;quot;)      fmt.Println(a,b)     } &lt;/code&gt;&lt;/pre&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;命名返回值&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;Go 的返回值可以被命名，并且就像在函数体开头声明的变量那样使用。&lt;/li&gt; &lt;li&gt;返回值的名称应当具有一定的意义，可以作为文档使用。&lt;/li&gt; &lt;li&gt;没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。 &lt;strong&gt;直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。&lt;/strong&gt;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import &amp;quot;fmt&amp;quot;          func split(sum int)(x,y int)  {         x = sum * 4 / 9         y = sum - x         return     }          func main(){         fmt.Println(split(80))     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上返回值像方法变量一样定义&lt;code&gt;(x,y int)&lt;/code&gt;，&lt;code&gt;return&lt;/code&gt;后面没有跟任何返回值，此时默认反回定义的返回值&lt;code&gt;x,y&lt;/code&gt;&lt;/p&gt; &lt;h3&gt;变量&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;var 语句定义了一个变量的列表；跟函数的参数列表一样，类型在后面。&lt;/li&gt; &lt;li&gt;就像在这个例子中看到的一样， var 语句可以定义在包或函数级别。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import &amp;quot;fmt&amp;quot;          var c,python,java bool          func main(){         var i int         fmt.Println(i,c,python,java)     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;初始化变量&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;变量定义可以包含初始值，每个变量对应一个。&lt;/li&gt; &lt;li&gt;如果初始化是使用表达式，则可以省略类型；变量从初始值中获得类型。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import &amp;quot;fmt&amp;quot;          var i , j int = 2,3          func main()  {         var c,python,java = true,false,&amp;quot;yes!&amp;quot;         fmt.Println(i,j,c,python,java)     } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;短声明变量&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在函数中， &lt;code&gt;:=&lt;/code&gt; 简洁赋值语句在明确类型的地方，可以用于替代 &lt;code&gt;var&lt;/code&gt; 定义。&lt;/li&gt; &lt;li&gt;函数外的每个语句都必须以关键字开始（ &lt;code&gt;var&lt;/code&gt; 、 &lt;code&gt;func&lt;/code&gt; 、等等）， &lt;code&gt;:=&lt;/code&gt; 结构不能使用在函数外。 &lt;strong&gt;注意：&lt;code&gt;:=&lt;/code&gt;只能在函数中使用，不能再函数外使用，函数外必须使用&lt;code&gt;var&lt;/code&gt; 、 &lt;code&gt;func&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main()  {  var i,j int=1,3  k := 4  c,python,java := true,false,&amp;quot;oops!&amp;quot;  fmt.Println(i,j,k,c,python,java)  } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;基本类型&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Go的基本类型有&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;bool  string  int  int8  int16   int32  int64 uint uint8 unint16 uint32 uint64  byte //uint8的别称  rune //int32的别称，代表一个Unicode码  float32 float64  complex64  complex128 &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;这个例子演示了具有不同类型的变量。 同时与导入语句一样，变量的定义“打包”在一个语法块中。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import (  &amp;quot;fmt&amp;quot;  &amp;quot;math/cmplx&amp;quot;  )  var (  ToBe bool = false  MaxInt uint64 = 1&amp;lt;&amp;lt;64 - 1  z complex128 = cmplx.Sqrt(-5 + 12i) )  func main() {  const f = &amp;quot;%T(%v)\n&amp;quot;  fmt.Printf(f,ToBe,ToBe)  fmt.Printf(f,MaxInt,MaxInt)  fmt.Printf(f,z,z) } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;1. 注意：这里使用的是&lt;code&gt;Printf&lt;/code&gt;而不是&lt;code&gt;Println&lt;/code&gt; &lt;code&gt;%T&lt;/code&gt;打印变量类型&lt;code&gt;%v&lt;/code&gt;打印变量值，&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;2. int，uint 和 uintptr 类型在32位的系统上一般是32位，而在64位系统上是64位。 当你需要使用一个整数类型时，你应该首选 int，仅当有特别的理由才使用定长整数类型或者无符号整数类型。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;零值&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;变量在定义时没有明确的初始化时会赋值为 零值 。 零值是： &lt;ul&gt; &lt;li&gt;数值类型为 &lt;code&gt;0&lt;/code&gt; ，&lt;/li&gt; &lt;li&gt;布尔类型为 &lt;code&gt;false&lt;/code&gt; ，&lt;/li&gt; &lt;li&gt;字符串为 &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; （空字符串）。&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  func main() {  var i int  var f float64  var b bool  var s string   fmt.Printf(&amp;quot;%v %v %v %q\n&amp;quot;,i,f,b,s)  } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;类型转换&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;表达式 T(v) 将值 v 转换为类型 T 。 一些关于数值的转换：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var i int = 42 var f float64 = float64(i) var u uint = uint(f) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;或者更加简单的形式&lt;/p&gt; &lt;pre&gt;&lt;code&gt;i := 42 f := float64(i) u := uint(f) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;例子：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import (         &amp;quot;fmt&amp;quot;         &amp;quot;math&amp;quot;         )          func main() {         var x,y int= 3,4         var f float64 = math.Sqrt(float64(x*x + y*y))         var u uint = uint(f)         fmt.Println(x,y,u)     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意：与 &lt;code&gt;C&lt;/code&gt; 不同的是 &lt;code&gt;Go&lt;/code&gt; 的在不同类型之间的项目赋值时需要显式转换。 试着移除例子中 &lt;code&gt;float64&lt;/code&gt; 或 &lt;code&gt;int&lt;/code&gt; 的转换看看会发生什么。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;类型推导&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;在定义一个变量却并不显式指定其类型时（使用 &lt;code&gt;:=&lt;/code&gt; 语法或者 &lt;code&gt;var&lt;/code&gt; &lt;code&gt;=&lt;/code&gt; 表达式语法）， 变量的类型由（等号）右侧的值推导得出。&lt;/li&gt; &lt;li&gt;当右值定义了类型时，新变量的类型与其相同：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;var i int j := i //j也是一个int &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;但是当右边包含了未指名类型的数字常量时，新的变量就可能是 int 、 float64 或 complex128 。 这取决于常量的精度：&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;i := 42           // int f := 3.142        // float64 g := 0.867 + 0.5i // complex128 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;例子如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-go"&gt;    package main          import &amp;quot;fmt&amp;quot;          func main() {      j := 42.0 //change me      fmt.Printf(&amp;quot;%T&amp;quot;,j)     } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当改变数值具体的精度时，类型也会随着变&lt;/p&gt; &lt;pre&gt;&lt;code&gt;j := 42 j := 42.0 j := 0.867 + 0.5i &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;常量&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;常量的定义与变量类似，只不过使用 const 关键字。&lt;/li&gt; &lt;li&gt;常量可以是字符、字符串、布尔或数字类型的值。&lt;/li&gt; &lt;li&gt;常量不能使用 := 语法定义。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  const PI  = 3.14  func main() {  const World = &amp;quot;世界&amp;quot;  fmt.Println(&amp;quot;Hello&amp;quot;,World)  fmt.Println(&amp;quot;Happy&amp;quot;,PI,&amp;quot;Day&amp;quot;)   const truth = true  fmt.Println(&amp;quot;Go rules?&amp;quot;,truth) } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;数值常量&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;数值常量是高精度的 值 。&lt;/li&gt; &lt;li&gt;一个未指定类型的常量由上下文来决定其类型。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-go"&gt;package main  import &amp;quot;fmt&amp;quot;  const (  Big = 1 &amp;lt;&amp;lt; 100  Small = Big &amp;gt;&amp;gt; 99 )  func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 {  return x * 0.1 }  func main() {  fmt.Println(needInt(Small))  fmt.Println(needFloat(Small))  fmt.Println(needFloat(Big)) } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Wed, 31 Jan 2018 14:39:13 GMT</pubDate>
    </item>
    <item>
      <title>Java缓存之瑞士军刀Guava Cahce初探</title>
      <link>https://www.zhangaoo.com/article/guava-cache</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2018/01/5895bsblumh1qqqpjda70b52bi.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;guava ['gwɑ:və]n. 番石榴，番石榴其果实&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;&lt;strong&gt;上图就是番石榴的样子，真是涨知识了。言归正传，下面我们来学习一下Google的Guava Cache&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;适用场景&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;重复计算或检索相对稳定的数据&lt;/li&gt; &lt;li&gt;愿意消耗一些内存空间来提升速度。&lt;/li&gt; &lt;li&gt;你预料到某些键会被查询一次以上。&lt;/li&gt; &lt;li&gt;缓存中存放的数据总量不会超出内存容量。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;GuavaCache与ConcurrentMap的区别&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;ConcurrentMap不会自动移除元素,也就是元素的有效性由使用者来维护,失效了需要式地移除。&lt;/li&gt; &lt;li&gt;Guava Cache为了限制内存占用，通常都设定为自动回收元素。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;CacheLoader介绍&lt;/h3&gt; &lt;p&gt;LoadingCache是附带CacheLoader构建而成的缓存实现。从字面意思理解就是缓存加载器,继承虚类CacheLoader需要重载方法 &lt;code&gt;public abstract V load(K key) throws Exception;&lt;/code&gt;该方法实现value的计算逻辑,当获取不到key对应的value值时 GuavaCache会计算一次值并把值缓存到内存中,再次获取如果缓存中存在对应的值就直接获取不在调用费时的计算逻辑,例如:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;new CacheLoader&amp;lt;Integer, String&amp;gt;() {     @Override     public String load(Integer integer) throws Exception {         return productValue(integer); }  final static String productValue(Integer integer) throws InterruptedException {     String value = &amp;quot;&amp;quot;;     if (integer != null) {         value += String.valueOf(integer) + System.currentTimeMillis();     }     Thread.sleep(1000);     return value; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;productValue&lt;/code&gt;是一个耗时操作,以一个整数为&lt;code&gt;key&lt;/code&gt;,以&lt;code&gt;key+System.currentTimeMillis()&lt;/code&gt;字符串为&lt;code&gt;value&lt;/code&gt;,这里为了模拟费时操作 使用了&lt;code&gt;Thread.sleep(1000);&lt;/code&gt;,第一次获取数据会比较慢,以后只要缓存有效获取效率能大幅提高。 &lt;em&gt;注意:自己定义的CacheLoader如果可能抛出异常使用&lt;code&gt;get&lt;/code&gt;,如果会抛出异常则使用&lt;code&gt;getUnchecked&lt;/code&gt;&lt;/em&gt;&lt;/p&gt; &lt;h3&gt;Callable介绍&lt;/h3&gt; &lt;p&gt;所有类型的Guava Cache，不管有没有自动加载功能，都支持get(K, Callable)方法。这个方法返回缓存中相应的值，或者用给定的Callable 运算并把结果加入到缓存中。这个方法简便地实现了模式”如果有缓存则返回；否则运算、缓存、然后返回”。例如:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;Cache&amp;lt;Integer, String&amp;gt; myCache = CacheBuilder.newBuilder()         .maximumSize(1000)         .build(); try {     for (int i = 0; i &amp;lt; 10; i++) {         final Integer a = i;         System.out.println(myCache.get(i % 5, new Callable&amp;lt;String&amp;gt;() {             @Override             public String call() throws Exception {                 return productValue(a);             }         }));     } } catch (Exception e) {     e.printStackTrace(); } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;显式插入&lt;/h3&gt; &lt;p&gt;使用&lt;code&gt;cache.put(key, value)&lt;/code&gt;方法可以直接向缓存中插入值，这会直接覆盖掉给定键之前映射的值。使用&lt;code&gt;Cache.asMap()&lt;/code&gt;视图提供的任何方法也能修改缓 存。但请注意，asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说，asMap视图的原子运算在Guava Cache的原子加载范畴之外， 所以相比于&lt;code&gt;Cache.asMap().putIfAbsent(K,V)&lt;/code&gt;，&lt;code&gt;Cache.get(K, Callable&amp;lt;V&amp;gt;)&lt;/code&gt; 应该总是优先&lt;/p&gt; &lt;h3&gt;缓存回收&lt;/h3&gt; &lt;p&gt;GuavaCache提供了三种基本的缓存回收方式：基于容量回收、定时回收和基于引用回收。&lt;/p&gt; &lt;h4&gt;基于容量的回收（size-based eviction）&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;只需使用&lt;code&gt;CacheBuilder.maximumSize(long)&lt;/code&gt;缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告：在缓存项的数目达到限定值之前， 缓存就可能进行回收操作——通常来说，这种情况发生在缓存项的数目逼近限定值时。&lt;/li&gt; &lt;li&gt;另外，不同的缓存项有不同的“权重”（weights）可以使用&lt;code&gt;CacheBuilder.weigher(Weigher)&lt;/code&gt;指定一个权重函数，并且用&lt;code&gt;CacheBuilder.maximumWeight(long)&lt;/code&gt; 指定最大总重。除了要注意回收也是在权重逼近限定值时就进行了，还要知道权重是在缓存创建时计算的，因此要考虑重量计算的复杂度。注意:权重大小并不决定 缓存对应是否会被优先回收,当缓存中的权重值逼近或达到指定的最大值时,缓存将尝试回收最近没有使用或总体上很少使用的缓存项。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;public class MyGuavaSimpleCache {     public static Cache&amp;lt;Integer, String&amp;gt; MY_CACHE = CacheBuilder.newBuilder()             .maximumSize(1000)             .maximumWeight(10000)             .weigher(new Weigher&amp;lt;Integer, String&amp;gt;() {                 @Override                 public int weigh(Integer key, String value) {                     return key;                 }             })             .build(); }  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;注意:当有多个缓存回收条件时,可能会先触发其中一个或单个&lt;/em&gt;&lt;/p&gt; &lt;h4&gt;定时回收（Timed Eviction）&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;expireAfterAccess(long, TimeUnit)&lt;/code&gt;当缓存项在指定的时间段内没有被读或写就会被回收。&lt;/li&gt; &lt;li&gt;&lt;code&gt;expireAfterWrite(long, TimeUnit)&lt;/code&gt;当缓存项在指定的时间段内没有更新就会被回收。&lt;/li&gt; &lt;li&gt;&lt;code&gt;refreshAfterWrite(long, TimeUnit)&lt;/code&gt;当缓存项上一次更新操作之后的多久会被刷新。&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;&lt;code&gt;expireAfterWrite&lt;/code&gt;和&lt;code&gt;refreshAfterWrite&lt;/code&gt;异同&lt;/h4&gt; &lt;p&gt;&lt;code&gt;refreshAfterWrite&lt;/code&gt;的特点是，在&lt;code&gt;refresh&lt;/code&gt;的过程中，严格限制只有1个重新加载操作，而其他查询先返回旧值， 这样有效地可以减少等待和锁争用，所以&lt;code&gt;refreshAfterWrite&lt;/code&gt;会比&lt;code&gt;expireAfterWrite&lt;/code&gt;性能好。但是它也有一个缺点， 因为到达指定时间后，它不能严格保证所有的查询都获取到新值。了解过guava cache的定时失效（或刷新）原来的同学都知道， guava cache并没使用额外的线程去做定时清理和加载的功能，而是依赖于查询请求。在查询的时候去比对上次更新的时间， 如超过指定时间则进行加载或刷新。所以，如果使用&lt;code&gt;refreshAfterWrite&lt;/code&gt;，在吞吐量很低的情况下，如很长一段时间内没有查询之后， 发生的查询有可能会得到一个旧值（这个旧值可能来自于很长时间之前），这将会引发问题。&lt;/p&gt; &lt;h4&gt;基于引用的回收（Reference-based Eviction）&lt;/h4&gt; &lt;p&gt;通过使用弱引用的键、或弱引用的值、或软引用的值，Guava Cache可以把缓存设置为允许垃圾回收&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;CacheBuilder.weakKeys()&lt;/code&gt;使用弱引用存储键。当键没有其它（强或软）引用时，缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式（==），使用弱引 用键的缓存用==而不是equals比较键。&lt;/li&gt; &lt;li&gt;&lt;code&gt;CacheBuilder.weakValues()&lt;/code&gt;使用弱引用存储值。当值没有其它（强或软）引用时，缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式（==），使用弱 引用值的缓存用==而不是equals比较值。&lt;/li&gt; &lt;li&gt;&lt;code&gt;CacheBuilder.softValues()&lt;/code&gt;使用软引用存储值。软引用只有在响应内存需要时，才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响， 我们通常建议使用更有性能预测性的缓存大小限定（见上文，基于容量回收）。使用软引用值的缓存同样用==而不是equals比较值&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;显式清除&lt;/h4&gt; &lt;p&gt;任何时候，你都可以显式地清除缓存项，而不是等到它被回收&lt;/p&gt; &lt;ul&gt; &lt;li&gt;个别清除：&lt;code&gt;Cache.invalidate(key)&lt;/code&gt;&lt;/li&gt; &lt;li&gt;批量清除：&lt;code&gt;Cache.invalidateAll(keys)&lt;/code&gt;&lt;/li&gt; &lt;li&gt;清除所有缓存项：&lt;code&gt;Cache.invalidateAll()&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;移除监听器&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;通过&lt;code&gt;CacheBuilder.removalListener(RemovalListener)&lt;/code&gt;，你可以声明一个监听器，以便缓存项被移除时做一些额外操作。缓存项被移除时，&lt;code&gt;RemovalListener&lt;/code&gt; 会获取移除通知[RemovalNotification]，其中包含移除原因[RemovalCause]、键和值。&lt;/li&gt; &lt;li&gt;请注意，&lt;code&gt;RemovalListener&lt;/code&gt;抛出的任何异常都会在记录到日志后被丢弃[swallowed]。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;public class MyCacheListener implements RemovalListener&amp;lt;Integer, String&amp;gt; {     @Override     public void onRemoval(RemovalNotification&amp;lt;Integer, String&amp;gt; notification) {         System.out.println(&amp;quot;key:&amp;quot; + notification.getKey());         System.out.println(&amp;quot;value:&amp;quot; + notification.getValue());     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;警告：默认情况下，监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的，代价高昂的监听器方法在同步模式下会拖慢正常的 缓存请求。在这种情况下，你可以使用&lt;code&gt;RemovalListeners.asynchronous(RemovalListener, Executor)&lt;/code&gt;把监听器装饰为异步操作。&lt;/em&gt;&lt;/p&gt; &lt;h3&gt;清理什么时候发生？&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;使用&lt;code&gt;CacheBuilder&lt;/code&gt;构建的缓存不会”自动”执行清理和回收工作，也不会在某个缓存项过期后马上清理，也没有诸如此类的清理机制。相反，它会在写操作时 顺带做少量的维护工作，或者偶尔在读操作时做——如果写操作实在太少的话。&lt;/li&gt; &lt;li&gt;这样做的原因在于：如果要自动地持续清理缓存，就必须有一个线程，这个线程会和用户操作竞争共享锁。此外，某些环境下线程创建可能受限制，这样&lt;code&gt;CacheBuilder&lt;/code&gt;就不可用了。&lt;/li&gt; &lt;li&gt;相反，我们把选择权交到你手里。如果你的缓存是高吞吐的，那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作，而你又不想清理工 作阻碍了读操作，那么可以创建自己的维护线程，以固定的时间间隔调用&lt;code&gt;Cache.cleanUp()&lt;/code&gt;。&lt;code&gt;ScheduledExecutorService&lt;/code&gt;可以帮助你很好地实现这样的定时调度。&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;刷新&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;刷新和回收不太一样。正如&lt;code&gt;LoadingCache.refresh(K)&lt;/code&gt;所声明，刷新表示为键加载新值，这个过程可以是异步的。在刷新操作进行时，缓存仍然可以向其 他线程返回旧值，而不像回收操作，读缓存的线程必须等待新值加载完成。&lt;/li&gt; &lt;li&gt;如果刷新过程抛出异常，缓存将保留旧值，而异常会在记录到日志后被丢弃[swallowed]。&lt;/li&gt; &lt;li&gt;重载&lt;code&gt;CacheLoader.reload(K, V)&lt;/code&gt;可以扩展刷新时的行为，这个方法允许开发者在计算新值时使用旧的值。&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code&gt;//有些键不需要刷新，并且我们希望刷新是异步完成的 LoadingCache&amp;lt;Key, Graph&amp;gt; graphs = CacheBuilder.newBuilder()         .maximumSize(1000)         .refreshAfterWrite(1, TimeUnit.MINUTES)         .build(             new CacheLoader&amp;lt;Key, Graph&amp;gt;() {                 public Graph load(Key key) { // no checked exception                     return getGraphFromDatabase(key);                 }          public ListenableFuture&amp;lt;Key, Graph&amp;gt; reload(final Key key, Graph prevGraph) {                     if (neverNeedsRefresh(key)) {                         return Futures.immediateFuture(prevGraph);                     }else{                         // asynchronous!                         ListenableFutureTask&amp;lt;Key, Graph&amp;gt; task=ListenableFutureTask.create(new Callable&amp;lt;Key, Graph&amp;gt;() {                         public Graph call() {                                 return getGraphFromDatabase(key);                             }                         });                         executor.execute(task);                        return task;                     }                 }         }); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意:&lt;code&gt;CacheBuilder.refreshAfterWrite(long, TimeUnit)&lt;/code&gt;可以为缓存增加自动定时刷新功能。和&lt;code&gt;expireAfterWrite&lt;/code&gt;相反，&lt;code&gt;refreshAfterWrite&lt;/code&gt;通过定 时刷新可以让缓存项保持可用，但请注意：缓存项只有在被检索时才会真正刷新（如果&lt;code&gt;CacheLoader.refresh&lt;/code&gt;实现为异步，那么检索不会被刷新拖慢）。 因此，如果你在缓存上同时声明&lt;code&gt;expireAfterWrite和refreshAfterWrite&lt;/code&gt;，缓存并不会因为刷新盲目地定时重置，如果缓存项没有被检索，那刷新就不会 真的发生，缓存项在过期时间后也变得可以回收。&lt;/p&gt; &lt;h3&gt;统计&lt;/h3&gt; &lt;p&gt;CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后，Cache.stats()方法会返回CacheStats对象以提供如下统计信息&lt;/p&gt; &lt;ul&gt; &lt;li&gt;hitRate()：缓存命中率；&lt;/li&gt; &lt;li&gt;averageLoadPenalty()：加载新值的平均时间，单位为纳秒；&lt;/li&gt; &lt;li&gt;evictionCount()：缓存项被回收的总数，不包括显式清除。 此外，还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的，在性能要求高的应用中我们建议密切关注这些数据。&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Wed, 17 Jan 2018 13:44:37 GMT</pubDate>
    </item>
    <item>
      <title>ArrayList之诡异的remove操作</title>
      <link>https://www.zhangaoo.com/article/arrayListRemove</link>
      <content:encoded>&lt;p&gt;这几天没事的时候把阿里的Java开发手册大概看了一遍，其中有一条如下：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式，如果并发操作，需要对Iterator对象加锁。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;&lt;em&gt;正例：&lt;/em&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;Iterator&amp;lt;String&amp;gt; iterator = list.iterator(); while (iterator.hasNext()) {     String item = iterator.next();     if (删除元素条件) {         iterator.remove();     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;em&gt;反例：&lt;/em&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;(); list.add(&amp;quot;1&amp;quot;); list.add(&amp;quot;2&amp;quot;); for (String item : list) {     if (&amp;quot;1&amp;quot;.equals(item)) {         list.remove(item);     } } &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;说明：以上代码的执行结果肯定会出乎大家的意料，那么试一下把“1”换成“2”，会是同样的结果吗？&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;  这个规则我之前是知道的，但是看到上面这个说明，就有点不太淡定了，难道还会有什么奇葩的事不成。于是把这行代码考到IDEA运行了一下，正例自然是不必说。&lt;/p&gt; &lt;p&gt;  果然&lt;code&gt;remove&lt;/code&gt;元素&lt;code&gt;1&lt;/code&gt;的时候正常，&lt;code&gt;remove&lt;/code&gt; &lt;code&gt;2&lt;/code&gt;报&lt;code&gt;java.util.ConcurrentModificationException&lt;/code&gt;异常。然后我又添加元素&lt;code&gt;3&lt;/code&gt;,这下情况又变了，&lt;code&gt;remove&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;和&lt;code&gt;3&lt;/code&gt;异常，&lt;code&gt;remove&lt;/code&gt; &lt;code&gt;2&lt;/code&gt;正常。发现一个规律，&lt;code&gt;remove&lt;/code&gt;倒数第二元素正常，其他元素就会抛异常。&lt;/p&gt; &lt;p&gt;  因为&lt;code&gt;foreach&lt;/code&gt;这种遍历的语法，本身就是Java的语法糖，我们要看真实的代码实现，上述反例经过Java编译器编译后实际上代码如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;ArrayList list = new ArrayList(); list.add(&amp;quot;1&amp;quot;); list.add(&amp;quot;2&amp;quot;); Iterator i$ = list.iterator();  while(i$.hasNext()) {     String item = (String)i$.next();     if(&amp;quot;2&amp;quot;.equals(item)) {         list.remove(item);     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简单的打了几个断点发现，问题和&lt;code&gt;hasNext()&lt;/code&gt;有很大关系。 看一下ArrayList的hasNext()的实现&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public boolean hasNext() {     return cursor != size; } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;看ArrayList的源码，很多方法比如：&lt;code&gt;next()&lt;/code&gt;、&lt;code&gt;previous()&lt;/code&gt;、&lt;code&gt;remove()&lt;/code&gt;在执行前都会先执行以下方法:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;final void checkForComodification() {     if (expectedModCount != ArrayList.this.modCount)         throw new ConcurrentModificationException(); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简单的说&lt;code&gt;checkForComodification()&lt;/code&gt;是为了保证访问的一致性。 下面我们通过人肉执行循环来看一下这其中报异常或者不报异常的原因 &lt;code&gt;remove&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;的循环如下：&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th&gt;步骤&lt;/th&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;表达式值&lt;/th&gt;&lt;th&gt;cursor&lt;/th&gt;&lt;th&gt;size&lt;/th&gt;&lt;th&gt;备注&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;code&gt;i$.hasNext()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;code&gt;String item = (String)i$.next();&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;code&gt;if(&amp;quot;1&amp;quot;.equals(item))&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;&lt;code&gt;list.remove(item);&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;&lt;code&gt;i$.hasNext()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;false&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;此时循环结束，走不到下一个&lt;code&gt;next()&lt;/code&gt;因此也不会抛出异常&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;code&gt;remove&lt;/code&gt; &lt;code&gt;2&lt;/code&gt;的循环如下：&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt;&lt;th&gt;步骤&lt;/th&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;表达式值&lt;/th&gt;&lt;th&gt;cursor&lt;/th&gt;&lt;th&gt;size&lt;/th&gt;&lt;th&gt;备注&lt;/th&gt;&lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;code&gt;i$.hasNext()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;code&gt;String item = (String)i$.next();&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;code&gt;if(&amp;quot;2&amp;quot;.equals(item))&lt;/code&gt;&lt;/td&gt;&lt;td&gt;false&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;&lt;code&gt;i$.hasNext()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;&lt;code&gt;String item = (String)i$.next();&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;&lt;code&gt;if(&amp;quot;2&amp;quot;.equals(item))&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;&lt;code&gt;list.remove(item);&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;code&gt;remove&lt;/code&gt;操作完成后&lt;code&gt;modCount != expectedModCount&lt;/code&gt;，因为没有执行到&lt;code&gt;next()&lt;/code&gt;方法，此时还不会抛出异常&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;&lt;code&gt;i$.hasNext()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;&lt;code&gt;String item = (String)i$.next();&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;此时抛出异常&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;strong&gt;现在终于明白为什么&lt;code&gt;remove&lt;/code&gt;倒数第二个元素不报错的原因了，因为&lt;code&gt;remove&lt;/code&gt;倒数第二元素后刚好&lt;code&gt;cursor == size&lt;/code&gt;，此时&lt;code&gt;hasNext()&lt;/code&gt;返回false。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;关于&lt;code&gt;modCount&lt;/code&gt;和&lt;code&gt;expectedModCount&lt;/code&gt;可参考如下资料：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="http://blog.csdn.net/qq_24235325/article/details/52450331" target="_blank"&gt;http://blog.csdn.net/qq_24235325/article/details/52450331&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="http://blog.csdn.net/java_zone/article/details/53127897" target="_blank"&gt;http://blog.csdn.net/java_zone/article/details/53127897&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 28 Dec 2017 04:54:23 GMT</pubDate>
    </item>
    <item>
      <title>由COUNT(*)引发的MySQL分区探索</title>
      <link>https://www.zhangaoo.com/article/mqsqlpartition</link>
      <content:encoded>&lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2017/12/2casvc62rujcrqv8ra1b2mu548.jpeg" alt="MySQL" /&gt;&lt;/p&gt; &lt;h1&gt;背景&lt;/h1&gt; &lt;p&gt;优化SQL过程中大家会发现一个规律，逻辑相对较复杂SQL优化的空间相对较大，优化起来思路相对也是比较多的。 尝试优化下面的SQL：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;SELECT count(*) FROM big_table; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;直接运行的结果（数据库引擎为innoDB）： &lt;img src="https://www.zhangaoo.com/upload/2017/12/0kq00rjl9qjpjp3hma7qv1u80m.png" alt="alt" /&gt; 你能想到哪些优化思路呢？这里给出几种可能的思路，但不一定适合所有业务场景。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在表中添加而外的字段记录COUNT(*)的值,插入一条数据就+1，删除一条数据就-1，也可以使用触发器完成加减的任务。&lt;/li&gt; &lt;li&gt;假设业务表的主键id是自增的且不会删除数据，可以根据id计算当前表中的行数&lt;/li&gt; &lt;li&gt;更换数据库引擎,比如MyISAM&lt;/li&gt; &lt;li&gt;使用EXPLAIN可得到近似值&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;EXPLAIN SELECT COUNT(id) FROM data USE INDEX (PRIMARY) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这就引出今天要讨论的话题 &lt;strong&gt;MySQL分区能否解决以上问题？？&lt;/strong&gt;&lt;/p&gt; &lt;h1&gt;MySQL分区功能简介&lt;/h1&gt; &lt;h2&gt;概念&lt;/h2&gt; &lt;p&gt;我理解的表分区把一张大表拆分成N个规模较小的子表，这些子表对用户透明，但是使用上有一些规则和技巧。&lt;/p&gt; &lt;h2&gt;分区的类型&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;范围分区（RANGE Partitioning）&lt;/li&gt; &lt;li&gt;列表分区（LIST Partitioning）&lt;/li&gt; &lt;li&gt;列分区  （COLUMNS Partitioning）&lt;/li&gt; &lt;li&gt;哈希分区（HASH Partitioning）&lt;/li&gt; &lt;li&gt;键值分区（KEY Partitioning）&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;简单介绍常用的三种分区方式&lt;/h3&gt; &lt;p&gt;范围分区示例1：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;    id INT NOT NULL,     fname VARCHAR(30),     lname VARCHAR(30),     hired DATE NOT NULL DEFAULT '1970-01-01',     separated DATE NOT NULL DEFAULT '9999-12-31',     job_code INT NOT NULL,     store_id INT NOT NULL ) PARTITION BY RANGE (store_id) (  /*1&amp;lt;=store_id&amp;lt;=5*/     PARTITION p0 VALUES LESS THAN (6),  /*6&amp;lt;=store_id&amp;lt;=10*/     PARTITION p1 VALUES LESS THAN (11),     PARTITION p2 VALUES LESS THAN (16),     /*PARTITION p3 VALUES LESS THAN (21)*/  PARTITION p3 VALUES LESS THAN MAXVALUE ); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;说明： 1、RANGE括号里必须包含一个返回整数的表达式，叫做分区表达式。比如：YEAR(separated)、UNIX_TIMESTAMP(report_updated)等。 2、分区区间大小是必须递增的，也就是说是有顺序&lt;/p&gt; &lt;p&gt;列表分区示例2：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE employees (     id INT NOT NULL,     fname VARCHAR(30),     lname VARCHAR(30),     hired DATE NOT NULL DEFAULT '1970-01-01',     separated DATE NOT NULL DEFAULT '9999-12-31',     job_code INT,     store_id INT ) PARTITION BY LIST(store_id) (     PARTITION pNorth VALUES IN (3,5,6,9,17),     PARTITION pEast VALUES IN (1,2,10,11,19,20),     PARTITION pWest VALUES IN (4,12,13,14,18),     PARTITION pCentral VALUES IN (7,8,15,16) ); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;说明： 列表分区的区间可以无顺序&lt;/p&gt; &lt;p&gt;列分区示例3：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;#按字符串比较 CREATE TABLE employees_by_lname (     id INT NOT NULL,     fname VARCHAR(30),     lname VARCHAR(30),     hired DATE NOT NULL DEFAULT '1970-01-01',     separated DATE NOT NULL DEFAULT '9999-12-31',     job_code INT NOT NULL,     store_id INT NOT NULL ) PARTITION BY RANGE COLUMNS (lname)  (     PARTITION p0 VALUES LESS THAN ('g'),     PARTITION p1 VALUES LESS THAN ('m'),     PARTITION p2 VALUES LESS THAN ('t'),     PARTITION p3 VALUES LESS THAN (MAXVALUE) ); #按元祖大小  CREATE TABLE rcx (          a INT,          b INT,          c CHAR(3),          d INT      )      PARTITION BY RANGE COLUMNS(a,d,c) (          PARTITION p0 VALUES LESS THAN (5,10,'ggg'),          PARTITION p1 VALUES LESS THAN (10,20,'mmm'),          PARTITION p2 VALUES LESS THAN (15,30,'sss'),          PARTITION p3 VALUES LESS THAN (MAXVALUE,MAXVALUE,MAXVALUE)      ); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;说明:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;列分区是范围和列表分区上的变体，列分区允许在分区键中使用多个列。&lt;/li&gt; &lt;li&gt;元组的大小是有顺序的，在不确定元组大小顺序时，可使用如下SELECT语句进行测试。例如：SELECT ROW(5,10) &amp;lt; ROW(5,12), ROW(5,11) &amp;lt; ROW(5,12), ROW(5,12) &amp;lt; ROW(5,12);&lt;/li&gt; &lt;li&gt;在创建这样的表之后，更改给定的数据库、表或列的字符集或排序规则可能会导致行分布的更改。&lt;/li&gt; &lt;li&gt;分区名称不区分大小写。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;注意：以上表无主键，有主键或唯一键的情况又是怎样的呢？下面会介绍表分区的规则&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;分区的基本规则&lt;/h2&gt; &lt;h3&gt;带分区的表无主键、无唯一键分区表达式可以使表中的任何一列。&lt;/h3&gt; &lt;p&gt;如上面的示例&lt;/p&gt; &lt;h3&gt;有主键、一个或多个唯一键&lt;/h3&gt; &lt;blockquote&gt; &lt;p&gt;All columns used in the partitioning expression for a partitioned table must be part of every unique key that the table may have.&lt;/p&gt; &lt;/blockquote&gt; &lt;blockquote&gt; &lt;p&gt;分区表达式中使用的所有列必须是表的每个唯一键的一部分。&lt;/p&gt; &lt;/blockquote&gt; &lt;h1&gt;实践MySQL分区功能&lt;/h1&gt; &lt;h2&gt;环境检查&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;SELECT      PLUGIN_NAME AS Name,     PLUGIN_VERSION AS Version,     PLUGIN_STATUS AS Status FROM     INFORMATION_SCHEMA.PLUGINS WHERE     PLUGIN_TYPE = 'STORAGE ENGINE'; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果如图： &lt;img src="https://www.zhangaoo.com/upload/2017/12/ronc5n2hroj6rpfadmto3ceq50.png" alt="环境检查" /&gt;&lt;/p&gt; &lt;h2&gt;新建分区表&lt;/h2&gt; &lt;p&gt;使用当前报表库中数据最多的表st_daily_part_stock进行测试，因为报表都含有时间字段，查询的SQL中也一定会包含时间字段，使用时间字段分区顺理成章。这里使用比较方便的列分区，当然范围分区也是可以的。分区SQL如下下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE `st_daily_part_stock_partition` (   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',   `id_date_dim` int(11) NOT NULL COMMENT '日期维度',   `id_part_dim` int(11) NOT NULL COMMENT '材料维度',   `id_storage_dim` int(11) NOT NULL COMMENT '仓库维度外键',   `out_number` decimal(18,2) DEFAULT NULL COMMENT '出库数量',   `creationtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',   `modifiedtime` datetime NOT NULL COMMENT '记录更新时间',   PRIMARY KEY (`id`,`id_date_dim`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='出入库明细事实表' PARTITION BY RANGE  COLUMNS(id_date_dim) (PARTITION p0 VALUES LESS THAN (1309) ENGINE = InnoDB,  PARTITION p1 VALUES LESS THAN (1340) ENGINE = InnoDB,  PARTITION p2 VALUES LESS THAN (1370) ENGINE = InnoDB,  PARTITION p3 VALUES LESS THAN (1401) ENGINE = InnoDB,  PARTITION p4 VALUES LESS THAN (1431) ENGINE = InnoDB,  PARTITION p5 VALUES LESS THAN (MAXVALUE) ENGINE = InnoDB); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;注意这里是联合主键，分区字段id_date_dim包含在其中，原因如上规则。&lt;/strong&gt;&lt;/p&gt; &lt;h2&gt;分区表基本测试&lt;/h2&gt; &lt;p&gt;创建了st_daily_part_stock_partition分区表，同时也创建了st_daily_part_stock供对照，st_daily_part_stock除了没有进行分区其他表结构和数据量两个表都一致，28007743条数据。 在《高性能MySQL》中看到基准测试的概念，使用了书中两个shell工具，抓取了一些测试性能数据，还有好多高级工具比如sysbench没来得及使用。如下： &lt;img src="https://www.zhangaoo.com/upload/2017/12/vsa97m4qcgj3fqps248epmnr12.png" alt="分区表基本测试" /&gt;&lt;/p&gt; &lt;h3&gt;导入数据测试&lt;/h3&gt; &lt;p&gt;两个表在大致相同的环境下，在我本机导入4.9G数据，花费时间接近约为12分钟左右，搜集的数据中还包含MySQL的load、QPS等信息。&lt;/p&gt; &lt;h3&gt;全表SELECT COUNT(*)测试&lt;/h3&gt; &lt;p&gt;查询未分区表： &lt;img src="https://www.zhangaoo.com/upload/2017/12/1riklnk58ejqcp2hujjfvcm5d1.png" alt="查询未分区表" /&gt; 查询分区表： &lt;img src="https://www.zhangaoo.com/upload/2017/12/vouvkj5690gs1oaidm3vt2ve9d.png" alt="查询分区表" /&gt; &lt;strong&gt;可以看到表分区后查询时间还略慢于未分区的情况。&lt;/strong&gt; &lt;img src="https://www.zhangaoo.com/upload/2017/12/t9of67vm0kjkkpell1s3lubeo7.png" alt="查询计划" /&gt; 按理说表分区后扫描小分区的速度应该元快于全表扫描，然后把分割分区的结果相加，应该也要更快才对。但事实上MySQL目前并没有这么实现，官网找到下面的说明。&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Other benefits usually associated with partitioning include those in the following list. These features are not currently implemented in MySQL Partitioning, but are high on our list of priorities. Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might beSELECT salesperson_id,COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By “parallelized,” we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;大概意思是说“目前的MySQL版本还没实现SUM() and COUNT()在多分区表的情况下并行执行，然后把执行的结果累加，但是这些功能已经在计划中，并且优先级很高。”&lt;/p&gt; &lt;h3&gt;部分数据SELECT COUNT(*)测试&lt;/h3&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2017/12/tiam4dgt28ipor34oqal0i8i6c.png" alt="部分数据SELECT COUNT(*)测试" /&gt; 看一下执行计划 未分区 &lt;img src="https://www.zhangaoo.com/upload/2017/12/6lreducefsgj9qufmmnh3ulbuq.png" alt="未分区" /&gt; 分区 &lt;img src="https://www.zhangaoo.com/upload/2017/12/3hjev6qdu8ijto1o0p34ig6kv1.png" alt="分区" /&gt; 分区数据分布情况 &lt;img src="https://www.zhangaoo.com/upload/2017/12/6q10ki9jneg9ipgeeto2o0vuou.png" alt="alt" /&gt; &lt;strong&gt;结论：在过滤条件中含有分区信息时，MySQL会自动过滤掉不相关的区间，这样扫描数据量就大大减小，速度也就上来了。我们目前的报表业务，就很符合这种场景，热点数据仅仅分布在最近的1-3月。&lt;/strong&gt;&lt;/p&gt; &lt;h3&gt;数据更新测试&lt;/h3&gt; &lt;p&gt;目前报表数据更新的场景是，每天的增量更新，再集计的话可能会更新多天的数据，最坏的情况就是月末可能会跨月更新几天的数据。为了测试假设我们一次1个月的数据，更细7月份的数据。数据量大概380W。 不分区&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;-- # id,  date -- 1309,  2017-08-01 UPDATE st_daily_part_stock SET creationtime=NOW() WHERE id_date_dim &amp;lt; 1309; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分区&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;-- # id,  date -- 1309,  2017-08-01 UPDATE st_daily_part_stock_partition SET creationtime=NOW() WHERE id_date_dim &amp;lt; 1309; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果 &lt;img src="https://www.zhangaoo.com/upload/2017/12/p832t4ti9mh3lp5ioc7jmco9li.png" alt="结果" /&gt;&lt;/p&gt; &lt;h3&gt;删除数据测试&lt;/h3&gt; &lt;p&gt;现在报表的实际场景，会有删除旧数据的需求，比如删除7月份的数据。 不分区&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;-- # id,  date -- 1309,  2017-08-01 DELETE FROM st_daily_part_stock WHERE id_date_dim &amp;lt; 1309; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分区 目前分区就是按时间来的，每个月一个分区，因此只需要删除一个分区&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;ALTER TABLE st_daily_part_stock_partition DROP PARTITION p0; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结果 &lt;img src="https://www.zhangaoo.com/upload/2017/12/psmrnmr5g8j7oockkbjgg4gm08.png" alt="结果" /&gt;&lt;/p&gt; &lt;h3&gt;分区拆分测试&lt;/h3&gt; &lt;p&gt;初始新建分区往往难以精确判断未来分区的数据量，随着业务和时间的变化，某些分区数据可能会很多各个分区之间数据不太均衡。这种情况就需要人工调整分区的大小，把大分区拆分成多个小分区，多个数据较少的小分区合并成数据相对合理的分区。 当前分区数量分布如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;# PARTITION_NAME, TABLE_ROWS p1, 4240875 p2, 4924135 p3, 5819610 p4, 6411635 p5, 1552360 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们尝试把p4分区拆成两个小分区分别为p41、p42，SQL如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;ALTER TABLE st_daily_part_stock_partition REORGANIZE PARTITION p4 INTO (     PARTITION p41 VALUES LESS THAN (1416),     PARTITION p42 VALUES LESS THAN (1431) ); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2017/12/12rhr92mmuhvfpcltqdumgtd3j.png" alt="alt" /&gt; 查看一下当前分区情况&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;# PARTITION_NAME, TABLE_ROWS p1, 4240875 p2, 4924135 p3, 5819610 p41, 0 p42, 0 p5, 1552360 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分区速度比我预想的要快，可能是由于每个分区都是独立的磁盘数据文件，因此数据只涉及到部分数据。但是令我最惊讶的是数据没了，原本以为再分区数据会自动拷贝到各自的新分区。&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;No data is lost in splitting or merging partitions using REORGANIZE PARTITION. In executing the above statement, MySQL moves all of the records that were stored in partitions s0 and s1 into partition p0.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;使用REORGANIZE PARTITION语法，MySQL官网明明说数据是不会丢的，这让我很疑惑。 然后我SELECT COUNT(*)了一下 &lt;img src="https://www.zhangaoo.com/upload/2017/12/o9u429jf8agpro8kq89hd6vf09.png" alt="alt" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-plain"&gt;4240875+4924135+5819610+1552360=16536980 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;简直了，分区数量和COUNT(*)值竟然不等，进一步确认一下重新分区后p4分区的数据还在不在 &lt;img src="https://www.zhangaoo.com/upload/2017/12/oood6lbp12gh0r71bpfle0poak.png" alt="alt" /&gt; 发现数据的确还在，但数量上好像有点差异，看来重建分区后有些数据已经不准了，可能是缓存数据吧。那么应该有对应的刷新机制。 果不其然Google了一下，别人也遇到了类似的&lt;a href="https://bugs.mysql.com/bug.php?id=86317" target="_blank"&gt;问题&lt;/a&gt;，原因是以下语句的统计信息并不会实时更新&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;SELECT      PARTITION_NAME, TABLE_ROWS FROM     INFORMATION_SCHEMA.PARTITIONS WHERE     TABLE_NAME = 'st_daily_part_stock_partition'; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;需要执行下面的SQL语句才能刷新再分区后表的统计信息&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;ALTER TABLE st_daily_part_stock_partition ANALYZE PARTITION p41; ALTER TABLE st_daily_part_stock_partition ANALYZE PARTITION p42; &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;# PARTITION_NAME, TABLE_ROWS p1, 4240875 p2, 4924135 p3, 5819610 p41, 3227256 p42, 3365449 p5, 1552360 &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;4240875+4924135+5819610+3227256+3365449+1552360=23129685 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是分区累计和COUNT(*)值还是不相等，干脆把剩余的分区都ANALYZE一下，结果如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;# PARTITION_NAME, TABLE_ROWS p1, 4801744 p2, 5187635 p3, 6196330 p41, 3227256 p42, 3365449 p5, 1587571 &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code&gt;4801744+5187635+6196330+3227256+3365449+1587571=24365985 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是发现这个值和COUNT(&lt;em&gt;)还是不等，并且每次ANALYZE每个分区值的总计都会变，但COUNT(&lt;/em&gt;)每个分区值都是一样的，猜测这个统计的数据应该是个大概值，就和查询计划中的数据类似。&lt;/p&gt; &lt;h3&gt;分区合并测试&lt;/h3&gt; &lt;p&gt;拆分的逆操作就是合并，如果存在很多的空闲分区，分区的功能就得不到应有的发挥，同时管理众多小分区也会损耗一定的性能。因此可能会在定期合并一些小的分区。以上面的拆分为例，把拆分的两个分区p41和p42合并为p4。 分区合并SQL如下：&lt;/p&gt; &lt;pre&gt;&lt;code&gt;ALTER TABLE st_daily_part_stock_partition REORGANIZE PARTITION p41,p42 INTO (     PARTITION p4 VALUES LESS THAN (1431) ); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2017/12/vhb19k9gqagb7ons41iqv8afol.png" alt="alt" /&gt;&lt;/p&gt; &lt;pre&gt;&lt;code&gt;# PARTITION_NAME, TABLE_ROWS p1, 4801744 p2, 5187635 p3, 6196330 p4, 6762460 p5, 1587571 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;分析 可见合并和拆分的效率几乎是一样的，合并两个300w的表和拆分一个600W的表时间都是40s左右。合并后也执行一下ANALYZE PARTITION，否则查询分区数据量可能还是0和上面一致。COUNT(*)操作数据不受影响。&lt;/p&gt; &lt;h1&gt;分区表的特点&lt;/h1&gt; &lt;h2&gt;优点&lt;/h2&gt; &lt;p&gt;1、在查询的WHERE条件中增加分区信心可以有效过滤分区，极大的优化某些查询。&lt;/p&gt; &lt;p&gt;2、分区表在创建分区表后可以更改，因此可以重新组织数据，以增强在分区方案首次建立时可能不经常使用的频繁查询。&lt;/p&gt; &lt;p&gt;3、每个分区可以给数据文件和索引文件制定一个目录，这些目录所在的物理磁盘分区可能也都是完全独立的，可以提高磁盘IO吞吐量。&lt;/p&gt; &lt;p&gt;4、 通过删除一个或多个分区来删除无用数据，使删除变得更简单。相反，在某些情况下添加新数据的过程可以通过添加一个或多个新分区来专门存储该数据而得到极大的便利。&lt;/p&gt; &lt;h2&gt;缺点&lt;/h2&gt; &lt;p&gt;1、目前应用中的大部分表都有主键，受限于分区表达式的规则，往往需要重新建表结构，这有可能是会影响现有业务逻辑的。&lt;/p&gt; &lt;p&gt;2、分区表的实现机制有而外的开销，当分区表很多时，开销会越来越大。“根据实际经验对于大多数系统100个左右的分区是没问题的”摘自《高性能MySQL》。&lt;/p&gt; &lt;p&gt;3、打开并锁住所有底层表的成本可能很高，当查询分区表的时候，MySQL需要打开并锁住所有的底层表，这是分区表的另外一个开销。&lt;/p&gt; &lt;p&gt;4、维护分区的成本可能会很高，新增、删除分区速度会很快。但是重组或ALERT分区表时，会先创建一个临时分区，然后将数据复制到其中，再删除原分区。&lt;/p&gt; &lt;p&gt;参考资料&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/5.7/en/partitioning-types.html" target="_blank"&gt;dev.mysql.com partitioning&lt;/a&gt;&lt;/li&gt; &lt;li&gt;《高性能MySQL》&lt;/li&gt; &lt;/ul&gt;</content:encoded>
      <pubDate>Thu, 14 Dec 2017 14:00:29 GMT</pubDate>
    </item>
    <item>
      <title>Linux建站小记篇一之环境准备</title>
      <link>https://www.zhangaoo.com/article/buildsitefirst</link>
      <content:encoded>&lt;h1&gt;背景&lt;/h1&gt; &lt;p&gt;很早就有建站的的打算了，两个月前看到亚马逊云VPS有优惠，赶紧注册了一个，打算购买一年。但是当验证phone code的时候老是有问题，恰好那段时间比较忙就放一边了。 &lt;img src="https://www.zhangaoo.com/upload/2017/11/20g0a4aatqgtnpkhie1o6rc7k3.jpg" alt="alt" /&gt; 直到双十一的前一天，同事在群里发了一个阿里云打折的链接，赶紧上去看了一下，当天下班就买了个低配版，一核、1GB内存、40GB机械硬盘、1M带宽不限流量。话说原价是三年￥2952，现在只要￥800，感觉还行就买了。刚刚我又上去看了一眼，还是这个价，看来双十一优惠力度越来越不行了。各位童鞋如果有需求，传送门如下：&lt;/p&gt; &lt;p&gt;&lt;a href="https://promotion.aliyun.com/ntms/act/qwbk.html?utm_content=se_1008647" target="_blank"&gt;&lt;strong&gt;非学生党阿里云购VPS&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt; &lt;p&gt;如果你是学生党，恭喜你，优惠力度就大了，前提是需要学生认证。&lt;/p&gt; &lt;p&gt;&lt;a href="https://promotion.aliyun.com/ntms/campus2017.html?spm=5176.8112568.738194.6.51CGL5" target="_blank"&gt;&lt;strong&gt;学生党阿里云购VPS&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt; &lt;h1&gt;进入正题&lt;/h1&gt; &lt;p&gt;购买及创建虚拟机实例非技术问题我就不赘述了，假设你已经有一台Linux CentOS的虚拟机。&lt;/p&gt; &lt;h2&gt;简单的环境配置&lt;/h2&gt; &lt;h3&gt;创建工作用户&lt;/h3&gt; &lt;p&gt;如果你接触过Linux,你可能听说过Linux有一条原则叫&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;&lt;strong&gt;最小权限原则&lt;/strong&gt;&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;这也是我要单独创建工作用户的根本原因，如果你所有操作都用root用户，潜在的风险很高。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;权限太高如果误操作的话，比如rm -rf *，那就只能哭了&lt;/li&gt; &lt;li&gt;如果系统有bug被黑客提权到root，那么黑客就可以为所欲为了&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;我们自己的博客其实还好，只要定期备份一下数据，顶多丢了几篇文章。我想说的是，安全意识一定要有。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#添加工作用户 [root@xxxxxx ~]# useradd zhangsan &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;配置新用户sudo命令&lt;/h3&gt; &lt;p&gt;有些命令执行需要root权限，这时需要加sudo执行该命令，新添加的用户需要配置sudo，新用户不配置，linux下面运行sudo命令，会提示类似：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;xxx is not in the sudoers file.  This incident will be reported.  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;切换到root用户配置如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#给sudoers文件添加写权限，默认情况root用户也是没有写权限的 chmod u+w /etc/sudoers  #编辑文件 vim /etc/sudoers  #找到这一 行：&amp;quot;root ALL=(ALL) ALL&amp;quot;，在下面添加，xxx是新建的用户名 xxx ALL=(ALL) ALL  #撤销文件的写权限 chmod u-w /etc/sudoers &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;设置用户公钥登录&lt;/h3&gt; &lt;p&gt;使用密码登录总是会有密码泄露或弱密钥遭暴力破解的风险，因此使用公钥登录是一种比较安全的且方便的做法，可谓是百利无一害。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;更改默认SSH22默认端口&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#找到#Port 22一行，默认是注释掉的，去掉注释之后填写自己的端口，最大不要超过65535 [root@xxxxxx ssh]# vim /etc/ssh/sshd_config &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;禁止密码登录&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#PasswordAuthentication 改为 no  [root@xxxxxx ssh]# vim /etc/ssh/sshd_config &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;在客户机生成自己的公私钥&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#一路回车就好了 ssh-keygen -t rsa -C &amp;quot;zhangsan@qq.com&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;生成好的密钥对默认在自己的工作目录的影藏文件夹（/Users/xxxxx/.ssh）下&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#id_rsa是私钥、id_rsa.pub是公钥,id_rsa一定不能泄露，否则... $.ssh ls config      id_rsa      id_rsa.pub  known_hosts &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注意:以上生成密钥对是在自己的客户机，或者说自己的笔记本上进行，以后要用这台笔记本或台式机远程管理虚拟机。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;在虚拟机上生成.ss目录 新建用户没有隐藏目录.ssh,可以通过以下命令自动新建一个,如果是root用户和普通用户的都是由同一个管理的话，可以直接把root目录下authorized_keys文件拷贝到普通用户下的.ssh目录就行了，这样就不用执行&lt;code&gt;ssh-keygen -t rsa&lt;/code&gt;和编辑&lt;code&gt;authorized_keys&lt;/code&gt;了&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;#以下命令也会生成自己的公钥和私钥 [zhangsan@xxxxxx ~]$ssh-keygen -t rsa #新建保存公钥信息文件，并把自己客户机的公钥拷贝到文件中 [zhangsan@xxxxxx ~]$vim authorized_keys &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;因为是手动新建的authorized_keys，其文件权限不对的话公钥ssh会报如下错误。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;Permission denied (publickey,gssapi-keyex,gssapi-with-mic). &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;检查目录和文件权限，不对的话按照以下修改&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;# chmod 700 /home/zhangsan # chmod 700 /home/zhangsan/.ssh # chmod 644 /home/zhangsan/.ssh/authorized_keys  //公钥文件的所有权限 # chmod 600 /home/zhangsan/.ssh/id_rsa        //私钥文件的所有权限 &lt;/code&gt;&lt;/pre&gt; &lt;h1&gt;小结&lt;/h1&gt; &lt;p&gt;至此一切正常的话就可以用公钥登录服务器了。管理Linux服务器一定要重视权限的配置。始终以最小权限为原则，无用的服务端口一定要关闭，定期备份重要数据。&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 21 Nov 2017 15:51:56 GMT</pubDate>
    </item>
    <item>
      <title>朋友再聚</title>
      <link>https://www.zhangaoo.com/article/friends</link>
      <content:encoded>&lt;h2&gt;时光荏苒&lt;/h2&gt; &lt;p&gt;  三年即逝，今日再聚。师姐还是当年的模样，甚至略显年轻。第一眼看师姐，有变化，但没细节看出来，吃饭的时候才发现单眼皮变双眼皮了。&lt;strong&gt;You are beautiful.&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  大师兄工作出差甚是可惜，已经荣升奶爸的大师兄，享受着家的温暖，工作也是忙的一刻不停。坐骑也由当年的环保电动车升级换代为丰田卡罗拉，我没记错的话。我呢，把我无名无牌的破自行车换成了崭新的二手自行车，没错，崭新的二手自行车。 &lt;img src="https://www.zhangaoo.com/upload/2017/11/rctb7iojbgh47red9hcog5sct3.jpeg" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;无独有偶&lt;/h2&gt; &lt;p&gt;  三年前，我们一个技术小组，参加公司的比赛，整个国庆在公司coding。在大师兄这位技术达人的带领下，一路过关斩将，取得了公司第一名。那种喜悦之情堪比金榜题名，庆功会那晚上，大师兄喝趴下了，大师兄跌跌撞撞地骑着他的环保电动车，我推着我的破自行车，一路摇摆，一直摇到家。不知道后来大师兄有木有被跪搓衣板。偷笑！&lt;/p&gt; &lt;p&gt;  这天做了一个极其错误的决定“让师姐给我倒酒”，本想装X一下，没想到啊，万万没想到。简直是羊入虎口。O(∩_∩)O哈哈~&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;我不是针对你,我是说在座的各位,谁没有装X且失败经历的请举手。&lt;/p&gt; &lt;/blockquote&gt; &lt;h2&gt;往事回首&lt;/h2&gt; &lt;p&gt;  虽然我们仨不是一个部门，但每周一次的下午咖啡时间，只能说是fantastic。聊聊自己的工作，最近的流行技术，工作中遇到的技术问题以及解决方案，那段时间也是自己提升比较快的时间。&lt;/p&gt; &lt;p&gt;  当时的公司业务主要是对日项目，学日语就在所难免了，刚开始还老老实实学习，背单词。后来不知怎么滴，就慢慢放弃了。我和大师兄就有点阿Q精神了，老是比谁学的最差。师姐就牛逼了，等级考试轻松无压力过。~摊手~&lt;/p&gt; &lt;h2&gt;只争朝夕&lt;/h2&gt; &lt;p&gt;  感谢老姊——师姐的姐姐带领我们夫子庙一日游，一同的小帅哥人也比较和蔼、幽默。每次师姐回来都给我们带特产，大师兄和我都感动的笑着流眼泪。美好的时光需要记下来，这不刚搭好个人博客没几天，冥冥之中早已注定。本来想贴张美照的，想想还是算了，泄露朋友的隐私就不好了。&lt;/p&gt; &lt;p&gt;  今天尽然错过了一件大事，今天老姊过生日，老姊，生日快乐！生日快乐！生日快乐！ &lt;img src="https://www.zhangaoo.com/upload/2017/11/q9mah74c6ai9mp76u924grafhe.jpg" alt="alt" /&gt;&lt;/p&gt; &lt;h2&gt;最后&lt;/h2&gt; &lt;pre&gt;&lt;code class="language-java"&gt;public static void main(String[] args){     System.out.println(\&amp;quot;I'm just a little programmer.\&amp;quot;); } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Sun, 19 Nov 2017 16:44:48 GMT</pubDate>
    </item>
    <item>
      <title>鱼和水的故事</title>
      <link>https://www.zhangaoo.com/article/2</link>
      <content:encoded>&lt;h2&gt;鱼和水的故事&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;&lt;strong&gt;&lt;center&gt;鱼说:没有人知道我在流泪，因为我活在水里。&lt;/center&gt;&lt;/strong&gt; &lt;strong&gt;&lt;center&gt;水说:我知道你在流泪，因为你活在我心里。&lt;/center&gt;&lt;/strong&gt;&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;&lt;img src="https://www.zhangaoo.com/upload/2017/11/61g6kln93sj0or76khrs1cl4bd.jpeg" alt="鱼和水的故事" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Sat, 18 Nov 2017 16:54:15 GMT</pubDate>
    </item>
  </channel>
</rss>

