书名:Spring Cloud微服务和分布式系统实践-
ISBN:978-7-115-53220-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 杨开振
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书从企业的真实需求出发,理论结合实际,深入讲解Spring Cloud微服务和分布式系统的知识。书中既包括Spring Cloud微服务的各类常用组件的讲解,又包括分布式系统的常用知识的介绍。Spring Cloud组件方面主要讲解服务注册和服务发现(Eureka)、服务调用(Ribbon和OpenFeign)、断路器(Hystrix和Resilience4j)、网关(Zuul和Gateway)、配置(Config)、全链路追踪(Sleuth)、微服务的监控(Admin)等;分布式系统方面主要讲解分布式数据库、分布式缓存、会话和权限以及发号机制等。本书的实践部分通过Apache Thrift讲解了远程过程调用(RPC)在分布式系统中的应用,并且分析了处理高并发的一些常用方法,最后还通过一个简单的实例讲解了微服务系统的搭建。
本书适合想要学习Spring Cloud微服务、分布式系统开发的各类Java开发人员阅读,包括初学者和开发工程师。本书对架构师也有一定的帮助。
伴随着互联网发展,个人计算机、手机和平板电脑等设备走进了我们的生活。现今我国互联网的普及率已经很高,但应用发展的空间还是很大,接下来就到了互联网的深耕阶段,这就导致对互联网系统的要求必然是大数据、高并发和快响应。在这个趋势下,单机系统已经很难满足互联网企业的这些要求,所以分布式系统是必然的发展方向。
所谓的分布式系统,就是一组计算机为了共同完成业务功能通过网络协作的多节点系统。分布式系统本身也有一系列需要解决的问题,包括多个计算机节点的路由选择、各个服务实例的管理、节点监控、节点之间的协作和数据一致性等,当然还有网络故障、丢包等问题。分布式系统的实施难度比单机系统大得多。
分布式系统比单机系统复杂得多,但经过多年的发展,业界已经有了丰富的分布式系统理论,也有了许多优秀的组件。在分布式系统理论里,最近流行的微服务架构理论成了佼佼者,微服务的概念也成了当前分布式系统实现方案中的主流,显然,微服务架构成了分布式系统的一种形式。优秀的分布式系统组件早期主要以国内阿里巴巴的Dubbo(现今已经被Apache归纳进入其孵化器)为主,后来从国外引入了Spring Boot和Spring Cloud,它们现在是微服务实现的主流方案。
为顺应技术的发展趋势,我对微服务进行了深入的学习和研究,并且于2018年创作出版了《深入浅出Spring Boot 2.x》。为了更进一步地讲解微服务,满足当前企业搭建微服务系统的需要,我竭尽所能编写了这本关于Spring Cloud的书。虽然Spring Cloud能够有效搭建微服务系统,但微服务系统只是分布式系统的一种形式,它并不能解决分布式系统的所有问题,例如,分布式缓存、会话、数据库及其事务等,都不能通过Spring Cloud来有效处理。但这些问题又是企业实施微服务系统时必须要面对的,甚至是一些企业的难点和痛点。因此,本书在详细介绍Spring Cloud的基础上,还会对常用的分布式技术进行讲解,以满足企业的需要。
应该说微服务系统只是在丰富的经验和实践中积累的组件,一切都还在快速发展和变化中,若读者关注Spring Boot和Spring Cloud的版本就会发现,其版本更替相当频繁。应该说分布式(微服务)系统没有绝对的权威,也没有绝对的形式,正如《孙子兵法》中所言:“兵无常势,水无常形,能因敌变化而取胜者,谓之神。”我们只能按照自己的业务需求来决定分布式(微服务)的实施方案。我编写本书的目的是,让读者通过学习前人的经验,吸取已有的教训,采用一些优秀的组件,就能快速便捷地搭建微服务系统,避免掉入陷阱中。
国内流行的早期的微服务解决方案是阿里巴巴的Dubbo,但这是一个不完整的方案,当前Spring Cloud已成为业界流行的微服务搭建方案。因此,本书以讲解Spring Cloud为主。
Pivotal团队收集了各个企业成功的分布式组件,用Spring Boot的形式对其进行封装,最终得到了Spring Cloud,简化了开发者的工作。Spring Cloud当前主要是通过Netflix(网飞)公司的组件来实施微服务架构,但是因为Netflix的组件更新较慢(如Zuul 2.x版本经常不能如期发布,最后取消),并且只按自身企业需要进行更新(如Hystrix停止增加新功能),所以Spring Cloud有“去Netflix组件”的趋势。不过,“去Netflix组件”也需要一定的时间,所以当前还是以Netflix组件为主,这也是本书的核心内容之一。从另外一个角度来看,组件的目的是完成分布式的某些功能,虽类别不同但思想相近,也就是“换汤不换药”。因此,现在学了Netflix组件,即使将来不再使用,也可以吸收其思想和经验,通过这些来对比将来需要学习的新组件,也是大有裨益的。
在编写本书的时候,我考虑了很久,除了Spring Cloud微服务的内容外,还要不要加入其他分布式系统的内容,如分布式发号机、分布式数据库、分布式事务和缓存等。加入这些内容,本书似乎就没有鲜明的特点了,内容会显得有点杂;不加入这些内容,企业构建分布式系统的讲解就会不全面。
反复思考之后,我最终决定将一些常用的分布式知识也纳入本书进行讨论。换一个角度来考虑,微服务作为分布式系统的一种,其自身也是为了简化分布式系统的开发,满足企业生产实践的需要,同样,加入这些知识的讲解也是为了让企业能更好地搭建网站,和微服务架构的目的是一致的。
本书基于一线企业的实际应用需求,介绍Spring Cloud微服务和常用的分布式系统。整体来说,全书分为4个部分。
为了方便读者阅读,本书做了如下约定。
/**** imports ****/
”进行代替,这样主要是为了缩减篇幅,读者可以使用IDE自动导入的功能进行导入。/**** setters and getters ****/
”代替POJO的setter和getter方法。约定后的代码呈现类似下面这样:package com.spring.cloud.chapter0.pojo
/**** imports ****/
public class Role {
private Long id;
private String roleName;
private String note;
/**** setters and getters ****/
}
阅读本书需要读者事先掌握Java EE基础、Spring Boot、数据库和Redis的相关知识。
阅读本书,读者除了可以学到通过Spring Cloud构建企业级微服务系统的方法,还可以学到一些常用的分布式方面的知识。因此,本书适合想要学习Spring Cloud微服务、分布式系统开发的各类Java开发人员阅读,包括初学者和开发工程师。本书对架构师也有一定的帮助。
本书的成功出版,要感谢人民邮电出版社的编辑们,没有他们的辛苦付出,就没有本书的顺利出版,尤其是杨海玲编辑,她在我的写作过程中给了我很多的建议和帮助,帮助我审阅了全稿,修正了不少错误。感谢他们付出的劳动。
感谢我的家人对我的支持和理解,当我在电脑桌前编写代码时,牺牲了很多本该好好陪伴他们的时光。
互联网技术博大精深,而且跨行业特别频繁,涉及特别多的技术门类,再有,技术更新较快(撰写本书时,我就遇到了这样的困难,例如Spring Cloud和Spring Boot的更新十分频繁),而且内容繁复。因个人能力有限,我只能尽力而为。但是,正如没有完美的程序一样,也没有完美的书,一切都需要完善的过程。尊敬的读者,如果您对本书有任何意见或建议,欢迎您发送邮件(ykzhen2013@163.com)与我联系,或者在我的博客上留言。
杨开振
2019年12月
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供源代码免费下载。要获得源代码,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
本部分将讲解分布式和微服务的基础知识和理念,并且简单介绍本书需要用到的基础知识。
本部分包含以下内容:
随着移动互联网的兴起以及手机和平板电脑的普及,当今时代已经从企业的管理系统时代走向了移动互联网系统的时代。自2000年以来,我国移动互联网获得了长足的发展,越来越多的人通过移动设备连接上了互联网。在诸多移动设备中,手机无疑是最具代表性的。图1-1为中国互联网络信息中心(CNNIC)在2018年底发布的第43次调查报告的数据。
图1-1 中国手机网民增长统计分析图(由中国互联网络信息中心发布)
从图1-1中可以看出,经过近10年的发展,以手机上网为代表的移动互联网已经成了时代的主流。到2018年年末,手机网民更是占据了整体网民的98.6%。在这一波浪潮下诞生出了许多新鲜事物,如电商、移动支付、共享汽车和共享单车,它们深刻地改变了人们的生活。
从图1-1中可以看出,2008年到2009年,手机网民占整体网民的比例呈极速增长趋势;2009年到2016年,手机网民人数快速增长;但是2016年以后,手机网民人数的增长速度就渐渐慢下来了。由此可见,手机网民数量的增速已然减缓。这只是表明,以手机上网为代表的移动互联网的普及速度开始减缓,但深入移动互联网业务的时代却才刚刚开始,尤其是企业互联网化已经成为当前我国发展的一个重要方向。简而言之,现今移动互联网已经从高速的普及阶段转变为深耕阶段。
基于我国人口众多、业务繁杂的现状,移动互联网系统也存在着特殊性。我国网站的会员数比其他国家的多得多,随之而来的必然是业务数据的增加,这就造成了热门网站面临大数据存储的问题。对于热门网站来说,在推出热门商品时,一般都会在发售之前先打广告。因此,在热门商品上线销售的刹那,往往会有大量会员抢购,这便会引发互联网系统特有的高并发现象。一般来说,网站对请求和响应的时间间隔要求在5秒之内,否则会导致客户等待时间过长,影响客户对网站的忠诚度,因为没人会愿意使用一个需要等待几十秒都不能响应的网站。
大数据、高并发和快响应是互联网系统的必然要求。但是在大数据和高并发的情况下,要求快响应是比较苛刻的,因为大量的数据会导致查找数据的时间变长,高并发会使互联网系统因繁忙而变慢,进而影响响应速度。
在大数据、高并发和快响应的要求下,单机系统已经不可能满足现今互联网了。为了满足互联网的苛刻要求,网站系统已经从单机系统发展为多台机器协作的系统,因而互联网系统已经从单机系统演变为多台机器的系统,我们把这种多台机器相互协作完成企业业务功能的系统,称为分布式系统。虽然与此同时,分布式系统也会引入许多新的问题,并且这些问题也很复杂,很难处理,但互联网技术经过多年的发展和积累,已经拥有了许多成功的分布式系统经验和实践,因此我们可以站在巨人的肩膀上,无须重复发明轮子。
分布式系统由一组为了完成共同任务而协调工作的计算机节点组成,它们通过网络进行通信。分布式系统能满足互联网对大数据存储、高并发和快响应的要求,采用了分而治之的思想。从实际成本来说,可以使用廉价的普通机器进行独立运算。然后通过相互协助来完成网站业务,这样就可以完成单个计算机节点无法完成的计算和存储任务了。为了让大家能够更好地了解分布式系统的好处,这里先给出一个简易的分布式架构,如图1-2所示。
我们先结合图1-2来讨论这个架构的工作原理。首先,移动端或者PC端会通过互联网对网站发出请求,当请求到达服务器后,网关通过合理的路由算法将请求分配到各个真实提供服务的Web服务器中,这样多台廉价的普通Web服务器就可以处理来自各方的请求了。严格来讲,这个架构也有诸多问题,但是因为这些问题相当复杂,所以本节暂且不讲,留到后面的章节再谈。这里先讨论它的几个好处。
图1-2 简易分布式架构
综上所述,分布式系统的使用,带来了许多的便利,满足了当今互联网系统的需求,成了时下的技术热点。
使用分布式系统,就意味着需要将系统按照不同的维度(如业务、数据等)切分给各个不同节点的机器。因此,需要对业务或者数据进行合理切分,让多个机器节点能够相互协作,以满足业务功能的需要。在下面几节中,我们将讨论分布式常见的切分方法。但是请注意,这里只是讨论了常用的切分方法,并不是只有这些切分方法。
所谓水平切分,就是将同一个系统部署到多台机器上,如图1-3所示。
图1-3 水平切分
从图1-3中可以看到,单体的Web服务器变成了多个Web服务器节点,每台服务器都有相同的应用,都能独立完成计算,互不相干。这样的切分有以下几个好处。
以上就是水平切分的优势,但是这样的分法,也有很大的弊端。随着业务的发展,业务会从简单变复杂。例如,一个电商的网站,用户和业务不断膨胀,所需的产品、卖家、交易和评论业务也会日趋复杂。如果此时还将所有的业务全部集中在一套系统里开发,那么显然所有业务都会耦合到一套系统里,日后的扩展和维护会越来越困难。一方面,我们会不断地通过打包来升级系统,使得系统的稳定性和可靠性不断下降;另一方面,维护起来也不方便,例如,要升级产品业务,就需要对全部节点进行升级,而这样的升级会比较麻烦。
如上所述,随着业务的增加和深入,以及用户数的膨胀,有时候,单一业务也会随之变得异常复杂,有必要按照业务的维度进行拆分,将各个业务独立出来,单独开发和维护。假设,我们将用户、交易和产品系统拆分出来,独立开发,就可以得到图1-4所示的架构。
图1-4 垂直切分
从图1-4中可以看出,我们把系统按照业务维度进行了切分,这样每一个系统就都能独立开发和维护了。垂直切分有以下好处。
虽然这样的划分带来了以上诸多好处,但其存在的弊端也是值得我们重视的。
上文我们讨论了水平切分和垂直切分,也讨论了它们的利弊。本节的混合切分是将水平切分和垂直切分结合起来的一种切分方法。现今微服务架构大部分采用了这种分法,因此这也是本章的重点内容之一。先看一下图1-5。
图1-5 混合切分
在图1-5中,先是把系统按照业务维度切分到不同的服务器群里。然后,又对其中一种业务系统进行了水平切分,使得这种业务系统可以在多个节点中运行。最后,系统之间采用了交互机制进行协作。
通过其中的垂直切分,我们能够将业务分隔为独立的系统。这样就不会形成大耦合,有利于灵活的管理和简化后续的开发。而对每一个独立的业务系统又采用了水平切分,即使某个节点因为某种原因不可用,也有其他业务系统节点可以代替它。这样系统就可以变为高可用的了。这样的划分依旧不能克服系统之间大量交互和难以维护的数据一致性的问题,同时,切分节点比较多也会使实施分布式系统的硬件成本提高。实际上,无论何种划分,都不可能使得分布式系统只有优点,没有缺点。相对于耦合性和缺乏灵活性来说,大量交互和数据一致性的问题则更容易处理,因此混合划分渐渐成为主流划分方式。
前面我们简单地对分布式系统的架构进行了分析。实际上,对系统进行切分后会造成很多问题。例如,会让许多初学者陷入误区,以为只要按上述架构进行设计就可以了。然而,在非单机节点的情况下,分布式系统只能通过网络来完成协作,它存在许多不确定性。
早在1994年,Peter Deutsch就提出了分布式计算的七大谬论,后来被 James Gosling(Java之父)等人完善为八大谬论。
从这八大谬论可以看出,网络的不可靠性(如丢包、延时等)、拓扑结构的差异和传输速率大小等因素对分布式系统都存在很多的限制,我们再归结为下面3点。
此处可以简单地总结为:因为网络和机器的众多不确定性,注定了分布式的难点在于,如何让多个节点之间保持一致性,服务于企业实际业务。因为数据一致性是分布式的核心问题之一,所以下面举两个执行交易的实例进行说明,如表1-1和表1-2所示。
表1-1 不一致数据情况(一)
时刻 |
业务1 |
业务2 |
备注 |
---|---|---|---|
T1 |
请求购买3个产品 |
||
T2 |
请求购买5个产品 |
||
T3 |
读取的产品库存为6 |
||
T4 |
读取的产品库存为6 |
||
T5 |
减库存,此时库存为3 |
此时业务2依旧会认为产品库存为6 |
|
T6 |
记录交易成功 |
业务1已经成功 |
|
T7 |
减库存,此时库存为-2 |
业务错误,已经超发 |
|
T8 |
记录交易成功 |
实际已经没有库存了 |
表1-2 不一致数据情况(二)
时刻 |
业务 |
备注 |
---|---|---|
T1 |
请求购买3个产品 |
|
T2 |
读取的产品库存为10 |
|
T3 |
减库存,此时库存为7 |
此时业务正常扣减了库存 |
T4 |
记录交易异常 |
此时库存扣减成功而交易记录却失败了,业务错误 |
注意,这里并不是讨论线程并发的问题,因为这里的产品和交易是在不同的机器节点上运行的。表1-1中,当时刻为T5时,尽管业务1做了减库存,但是此时业务2并不知晓业务1减库存的情况,它依旧认为产品库存为6,认为可以继续扣减业务请求的5个产品,但实际情况已经不是它认为的那样了。这是一种情况,那么这里再举另外一种情况,如表1-2所示。
表1-2看似是数据库事务回滚的简单问题,实际上并不是。正如之前论述的那样,产品和交易是两个不同的机器节点。换句话说,T3时刻是产品节点提供操作,而到了T4时刻则是交易节点提供操作。因为它们不是在同一个事务内进行操作的,所以不能通过一个简单的事务进行处理。因此,需要通过分布式事务或者其他方式提供保证。
正因为分布式存在数据或者业务操作大量的不一致性,因此需要协议或者相应手段来保证其数据的一致性。但引入过多的协议来保证数据一致性,会使系统性能大幅下降,影响用户体验。因此,除了考虑数据的一致性问题,还需要考虑系统的性能问题,这是分布式系统的难点问题,也是核心问题之一。
既然分布式存在那么多的问题需要解决,那么应该如何衡量一个分布式系统的标准呢?
在Andrew S. Tanenbaum创作的《分布式系统:原理与范例》中,指出了以下几点。
鉴于分布式系统的复杂性,一些专家和学者提出了不同的理论,其中最著名、最有影响力的当属CAP原则和BASE理论。
分布式系统有许多优点和缺点,其主要特点是一致性、可用性和分区容忍。它们的具体含义如下。
针对这3个特点,Eric Brewer教授在2000年提出了CAP原则,也称为CAP定理。该原则指出,任何分布式系统都不能同时满足3个特点,如图1-6所示。
图1-6 CAP原则
根据CAP原则,从图1-6中可以看出分布式系统只能满足3种情况。
也就是说,任何的分布式系统都只能较好地完成其中的两个指标,无法完成3个指标。
在当今互联网中,保持可用性往往是第一位的,其次是性能。因为从客户的感知来说,可用和快速响应能够提供更好的体验。一致性可以通过其他手段来保证,本书后面会给出具体的方法。
微服务主要追求可用性和分区容忍性(AP),轻一致性(C)。
在现实的业务中,金额和商品的库存数据是企业生产的核心数据,在分布式系统中保证这些数据的一致性,是分布式系统的核心任务之一。在不同的线程和机器之间保持数据的一致性是十分困难的,需要使用很多协议才能保证。在保证一致性的同时,也会给系统带来复杂性和性能的丢失。在BASE理论中,一致性又分为强一致性和弱一致性。需要注意的是,CAP原则中的一致性是指强一致性。这里,我们先来了解什么是强一致性和弱一致性。
BASE理论是eBay的架构师Dan Pritchett在ACM上发表的文章中正式提出来的,是对大型分布式系统的实践总结。
BASE理论的核心思想是:即使分布式系统无法做到强一致性,也可以采用适当的方法达到最终一致性。
BASE并非一个英文单词,而是几个英文单词的简写。
BASE理论的应用场景是大型分布式系统,它的核心内容是放弃强一致性,保证系统的可用性。因为分布式系统自身的融合和扩展就相当复杂,如果需要保证强一致性就需要额外引入许多复杂的协议,这会导致技术的复杂化,同时对性能也有影响。BASE理论则建议让数据在一段时间内不一致,从而降低技术实现的复杂性,并提高系统的性能,最后再通过某种手段使得数据达成最终一致即可。
因为分布式非常复杂,所以一直以来都没有权威的架构和设计,更多的只是前人的积累和实践。前人总结出了许多有用的理念,积累了许多经验,开发了很多实施分布式的软件。近几年来,最热门的分布式架构非微服务架构莫属。它是由美国科学家Eric Brewer在其博客上发表的概念。微服务是当前分布式开发的热点内容,也是本书的核心内容。下面先来了解什么是微服务架构。
在讲解微服务架构前,我们需要先了解单体系统的概念和弊端。
一个单体应用,一般分为用户接口(包含移动端和网页端)、服务端应用(类似Java、PHP等动态网站服务器)和数据源(数据库和NoSQL等)。因为它是一个整体,所以如果当中某个业务模块发生了变化或者出现了bug,就需要整体重新构建和部署。随着时间的累积,各个业务模块很难保持很好的模块化结构,很容易有一个业务模块影响别的业务模块的情况。从可扩展的角度来说,扩展任何业务模块都需要扩展整个单体服务,而不能部分扩展。从部署和维护的角度来说,任何的扩展和修正都需要重新升级所有的服务,做不到部署和维护单个模块。从业务的角度来说,随着业务的复杂化,系统模块之间的耦合也会日趋严重。
事实上,微服务架构只是将一个单体应用程序拆分为多个相对独立的服务,每一个服务拥有独立的进程和数据,每一个服务都是以轻量级的通信机制进行交互的,一般为HTTP API(现今最流行的是REST风格)。一般来说,这些服务都是围绕着业务模块来建设的,是独立的产品,因此完全可以独立地自动化部署和维护,这样更加有利于我们进行更小粒度的开发、维护和部署。这些服务可以由不同的语言编写,采用不同的数据存储,最低限度地集中管理。
微服务是一个模糊的概念,而不是一个标准,没有明确的定义。但微服务存在一定的风格,只要系统架构满足一定的风格,就可以被称为微服务架构。接下来,我们来了解一下微服务的风格。
为了更好地实现微服务的风格,Eric Brewer提出了微服务架构的九个风格。也就是说,对于满足以下九种风格的系统架构,我们都可以称之为微服务。
这里,首先明确定义组件化(componentization)和服务(service)的含义。把一个单体系统拆分为一个个可以单独维护和升级的软件单元,每一个单元就称为组件。每一个组件能够运行在自己独立的进程里,可以调用自己内部的函数(或方法)来完成自身独立的业务功能。但是更多的时候组件之间需要相互协作才能完成业务,这些就需要通过服务来完成了。这里的服务是指进程外的组件,它允许我们调用其他的组件,服务一般会以明确的通信机制提供,如HTTP协议、Web Service或者远程过程调用(RPC)等。
这样的组件化和服务有助于简化系统的开发,我们可以单独维护和升级。其次,在开发人员明确了组件的含义之后,只需要开发自己的组件,无须处理其他人的组件。在他人的组件需要调用我们开发的组件功能时,我们只需要提供编写服务即可。服务只需要明确以什么协议(如HTTP协议)和规范进行提供即可,这样各个组件之间的交互就相对简单和明确了。
这显然带来了开发和维护的便利,但是也会引来其他的问题。首先,如何将一个单体系统拆分为各个组件,这是一个边界界定的问题。其次,在使用通信机制进行交互的情况下,性能远没有在单机内存的进程中运行高。
上文谈过单体应用包含用户界面、服务逻辑和数据源等内容。如果对团队进行划分,可以分为前端团队、后端团队、数据库团队和运维团队等。如果以这样的团队划分作为微服务的划分,会出现比较大的问题。因为一个改动往往会同时牵涉到前端、后端和运维团队,所以即使是很小的业务改动,也会牵涉跨团队的协作。而跨团队的协作必然会引发沟通成本,严重时甚至会出现内耗,这会极大地增加系统的维护成本。为了避免这个问题,微服务架构建议按业务模块来划分团队。这样,每次修改系统的工作,就只需要在相关的业务团队之间进行了,不需要牵涉全局。如此,牵涉的团队最少,也减少了不必要的沟通和内耗。
传统的软件开发组织一开始会按业务模块进行划分,然后进行开发。一旦开发完成,将软件交付给维护部门,开发团队就解散了。而微服务则认为,这样的模式是不可取的,并且认为开发团队应该维护整个产品的生命周期,也就是谁开发谁负责后续的改进。因为微服务是帮助用户持续处理业务功能的,所以开发者持续关注软件,不断地改善软件,让软件更好地服务于业务,而且越小的粒度也越容易促进用户和服务供应商之间的关系。
微服务的应用致力松耦合和高内聚,也就是业务模块的划分具有高内聚的特点,而各个业务组件则呈现出松耦合的特点。但是系统拆分后,需要各个组件相互协助才能完成业务,因此组件之间需要相互通信,为此开发者需要引入各种各样的通信协议。通信协议分很多种,如HTTP、Web Service和RPC等。在微服务的构建中,建议弱化通信协议的复杂性,因此推荐使用以下两种。
在一些非常强调性能的网站,也许还会使用二进制来传递协议,但是这仍然不能解决分布式的丢包和请求丢失等问题。微服务推荐使用的两种方式,虽然在性能和可靠性上比不上其他的一些协议,但是在可读性上却大大提高了。也许绝大部分的系统并不需要在两者之间做出选择,因为能获得可读性的便利就已经很不错了。毕竟引入那些性能高或者可靠的协议会大大降低可读性,并且在很大程度上会提高系统的开发和日后维护的难度。
和单体系统构建不一样,微服务架构允许我们分散治理。微服务架构的每一个组件所面对的业务焦点都是不一样的,因此在选型上有很大的差异。例如,C++适合做那些实时高效的组件,Node.js适合做报表组件,而Matlab则适合做数字图像分析。不同的业务组件也许需要不同的语言进行开发,而微服务架构允许我们使用各类语言构建组件,各组件之间只需要约定好服务接口即可。微服务架构没有编程语言的限制,不同的业务组件可以根据自己的需要来选择构建平台。
分散治理带来了很大的灵活性。与此同时,我们只需要通过接口约定即可实现组件之间的相互通信。例如,使用现在流行的HTTP请求的REST风格,就能够使系统之间十分简单地交互。
单体系统拆分后,微服务架构建议使用分散的数据管理,也就是每一个组件都应该拥有自己的数据源,包括数据库和NoSQL等。这样,我们就可以按照微服务组件划分的规则,划分对应的数据。这有助于更为精确地管理数据,可以使数据存储更加合理,同时还可以简化数据模型。
但是,分散数据管理也会引发两个弊端。
第一个弊端是,因为数据库的拆分会导致原有的ACID特性不复存在,所以需要实现分布式数据库事务的一致性。为了实现它,还需要引入其他协议,如XA协议等。然而,这会使开发变得十分复杂,大大提高开发难度。所以微服务并不建议使用分布式事务来处理数据的一致性,而是建议使用最终一致性的原理。在第15章中,我们会再谈到这些问题。
第二个弊端是,拆分之后关联计算会十分复杂。例如,交易组件要查看产品详情的时候,而产品详情却放在产品组件里,如果是在统计分析的情况下,则无法进行数据库的表关联计算,需要大量的远程过程调用才行,这样会造成性能低下,但是从现实来说在分布式系统中使用统计分析的场景较少,所以这样的场景出现频率较低。需要统计分析时,可以抽取数据到对应的系统再进行统计分析,毕竟统计分析一般不需要实时数据。
因为微服务是将一个单体系统拆分为多个组件,所以势必造成多个组件的测试和部署,这样就会大大增加测试人员和开发人员的工作量。在业务不断扩大的情况下,这些将会成为测试和运维人员的噩梦。好在当前的云计算、测试开发、容器(如Docker)等技术已经有了长足发展,减少了微服务的测试、构建和发布的复杂性。
正如之前所提到的,实施微服务是对每一个组件都是以产品的态度不断深化改造以满足用户需求,所以每次进行改造之时必然会涉及构建、测试和发布。对于自动化测试,当前已有许多语言可用,如Node.js、Python等语言,都可以构建测试开发,验证测试案例。这是部署之前需要做的事情,可以降低测试人员的工作量。对于部署来说,借助容器化技术(如Docker)进行构建、部署微服务,可以极大地简化部署人员重复的操作和多环境的配置。
使用服务作为组件的一个结果,在于应用需要有能容忍服务的故障的设计。一般来说会出现两种情况。
第一种情况是,任何服务器都可能出现故障、断电和宕机。在这样的情况下,微服务架构应当可以给出仪表盘,监控每一个节点的状态是否正常、吞吐情况、内存等。一旦出现故障不可用时,微服务系统自动就会切断转发给它的请求,给出故障节点的提示,并且将被切断的请求转发给其他可用节点。微服务系统也允许监测组件节点的状态(上线、下线或不可用),在某些组件节点出现故障、断点和宕机时,系统允许组件节点优雅下线进行维护。在企业维护成功后,允许其重新上线,再次提供服务。
第二种情况是,当系统接收大量请求时,可能出现某个组件响应变得缓慢的情况。此时,如果其他的组件再调用该组件,就需要等待大量的时间。这样,其他的组件也会因为等待超时而引发自身组件不可用,继而出现服务器雪崩的场景。当一个组件变得响应缓慢,造成大量超时,如果微服务能够发现它,并且通过一些手段将其隔离出去,这种情况就不会蔓延到调用者了。这就好比电流突然增大,可能会发生危险,保险丝便自动熔断保护用电安全一样。因此,我们把这种情况称为断路,把微服务中处理这种情况的组件称为断路器(Circuit Breaker)。
从上述的特征来看,实施微服务比实施一个单体系统复杂得多,代价也大得多。从实践的角度来说,微服务的设计是循序渐进的,在起初业务量不大的时候,系统是相对简单的,业务也是相对单一的。早期的核心架构在后期不会发生很大的变化,但系统会引入新的业务,使得一些内容发生变化,有些组件会被停用,有些组件会被加入进来。例如,用户数量不断增大且构成变得更复杂,这个时候可以把现有的用户服务拆分为高级用户服务和普通用户服务两个微服务产品,对外提供服务。经过时间的推移,那些核心架构的组件往往就会相对稳定下来,从而成为微服务的核心。而那些需要经常变化的组件,则需要不断地进行维护和改进,来满足业务的发展需要。
应该说,微服务是分布式系统设计和架构的理念之一。但是从微服务的风格来看,它并不是为了克服所有的分布式系统的缺陷而设计的,而是为了追求更高的可读性、可用性和简易性。但与此同时,也弱化了其一致性,正如这句老话——“两害相较取其轻者”。
所以,微服务并不能解决所有的分布式系统的问题,它只是寻求一个平衡点,让架构师能够更为简单、容易地构建分布式系统。但微服务并非金科玉律,对于一些特殊的分布式需求,还需要我们使用其他的方法来得以实现,正如方法是死的,而人是活的,需要实事求是地解决问题。
如上所述,实现微服务需要大量的软件,而这些软件是十分复杂的。应该说,大部分的企业,包括一些大企业,都无力支持这些软件的开发。但是我们并不沮丧,因为我们可以“站在巨人的肩膀上”,无论是国内还是国外,都为分布式系统做了大量的尝试,积累了丰富的成果。例如,下面的工具是我们常常在构建分布式系统中见到的。
目前,国内最流行的是阿里巴巴的Dubbo,它已经在很多互联网企业广泛使用。但无论如何,这些软件都是某些公司为了解决各自某些问题而开发出来并将其开源的。严格来说,它们并不是一套完整的解决方案。而在国外,Spring Cloud大行其道。Spring Cloud是由Pivotal团队开发的,它没有重复造轮子,而是通过考察各家开源的分布式服务框架,把经得起考验的技术整合起来,形成了现在的Spring Cloud的组件。Spring Cloud就是通过这种方式构建了一个较为完整的企业级实施微服务的方案。更令人振奋的是,Pivotal团队将这些分布式框架通过Spring Boot进行了封装,屏蔽了那些晦涩难懂的细节,给开发者提供了一套简单易懂、易部署和易维护的分布式系统开发工具包。在引入国内之后,Spring Cloud渐渐成了构建微服务系统的主要方案,成为市场的主流。当然,这也是本书需要深入讨论的核心内容之一。
通过上述介绍,大家可以知道,Spring Boot是构建Spring Cloud微服务的基石,所以它是阅读本书的基础。但本书只是简单介绍Spring Boot的知识,如果读者想要进一步深入,还需自行学习。
为了构建微服务架构,Spring Cloud容纳了很多分布式开发的组件。
通过上述组件描述可以相对容易地构建微服务系统。只是本书不会介绍所有的组件,而是根据需要介绍最常用的组件,这些将是后续章节的重点内容。当前,Spring Cloud以Netflix公司的各套开源组件作为主要组件,通过Spring Boot的封装,给开发者提供了简单易用的组件。但由于Netflix的断路器Hystrix已经宣布进入维护阶段,不再开发新的功能,因此,Spring Cloud即将把Resilience4j作为新的熔断器加入进来。本书会对Resilience4j进行详细的讲解,以适应未来的需要。Spring Cloud的未来趋势是去Netflix组件,因为需要大幅度地更新组件,所以周期较长。但是,即使更替新的组件,其设计思想也是大同小异的,正如这句老话——“换汤不换药”,所以我们还是会讲解Netflix组件。
因为Spring Cloud融入了大量的其他企业的开源组件,所以这些组件的版本往往并不一致,不同的组件由不同的公司进行维护。为了统一版本号,Pivotal团队决定使用伦敦地铁站点名称作为版本名。首先是将这些站点名称进行罗列,然后按顺序使用。Spring Cloud发布的版本历史(截至本书编写时)如表1-3所示。
表1-3 Spring Cloud版本更替史
Cloud代号 |
Boot版本(正式发布版本) |
Boot版本(已经测试版本) |
当前状态 |
---|---|---|---|
Angle |
1.2.x |
不兼容1.3 |
终止 |
Brixton |
1.3.x |
1.4.x |
终止 |
Camden |
1.4.x |
1.5.x |
启用 |
Dalston |
1.5.x |
不支持2.x |
启用 |
Edgware |
1.5.x |
不支持2.x |
启用 |
Finchley |
2.x |
不支持 1.5.x |
启用 |
Greenwich |
2.1.x |
不支持 1.5.x |
启用 |
在编写本书时,版本已更替到了Greenwich.SR2,其中,Greenwich是伦敦的一个地铁站名,SR2代表的意思是“Service Releases 2”,这里的2代表的是第2个Release版本,它代表着Pivotal团队在发布Greenwich版本后,进行修正的第二个版本。
由于Spring Boot已经发展到了2.1.x版本,Spring Cloud也发布到了Greenwich版本,因此本书是基于Spring Boot 2.1.0和Greenwich.RELEASE进行讲解的。
为了更好地讲述Spring Cloud微服务和分布式系统的知识,这里我们来模拟微服务系统。当今互联网的世界中,互联网金融是一个很大的课题,所以这里采用互联网金融的例子来讲解微服务系统和分布式应用的知识。
假设,有一家互联网金融公司主营互联网金融借贷业务。它先收集借款人信息,再根据借款人的资金需要生成理财产品。然后,通过理财产品约定利息、时间、还款方式等内容后,发送到公司的互联网平台。最终用户就可以在该公司互联网平台上看到这些理财产品了。那些拥有闲置资金的用户就可以购买这些理财产品,从而获得较高的利息收益。其业务如图1-7所示。
图1-7 互联网金融平台
为了使业务能够进行,这里先做业务分析,此为开发系统的第一步。
这里需要管理和审核借款人,公司信贷审核人员将审核借款人的身份、信用、资质和财产等情况,以保证不发生金融诈骗,因此我们需要一个借款人微服务。
而投资人是平台的用户,因为大额投资和经常投资的用户应该要被给予更多的优惠,所以投资人也会根据具体的情况分成不同的等级。为了更好地管理,需要一个用户微服务。
平台会根据借款人的资金需要来生成对应的理财产品,理财产品分为定期和活期。用户购买产品的交易记录也会记录在内。这里需要一个理财产品微服务。
因为涉及金钱,所以这里需要一个资金微服务,帮助投资人和借款人管理自己资金。投资人可以将自己银行卡上的闲置资金转入系统来购买理财产品,而平台也会根据借款人的资金需要将资金转到借款人账户。
平台也许还会和第三方合作,让第三方介绍投资人或者借款人,或者进行广告等,因此还需要一个第三方微服务……
不过也许并不需要考虑那么多的微服务,因为大部分情况是类似的,而且全部考虑也会太复杂。因此,本书只讨论用户(投资人)、理财产品和资金微服务,如图1-8所示。
图1-8 本书互联网金融平台粗略架构
从图1-8中可以看到,请求会先到网关,网关会拦截请求,进行验证、路由和过滤。这样做可以保护微服务,避免一些恶意的攻击,同时还可以限制通过的流量,避免过大的请求量压垮系统。各个微服务则提供实际的业务功能,对于微服务之间需要交互才能共同完成相关的业务,按照微服务的建议进行集成,这里采用REST风格的请求进行集成。
为了更好地介绍Spring Cloud,这里稍微介绍一下Spring Boot和HTTP的REST风格。因为Spring Cloud是以Spring Boot作为基石的,而各个服务系统又是通过REST风格的请求集成在一起的,所以学习它们将有助于我们深入学习Spring Cloud。当然,如果你已经对它们很熟悉了,也可以跳过本章,直接学习第3章的内容。
从第1章可以看出,Spring Cloud的组件是通过Spring Boot的方式进行封装的,所以这里先简单地介绍一下Spring Boot的应用。Spring Boot是由Pivotal团队提供的全新框架,它采用约定优于配置的思想,极大简化了Spring项目的开发。它是当前最为流行的微服务开发框架,在企业的实际开发中,越来越受欢迎,使用率也稳步上升。
本书的开发环境是IntelliJ IDEA。
下面来创建Spring Boot工程。如果使用的是Eclipse,可以通过STS插件来完成,其步骤也是相似的。
首先让我们新建一个工程,如图2-1所示。
图2-1 用IDEA新建一个Spring Boot工程
这里选择“Spring Initializr”,然后点击选择适当的JDK,再点击“Next”,就可以看到如图2-2所示的界面。
图2-2 配置Spring Boot工程
此处我们可以根据需要配置自己的工程信息。这里的“Type”可以选择Maven或者Gradle工程。当今企业主要使用Maven,所以本书也采用Maven来介绍。如果使用的是Gradle,也没有问题,其使用方式和Maven差别不大,这里就不介绍了。这里的“Packaging”选择“War”,这意味着可以使用JSP作为视图,如果不需要使用JSP,也可以选择“Jar”。然后点击“Next”,就可以看到如图2-3所示的界面了。
图2-3 选择Spring Boot依赖
从图2-3中可以看到,IDEA提供了很多可以依赖的starter包。这里,我只选择了Spring Web Starter和Thymeleaf,意为开发一个关于Spring MVC的工程。其中“Template Engines”使用了Thymeleaf 模板引擎。然后就可以点击“Next”了。跟着输入自己的工程名称,选择工程目录,就可以新建一个Spring Boot工程了。
在上一节中,我们新建了一个Spring Boot工程。下面来查看它的目录,了解目录和相关文件的作用,如图2-4所示。
图2-4 Spring Boot工程目录
从图2-4中可以看到pom.xml文件,它是Maven的配置文件。因为我们介绍的Spring Boot版本是2.1.0.RELEASE,而通过IDEA创建的是2.1.7.RELEASE,所以需要手工把版本修改为2.1.0.RELEASE。修改后的pom.xml如代码清单2-1所示。
代码清单2-1 pom.xml(Chapter2工程)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 项目信息配置 -->
<modelVersion>4.0.0</modelVersion>
<groupId>spring.cloud</groupId>
<artifactId>chapter2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!--打包为war包-->
<packaging>war</packaging>
<name>chapter2</name>
<description>chapter2 for Spring Cloud</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--属性配置-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>
UTF-8
</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<!--导入依赖包-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!--Spring Boot插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意加粗的代码。下面对它们进行说明。
再看一下图2-4,这里需要对它的文件和目录进行简要说明,如表2-1所示。
表2-1 Spring Boot工程目录说明
目录/文件 |
说明 |
备注 |
---|---|---|
Chapter2Application.java |
IDEA生成的主类(含有main方法),我们通过它运行Spring Boot工程 |
以Java Application方式运行 |
ServletInitializer.java |
初始化DispatcherServlet,使用它来进行外部服务器部署 |
将工程打包成的war包,放到外部服务器时,通过它初始化Spring MVC的核心类DispatcherServlet |
static目录 |
静态资源目录,如果是Web工程,可以放置HTML、JavaScript和CSS等静态文件 |
|
templates |
Spring Boot默认配置的动态模板路径 |
默认使用Thymeleaf模板作为动态页面 |
application.properties |
Spring Boot配置文件 |
一个最常用的配置文件,在分布式开发中常常使用application.yml代替它 |
Chapter2ApplicationTests.java |
Spring Boot测试类 |
测试开发的代码 |
pom.xml |
Maven配置文件 |
我们直接通过Java Application的形式运行Chapter2Application.java,就能够运行Spring Boot项目。在默认的情况下,Spring Boot会使用8080端口启动服务。如果想切换端口,就要修改核心配置文件application.properties,这里先把它重命名为application.yml。因为在分布式和微服务开发中,使用的大部分是YAML文件,而非properties文件,所以本书也主要使用YAML文件进行配置。修改application.yml,如代码清单2-2所示。
代码清单2-2 application.yml(Chapter2工程)
server:
port: 8001 # 修改内嵌Tomcat端口为8001
此时,如果再次使用Java Application的形式运行Chapter2Application.java,就可以看到Spring Boot在8001端口启动服务了。接下来,改造一下Chapter2Application.java文件,如代码清单2-3所示。
代码清单2-3 Chapter2Application.java(Chapter2工程)
package com.spring.cloud.chapter2.main;
/**** imports ****/
@SpringBootApplication(scanBasePackages = "com.spring.cloud.chapter2.*")
// 标识控制器
@Controller
// 请求前缀
@RequestMapping("/chapter2")
public class Chapter2Application {
public static void main(String[] args) {
SpringApplication.run(Chapter2Application.class, args);
}
// HTTP GET请求,且定义REST风格路径和参数
@GetMapping("/index/{value}")
public ModelAndView index(ModelAndView mav,
@PathVariable("value") String value) {
// 设置数据模型
mav.getModelMap().addAttribute("key", value);
// 请求名称,定位到Thymeleaf模板
mav.setViewName("index");
// 返回ModelAndView
return mav;
}
}
这里看一下index方法。首先是获取请求路径的参数,在数据模型中设置一个键为key的参数,然后再把视图名称设置为index,最后返回ModelAndView。因为这里返回的视图名称为index,所以需要在templates目录下新建一个视图index.html文件,如代码清单2-4所示。
代码清单2-4 /resources/templates/index.html(Chapter2工程)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试Thymeleaf</title>
</head>
<body>
<span th:text="${key}"></span>
</body>
</html>
这个HTML很简单,只需要解释一下加粗的代码就可以了。因为之前我们在数据模型中设置了键为key的参数,所以这里加粗的代码只是读取数据模型的这个参数而已。
到这里,一个简单的Spring Boot工程就开发好了。让我们以Java Application的形式运行代码清单2-3,这样就可以启动Spring Boot工程了。然后使用浏览器访问地址http://localhost:8001/chapter2/ index/myvalue,就可以看到如图2-5所示的界面了。
图2-5 测试Spring Boot工程
在Spring Cloud中,一个服务下可以包含多个实例,因此同一个工程可能需要在不同的配置(如端口)下启动。对于IDEA构建的工程,我们之前论述过,它会为我们创建application.properties文件,只是在分布式的开发环境下,更为流行的是YAML文件,所以本书都会将application.properties修改为application.yml文件。
为了更好地适应多个环境的运行,Spring Boot配置项会按照一定的优先级进行加载,优先级从高到低的顺序如下。
上面的顺序比较复杂,在大部分情况下,并不需要使用所有的配置。为了能够在IDEA工程中运行同一个项目的多个实例,可以使用很简易的方法。我们先在resources目录下新增两个配置文件application-peer1.yml和application-peer2.yml,然后进行配置,如代码清单2-5和代码清单2-6所示。
代码清单2-5 application-peer1.yml(Chapter2工程)
server:
#修改内嵌Tomcat端口为8001
port: 8001
代码清单2-6 application-peer2.yml(Chapter2工程)
server:
#修改内嵌Tomcat端口为8002
port: 8002
这里的两个文件只是修改了启动的端口而已,Spring Boot不会识别它们。为了让它们能够启动,我们需要修改application.yml文件,如代码清单2-7所示。
代码清单2-7 application.yml(Chapter2工程)
spring:
profiles:
# 设置环境变量,启用application-peer1.yml作为配置文件
# 需要启用配置文件启用application-peer2.yml时,只需要修改为peer2即可
active: peer1
这里配置项spring.profiles.active配置为peer1,这样就可以指向application-peer1.yml文件了,Spring Boot就会以application-peer1.yml文件作为配置文件在端口8001中启动项目。同理,如果将spring.profiles.active修改为peer2,则会使用application-peer2.yml文件配置的端口8002启动项目。这样一个工程就可以启动多个实例了。
但是在IDEA中,默认的情况下,只允许同一个Java文件启动一次。为此,让我们选择菜单Run→EditConfigurations...,打开图2-6所示的对话框。
图2-6 取消类的单例运行
将图2-6中红色方框圈起来的“Allow running in parallel”选项勾上,就可以在IDEA中让一个类运行多个实例了,然后就可以根据需要配置application.yml的配置项spring.profiles.active来选择具体的配置文件启动项目了。
当然,如果配置项比较少,例如,只需要考虑端口的改变,而不需要考虑其他复杂的配置,那么也可以使用命令行参数来实现上述的功能。这里,再看一下图2-6,选中“Chapter2Application”,然后点击左上角的复制键(),就可以看到一个新的运行配置,跟着修改其运行的名称和相关参数,如图2-7所示。
图2-7 配置Spring Boot的命令行参数
在图2-7中,运行的名称被修改为“Chapter2Application 2”,命令行参数server.port的值被修改为8002。使用同样的方法,也可以将运行名称为“Chapter2Application”配置的命令行参数server.port的值修改为8001,这样就可以得到两个运行的配置了。它们将根据命令行参数所配置的端口进行运行。正常配置完毕后,IDEA会提示打开Spring Boot的运行面板(Run Dashboard)。跟着打开它,就可以在运行面板中运行对应的Spring Boot工程了,这是非常方便的,如图2-8所示。
图2-8 Spring Boot运行面板
在图2-8所示的面板中,截取的是启动两个运行配置后的图,可以看到它们分别在8001和8002端口启动。
使用Maven构建工程,可以使用IDEA进行打包,也可以自己使用命令打包。关于IDEA打包,相关资料介绍比较多,所以这里就不再介绍了。这里主要介绍命令打包。首先自己安装好Maven,并且配置好Maven的环境,做好这些后,打开工程所在的目录(我的本地的目录为E:\IdeaProjects\chapter2),然后在命令行窗口输入:
mvn clean package
通过这个命令就能成功打包了。然后,打开工程目录下的target目录查看打包结果,如图2-9所示。
图2-9 Maven打包后的目录
打包结果是一个war文件。如果需要将它部署到第三方服务器,那么只需要将它放到第三方服务器的部署目录即可。例如,放到Tomcat的webapps目录下。Spring Boot还允许我们使用命令运行它,只要在这个目录下运行命令:
java -jar .\chapter2-0.0.1-SNAPSHOT.war
就可以运行工程了。只是它是以application.yml配置的配置项spring.profiles.active选择对应的配置文件进行运行。如果我们想使用自己的配置项,如想使用8003端口启动项目,那么可以通过命令参数来代替它,如执行下面的命令:
java -jar .\chapter2-0.0.1-SNAPSHOT.war --server.port=8003
这样,就能在8003端口启动服务了。如果需要指定配置文件,也可以使用命令行参数进行指定。例如,使用application-peer2.yml文件启动Spring Boot工程,就可以使用以下命令:
java -jar .\chapter2-0.0.1-SNAPSHOT.war --spring.profiles.active=peer2
Spring Boot,除了方便我们开发外,还提供了系统监控的功能。在需要对系统进行监控时,只需要引入Actuator就可以了。下面让我们在pom.xml文件中引入Actuator,如代码清单2-8所示。
代码清单2-8 pom.xml引入Actuator(Chapter2工程)
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
但是在Spring Boot 2.x之后,出于安全的考虑,大部分监控端点都不会直接暴露。要暴露这些端点,我们需要对YAML文件增加对应的配置,如代码清单2-9所示。
代码清单2-9 application.yml文件配置端点是否暴露(Chapter2工程)
management:
endpoints:
web:
exposure:
# 暴露的端点,“*”代表全部暴露
include : "*"
# 不暴露的端点
exclude : env
通过这些端点配置就可以暴露除了env之外的端点了。此时启动Spring Boot项目(假设在8001端口启动),在浏览器地址栏输入http://localhost:8001/actuator/beans,就可以看到如图2-10所示的场景了。
图2-10 查看beans监控端点
从图2-10中可以看出,通过请求,已经可以查看到Actuator提供的端点了,这说明Spring Boot项目已经被监控起来了。但是这样暴露端点也会存在一定的安全隐患,这个时候可以使用Spring Security来进行安全验证,规避这些安全隐患。这部分内容比较烦琐,在第18章会有介绍,这里就不演示了。
实际上,Spring Boot可监控的端点还有很多。
从上述开发中,我们可以看到,没有原有Spring项目的复杂度。在大部分情况下,想要自定义开发,只需要根据自己的需要配置application.yml(或者application.properties)即可。这便是Spring Boot的主导思维“约定优于配置”,这种思维极大地简化了Spring项目的开发,因此Spring Boot成了蓬勃发展的快速应用开发领域(rapid application development)的领导者。
Spring Cloud会通过Spring Boot的方式封装许多开源分布式框架和工具,形成许多能帮助我们构建微服务系统的简单易用的组件。因此,学习Spring Cloud之前需要先掌握Spring Boot。
REST风格是微服务推荐的各个系统交互的方式,所以这是需要掌握的内容之一。在HTTP发展的过程中,制定了很多规范,而这些规划是相当复杂的。为了简化HTTP协议的编程,Roy Thomas Fielding在他2000年的博士论文中提出了REST风格。Fielding博士是HTTP协议(1.0版和1.1版)的主要设计者,Apache服务器软件的作者之一,Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了广泛的关注,并且对互联网开发产生了深远的影响。Fielding将他对互联网软件的架构原则,命名为REST。注意,REST不是一个标准而是一种风格,一旦你的架构符合REST风格的原则,就可以说你在采用REST风格架构了。
REST的全称为Representational State Transfer,中文可翻译为表现层状态转换。理解它的关键在于名称的解释,这里有3个关键的名词。
根据上述的3个名词,REST风格做了如下约定。
这里,每一个访问资源的URI也可以称为REST风格的一个端点(EndPoint)。关于这里的HTTP动作这个概念,还需要进一步的解释。在HTTP协议中,常见的动作(请求)主要有7种:GET、POST、PUT、PATCH、DELETE、HEAD和OPTIONS。但在实际开发中,主要有4种:GET、POST、PUT和DELETE。因此,这里我们就介绍这4种。
这里,我没有谈PATCH请求,它被定义为提交部分资源的属性,让服务器修改资源对应提交的属性。但是,因为很多现有的Java API,对它的支持都有限,所以经常会引发没有必要的异常。因此,我在开发时,经常用PUT请求来代替它。
为了更好地描述REST风格,下面举一些URI的例子来说明。
# GET动作代表获取资源,fund/account代表资金账户,{id}代表占位符,表示获取资金账户的信息
# 例如 GET /fund/account/1,就表示获取账户id为1的资金账户的信息
GET /fund/account/{id}
# GET动作表示获取资源,fund/accounts代表资金账户,accounts为复数名称,表示返回结果也为复数
# {accountName}代表占位符,表示按照账户名称查询账户信息
# 例如 GET /fund/accounts/张三 就代表按账户名称“张三”进行查询
GET /fund/accounts/{accountName}
# POST动作代表创建某个资源,fund/account代表资金账户,一般可以以请求体进行提交
# 现今的请求体主要以JSON为主,后续会加以介绍
POST /fund/account
# PUT动作代表提交资源全部属性进行修改资源,这里的/fund/account表示资金账户,
# 注意:在实现中,因为PATCH动作在某些API中存在较多问题,所以我经常使用它进行部分属性提交
PUT /fund/account
# DELETE表示删除资源,{id}为编号,例如DELETE /fund/account/1,表示删除编号为1的账户
DELETE /fund/account/{id}
这里对于GET/fund/accounts/{accountName}这类查询的请求可能存在一个问题:如果查询条件过多,这个URI就要写得相当复杂。这个时候可以考虑换为POST请求,提交请求体以简化它的开发。此外,在开发REST风格的过程中还容易犯设计URI的错误,下面举几个常见的设计错误的例子进行说明。
# 这里的URI是只存在名词的,而get是动作,所以不符合REST风格的规则
GET /fund/account/get/{id}
# 这里的v1代表版本,我们之前论述过,在REST风格中,一个资源只对应一个URI,
# 所以这里也是不符合REST风格的。关于版本,我们可以使用请求头参数来设置,
# 如Accept: version = v1,这样就可以把URI设计为GET /fund/account/{id},
# 通过请求头参数来控制版本编号了
GET /fund/account/v1/{id}
# 这里的请求参数不在URI中,也是不符合REST风格的,为此可以修改为:
# PUT /fund/account/1/account_1,后端只需要从请求路径中获取参数即可
PUT /fund/account?Id=1&accountName=account_1
为了更好地展示REST风格,我们将基于2.1节中的Spring Boot工程来开发REST风格的端点。为此,我们在Spring Boot工程中新建控制器,如代码清单2-10所示。
代码清单2-10 新增控制器(AccountController)
package com.spring.cloud.chapter2.controller;
/** imports **/
@Controller
@RequestMapping("/fund")
public class AccountController {
// 返回账户thymeleaf页面
@GetMapping("/account/page")
public String page() {
return "account";
}
}
在本节的测试中,我们会使用账户实体(Account)和结果响应对象(ResultMessage),如代码清单2-11和代码清单2-12所示。
代码清单2-11 账户实体(Account)
package com.spring.cloud.chapter2.pojo;
public class Account {
private Long id;
private String accountName;
private Double balance;
private String note;
/** setters and getters **/
}
代码清单2-12 结果消息(ResultMessage)
package com.spring.cloud.chapter2.vo;
public class ResultMessage {
private boolean success;
private String message;
public ResultMessage() {
}
public ResultMessage(boolean success, String message) {
this.success = success;
this.message = message;
}
/**** setter and getter ****/
}
代码清单2-10中的page方法返回一个字符串,用来打开Thymeleaf页面,这个页面的内容如代码清单2-13所示。
代码清单2-13 Thymeleaf测试页面(/resources/templates/account.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>REST风格测试</title>
<!-- 引入jQuery-->
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
<!--此处加入测试JavaScript代码-->
</script>
</head>
<body>
</body>
</html>
在这个页面引入了jQuery,这样方便对REST端点进行测试。在下面的测试中,只要在加粗注释处引入对应的JavaScript代码,就可以进行测试了。有了上述的准备,我们在AccountController里,加入相关的REST端点,这里是GET请求,它在REST风格里代表的是获取资源,如代码清单2-14所示。
代码清单2-14 获取账户(HTTP GET请求)
@GetMapping("/account/{id}") // @GetMapping代表GET请求
@ResponseBody // 结果转换为JSON
public Account getAccount(@PathVariable Long id) {
Account account = new Account();
account.setId(id);
account.setAccountName("account_" + id);
double balance = id %10 * 10000.0 * Math.random();
account.setBalance(balance);
account.setNote("note_" + id);
return account;
}
这里使用了注解@GetMapping,这是Spring 4.3版本后的注解,代表HTTP的GET请求。此外,@PostMapping代表POST请求,@PutMapping代表PUT请求,@DeleteMapping代表DELETE请求……@ResponseBody则代表表现层(展示数据类型)为JSON数据集。在URL的设计中,使用了{id}占位,这是REST风格的特点,在Spring MVC中是使用@PathVariable获取URI中的参数。为了测试这段代码,在代码清单2-13中加入测试的JavaScript,如代码清单2-15所示。
代码清单2-15 测试GET请求
function get() {
$.get("./1",{}, function(result) {
alert(JSON.stringify(result));
})
}
接下来,我们来开发POST请求的REST端点。为此,在类AccountController里加入代码清单2-16所示的内容。POST请求在REST风格里代表的是创建资源。
代码清单2-16 POST端点
@PostMapping("/account") // POST请求
@ResponseBody
public Account createAccount(@RequestBody Account account) {
long id = (long)(10000.0*Math.random());
account.setId(id);
return account;
}
这里使用@PostMapping代表POST请求,在方法参数里面使用@RequestBody代表接收客户端发送过来的类型为JSON的请求体,然后将请求体转换为账户类型(Account)。下面我们编写JavaScript进行测试,如代码清单2-17所示。
代码清单2-17 测试POST端点
function post() {
// 向后端提交的账户对象
var account = {
accountName: "account_name_x",
balance : 12345678.90,
note : "note_x"
}
$.post({ // POST请求
url:"./../account",
// 设置请求体为JSON类型
contentType: "application/json",
// 提交请求体
data : JSON.stringify(account),
success : function(result) {
alert(JSON.stringify(result));
}
});
}
这里加粗的代码的作用是,将请求体声明为JSON类型。这步是注解@RequestBody接收JSON数据的基础,是不能缺少的,这样就可以通过POST请求将对象提交到后端了。
有时候,我们也要修改资源。修改资源使用的是PUT请求,为此,我们在AccountController中加入代码清单2-18所示的代码。
代码清单2-18 PUT端点
@PutMapping("/account") // HTTP PUT请求
@ResponseBody
public ResultMessage updateAccount(@RequestBody Account account) {
System.out.println("更新账户");
return new ResultMessage(true, "更新账户成功");
}
这里和POST请求差不多,只是修改为了PUT请求。为了测试PUT请求,我们可以使用代码清单2-19中的JavaScript。
代码清单2-19 测试PUT端点
function put() {
var account = {
id : 8765,
accountName: "account_name_x",
balance : 12345678.90,
note : "note_x"
}
$.ajax({
url:"./../account",
// 定义为HTTP PUT请求
type :"PUT",
contentType: "application/json",
data : JSON.stringify(account),
success: function(result) {
alert(JSON.stringify(result));
}
});
}
这里的Ajax请求中,将type设置为了PUT,所以这是一个PUT请求。其他的和POST请求接近,就不再多加论述了。
有时我们也需要删除资源,这便是HTTP的DELETE请求了,如代码清单2-20所示。
代码清单2-20 DELETE端点
@DeleteMapping("/account/{id}") // DELETE请求
@ResponseBody
public ResultMessage deleteAccount(@PathVariable("id") Long id) {
System.out.println("删除账户");
return new ResultMessage(true, "删除账户成功");
}
这里使用了@DeleteMapping,它代表HTTP的DELETE请求。这里将id参数设计为通过URL传递,然后使用注解@PathVariable进行获取。为了测试这段代码,我们可以使用代码清单2-21进行验证。
代码清单2-21 测试DELETE端点
function del() {
$.ajax({
type :"DELETE",
url : "./../account/897",
success : function(result) {
alert(JSON.stringify(result));
}
});
}
这里和PUT请求差不多,只是将请求类型修改为了DELETE而已,这样就可以对后端发起DELETE请求了。
当对后端发送POST请求新增资源时,正常情况下,应该返回回填了主键的对象信息,此时请求就会返回成功的状态码(200)。但事实上,这还不够准确,如果需要更准确,状态码应该为201,它代表创建资源成功。从另一个角度来说,提交请求后,后端也可能发生异常,当发生异常时,就不是一个正常的返回了。对于这样的情况,如果我们只是获得服务器状态码,而没有其他的信息,那么将无法对异常进行分析。为了处理这些问题,我们还可以使用状态码和响应头信息。本节我们将学习这些知识。
Spring MVC提供了类ResponseEntity<T>给我们使用,通过它可以设置请求头和状态码。下面在AccountController中新增一个新的POST请求REST风格端点,如代码清单2-22所示。
代码清单2-22 新增POST端点(使用ResponseEntity<T>)
@PostMapping("/account2") // POST请求
@ResponseBody
public ResponseEntity<Account> createAccount2(@RequestBody Account account) {
ResponseEntity<Account> response = null;
HttpStatus status = null;
// 响应头
HttpHeaders headers = new HttpHeaders();
// 异常标志
boolean exFlag = false;
try {
long id = (long)(10000.0*Math.random());
account.setId(id);
// 测试时可自己加入异常测试异常情况
throw new RuntimeException();
} catch(Exception ex) {
// 设置异常标志为true
exFlag = true;
}
if (exFlag) { // 异常处理
// 加入请求头消息
headers.add("message", "create account error,plz check ur input!!");
headers.add("success", "false");
// 设置状态码(200-请求成功)
status = HttpStatus.OK;
} else { // 创建资源成功处理
// 加入请求头消息
headers.add("message", "create account success!!");
headers.add("success", "true");
// 设置状态码(201-创建资源成功)
status = HttpStatus.CREATED;
}
// 创建应答实体对象返回
return new ResponseEntity<Account>(account, headers, status);
}
这段代码中加入了异常代码,这样会将异常标志设置为true。在返回ResponseEntity<Account>对象前,创建了响应头对象HttpHeaders,设置了它的两个消息success和message,并且根据是否发生异常将响应码设置为201(创建资源成功)或者200(成功,但返回错误消息)。最后,才通过这些数据来创建ResponseEntity<Account>对象进行返回。下面我们来测试一下这段代码,在页面加入下面这段JavaScript脚本,如代码清单2-23所示。
代码清单2-23 测试POST端点的异常处理
function post2() {
// 向后端提交的账户对象
var account = {
accountName: "account_name_x",
balance : 12345678.90,
note : "note_x"
}
var result = $.post({ // POST请求
url:"./../account2",
// 设置请求体为JSON类型
contentType: "application/json",
// 提交请求体
data : JSON.stringify(account),
success : function(result, status, xhr) {
// 获取响应码
var status = xhr.status;
// 获取响应头消息
var success = xhr.getResponseHeader("success");
// 判断响应头成功标志
if (success === "true") { // 成功
alert(JSON.stringify(result));
} else { // 请求错误处理
// 请求头错误消息
var message = xhr.getResponseHeader("message");
alert("success:"+success + ", message:" + message);
}
}
});
}
这里加粗的函数是需要关注的重点。在这个函数中,首先获取了响应码和请求头的消息,然后通过对成功标志位success的判断来确定后端处理是否已经成功。如果成功则打印正常返回的消息,否则打印请求头错误消息。当使用这段代码测试的时候,可以看到图2-11所示的结果。
图2-11 获取响应头信息
通过对响应码和响应头消息的分析,我们可以获取是否发生异常以及相关信息,进行下一步处理。代码清单2-22中的代码可读性不好,重用率也不高。我们可以通过封装ResponseEntity<T>对象的生成更好地进行开发,如代码清单2-24所示。
代码清单2-24 生成ResponseEntity<T>对象的工具类
package com.spring.cloud.chapter2.response.utils;
/** imports **/
public class ResponseUtils {
/**
* 获取请求结果响应对象
* @param data -- 封装的数据
* @param status -- 响应码
* @param success -- 成功标志
* @param message -- 响应结果消息
* @param <T> -- 封装数据泛型
* @return HTTP响应实体对象
*/
public static <T> ResponseEntity<T> generateResponseEntity(
T data, HttpStatus status, Boolean success, String message) {
// 请求头
HttpHeaders headers = new HttpHeaders();
headers.add("success", success.toString());
headers.add("message", message);
ResponseEntity<T> response = new ResponseEntity<>(data, headers, status);
return response;
}
}
这个类对ResponseEntity<T>对象的生成进行了封装,这简化了后续的使用。基于这个工具类,我们可以把代码清单2-23修改为代码清单2-25。
代码清单2-25 重构POST端点代码
@PostMapping("/account2") // POST请求
@ResponseBody
public ResponseEntity<Account> createAccount2(@RequestBody Account account) {
// 异常标志
boolean exFlag = false;
try {
long id = (long)(10000.0*Math.random());
account.setId(id);
// 测试时可自己加入异常测试异常情况
throw new RuntimeException();
} catch(Exception ex) {
// 设置异常标志为true
exFlag = true;
}
return exFlag ?
ResponseUtils.generateResponseEntity(account, // 异常处理
HttpStatus.OK, false, "create account error,plz check ur input!!") :
ResponseUtils.generateResponseEntity(account, // 正常返回
HttpStatus.CREATED, true, "create account success!!");
}
显然,在加粗代码处使用的工具类大大提高了代码的可读性和可重用率,更有利于我们后续的开发。
在上节中,我们讨论了REST风格端点的开发,但在微服务的开发中,推荐的是使用REST风格进行微服务系统之间的交互。在Spring中,提供了RestTemplate这样的模板来调用REST风格的请求,以简化我们的开发。在Spring Cloud的Ribbon中,也是使用它为主的,所以这里将讲解它的使用。本节会使用RestTemplate对2.2.2节和2.2.3节开发的REST端点进行请求,通过这样来讲解RestTemplate的使用。
当一个微服务系统需要向另外一个服务获取数据的时候,往往使用的都是GET请求,这是在实际工作中使用得最多的场景,如代码清单2-26所示。
代码清单2-26 使用RestTemplate请求REST风格GET动作端点
public static void get() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8001/fund/account/{id}";
// GET请求,返回对象
Account account = restTemplate.getForObject(url, Account.class, 1L);
System.out.println(account.getAccountName());
}
注意这里URL的编写,它有1个占位字符串{id},这代表可以接收一个参数。在使用RestTemplate的getForObject方法时,采用了3个参数:第一个是请求地址url;第二个是Account.class,表示请求将返回什么泛型的对象;第三个是1L,一个长整型(Long)参数,它对应URL中的占位符{id}。这样就可以完成对REST风格下的GET请求了。
接着就是POST请求。POST请求意在新增资源,如代码清单2-27所示。
代码清单2-27 使用RestTemplate请求REST风格POST动作端点
public static void post() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8001/fund/account";
// 请求头
HttpHeaders headers = new HttpHeaders();
// 设置请求体为JSON
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
Account account = new Account();
account.setAccountName("account_xxx");
account.setBalance(12345.60);
account.setNote("account_note_xxx");
// 封装请求实体对象,将账户对象设置为请求体
HttpEntity<Account> request = new HttpEntity<>(account, headers);
// 发送POST请求,返回对象
Account result = restTemplate.postForObject(url, request, Account.class);
System.out.println(result.getId());
}
因为POST请求需要提交的是JSON数据集,所以这里先处理了请求头(headers),并将请求体设置为了JSON的数据集。接着使用请求实体(HttpEnity)对象,封装了请求头和账户信息。使用了RestTemplate的postForObject方法来提交POST请求。在这个方法中,第一个参数是URL;第二个参数request是一个请求实体对象,它封装了请求头和账户信息;第三个参数Account.class,代表的是返回的对象的类型。通过这样就能够发起POST请求了。
有时候,我们需要修改资源,这时需要使用PUT请求。PUT请求和POST请求差不多,所以很多时候可以参考POST请求编写PUT请求,代码清单2-28就是这样的。
代码清单2-28 使用RestTemplate请求REST风格PUT动作端点
public static void put() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8001/fund/account";
// 请求头
HttpHeaders headers = new HttpHeaders();
// 设置请求体媒体类型为JSON
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
Account account = new Account();
account.setAccountName("account_xxx");
account.setBalance(12345.60);
account.setNote("account_note_xxx");
// 封装请求对象
HttpEntity<Account> request = new HttpEntity<>(account, headers);
// 发送请求
restTemplate.put(url, request);
}
这里的内容和POST请求差不多,只是注意加粗的代码,对于put请求是无返回值的。
有时候,我们还需要删除资源,这时需要使用DELETE请求。使用DELETE请求时要慎重,因为数据一旦删除就再也找不回来了。一般来说,删除会以主键(PK)作为参数进行删除。代码清单2-29就是这样的。
代码清单2-29 使用RestTemplate请求REST风格DELETE动作端点
public static void delete() {
RestTemplate restTemplate = new RestTemplate();
// {id}是占位
String url = "http://localhost:8001/fund/account/{id}";
// DELETE请求没有返回值
restTemplate.delete(url, 123L);
}
这里的代码比较简单,和PUT请求一样,RestTemplate的delete方法也没有返回值。
上述就是简单的增删查改,但是有时候提交数据进行新增资源操作,未必会成功。在不成功的情况下,服务端可能会像2.2.3节中那样返回对应的状态码和响应头。这时就需要对状态码和响应头进行分析了。下面将使用RestTemplate对代码清单2-24进行请求,如代码清单2-30所示。
代码清单2-30 使用RestTemplate获取HttpEnity对象
public static void post2() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8001/fund/account2";
// 请求头
HttpHeaders headers = new HttpHeaders();
// 设置请求体为JSON
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
Account account = new Account();
account.setAccountName("account_xxx");
account.setBalance(12345.60);
account.setNote("account_note_xxx");
// 封装请求对象
HttpEntity<Account> request = new HttpEntity<>(account, headers);
// 发送请求
ResponseEntity<Account> result
= restTemplate.postForEntity(url, request, Account.class);
// 获取响应码
HttpStatus status = result.getStatusCode();
// 获取响应头
String success = result.getHeaders().get("success").get(0);
// 获取响应头成功标识信息
if ("true".equals(success)) { // 响应成功
Account accountResult = result.getBody();// 获取响应体
System.out.println(accountResult.getId());
} else { // 响应失败处理
// 获取响应头消息
String message = result.getHeaders().get("message").get(0);
System.out.println(message);
}
}
这里的请求代码和代码清单2-27中的POST请求接近,所以不再赘述。发送请求的这步使用的是postForEntity方法,它将返回一个响应实体(ResponseEntity)对象,通过该对象可以获取响应码和响应头的信息。在代码中,我通过它获取了响应头中的成功标志参数success,并且通过它来判断请求是否成功。如果成功,则获取响应体打印后端返回的编号(id);否则,获取响应头中的错误消息,将其打印出来。这样就能够判断后端响应是否正常,如果是非正常响应,就获取服务端返回的错误消息自行处理业务。