Java RESTful Web Service实战(第2版)-阿里云开发者社区

开发者社区> 华章出版社> 正文

Java RESTful Web Service实战(第2版)

简介:

b88df968721a0a5a4527a798a94fb1a2f17c545a

Java核心技术系列

Java RESTful Web Service实战

(第2版)

韩陆 著





图书在版编目(CIP)数据

Java RESTful Web Service实战 / 韩陆著. —2版. —北京:机械工业出版社,2016.7

(Java核心技术系列)

ISBN 978-7-111-54213-1

Ⅰ. J…   Ⅱ. 韩…   Ⅲ. JAVA语言-程序设计   Ⅳ. TP312

中国版本图书馆CIP数据核字(2016)第156331号



Java RESTful Web Service实战(第2版)

出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037)

责任编辑:李 艺 责任校对:董纪丽

印  刷: 版  次:2016年8月第2版第1次印刷

开  本:186mm×240mm 1/16 印  张:18.75

书  号:ISBN 978-7-111-54213-1 定  价:59.00元

凡购本书,如有缺页、倒页、脱页,由本社发行部调换

客服热线:(010)88379426 88361066 投稿热线:(010)88379604

购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com

版权所有·侵权必究

封底无防伪标均为盗版

本书法律顾问:北京大成律师事务所 韩光/邹晓东





Foreword 第2版序一

韩陆是我在阿里巴巴的同事,业余时间大家经常一起聊新的Java技术。REST对当前软件开发非常重要,除了我们一直了解的Service API、Open API、移动端对后端的Gatway API调用(这些基本都是REST模式设计的),现在很多的DevOps操作也是通过REST API完成的,如我们了解的Docker和SpringBoot Actuator API都是REST风格的,另外HTTP/2的逐步采用,也为REST带来更多的功能和性能的提升。对Java程序员来说,本书非常难得,你可以深入了解JAX-RS标准和Jersey框架;为了方便落地开发,书中更着重介绍了Spring Boot和Spring Cloud,这些知识目前涉及的中文图书并不多;最后结合Docker容器技术,给出了完整基于SpringBoot REST服务应用容器部署的思路。本书的每一个技术点都可以单独成书,用以详细阐述,但是能够浓缩到一本图书中,挑战和难度确实比较大,希望这些新的技术和思想能够帮助到真正前进的程序员。


阿里巴巴资深技术专家、速卖通中间件掌门人 陈立兵(花名:雷卷)





第2版序二 Foreword

认识韩陆已有近10 年的时间,那时他在北京航空航天大学软件学院做硕士毕业论文。他的论文写的是面向对象数据库引擎的设计与实现,完全自主实现了包括文件读写、缓存、索引和事务、数据访问接口等完整的面向对象数据库引擎。从那时就了解到他是一位技术达人,喜欢钻研和实践各种最新的技术。所以当听说他出版本书时一点都不觉得意外,他就是这样一个热衷于新技术的人。

早在本书第1版的时候,他就找到我希望为本书写一篇序,那时我婉拒了,因为我本人对RESTful相关技术并没有太多的接触,不敢贸然推荐。作为一种轻量级Web服务实现架构,两年多来RESTful架构得到了普遍认可和使用;越来越多的学生也开始学习相关的技术,而本书就是这方面非常有意义的参考资料。书中首先系统地解读了JAX-RS2标准,之后基于JAX-RS标准的参考实现:Jersey开发框架,系统地讲解了如何基于该框架开展RESTful Web服务的实践。本书实践性很强,体系较为完整,涵盖了RESTful Web服务开发各个层面的问题;书中不仅提供了一些典型场景的代码示例,还有完整的项目案例的讲解,这些实践代码能够有助于读者开展具体的项目实践。与第1版相比,第2版还新增了有关微服务和容器化等目前热门应用技术实践的内容,有助于读者了解最新的技术发展方向。


北京航空航天大学 谭火彬





Foreword 第1版序一

——REST开发的理想与现实

REST是一种分布式应用的架构风格,也是一种大流量分布式应用的设计方法论。REST是由(构成了Web基础架构的)HTTP、URI等规范的主要设计者Roy Fileding博士在其2000年的博士论文(中文版名为《架构风格与基于网络应用软件的架构设计》)中提出的。到目前为止,关于REST最系统、最全面的论述,仍然是Fielding的博士论文。

REST就是Web(World Wide Web,简称Web或者WWW)本身的架构风格,是设计、开发Web相关规范、Web应用、Web服务的指导原则。不符合REST风格要求的架构和技术,很难在Web这个生态系统中得到繁荣发展。在我看来,Roy Fielding博士就是15年以来对于分布式应用架构设计理论贡献最大的人。Fielding在HTTP规范的设计过程中,并没有采用当时大行其道的DO(Distributed Object,分布式对象)风格,而是自出机杼、另辟蹊径,提出了一整套新的设计方法论。Fielding的开创性工作,极大地推动了分布式应用设计理论的发展。

有趣的是,其实基于SOAP/WSDL的“大Web Service”(以下简称Web Service),几乎是与REST同时发展起来的。虽然在Web Service中也使用了对象,但是Web Service其实是RPC风格的,而不是DO风格的。Web Service在最初几年发展很快,很大原因是它解决了DO风格难以解决的异构系统(不同的硬件系统、不同操作系统、不同的编程语言,等等)之间互操作性的问题。

然而遗憾的是,设计Web Service协议栈的核心人员,几乎都是来自于企业应用阵营的,尤其是来自于IBM和微软两家公司的人。这些企业应用的专家们没有充分认识到Web基础架构的巨大优点,甚至可以说并没有理解HTTP协议究竟是用来做什么的、为何要如此设计。在Web Service协议栈的设计之中,仍然有深深的企业应用痕迹。Web Service虽然宣称能够很好地支持互操作,然而因为协议栈的复杂性很高,在实战中互操作性并不好(例如升级过程困难而且复杂)。此外,Web Service仅仅将HTTP协议当做一种传输协议来使用,还依赖XML这种冗余度很高的文本格式,这导致Web Service应用性能低下。很多开发团队宁可使用Hessian等轻量级的RPC协议,也不愿意使用Web Service。在面向互联网的大流量Web应用(包括Web服务在内)这种运行环境中,Web Service在复杂性、互操作性、性能、可伸缩性等方面的短板更加突出。因此,设计今日面向互联网的API,已经很少有人会考虑Web Service。这使得Web Service的使用被局限在企业应用运行环境之中,其名称中的“Web”更像是一个笑话(除了都使用HTTP协议,基本上与Web没什么关系)。假如在2000年,设计Web Service规范的专家们,能够认真读一下Fielding的博士论文,或者找HTTP、URI等Web基础架构规范的核心设计人员深入交流一下,Web Service很可能就不是现在这个样子了。不过,历史是无法假设的。

在Java世界中,与大Web Service相对应的规范是JAX-WS。在大Web Service已经成为明日黄花之后,Java世界急需一套新的规范来取代JAX-WS。这套新的规范就是JAX-RS:Java世界开发RESTful Web Service(与RESTful API含义相同,可混用)的规范。虽然起步很晚,毕竟走上了正确的道路。

从Java EE 6开始,JAX-RS在Java EE版图中,作为最重要的组成部分之一,逐步取代了JAX-WS的地位。在所有Java EE相关规范中,JAX-RS是优点很突出的一个。例如,完全基于POJO、很容易做单元测试、将HTTP作为一种应用协议而不是可替代的传输协议(因此提高了性能)、优秀的IDE集成,等等。可以说,在大多数场合,JAX-RS完全可以取代JAX-WS,作为Java Web Service开发的主要技术。JAX-RS同样也可以完全取代Hessian等基于HTTP协议的RPC风格远程调用协议。毕竟HTTP本身就是一种REST风格的应用协议,以REST风格来使用HTTP,才是最高效的使用方式。

Jersey、CXF等支持JAX-RS规范的REST开发框架还支持输出WADL。WADL支持客户端代码自动生成,还可以将WADL导入到SoapUI等测试工具中,然后做自动化集成测试。从开发Java企业应用、取代JAX-WS的角度来看,JAX-RS已经做得非常棒了。

尽管如此,不可不提的是,JAX-RS这套规范,仍然存在着很多遗憾。需要特别指出的是,JAX-RS规范并不等于REST架构风格本身,REST的内涵要比JAX-RS广泛得多。学会了使用JAX-RS了,并不等于就完全理解了REST,开发者仍然需要下工夫认真学习一下本源的REST究竟是什么。

例如,JAX-RS规范对于应该如何定义一个资源,以及应该如何使用HTTP作为一个统一接口来操作资源,显然缺乏必要的指导。有很多开发者只是简单地将以前JAX-WS中的一个endpoint接口转换成一个资源接口。接口的方法太多,导致映射到的HTTP方法不够用,这也难不倒他们,在URI路径中加一些字符串就能够解决(例如,三个接口方法都映射到POST,但是其PATH不同)。这样的开发方式非常常见,虽然开发者使用了JAX-RS规范,但是开发方式完全是RPC风格的,可以说与REST风格没有任何关系。

此外,JAX-RS规范目前尚不支持HATEOAS(将超文本作为应用状态的引擎,REST风格的核心特征之一),从著名的Richardson成熟度模型(由《RESTful Web APIs》的作者Richardson提出)来衡量,基于JAX-RS规范实现的RESTful API仅仅能够达到成熟度模型的第二级,即支持资源抽象、统一接口的“CRUD式Web服务”。

可以这样说,JAX-RS规范与真正的REST风格,覆盖的范围其实是不同的。JAX-RS覆盖的是简单基于HTTP协议(没有使用SOAP/WSDL)的各种远程调用需求,很多需求对于可伸缩性、松耦合的要求并不高,仅仅是希望使用HTTP本身来取代大Web Service作为一种轻量级、容易测试的远程调用协议。REST架构风格的严格要求,在这些场合并不是非常重要。慵懒是人类的天性,大多数开发者写代码只是为了解决手头的问题,JAX-WS并不好用,JAX-RS解救了他们。

如果按照Roy Fielding博士的严格要求(REST APIs must be hyper-text driven),那么包括JAX-RS规范在内都不能算是真正的RESTful。然而,从实战角度,我认为革命不分先后,只要能够达到Richardson成熟度模型第一级,即有清晰的资源抽象,就可以认为是RESTful API了。如果连第一级都达不到,所设计的架构根本就不是面向资源的,那八成还是RPC风格的,就没有必要非要往RESTful API阵营里面挤了。从来没有人说过RPC就是万恶的,RPC在企业应用的大多数场合其实都非常有效,只是不适合面向互联网的大流量Web应用而已。

因此,能够完美支持HATEOAS,攀登到成熟度模型第三级,是一种理想情况(当然也是值得追求的)。而通过部分拥抱REST风格的要求,来更好地解决手头的问题,是更多开发者所面对的现实情况。JAX-RS反映的正是这种现实情况,从实战的角度,它是一套非常有用也很好用的规范。

韩陆兄的新著《Java RESTful Web Service实战》是JAX-RS规范方面的专著,也是国内第一本REST开发的原创著作。这本书的实战性非常强,全面介绍了JAX-RS 2.0的方方面面,可以作为一线Java分布式应用开发者的案头必备书。如同我在前面所指出的,JAX-RS规范并不等于REST架构风格本身,它们有着不同的覆盖范围。在本书中,作者也介绍了很多设计RESTful API的最佳实践,这些内容假如读者不理解REST,甚至在亲自阅读了JAX-RS规范之后也未必能够总结出来。读者在阅读本书的过程中,不应该仅仅满足于掌握了JAX-RS开发的基本方法、解决了手头的问题、用其完全取代JAX-WS,更重要的是,读者还应该就REST架构风格本身做更多的学习。幸运的是,除了本书之外,目前REST设计和开发方面的图书资料已经非常多了。

本书的内容非常严谨,有非常好的系统性,对于设计开发大流量Web服务会面临的各种问题都有涉及。特别是在自动化测试方面着墨颇多,在我看来是本书的一大亮点。RESTful API的自动化测试非常重要,需要在设计之初就充分考虑到。本书是一本难得的原创佳作,值得所有Java分布式应用的开发者购买。

理想富丽丰满,现实贫瘠骨感,追求理想和注重解决现实问题其实并不矛盾。JAX-RS规范的发展,反映出了Java社区在更好地开发RESTful Web Service方面的求索。尽管存在争议,在我看来,规范化是推动RESTful Web Service取得更大发展的必由之路。目前对于优秀的RESTful API有哪些判断标准,Web开发者社区已经达成了高度共识,也积累了大量非常有价值的成果。JAX-RS规范的发展,离不开Web开发者社区的这些成果。在未来的JAX-RS 3.0规范中,我们将会看到更多令人兴奋的成果被规范化。JAX-RS 2.0已经做得不错了,但是在RESTful Web Service规范化的道路上,其实才刚刚起步,任重而道远。


李锟 于上海





Foreword 第1版序二

半年前初识韩陆的时候,我们就聊到他正在写的这本书,当得知我从2006年就参与了Apache CXF开发,他立即邀请我为他的新书写序,我也就欣然答应了。

Apache CXF作为JAXWS以及JAX-RS规范的实现框架,已经成为很多Web服务开发者必选的开发框架。作为这一框架的开发维护者之一,我的日常工作经常需要熟悉这些JSR规范,并实现JSR所定义的API,解决最终用户的使用问题。

熟悉Java的人大多都听说过JSR(Java Specification Requests)、JCP(Java Community Process),通过JSR可以就Java某一方面的应用定义一组标准的API或者服务。对于最终用户来说,他们的代码只需要调用JSR定义的标准API,不做任何修改就可以调用不同的JSR实现。这里常见的例子就是Java Servlet应用,用户开发的Web应用可以不做任何修改就部署到Tomcat、JBoss等不同的Web容器中。

JAXRS是JCP为Java RESTful Web Service定义的一套API。由于Web服务的描述模型与Java类和接口有一定的差距,JAX-RS定义了很多annotation,通过这些annotation我们可以很方便地将Java类描述成为相关的REST服务。由于RESTful Web Service通常需要部署到Web容器中,JAX-RS也定义了相关服务来发现部署到容器中的JAX-RS应用。

读过JSR规范的朋友或多或少都会有这样的体会,JSR作为规范文档,其目标是将API定义以及实现功能描述清楚、完备,其目标读者是相关API的实现人员,或者是相关API的高级使用人员。如果读者对相关的背景知识还不熟悉的话,JSR文档读起来会比较晦涩而且难以理解。加之绝大部分JSR文档都没有相关的中文翻译,对于绝大多数初学者来说,通过阅读JSR文档来学习相关的API的知识是一个艰难的过程。

如果我们想要对JAX-RS规范有一个比较快速并且全面的了解应该怎么办呢?一般来我们可以通过JSR的相关参考实现入手,我们不但可以通过运行相关的参考实现的例子快速入门,还可以通过跟踪相关的代码对实现细节有一个全面的了解。韩陆的这本新作以JAX-RS的参考实现Jersey为蓝本,由浅入深地向大家介绍了JAX-RS的由来,以及与RESTful Web服务开发的相关API,并结合实例分享了作者的实战经验。

好了,现在打开你熟悉的IDE工具,加载Jersey代码库,沿着本书的指引去探索Java RESTful Web Services开发世界吧。


RedHat姜宁




Preface 前  言

本书第1版发行后,Jersey版本从2.9更新到了2.22.2,此间REST服务得到了更广泛的认可和使用。与此同时,Java 8、Spring Boot和Docker的爆发式发展,使得Java领域的RESTful开发有了新的发展。

第2版变更

迫不及待,这是我想为读者更新REST服务新发展的心情,遂有此第2版。首先,我们要拥抱Java 8。lambda表达式在大数据处理,尤其在Spark中是默认的语法表达;Java 8带给我们的不只是“语法糖”,而是开发和执行效率的提升。我从实践中得到了其中的好处,也希望读者能跟上时代的步伐。其次是Spring Boot,这是Java领域实现微服务的事实标准框架。我已经无法回去适应部署war到Tomcat的时代,请保守的读者原谅我的情不自禁。再次是Docker,我希望读者具备使用Docker完成开发自测阶段的一切,也希望读者能运用Docker实现微服务的部署和可伸缩实践。

从第1版第1次印刷至今,我始终关注着读者的反馈。邮件都做了认真的回复。根据读者的反馈,我在第2版中重新梳理了章节的结构,删除了第1版中反馈不好的第9章和第11章,调整后的章节与第1版的对应关系如下。

第1章合并了第1版的第1章和第2章。

第2章对应第1版第3章。

第3章对应第1版第4章。

第4章包含了第1版的第8章。

第5章在第1版的基础上做了更新。

第6章包含了第1版的第7章,并升级了第1版2.5节的示例。

第7章和第8章是新增章节。

第9章对应第1版第10章。

第10章包含了第1版的第6章。

与许多技术作者一样,写书的时间是挤出来的。如果精力尚可,每晚7点到9点、11点到凌晨2点是我动笔的时间,偶尔,早上6点到8点我也会赶赶。写书成为我梳理、总结和思考的最佳方式。

于此过程,我总结了3句话与读者共享。搞技术的人,是停不下来的。时而要开疆拓土,学习和研究新的知识点,弥补自己的技术债;时而要运筹帷幄,将知识点梳理成线,编织成网;时而要深耕细作,面对当下要攻坚的业务所对应的知识点,深入研究、反复实践、勤于思考、勇于交流。只有这样,我们才可以坦然地用手推一下眼镜,谦虚地告诉别人,“其实我是个程序员”。

源代码

本书提供源代码下载,地址是https://github.com/feuyeux/jax-rs2-guide-II。

勘误和交流

本书的勘误会在https://github.com/feuyeux/jax-rs2-guide-II/wiki发布,欢迎读者批评指正。

我的邮箱:feuyeux@163.com

我的新浪微博:六爷1_1

致谢

感谢我的妻子Caroline和女儿Doris一直以来的关心和陪伴。

感谢华章公司的杨福川对我的专业指导。感谢华章公司编辑高婧雅、李艺专业和耐心的审阅和指正。

感谢阿里巴巴速卖通中间件团队在微服务、容器化上对我的影响。感谢雷卷、许晓斌在DDD、Spring Boot和Docker上对我的帮助。感谢Technicolor的敏捷团队、阿里巴巴国际站测试架构团队,前者带我悟得Jersey,后者给我深入实践的机会。

最后我要感谢阿里巴巴阿里云事业群大安全的各位兄弟对我的支持。我正在这里,与大家一天天、一步步将微服务和容器化落地生花。





Contents 目  录

第2版序一

第2版序二

第1版序一

第1版序二

前言

第1章 JAX-RS2入门 1

1.1 解读REST 1

1.1.1 一种架构风格 2

1.1.2 基本实现形式 2

1.2 解读REST服务 3

1.2.1 REST式的Web服务 3

1.2.2 对比RPC风格 3

1.2.3 对比MVC风格 4

1.3 解读JAX-RS标准 5

1.3.1 JAX-RS2标准 5

1.3.2 JAX-RS2的目标 5

1.3.3 非JAX-RS2的目标 6

1.3.4 解读JAX-RS元素 7

1.4 Jersey项目概要 7

1.4.1 获得Jersey 8

1.4.2 Jersey问答 8

1.4.3 Jersey项目管理 8

1.4.4 Jersey许可 9

1.4.5 Jersey的模块 10

1.4.6 GlashFish项目 10

1.5 快速实现Java REST服务 12

1.5.1 第一个REST服务 13

1.5.2 第一个Servlet容器服务 17

1.6 快速了解Java REST服务 19

1.6.1 REST工程类型 19

1.6.2 REST应用描述 24

1.7 Java领域的其他REST实现 27

1.7.1 JAX-RS的其他实现 27

1.7.2 其他的REST实现 31

1.8 REST调试工具 33

1.8.1 命令行调试工具 33

1.8.2 基于浏览器的图形化调试插件 34

1.9 本章小结 37

第2章 REST API设计 38

2.1 统一接口 38

2.1.1 GET方法 39

2.1.2 PUT方法 41

2.1.3 DELETE方法 43

2.1.4 POST方法 44

2.1.5 WebDAV扩展方法 45

2.2 资源定位 47

2.2.1 资源地址设计 48

2.2.2 @QueryParam注解 50

2.2.3 @PathParam注解 52

2.2.4 @FormParam注解 55

2.2.5 @BeanParam注解 57

2.2.6 @CookieParam注解 58

2.2.7 @Context注解 58

2.3 传输格式 59

2.3.1 基本类型 59

2.3.2 文件类型 60

2.3.3 InputStream类型 61

2.3.4 Reader类型 62

2.3.5 XML类型 62

2.3.6 JSON类型 66

2.4 连通性 82

2.4.1 过渡型链接 82

2.4.2 结构型链接 83

2.5 处理响应 84

2.5.1 返回类型 85

2.5.2 处理异常 86

2.6 内容协商 89

2.6.1 @Produces注解 89

2.6.2 @Consumes注解 91

2.7 本章小结 92

第3章 REST请求处理 93

3.1 Jersey的AOP机制 93

3.2 Providers详解 94

3.2.1 实体Providers 94

3.2.2 上下文Providers 100

3.3 REST请求流程 100

3.4 REST过滤器 102

3.4.1 ClientRequestFilter 102

3.4.2 ContainerRequestFilter 103

3.4.3 ContainerResponseFilter 104

3.4.4 ClientResponseFilter 105

3.4.5 访问日志 107

3.5 REST拦截器 109

3.6 绑定机制 111

3.6.1 名称绑定 111

3.6.2 动态绑定 113

3.7 优先级 115

3.8 本章小结 116

第4章 REST服务与异步 117

4.1 为什么使用异步机制 117

4.1.1 服务器异步机制 117

4.1.2 客户端异步机制 118

4.2 JAX-RS2的异步机制 119

4.2.1 服务端实现 119

4.2.2 客户端实现和测试 122

4.3 基于HTTP1.1的异步通信 124

4.3.1 Polling技术 124

4.3.2 Comet技术 126

4.3.3 Web Hook异步通信 127

4.3.4 SSE技术 128

4.4 基于HTML5的异步通信 129

4.4.1 SSE的原理 129

4.4.2 发布—订阅模式的实现 131

4.4.3 广播模式的实现 135

4.4.4 WebSocket技术 137

4.5 本章小节 138

第5章 REST客户端 139

5.1 客户端接口 140

5.1.1 Client接口 140

5.1.2 WebTarget接口 141

5.1.3 Invocation接口 142

5.2 连接池 142

5.2.1 资源释放 142

5.2.2 连接器 144

5.2.3 HTTP连接池 146

5.3 封装Client 147

5.4 请求Spring Boot微服务 148

5.4.1 不同的JSON解析方式 148

5.4.2 完整示例 150

5.5 JavaScript客户端 150

5.5.1 jQuery客户端 151

5.5.2 AngularJs客户端 152

5.6 本章小结 152

第6章 REST测试 153

6.1 Jersey测试框架 153

6.2 单元测试 156

6.2.1 集成Spring的单元测试 156

6.2.2 异步测试 158

6.3 集成测试 158

6.4 日志增强 159

6.5 本章小结 160

第7章 微服务 161

7.1 微服务技术栈 162

7.1.1 服务发现 163

7.1.2 可伸缩性 163

7.1.3 回到起点 164

7.2 REST服务与Spring Boot 165

7.2.1 Bootiful 165

7.2.2 RESTful 167

7.2.3 Actuator 168

7.3 REST服务与Spring Cloud 172

7.3.1 Spring Cloud Zookeeper 172

7.3.2 Spring Cloud Consul 182

7.3.3 Spring Cloud Etcd 187

7.4 本章小结 193

第8章 容器化 195

8.1 容器技术 195

8.1.1 容器 195

8.1.2 Docker技术栈 197

8.1.3 容器文化 199

8.2 REST服务与容器 201

8.2.1 开始容器化之路 201

8.2.2 开发自测容器化 204

8.3 容器化微服务 206

8.3.1 Zookeeper 207

8.3.2 Kafka 212

8.3.3 微服务 214

8.3.4 Nginx 217

8.4 本章小结 220

第9章 JAX-RS调优 223

9.1 使用缓存优化负载 223

9.1.1 缓存协商 223

9.1.2 条件GET 225

9.1.3 REST缓存实践 227

9.1.4 ab测试 229

9.2 使用版本号优化服务 229

9.2.1 何时使用版本号 230

9.2.2 如何使用版本号 230

9.3 使用参数配置优化服务 232

9.3.1 通用配置 232

9.3.2 服务器端和客户端配置类 233

9.4 Java虚拟机调优 234

9.4.1 虚拟机概述 234

9.4.2 内存溢出与内存泄漏 236

9.5 本章小结 238

第10章 REST安全 239

10.1 身份认证 240

10.1.1 基本认证 241

10.1.2 摘要认证 241

10.1.3 表单认证 242

10.1.4 证书认证 242

10.2 资源授权 244

10.2.1 容器管理权限 244

10.2.2 应用管理权限 246

10.3 认证与授权实现 247

10.3.1 基本认证与JDBCRealm 247

10.3.2 摘要认证与UserDatabase-Realm 255

10.3.3 表单认证与DataSource-Realm 258

10.3.4 Form认证和JAASRealm 263

10.3.5 证书认证与UserDatabase-Realm 266

10.4 JAX-RS2实现 270

10.4.1 Application类 270

10.4.2 资源类 271

10.4.3 资源测试类 271

10.5 REST服务与OAuth2 273

10.5.1 OAuth2概述 274

10.5.2 OAuth2流程 275

10.5.3 OAuth2实现 276

10.6 本章小结 280

参考资料 282

第1章
JAX-RS2入门
本章将详细讲述REST服务(RESTful Web Service)的概念、生态环境,并通过简单的示例,使读者快速掌握REST服务开发的基本能力。
前四节将逐一解读REST的概念、REST服务、JAX-RS标准和Jersey项目。这四者之间的联系是:REST是一种跨平台、跨语言的架构风格,REST式的Web服务是对REST在Web领域的实现;JAX-RS标准是Java领域对REST式的Web服务制定的实现标准,Jersey是JAX-RS标准的参考实现,是Java EE参考实现项目GlassFish的成员项目。接下来的三节介绍基于Jersey的REST服务开发,以及Java领域中其他的REST服务框架。最后,介绍REST服务的调试工具。
1.1 解读REST
REST(Representational State Transfer)翻译为表述性状态转移,源自Roy Thomas Fielding博士在2000年就读加州大学欧文分校期间发表的一篇学术论文《Architectural Styles and the Design of Network-based Software Architectures》。REST之父在该论文中提出了REST的6个特点,分别是:客户端–服务器的、无状态的、可缓存的、统一接口、分层系统和按需编码。
REST具有跨平台、跨语言的优势。从其诞生开始,就得到了诸多语言的快速支持,最著名的是ROR(Ruby on Rails)框架。新兴的语言(比如NodeJs、Golang)、工具平台(Docker、Spark)和公有云,更是将REST默认为服务的开放形式。
1.1.1 一种架构风格
REST是一种架构风格。在这种架构风格中,对象被视为一种资源(resource),通常使用概念清晰的名词命名。
表述性状态是指资源数据在某个瞬时的状态快照。资源可以有多种表述(representation),表述状态具有描述性,包括资源数据的内容、表述格式(比如XML、JSON、Atom)等信息。
REST的资源是可寻址的,通过HTTP1.1协议(RFC 2616)定义的通用动词方法(比如GET、PUT、DELETE、POST),使用URI协议(RFC3305)来唯一标识某个资源公布出来的接口。
请求一个资源的过程可以理解为访问一个具有指定性和描述性的URI,通过HTTP协议,将资源的表述从服务器“转移”到客户端或者相反方向。
阅读指南
REST不是一种技术(technology),也不是一个标准(standard)/协议(protocol),而是一种使用既有标准:HTTP+URI+XML(XML似乎成为了数据格式的借指,不仅代表XML本身)来实现其要求的架构风格。因此,与之对应的不是SOAP协议,而是像RPC这样的架构风格。
1.1.2 基本实现形式
HTTP+URI+XML是REST的基本实现形式,但不是唯一的实现形式。REST一开始便使用已有的HTTP协议(RFC 2616)、URI协议(RFC3305)来描述其特征,而对如何使用一种编程语言来实现,并没有进行任何描述和规定,甚至应该包含哪些传输类型或者数据格式也没有描述,但通常的实现至少包含XML格式。
具体而言,HTTP协议和URI用于统一接口和定位资源,文本、二进制流、XML和JSON等格式用来作为资源的表述。正如采用已有技术XMLHttpRequest+JavaScript+XML(XML后来几乎被JSON替代)实现Ajax一样,使用HTTP+URI+XML实现REST的好处是让开发者持有这些已知的技术来开发REST的入门门槛较低,关注点更容易放到REST的核心概念和业务逻辑上。
阅读指南
以HTTP+URI+XML实现的应用并不一定是REST服务,但对于Ajax,这个逆命题是成立的。因为Ajax是一种技术,而REST是一种架构风格。学习和使用REST的关键是掌握这种思想,而不是具体的实现形式。
1.2 解读REST服务
RESTful对应的中文是REST式的,RESTful Web Service的准确翻译应该是REST式的Web服务,我们通常简称为REST服务。RESTful的应用或者Web服务是最常见的两种REST式的项目部署、存在的方式。本节将介绍REST服务并对比其与传统Web Services的不同。
1.2.1 REST式的Web服务
RESTful Web Service是一种遵守REST式风格的Web服务。REST服务是一种ROA(Resource-Oriented Architecture,面向资源的架构)应用。其主要特点是方法信息存在于HTTP协议的方法中(比如GET、PUT),作用域存在于URI中。例如,在一个获取设备资源列表的GET请求中,方法信息是GET,作用域信息是URI中包含的对设备资源的过滤、分页和排序等条件。
1.2.2 对比RPC风格
相比Web服务领域广为流行的RPC(Remote Procedure Call Protocol,远程过程调用协议)风格,REST风格更轻量和快速。从方法信息角度看,REST采用标准的HTTP方法,而RPC请求都是HTTP协议的POST方法,其方法信息包含于SOAP协议包或HTTP协议包中,方法名称不具有通用性。从作用域角度看,REST采用URI显式定义作用域,而RPC的这一信息同样包含于协议包中,不能直观呈现。
RPC风格的开发关注于服务器–客户端之间的方法调用,而不关注基于哪个网络层的哪种协议。也就是说,RPC是面向方法调用过程的,相比而言,REST是面向资源状态的。RPC风格的两个代表是XML-RPC和大Web服务。
1. XML-RPC
XML-RPC是一种使用XML格式封装方法调用,并使用HTTP协议作为传送机制的RPC风格的实现。XML-RPC的请求方法都是HTTP协议的POST方法,请求和响应的数据格式均为XML。
XML-RPC的数据格式和使用XML作为资源的表述的REST外观上很相似,但数据的内容则大相径庭。REST式的XML信息的主体是对一个资源状态的表述,无须包含方法信息,因为其请求的HTTP方法就已经决定了这一点。XML-RPC的请求数据结构额外包含方法调用信息和参数信息。
对于响应信息的内容两者也截然不同,REST式通常会包含响应实体信息,以及HTTP状态码和可选的异常信息,而XML-RPC的返回信息仅仅是对方法调用的响应信息。
XML-RPC是一种遗留技术,已经被SOAP取代。在Java领域,JAX-RPC标准已经并入JAX-WS2标准。XML-RPC的应用依然存在,著名的测试用例管理系统TestLink的对外接口就是使用PHP开发的XML-RPC。
2. 大Web服务
大Web服务(Big Web Service)是Leonard Richardson和Sam Ruby在其所著的《RESTful Web Services》一书中,对基于SOAP+ WSDL+UDDI+WS-标准栈等技术实现RPC风格的大型Web服务的统称。事实上,“大Web服务”这一说法也被Java EE 7的布道者们在多次演讲中使用。在Java领域,对应的标准主要是JAX-WS 2.0/2.1/2.2(JSR 224)。相较REST式的Web服务,大Web服务功能更强大,设计更复杂。大Web服务同样是跨平台、跨语言的,对复杂的数据类型的支持也非常好。大Web服务是基于RPC风格的重量设计,因此方法和作用域无法通过直观断定,需要定义在消息中,而且方法名不是统一和通用的。同时,大Web服务走HTTP协议时,请求都是基于POST方法的。
对比RPC风格的Web服务,REST式的Web服务形式更简单、设计更轻量、实现更快捷。REST无须引入SOAP消息传输层,无须注册服务,也没有客户端stub的概念等。但是,REST式的Web服务并没有像大Web服务那样提供诸如安全策略等全面的标准规范。
大Web服务和REST式的Web服务各有优势,并不是一种替换关系。在实际开发中,两者共存于一个项目中也是一种解决方案。
1.2.3 对比MVC风格
MVC风格的出现将模型、视图、控制解耦,其亮点是从前到后的一致性,其结构整洁、逻辑清晰,易于扩展和增强。MVC在Java领域的普遍实现方式是在Web前端使用标签库来对应服务端的模型类实例和控制类实例,标签库和服务端依赖库可以是松散的耦合,比如Spring生态系统,也可以是全栈式的统一体系,比如JSF体系。但无论如何实现,在Web前端的开发过程中,必须时刻考虑页面标签和服务端的映射关系,包括模型类的匹配和转换、数据结构、控制类的输入和输出的参数类型和数量等。
因此,MVC风格偏重于解决服务器端的逻辑分层问题,以及客户端是逻辑分层的延伸问题。MVC的标签库虽然其形态已经和HTML页面融合,但本质上还是Java编写的装饰模式的类实例,对应的是服务器端使用Java编写的模型类或者控制器类,因此MVC很难实现跨语言解耦。而REST风格偏重于统一接口,因此具体实现就可以跨平台和跨语言。REST推动了Web开发的新时代,使用平庸的纯HTML作为客户端,没有服务器端和客户端的耦合。显而易见,使用纯HTML开发的REST客户端和使用Java开发的REST服务器端并不存在语言上的耦合。
MVC和REST式并不是互斥的,如Spring的MVC模块已经开始支持REST式的开发。Jersey作为JAX-RS标准的实现,也实现了MVC的功能,请参考相关模块:jersey-mvc、jersey-mvc-freemarker和jersey-mvc-jsp。需要说明的是,本书致力于讲述JAX-RS,对于Jersey实现中的JAX-RS之外的功能,只做必要的讲述。由于MVC和REST之间有更多的并行存在性,本书余文没有将MVC放入讲述之列。
1.3 解读JAX-RS标准
JAX-RS是Java领域的REST式的Web服务的标准规范,是使用Java完成REST服务的基本约定。
1.3.1 JAX-RS2标准
Java领域中的Web Service是指实现SOAP协议的JAX-WS。直到Java EE 6(发布于2008年9月)通过JCP(Java Community Process)组织定义的JSR311(http://www.jcp.org/en/jsr/detail?id=311),才将REST在Java领域标准化。
JSR311名为The Java API for RESTful Web Service,即JAX-RS,其参考实现是Glassfish项目中的Jersey1.0。此后,JSR311进行了一次升级(2009年9月),即JAX-RS1.1。JAX-RS诞生后,时隔5年(2013年5月)发布的Java EE7包含了JSR339,将JAX-RS升级到JAX-RS2(http://www.jcp.org/en/jsr/detail?id=339)。JAX-RS2.0在前面版本的基础上增加了很多实用性的功能,比如对REST客户端API的定义,异步REST等,对REST的支持更加完善和强大。
JAX-RS的版本对应的参考实现Jersey项目版本信息参见表1-1。
表1-1 JAX-RS标准和Jersey版本信息
JSR标准 JSR名称 标准发布时间 JSR实现
jsr311 JAX-RS 1.0 2008年9月8日 Jersey1.x
jsr311 JAX-RS 1.1 2009年9月17日 Jersey1.x
jsr339 JAX-RS 2.0 2013年5月22日 Jersey2.x

1.3.2 JAX-RS2的目标
JAX-RS2标准(即JSR339)中定义了目标、非目标和元素等内容。JSR339标准中的这部分内容通常被以实现业务功能为目的的开发人员所忽视,在此和读者分享的一个开发经验是:要掌握一项技术,先要掌握它背后标准的定义。首先我们来看看JAX-RS2的目标。
1)基于POJO:JAX-RS2的API提供一组注解(annotation)和相关的接口、类,并定义了POJO(Plain Ordinary Java Object)对象的生命周期和作用域。规定使用POJO来公布Web资源。
2)以HTTP为中心:JAX-RS2采用HTTP协议,并提供清晰的HTTP和统一资源定位(URI)元素来映射相关的API类和注解。JAX-RS2的API不但支持通用的HTTP使用模式,还对WebDAV和Atom等扩展协议提供灵活的支持。
3)格式独立性:JAX-RS2对传输数据(HTTP Entity)的类型/格式的支持非常宽泛,允许在标准风格之上使用额外的数据类型。
4)容器独立性:JAX-RS2的应用可以部署在各种Servlet容器中,比如Tomcat/Jetty,也可以部署在支持JAX-WS的容器中,比如GlassFish。
5)内置于Java EE:JAX-RS2是Java EE规范的一部分,它定义了在一个Java EE容器内的Web资源类的内部,如何使用Java EE的功能和组件。
阅读指南
WebDAV(Web-based Distributed Authoring and Versioning,基于Web的分布式创作和版本控制)是IETF组织的RFC2518协议。WebDAV基于并扩展了HTTP 1.1,在HTTP标准方法以外添加了以下内容。
Mkcol:创建集合。
PropFind/PropPatch:针对资源和集合检索和设置属性。
Copy/Move:管理命名空间上下文中的集合和资源。
Lock/Unlock:改写保护,支持文件的版本控制。
针对在REST风格的Web服务中是否应该使用WebDAV,业内的声音并不一致,持反对意见的主要观点是WebDAV带来了非统一的接口,这违背了REST的初衷。本书的示例将不采用WebDAV,但文字部分将讲述如何支持WebDAV。Atom类型传输格式将在2.3节讲述。
1.3.3 非JAX-RS2的目标
那么哪些不是JAX-RS2的目标呢?
1)对J2SE 6.0之前版本的支持:JAX-RS2中大量使用了注解(annotation),需要J2SE 6.0以及更新的版本,因此不提供对J2SE 6.0以下版本的支持。
2)对服务的描述、注册和探测:JAX-RS2没有定义也无须支持任何服务的描述(description)、服务的注册(registration)和服务的探测(discovery)。
3)HTTP协议栈:JAX-RS2没有定义新的HTTP协议栈。承载JAX-RS2应用的容器提供对HTTP协议的支持。
4)数据类型/格式类:JAX-RS2没有定义处理实体内容的类,它将这一类型的类交由使用JAX-RS2的应用中的类去实现。
1.3.4 解读JAX-RS元素
最后,我们来看看JAX-RS2中定义了哪些元素。
1)资源类:使用JAX-RS注解来实现相关Web资源的Java类。如果用MVC的三层结构来解读,那么资源类位于最前端,用于接收请求和返回响应。通常,但不是约定,我们使用resource作为包名,三层的包定义形如:resource-service-dao。
2)根资源类:使用@Path注解,提供资源类树的根资源及其子资源的访问。资源类分为根资源类和子资源类,由于Jersey默认提供WADL(参见1.6节),每个应用公布的全部资源接口可以通过WADL页面查阅。
3)请求方法标识符:使用运行期注解@HttpMethod,用来标识处理资源的HTTP请求方法。该方法将使用资源类的相应方法处理,标准的方法包括DELETE、GET、HEAD、OPTIONS、POST、PUT,详见2.1节。
4)资源方法:资源类中定义的方法使用了请求方法标识符,用来处理相关资源的请求。就是上面提到的资源类的相应方法。
5)子资源标识符:资源类中定义的方法,用来定位相关资源的子资源。
6)子资源方法:资源类中定义的方法,用来处理相关资源的子资源的请求。
7)Provider:一种JAX-RS扩展接口的实现类,扩展了JAX-RS运行期的能力。第4章详述了各种Provider及其实现。
8)Filter:一种用于过滤请求和响应的Provider,详见3.3节。
9)Entity Interceptor:一种用于处理拦截消息读写的Provider,详见3.5节。
10)Invocation:一种用于配置发布HTTP请求的客户端API对象,详见5.1.3节。
11)WebTarget:一种使用URI标识的Invocation容器对象,详见5.1.2节。
12)Link:一种携带元数据的URI,包括媒体类型、关系和标题等,详见2.4节。
1.4 Jersey项目概要
Jersey是JAX-RS标准的参考实现,是Java领域中最纯正的REST服务开发框架。本节将带读者走近Jersey的世界。
Jersey项目是GlashFish项目的一个子项目,专门用来实现JAX-RS(JSR 311 & JSR 339)标准,并提供了扩展特性。
1.4.1 获得Jersey
Jersey项目的地址是https://jersey.java.net。该网站同时提供了JAX-RS和JAX-RS2两个并行版本,分别是JAX-RS1.1(截至本书发稿,最新版本是Jersey1.19)和JAX-RS2(截至本书发稿,最新版本是Jersey2.22.1)。读者可以通过单击latest Jersey User Guide获取和阅读最新版本的用户手册,这是官方发布的第一手参考资料。
Jersey项目的下载地址http://jersey.java.net/download.html。该页面自上而下的内容分别如下。
JAX-RS标准列表链接(JAX-RS 2.0 API)。
Jersey最新参考实现的jar包下载(Jersey JAX-RS 2.0 RI bundle)。
Jersey最新参考实现的示例代码下载(Jersey 2.22.1 Examples bundle)。
通过Maven模板(archetype),使用Jersey最新版本创建REST服务的命令。
Jersey最新参考实现的模块和依赖(Jersey 2 modules and dependencies)。
JAX-RS1.1的参考实现包下载。
Jersey源代码的托管地址是https://github.com/jersey/jersey,我们可以通过git命令,将Jersey主干代码迁出到本地。示例如下。
git clone https://github.com/jersey/jersey.git
1.4.2 Jersey问答
StackOverflow是专业的程序员问答系统,Jersey的问题列表地址是:http://stackoverflow.com/questions/tagged/jersey。该链接在Jersey官网首页底部被列出,可见Jersey对问答系统的重视。另外,邮件列表也是一种知识共享的途径,读者可以自行订阅,地址是:https://jersey.java.net/mailing.html。
1.4.3 Jersey项目管理
Jersey使用JIRA作为项目管理平台,相应的地址是:https://java.net/jira/browse/JERSEY。JIRA和StackOverflow不同的是,JIRA平台是Jersey团队日常开发的管理平台,即Jersey官方的缺陷管理平台,用于上报缺陷和改进意见,而不是社区性质的交流平台。通过这个平台我们可以从中了解到Jersey项目的进展情况。Jersey是一个非常活跃的项目,不仅可以从github源代码的提交活动中看到该项目频繁的更新,在JIRA中也可以看到该项目推进的速度。
这里为喜欢开源社区活动的读者举个例子。在撰写本书第1版的开始,Jersey2.0并不支持与Spring的集成,因为Jersey的IoC容器由GlashFish的另一个子项目HK2来支持。随后,我在JIRA上发现一个Jersey2.x支持与Spring集成的任务被创建了(https://java.net/jira/browse/JERSEY-1957),此后我经常观察其进展状态,最终看到了这个功能在Jersey2.2中以扩展包的形式发布了。
因此,在使用Jersey的过程中,如果读者遇到Jersey本身的问题,可以跟踪Jersey的JIRA平台检索、查看Bug的修复状态,包括将在哪个版本修复,有什么样的临时解决办法(workaround)。同时,跟踪JIRA也可以了解新版本的发布情况,包括新增哪些功能,升级对哪一部分带来性能、安全的提升等。换句话说,JIRA展示了Jersey项目的缺陷修复和新功能发版的计划(roadmap)。
1.4.4 Jersey许可
开发者使用开源软件的前提是了解它的许可证版本,否则可能会带来侵权问题。相信在正规的公司,大家都有被开发管理部门的人“恐吓”的经历。开发者需感谢这样的团队所做的工作,他们为公司规避了商业侵权的风险,因为引用的源代码如果出自“传染性”许可,该项目是不能用于闭源的商业用途的。
Jersey的许可证说明地址是:https://jersey.java.net/license.html。从中我们可以了解到Jersey使用的是双许可证:CDDL(Common Development and Distribution License,开源通用开发和分发许可证)1.1和GPLv2(类路径例外)许可证。双重许可是依照两套(或更多套)不同的条款和条件分发相同软件的作法。在为软件授予双重许可时,接收人可以选择他们希望依照哪种条款获得软件。使用双重许可的两个常见动机是遵循商业模式和保持许可证兼容性。GPLv2.0许可证为无法依照CDDL许可证使用Jersey的供应商提供了一个额外选项。Jersey许可证使整套产品和包保持一致(GlassFish项目同样依照CDDL和GPLv2(类路径例外)授予双重许可)。
阅读指南
类路径例外是由自由软件基金会的GNU/类路径项目制订的。它允许将依照任何许可证提供的应用程序链接到依照GPLv2许可的软件中包含的库,而该应用程序不受GPL要求公开其本身的限制。
为什么需要使用类路径例外?因为作为“基于GPL程序的作品”的一部分提供的所有代码还应获得GPL许可。因此,需要指定GPL许可证例外的情况,以便明确将链接到GPL实现的任何应用程序从该许可要求中排除。类路径例外就实现了这一目的。
1.4.5 Jersey的模块
Jersey框架是由核心模块、容器模块、连接器模块、Media模块、扩展模块、测试框架模块、安全模块以及Glassfish Bundle模块等8个大的模块组成。详情请读者浏览官方文档:https://jersey.java.net/documentation/latest/modules-and-dependencies.html。
Jersey核心模块包括3个子模块,分别是通用包、服务器端实现包和客户端实现包。Jersey提供了3种HTTP容器,分别是Grizzly2、JDK-HTTP和SIMPLE-HTTP,Grizzly2同时提供了Servlet容器。Jersey客户端底层依赖于连接器来实现网络通信,如果标准的客户端模块功能不能满足业务需求,读者可以考虑引入Grizzly连接器包或者Apache连接器包。
阅读指南
Jersey在2.6版本做了一次包重构,清除了对guava和ASM的自然依赖。如果你的项目需要做Jersey版本迁移,则需要注意这一点。新的包名为:jersey.repackaged.com.google.common和 jersey.repackaged.objectweb.asm。
1.4.6 GlashFish项目
GlashFish项目地址为https://glassfish.java.net。GlashFish著名于世的是Java EE服务器项目Oracle GlassFish Server,该项目还同时包含Java EE中的一系列标准规范的参考实现,这些参考实现集成于GlashFish Server,为其Java EE容器提供支持。其中对应JAX-RS2的实现项目是Jersey。
为什么要在JAX-RS2的介绍中提及和罗列GlashFish项目集呢?因为Jersey处于GlashFish生态环境中,GlashFish又是Java EE生态环境的参考实现。通过了解GlashFish项目,我们可以更好地设计和实现REST服务。
这里所列的项目是除Jersey以外,其他的GlashFish项目,排列顺序并不严谨,大体上以其与Jersey的紧密关系降序排列。
HK2项目:JSR-330参考实现,项目地址为http://hk2.java.net。HK2是轻量级DI架构,实现IoC和DI的内核。是Jersey实现容器内管理Bean的基础。
Grizzly项目:中文直译为灰熊。JSR-356参考实现,项目地址为https://grizzly.java.net。Grizzly是一个异步IO的、高效而健壮的服务器,可以被用作HTTP服务器、Servlet容器,支持Ajp、Comet、WebSocket以及相对于RESTful的另一种Web Service实现(JAX-WS)。
EclipseLink项目:该项目实现了多个JSR标准,包括JSR-338/JPA2.1、JSR-222/JAXB2.2、JSR-235/SDO2.1.1、JSR-353/Java API for Processing JSON。项目地址为http://www.eclipse.org/eclipselink。EclipseLink是JPA2.1的一个实现,同时它还实现了其他的JSR作为扩展。JPA2.1是Java EE 7的成员,是对JSR317(JPA2.0)的升级。JPA2.1的实现中,最常用的是JBoss的Hibernate,该项目从4.3开始实现JPA2.1。也就是说Hibernate4.2是JPA2.0的最后一个版本。读者在开发的时候要注意依赖项目版本对标准的支持。JPA标准还有其他的实现,请参考http://en.wikipedia.org/wiki/Java_Persistence_API。
Metro项目:该项目是JSR中多个标准的官方实现集,目的是实现全栈式的Web Service。包括JSR-224/JAX-WS 2.2、JSR-222/JAXB2.2、JSR-206/JAXP 1.4.6、JSR-067/SAAJ1.3。项目地址为https://metro.java.net。Metro项目中的多个标准作用各有不同。
JAX-WS标准结合了XML-RPC,使用SOAP协议来实现Web Service。在JAX-WS的实现中,不可不提的另外两个实现分别是Apache的CXF和Axis。
WSIT的前身是Tango,是一种JAX-WS和.NET互操作的技术,实现了WS*标准。
SAAJ规范的作用是基于SOAP协议XML格式传递带附件的SOAP消息。
JAXP标准涵盖了Java对XML过程式处理的诸多技术,包括DOM、SAX和StAX,同时该标准定义了解读XML样式的XSLT。
JAXB标准是Java处理XML和POJO映射的技术,是Jersey中处理传输数据的重要依赖。
Open MQ项目:地址https://mq.java.net。Open MQ是JMS 2.0的参考实现。JSR-343是Java EE 7的成员,旨在简化JMS的API。关于消息队列的实现数量,恐怕是其他任何一个标准都望尘莫及的。几乎每一个有能力开发服务器软件、中间件的公司都有自己的MQ,请参考http://en.wikipedia.org/wiki/Message_queue。
Mojarra项目:JSR-344/JSF2参考实现,项目地址为https://javaserverfaces.java.net。JSF是一种全栈式的、事件驱动的B/S开发模式框架,它包括浏览器端的丰富组件,服务器端覆盖Java EE的各种特性。JSF相对于Spring,借鉴了其核心思想IoC和AOP,同时给出了标准规范。这有点类似JPA借鉴了hibernate的O/R Mapping思想并标准化。JSF的另一个实现是Apache的myfaces,当前版本为2.0.18。另外,JBoss的RichFaces是基于JSF的扩展中最为完善和常用的。更多有关JSF的内容和原理,请参考笔者的拙作《JSF2和RichFaces4使用指南》。
OpenJDK项目:项目地址为http://openjdk.java.net。OpenJDK是开源的JDK,从版本1.7开始成为官方JDK的先行版本,因此是Java开发者窥探Java发展的第一线的最好资源,同时也是活跃的Linux发行版本Ubuntu和Fedora等默认安装的JDK版本。
阅读指南
当前使用的JDK版本号的升级规则是从JDK5.0发布开始的,Java升级发布一直采用两种方式发布更新。
有限升级(Limited Update)包含新功能和非安全修正。
重要补丁升级(Critical Patch Update,CPU)只包含安全修正。
有限升级发行序号为20的倍数,即一个偶数;重要补丁升级顺延上一个CPU的版本号加5的倍数并取奇数(必要时加1)。
举例来说,下一个有限升级的版本号为7u40,那么接下来的3个CPU版本号依次为40+5=7u45,45+5+1=7u51和51+5=7u55。再下一个有限升级的版本号为7u60,随后的CPU版本号依次为7u65、7u71和7u75。
这种命名规则会为重要补丁升级保留几个版本序号,以便新的CPU版本号可以取区间值之和而不是在最新版本号上顺延。
1.5 快速实现Java REST服务
本节包含两个Jersey实战示例,目的是让读者具备快速创建REST服务的能力。
在开始实战之前,首先需要读者确认你的环境是否已经安装了Java和Maven。这里使用Maven命令,示例如下。
mvn -v
 
Apache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T19:57:37+08:00)
Maven home: /usr/local/Cellar/maven/3.3.3/libexec
Java version: 1.8.0_40, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.1", arch: "x86_64", family: "mac"
从Maven版本显示命令的结果中,自上而下可以看到Maven的版本信息和HOME路径信息、Java的版本信息和HOME路径信息、本地语言、平台字符集以及操作系统信息。
1.5.1 第一个REST服务
Jersey提供了Maven原型(archetype)来快速创建REST服务项目。
1. 创建项目
我们首先使用archetypeGroupId为org.glassfish.jersey.archetypes的原型、archetypeArtifactId为jersey-quickstart-grizzly2的原型,创建REST服务项目。示例如下。
mvn archetype:generate \
-DarchetypeArtifactId=jersey-quickstart-grizzly2 \
-DarchetypeGroupId=org.glassfish.jersey.archetypes \
-DinteractiveMode=false \
-DgroupId=my.restful \
-DartifactId=my-first-service \
-Dpackage=my.restful \
-DarchetypeVersion=2.22.1
上述命令将创建一个标准的Maven工程。其中,interactiveMode=false代表无需交互,archetypeVersion指定原型的版本,这个版本与Jersey的版本一致。groupId、artifactId和package分别定义了我们这个项目的组ID为my.restful,工件ID为my-first-service,包名为my.restful。我们通过观察项目根目录下的pom.xml,可以对应出上述命令参数与Maven坐标的关系。相关部分的示例如下。
<groupId>my.restful</groupId>
<artifactId>my-first-service</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>my-first-service</name>
2. 运行服务
Maven工程建立好后,我们首先启动REST服务体验一下该项目的功能。进入项目的根目录,并执行如下命令构建和启动服务。
cd my-first-service
mvn package
mvn exec:java
 
Jersey app started with WADL available at http://localhost:8080/myapp/application.wadl
Hit enter to stop it...
该命令启动了REST服务,端口是8080,我们可以随时通过回车键停止这个服务。同时,该服务还提供了WADL(详见1.6节)。通过访问application.wadl,可以获取当前REST服务公布的接口。本例WADL的关键部分,示例如下。
<ns0:resources base="http://localhost:8080/myapp/">
    <ns0:resource path="myresource">
        <ns0:method id="getIt" name="GET">
            <ns0:response>
                <ns0:representation mediaType="text/plain" />
            </ns0:response>
        </ns0:method>
    </ns0:resource>
</ns0:resources>
这里定义了一个资源路径myresource,在该路径下,定义了一个GET方法getIt,表述类型为text/plain。
3. 访问服务
我们使用cURL(详见1.8节)来访问REST服务公布的myresource资源方法getIt,示例如下。
curl http://localhost:8080/myapp/myresource
 
Got it!
HTTPie(读作H-T-T-Pie)是和cURL类似的CLI工具,但交互上更人性化。我们使用HTTPie请求相同的资源地址,请求和响应信息如下。
http http://localhost:8080/myapp/myresource
 
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/plain
Date: Sat, 14 Nov 2015 04:08:54 GMT
 
Got it!
响应信息的第一行包含了HTTP协议版本和状态码,接下来是部分HTTP HEAD信息,最后是HTTP BODY信息。cURL携带-i或者--include参数可以得到相同的结果,示例如下。
curl -i http://localhost:8080/myapp/myresource
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sat, 14 Nov 2015 04:08:54 GMT
Content-Length: 7
 
Got it!
要想获得更多的cURL请求响应信息,可以使用-v参数,示例如下。
curl -v http://localhost:8080/myapp/myresource
 
* Hostname was NOT found in DNS cache
*   Trying ::1...
* connect to ::1 port 8080 failed: Connection refused
*   Trying fe80::1...
* connect to fe80::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /myapp/myresource HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 14 Nov 2015 04:08:56 GMT
< Content-Length: 7
<
* Connection #0 to host localhost left intact
Got it!
4. 分析项目
完成了最初的体验后,我们来分析下面这个示例工程。首先,从启动服务的命令mvn exec:java入手。该命令实际调用了exec-maven-plugin插件中定义的一个值为java的goal,用以触发mainClass中的main函数。本例的mainClass定义为my.restful.Main。在pom.xml中,exec插件完整定义如下。
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.2.1</version>
    <executions>
        <execution>
            <goals>
                <goal>java</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <mainClass>my.restful.Main</mainClass>
    </configuration>
</plugin>
除了pom.xml和Main类,示例还包含哪些内容呢?我们可以使用如下命令查看。
tree .
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── my
    │           └── restful
    │               ├── Main.java
    │               └── MyResource.java
    └── test
        └── java
            └── my
                └── restful
                    └── MyResourceTest.java
源代码中,还包括了资源类MyResource和它的单元测试类MyResourceTest。
在资源类MyResource中,@Path中定义了资源路径,@GET中定义了GET方法getIt(),@Produces中定义了响应的类型为普通的字符串,示例如下。
@Path("myresource")
public class MyResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getIt() {
        return "Got it!";
    }
}
相应地,资源测试类MyResourceTest中实现了对getIt()方法的测试,示例如下。
@Test
public void testGetIt() {
    String responseMsg = target.path("myresource").request().get(String.class);
    assertEquals("Got it!", responseMsg);
}
在上述代码中,target()、path()、request()和get()方法都是Jersey Client中定义的方法,这些方法组合在一起,形成了流式风格的API。我们期待响应值为“Got it!”的字符串。
5. 单元测试
最后,我们使用如下命令执行单元测试。使用IDE可以直接通过图形界面单击对该方法的测试。
mvn test
 
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running my.restful.MyResourceTest
Results:
 
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
jersey-quickstart-grizzly2原型提供的模板代码,使用了main函数,并在其中启动了Grizzly的HttpServer。这是典型的Java SE形式的REST应用。更多情况下,我们希望得到的是一个可以以war包形式部署到Servlet容器的轻量级Java EE项目。接下来的示例就是这样的Web形式的项目。
1.5.2 第一个Servlet容器服务
jersey-quickstart-webapp原型会为我们生成Servlet容器服务。
1. 创建项目
使用如下命令创建名为my-first-webapp的Web项目。
mvn archetype:generate \
-DarchetypeArtifactId=jersey-quickstart-webapp \
-DarchetypeGroupId=org.glassfish.jersey.archetypes \
-DinteractiveMode=false \
-DgroupId=my.restful \
-DartifactId=my-first-webapp \
-Dpackage=my.restful \
-DarchetypeVersion=2.22.1
2. 运行服务
由于这是一个Web项目,没有main函数,我们必须将其部署到Servlet容器(比如Tomcat、Jetty)中,才能将其运行。在开发阶段,我们无需真正将其部署,而是使用Maven插件这种更轻量级的方式启动服务。在pom.xml中,增加如下定义来添加插件。
<plugin>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-maven-plugin</artifactId>
   <version>9.3.5.v20151012</version>
</plugin>
有了插件,我们可以使用如下命令编译和启动服务,使用Ctrl+C停止服务。
mvn jetty:run
如果我们要对示例项目进行断点调试,应在服务启动前设置监听端口等信息。这里以IntelliJ IDEA所使用的5050端口为例,示例如下。
export MAVEN_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp: transport=dt_socket,address=5050,server=y,suspend=y"
mvn jetty:run
以这样的方式启动服务,需要IDE与之交互。过程是首先启动端口,然后IDE向该端口请求监听,服务启动并接收请求,在代码的某个断点处,服务会向该端口推送事件,IDE在代码的断点处停留并高亮显示该行。
3. 访问服务
服务启动后,我们使用HTTPie请求资源地址,示例如下。
http http://localhost:8080/webapi/myresource
 
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/plain
Date: Sat, 14 Nov 2015 08:00:03 GMT
Server: Jetty(9.3.5.v20151012)
 
Got it!
4. 分析项目
本例是一个标准的Maven Web工程。Web的根目录默认名称为webapp,默认的Servlet版本为2.5,需要使用WEB-INF/web.xml文件来配置REST服务。我们通过tree命令得到完整的工程结构如下。
tree .
.
├── my-first-webapp.iml
├── pom.xml
└── src
    └── main
        ├── java
        │   └── my
        │       └── restful
        │           └── MyResource.java
        ├── resources
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            └── index.jsp
5. 扩展项目
本例与前例提供的资源类和资源方法相同,我们在此基础上增加两个资源方法,分别用来新增和查询资源,示例如下。
private static ConcurrentHashMap<String, MyDomain> map=new ConcurrentHashMap<>();
 
@GET
@Path("{key}")
@Produces(MediaType.APPLICATION_XML)
public MyDomain getMy(@PathParam("key") final String key) {
    final MyDomain myDomain = map.get(key);
    if (myDomain == null) {
        return new MyDomain();
    }
    return myDomain;
}
 
@POST
@Consumes(MediaType.APPLICATION_XML)
public void addMy(final MyDomain myDomain) {
    map.put(myDomain.getName(), myDomain);
}
如上所示,POST方法addMy用于接收并存储新增的表述,GET方法getMy用于查询表述。MyDomain类是基于JAXB的POJO类,用于表示XML格式的表述。
首先,我们通过如下命令,新增一条记录。
curl -X POST http://localhost:8080/webapi/myresource -d '<myDomain name="eric" value="feuyeux@gmail.com"/>' -H "Content-type:application/xml"
然后通过如下命令查询和验证新增记录的存在。
curl http://localhost:8080/webapi/myresource/eric
 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><myDomain name="eric" value="feuyeux@gmail.com"/>
1.6 快速了解Java REST服务
1.6.1 REST工程类型
在REST服务中,资源类是接收REST请求并完成响应的核心类,而资源类是由REST服务的“提供者”来调度的。这一概念类似其他框架中自定义的Servlet类,该类会将请求分派给指定的Controller/Action类来处理。本节将讲述REST中的这个提供者,即JAX-RS2中定义的Application以及Servlet。
Application类在JAX-RS2(JSR339,详见参考资料)标准中定义为javax.ws.rs.core.Application,相当于JAX-RS2服务的入口。如果REST服务没有自定义Application的子类,容器将默认生成一个javax.ws.rs.core.Application类。
本节根据JAX-RS2规范第2章中对REST服务场景的定义,将REST服务分为四种类型,如图1-1所示。
图1-1将JAX-RS2标准中对REST服务的类型图形化,依据不同的条件分为了四种类型。
类型一:当服务中没有Application子类时,容器会查找Servlet的子类来做入口,如果Servlet的子类也不存在,则REST服务类型为类型一,对应图1-1中的例1。
类型二:当服务中没有Application子类时,存在Servlet的子类,则REST服务类型为类型二,对应图1-1中的例2。
类型三:服务中定义了Application的子类,而且这个Application的子类使用了@ApplicationPath注解,则REST服务类型为类型三,对应图1-1中的例3。
类型四:如果服务中定义了Application的子类,但是这个Application的子类没有使用@ApplicationPath注解,则REST服务类型为类型四,对应图1-1中的例4。

图1-1 REST工程类型示意图
上面提到的四个示例在下面的“阅读指南”中给出了源代码目录和Github下载地址,需要读者仔细体会示例之间的差异,以更好地理解和使用不同类型的REST服务。
1. REST服务类型一
类型一对应的是图1-1中的例1,相应的逻辑是服务中同时不存在Application的子类和Servlet子类。在JAX-RS2(JSR339)中定义这种情况下应作如下处理:为REST服务动态生成一个名称为javax.ws.rs.core.Application的Servlet实例,并自动探测匹配资源。与此同时,需要根据Servlet的不同版本,在web.xml定义REST请求处理的Servlet为这个动态生成的Servlet,并定义该Servlet对资源路径的匹配。在没有Application的子类存在的情况下,在web.xml中定义Servlet是必不可少的配置。
阅读指南
REST服务类型一所对应的示例,即例1的源代码地址如下。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.1.myrest-servlet2-webxml。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.2.myrest-servlet3-webxml。
请使用mvn jetty:run启动服务,使用curl http://localhost:8080/webapi/myresource测试服务。
REST服务类型一的示例包含两个小项目,分别对应Servlet2和Servlet3两种容器依赖场景。我们只须关注Maven配置文件(pom.xml)和Web服务配置文件(web.xml)的区别即可理解无Application子类情况下,如何实现基于Servlet2和Servlet3容器内的服务。
Servlet3的最简配置示例代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http://java.sun.com/xml/ns/Java EE http://java.sun.com/xml/ns/Java EE/web-app_3_0.xsd">
    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
    </servlet>
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/webapi/*</url-pattern>
    </servlet-mapping>
</web-app>
相对于Servlet2而言,在Servlet3中,servlet的定义可以只包含servlet-name。再次强调,Jersey的Servlet3的容器支持包是jersey-container-servlet。 Servlet2的最简配置示例代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/Java EE http://java.sun.com/xml/ns/Java EE/web-app_2_5.xsd">
    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.example</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/webapi/*</url-pattern>
    </servlet-mapping>
</web-app>
servlet的定义包含servlet-name和servlet-class,其初始化参数需要显示给出要加载的资源类所在的包名,可以看出Servlet2的支持包jersey-container-servlet-core不具备自动扫描资源类的功能。
2. REST服务类型二
类型二对应的是图1-1中的例2,相应的逻辑是不存在Application的子类但存在Servlet的子类。
阅读指南
REST服务类型二所对应的示例,即例2的源代码地址如下。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.3.myrest-subservlet。
本例定义了Servlet子类AirServlet,该类继承自org.glassfish.jersey.servlet.ServletContainer类,这是Jersey2中Servlet的基类,继承自HttpServlet。AirServlet类的代码示例如下。
@WebServlet(
initParams = @WebInitParam(
name = "jersey.config.server.provider.packages", value = "com.example"),
urlPatterns = "/webapi/*",
loadOnStartup = 1)
public class AirServlet extends ServletContainer {
AirServlet使用了WebServlet注解来配置Servlet参数。包括初始化参数initParams中定义扫描的资源类所在的包名:com.example,Servlet匹配的资源路径:urlPatterns="/webapi/*"和启动时的加载标识:loadOnStartup=1。
例2是基于Servlet3容器的REST服务,使用了WebServlet注解和无web.xml等Servlet3引入而Servlet2没有的功能。在自定义Servlet3.x子类的场景下,web.xml可以省略,但需要修改Maven的maven-war-plugin插件的配置,添加failOnMissingWebXml为false,这样编译时才不会报错。Maven配置文件中相关信息如下所示。
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>2.3</version>
    <configuration>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </configuration>
</plugin>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
3. REST服务类型三
类型三对应的是图1-1中的例3,相应的逻辑是存在Application的子类并且定义了@ApplicationPath注解。
阅读指南
REST服务类型三所对应的示例,即例3的源代码地址如下。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.4.myrest-servlet3-application。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.5.myrest-servlet2-rc。
REST服务类型三的示例包含两个小项目。其中,servlet2-rc项目基于Servlet2,AirResourceConfig类继承自Application的子类ResourceConfig类;servlet3-application项目基于Servlet3,AirApplication类继承自Application类。基于Servlet2的REST服务需要定义web.xml(但内容可以是“空的”,即只有web-app的基本定义),基于Servlet3的REST服务可以省略此文件。AirApplication类代码示例如下。
@ApplicationPath("/webapi/*")
public class AirApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        final Set<Class<?>> classes = new HashSet<Class<?>>();
        classes.add(MyResource.class);
        return classes;
    }
}
AirApplication类覆盖了getClasses()方法,注册了资源类MyResource,这样在服务启动后,MyResource类提供的资源路径将被映射到内存,以便请求处理时匹配相关的资源类和方法。AirResourceConfig类代码示例如下。
@ApplicationPath("/webapi/\*")
public class AirResourceConfig extends ResourceConfig {
    public AirResourceConfig() {
        packages("com.example");
    }
}
AirResourceConfig类在构造子中提供了扫描包的全名,这样在服务启动后,com.example包内资源类所提供的资源路径将被映射到内存。
4. REST服务类型四
类型四对应的是图1-1中的例4,相应的逻辑是一有二无:一有是存在Application的子类;二无是不存在Servlet子类、不存在或者不允许使用注解@ApplicationPath。
阅读指南
REST服务类型四所对应的示例,即例4的源代码地址如下。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.6.myrest-servlet2-application。
https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.7.myrest-servlet3-application。
REST服务类型四的示例包含两个小项目,演示了基于Servlet2和Servlet3两个版本的REST服务,其差异仅此而已,关于差异性配置前面的例子已经讲过,不再冗述。如下以servlet3-application为例说明。AirApplication类是Application的子类,代码示例如下。
public class AirApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        final Set<Class<?>> classes = new HashSet<Class<?>>();
        classes.add(MyResource.class);
        return classes;
    }
}
代码和类型三的示例相仿,但是该类没有定义@ApplicationPath注解,因此我们需要在web.xml中配置Servlet和映射资源路径,代码示例如下。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/Java EE http://java.sun.com/xml/ns/Java EE/web-app_3_0.xsd"version="3.0">
    <servlet>
        <servlet-name>com.example.AirApplication</servlet-name>
    </servlet>
    <servlet-mapping>
        <servlet-name>com.example.AirApplication</servlet-name>
        <url-pattern>/webapi/\*</url-pattern>
    </servlet-mapping>
</web-app>
在servlet-name中使用自定义的Application子类com.example.AirApplication的全名作为Servlet名称,并在url-pattern中映射资源路径。
1.6.2 REST应用描述
在明白如何创建和部署各种类型的REST服务后,我们来了解一下部署好的REST服务中一个特殊的成员,REST应用的描述:以XML格式展示当前REST环境中所提供的REST服务接口。这种XML格式的描述就是WADL(Web Application Description Language,Web应用描述语言)。
WADL是用来描述基于HTTP协议的REST式Web服务部署情况的。它采用XML格式,支持多种数据类型的描述。WADL由Sun公司提出,尚未成为W3C或者OASIS的标准,JAX-RS标准中并没有关于WADL的定义和说明。Jersey作为JAX-RS2的参考实现默认支持服务的WADL。通过浏览器访问“服务根路径/application.wadl”即可打开该服务的WADL内容。相对于REST服务,WSDL更为人们所熟知,WSDL是RPC风格的基于SOAP的Web服务的描述语言。两者缩写类似而且都使用XML格式,此外共性不多。
1. 应用的描述
以REST服务类型四的示例项目1.6.7.myrest-servlet3-application为例,该应用的WADL路径如下:http://localhost:8080/myrest-servlet3-application/webapi/application.wadl。
通过浏览器访问该路径,可以一览WADL的schema结构。WADL的最外层标签是application,代表应用。然后自上而下分别是doc、grammars和resources。resources是应用提供的资源集合,里面至少包含application.wadl,以及应用中包含的资源描述,比如本例的资源信息描述在资源路径myresource之内,如下所示。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application>
    <doc jersey:generatedBy="Jersey: 2.3 2013-09-20 13:59:07"/>
    <grammars/>
<resources
base="http://localhost:8080/myrest-servlet3-application/webapi/">
        <resource path="myresource">...</resource>
        <resource path="application.wadl">...</resource>
    </resources>
</application>
2. 资源的描述
可以展开myresource来查看具体某个方法的WADL,也可以通过发送一条请求并定义请求头信息来获取。以cURL(详见1.8节)为例,命令如下。
curl -X OPTIONS -H "Allow: application/vnd.sun.wadl+xml" -v  http://localhost: 8080/myrest-servlet3-application/webapi/myresource
myrest-servlet3-application提供的资源接口,对照服务器返回的XML,可以更清晰地理解WADL的内容。其WADL内容如下。
<resource path="myresource">
    <method id="getIt" name="GET">
        <response>
            <representation mediaType="text/plain"/>
        </response>
    </method>
    <method id="apply" name="OPTIONS">
        <request>
            <representation mediaType="*/*"/>
        </request>
        <response>
            <representation mediaType="application/vnd.sun.wadl+xml"/>
        </response>
    </method>
    <method id="apply" name="OPTIONS">
        <request>
            <representation mediaType="*/*"/>
        </request>
        <response>
            <representation mediaType="text/plain"/>
        </response>
    </method>
    <method id="apply" name="OPTIONS">
        <request>
            <representation mediaType="*/*"/>
        </request>
        <response>
            <representation mediaType="*/*"/>
        </response>
    </method>
</resource>
在这段代码中,公布了四个方法。其中,getIt方法代码如下。其他三个OPTIONS请求方法是Jersey默认实现的,用以描述getiIt方法,分别返回text/plain类型,*/*类型和application/vnd.sun.wadl+xml类型。
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getIt() {
return "Got it!";
}
getIt方法定义为GET请求方法,@Produces中定义的媒体类型是MediaType.TEXT_PLAIN,即响应过程中生产的数据,其表述性状态以text/plain媒体类型转移。
3. WADL的配置
上述OPTIONS请求方法的实现是Jersey默认支持的,如果读者不希望在REST服务中让Jersey自动生成,可以通过配置jersey.config.server.wadl.disableWadl=true来实现。代码示例如下。
public class AirApplication extends ResourceConfig {
    public AirApplication() {
        property(ServerProperties.WADL_FEATURE_DISABLE, true);
        packages("com.example.resource");
    }
}
在构造函数中,我们通过定义ServerProperties.WADL_FEATURE_DISABLE属性为true以实现去除WADL自动生成的功能。 或者,可以通过修改Web配置文件中servlet启动参数来实现,代码示例如下。
<servlet>
    <servlet-name>com.example.AirApplication</servlet-name>
    <init-param>
        <param-name>jersey.config.server.wadl.disableWadl</param-name>
        <param-value>true</param-value>
    </init-param>
</servlet>
配置文件中定义了启动参数jersey.config.server.wadl.disableWadl,其值定义为true,以实现去除WADL自动生成的功能。
1.7 Java领域的其他REST实现
Java领域存在很多REST实现,我们以是否遵循JAX-RS标准,将它们分为两组。前者是JAX-RS标准参考实现之外的厂商实现,后者要么是因为出现较JAX-RS标准早,要么干脆跳出了JAX-RS标准的定义,以自身框架一致性为目标,实现了一套独有的对REST开发的支持。本节将概括性地介绍这些实现工具,以便读者有所对比和选择。
1.7.1 JAX-RS的其他实现
JAX-RS标准发布后,诸多厂商推出了自己的基于JAX-RS标准的实现。其中最有影响力的当属来自JBoss社区的RESTEasy和来自Apache社区的CXF。本节将简述这两个项目。如果读者的项目确实和它们结合得紧密,Jersey未必是最佳选择,读者尽可拥抱这两个基于JAX-RS标准的项目。
1. JBoss的RESTEasy
RESTEasy是JBoss社区提供的JAX-RS项目。JBoss这一名词目前已经不再代表Java EE容器,曾经的JBoss已经更名为WildFly。现在,JBoss特指RedHat公司旗下的开源社区。RESTEasy自2009年1月第一个GA版本以来,发展到3.0.x,从版本3.0.0.Final开始支持JAX-RS2.0。RESTEasy的基本信息如下。
官方网站:http://resteasy.jboss.org。
官方文档:提供单页面HTML、按章节HTML和PDF 3种格式,可以按照阅读习惯选择。地址为http://resteasy.jboss.org/docs.html。
源代码:由GitHub托管,地址为https://github.com/resteasy/Resteasy。
缺陷管理:地址为https://issues.jboss.org/projects/RESTEASY,隶属于JBoss开发者社区的JIRA系统。
项目下载:由sourceforge托管,地址为http://sourceforge.net/projects/resteasy/files/Resteasy%20JAX-RS。维护的最后一个版本是3.0.9.Final,日期为2014-09-17,而目前RESTEasy的最新版本为3.0.13.Final。
(1)快速开始
最快速地了解一个新框架的办法,一定是从官方提供的示例开始。我们首先迁出RESTEasy的源代码,然后进入示例目录,选择最易上手的示例项目。 这里以resteasy-springMVC为例,示例操作如下。
git clone https://github.com/resteasy/Resteasy.git
cd Resteasy/jaxrs/examples/resteasy-springMVC
mvn clean install
mvn jetty:run
上述操作分别执行了代码迁出、进入示例目录、构建示例项目和启动示例服务。当服务启动完毕后,我们可以在浏览器的地址栏输入http://localhost:8080/contacts,在页面中输入测试数据并提交表单,这里提交的是Eric和Han。测试数据会被保存在REST服务的内存中,可以通过如下GET请求获取服务端保存的数据,示例结果如下。
curl http://localhost:8080/contacts/data/Han
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<contact>
  <firstName>Eric</firstName>
  <lastName>Han</lastName>
</contact>
从上面的测试结果中,我们得到了lastName为Han,XML格式的contact资源数据。
(2)简单分析
这个示例项目是非常典型的JAX-RS实践。观察如下所示的目录树,源代码路径中包含了4个类,其中Contact和Contacts是实体类,ContactService是服务类,ContactsResource是REST服务的资源类,负责定义REST接口。单元测试类ContactsTest是对ContactsResource功能的全面测试。
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── jboss
    │   │           └── resteasy
    │   │               └── examples
    │   │                   └── springmvc
    │   │                       ├── Contact.java
    │   │                       ├── ContactService.java
    │   │                       ├── Contacts.java
    │   │                       └── ContactsResource.java
    │   ├── resources
    │   │   └── springmvc-servlet.xml
    │   └── webapp
    │       └── WEB-INF
    │           ├── contacts.jsp
    │           └── web.xml
    └── test
        └── java
            └── org
                └── jboss
                    └── resteasy
                        └── examples
                            └── springmvc
                                └── ContactsTest.java
示例项目所依赖的包简述如下。
org.jboss.resteasy
resteasy-spring:支持RESTEasy与Spring集成,底层依赖resteasy-jaxrs。resteasy-jaxrs是RESTEasy的JAX-RS核心包。
resteasy-client:RESTEasy客户端包。
resteasy-jaxb-provider:RESTEasy的XML处理包。
org.jboss.spec.javax.servlet
jboss-servlet-api_3.1_spec:RESTEasy的JBoss Servlet实现。
org.springframework
spring-webmvc:Spring MVC包。
2. Apache的CXF
CXF是Apache开源社区提供的JAX-RS项目,CXF的名称是由Celtix项目和XFire项目合并而来。其中Celtix由IONA Technologies开发,XFire来自Codehaus。CXF是JAX-WS的著名实现,同时实现了JAX-RS,从版本2.7.0开始几乎全面支持JAX-RS2.0全部特性。从版本3.0.0开始实现JAX-RS2客户端API。CXF的基本信息如下。
官方网站:http://cxf.apache.org。
官方文档:http://cxf.apache.org/docs/jax-rs.html。
项目下载:Apache CXF当前版本是3.1.3。下载地址为http://cxf.apache.org/download.html。
源代码:由Apache的GIT服务器托管,地址为https://git-wip-us.apache.org/repos/asf?p= cxf.git;另有一个自动镜像由GitHub托管,地址为https://github.com/apache/cxf。
邮件列表:http://cxf.apache.org/mailing-lists.html。
(1)快速开始
我们依然从官方提供的示例开始。首先迁出CXF的源代码,然后进入示例目录,这里选择的示例项目为jaxrs_spring_boot,示例操作如下。
git clone https://git-wip-us.apache.org/repos/asf/cxf.git
git clone https://github.com/apache/cxf.git
cd cxf/distribution/src/main/release/samples/jax_rs/jaxrs_spring_boot
mvn spring-boot:run
可以从上述的前2行中选择一个地址,将CXF源代码迁出。进入jaxrs_spring_boot目录,然后执行maven命令,启动基于spring-boot的REST服务。服务启动后,我们在另一个终端中使用cURL测试,将得到如下的输出。
curl ":8080/services/helloservice/sayHello/ApacheCxfUser"
 
Hello ApacheCxfUser, Welcome to CXF RS Spring Boot World!!!
(2)简单分析
该示例项目也是典型的JAX-RS实践。观察如下所示的目录树,HelloService是资源类,SampleRestWSApplication是服务器端包含main方法的入口类,SampleRestClientApplication是客户端包含main方法的入口类。
├── pom.xml
└── src
    └── main
        └── java
            └── sample
                └── rs
                    ├── client
                    │   └── SampleRestClientApplication.java
                    └── service
                        ├── HelloService.java
                        └── SampleRestWSApplication.java
示例项目所依赖的包简述如下。
spring-boot:Spring-boot启动包
org.springframework.boot:spring-boot-starter
org.springframework.boot:spring-boot-starter-web
cxf:CXF包
org.apache.cxf:cxf-rt-frontend-jaxrs
org.apache.cxf:cxf-rt-rs-client
org.apache.cxf:cxf-rt-transports-http
org.apache.cxf:cxf-rt-rs-service-description
jackson:JSON处理包
com.fasterxml.jackson.core:jackson-core
com.fasterxml.jackson.core:jackson-databind
com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider
com.fasterxml.jackson.core:jackson-annotations
jetty:Servlet容器Jetty包
org.eclipse.jetty:jetty-servlet
org.eclipse.jetty:jetty-webapp
org.eclipse.jetty:jetty-servlets
1.7.2 其他的REST实现
本节将介绍Java领域,没有遵循JAX-RS规范的REST式Web服务开发工具,包括Restlet、LinkedIn的Rest.li以及Spring MVC。
1. Restlet项目
Restlet是一款遵从REST风格的、基于Java平台的轻量级框架,当前版本为2.3。Restlet许可为免费开源,提供REST开发的完整支持。Restlet的基本信息如下。
官方网站:http://restlet.org。
源代码:由GitHub托管,地址为https://github.com/restlet/restlet-framework-java。
StackOverflow:http://stackoverflow.com/questions/tagged/restlet。
学习指南文档:http://restlet.org/learn/tutorial。
(1)快速开始
为了快速了解Restlet的使用,我们从一个现成的Restlet示例项目开始学习,示例如下。
wget http://restlet.com/technical-resources/restlet-framework/archives/examples/maven-spring/2.3/restlet-basecamp.zip
unzip restlet-basecamp.zip
cd restlet-basecamp
mvn jetty:run-war
上述操作分别执行了下载示例包、解压缩、进入示例项目目录以及启动服务。服务启动后,我们使用cURL测试,将得到如下的输出。
curl ":8080/basecamp/hello"
Hello World!
(2)简单分析
Restlet使用配置文件声明服务,我们观察如下所示的目录树,applicationContext.xml是配置文件,BaseCampApplication是REST服务入口类,BaseCampResource是资源类。
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── mycompany
        │           └── restlet
        │               └── basecamp
        │                   ├── application
        │                   │   └── BaseCampApplication.java
        │                   └── resource
        │                       └── demo
        │                           └── BaseCampResource.java
        ├── resources
        └── webapp
            └── WEB-INF
                ├── applicationContext.xml
                └── web.xml
配置文件applicationContext.xml展示如下,首先需要定义一个Spring组件basecamp-Component,它的defaultTarget属性指向BaseCampApplication的实例basecampAppliction。basecampAppliction的根路径路由指向SpringBeanRouter的实例。最后,/hello路径指向BaseCampResource的实例。
<bean id="basecampComponent" class="org.restlet.ext.spring.SpringComponent">
  <property name="defaultTarget" ref="basecampAppliction" />
</bean>
 
<bean id="basecampAppliction" class="com.mycompany.restlet.basecamp.application.BaseCampApplication">
  <property name="root" ref="router" />
</bean>
 
<!--  Define the router -->
<bean name="router" class="org.restlet.ext.spring.SpringBeanRouter" />
 
<!-- Define all the routes -->
<bean name="/hello" class="com.mycompany.restlet.basecamp.resource.demo.BaseCampResource" scope="prototype" autowire="byName" />
示例项目所依赖的包简述如下。
org.restlet.jee:org.restlet:jar:2.0.1
org.restlet.jee:org.restlet.ext.servlet:jar:2.0.1
org.restlet.jee:org.restlet.ext.spring:jar:2.0.1
org.springframework:spring-webmvc:jar:3.0.1.RELEASE。
其中,jee代表支持Java EE平台的包。此外,Restlet支持的平台体系如下。
jse (Java SE edition)
jee (Java EE edition)
gae (Google App Engine edition)
android (Android edition)
gwt (Google Web Toolkit edition)
osgi (OSGi Environments edition)
2. LinkedIn的Rest.li
Rest.li是社交网站LinkedIn开发的REST+JSONREST式服务框架。rest.li基本信息如下。
官方地址:rest.li(http://rest.li)。
源代码:由GitHub托管,地址为https://github.com/linkedin/rest.li。
wiki:https://github.com/linkedin/rest.li/wiki。
快速开始教学文档:https://github.com/linkedin/rest.li/wiki/Quickstart:-A-Tutorial-Introduction-to-Rest.li。
快速开始
Rest.li的项目是使用Gradle构建的,完整的代码迁出和构建示例如下。
git clone https://github.com/linkedin/rest.li.git
cd rest.li/examples/spring-server
gradle build
gradle JettyRunWar
curl -v http://localhost:8080/fortunes/1
3. Spring MVC项目
Spring框架使用Gradle构建和管理项目,使用GIT管理源代码,地址为https://github.com/spring-projects/spring-framework,其中MVC模块位于spring-framework/spring-webmvc目录下。
Spring从版本3.0开始提供了对REST式应用开发的支持,但Spring目前并没有也没必要推出一个实现JAX-RS标准的模块。MVC模块提供的REST功能并没有采用JAX-RS提出的标准。本质上,Spring MVC控制流程是使用Controller处理Model在某种动词性的业务逻辑操作,而JAX-RS的控制流程是使用资源类Resource处理名词性的资源表述。
在云概念和微服务大行其道的今天,Spring Boot成为依赖spring-framework项目的事实标准。本书的第7章将讲述Spring Boot在REST服务领域中的影响力及实践。
1.8 REST调试工具
在上节中,我们不但领略了REST请求处理流程,还对IDE中设置断点、观察服务器端运行时变量有了了解。本节将讲述如何在客户端对REST服务进行调试。为何需要在客户端调试REST服务呢?因为在REST开发过程中,需要对请求资源地址、资源所支持的数据媒体类型和返回值类型等进行调试和测试。因此,掌握客户端的调试工具是开发优秀的REST服务的前提。下面将介绍这一领域常用的REST请求工具,以使读者更进一步地熟悉REST开发和调试。
1.8.1 命令行调试工具
cURL(http://curl.haxx.se)是非常易用、强大的基于URL标准(RFC 3986)的命令行工具,通过命令行即可完成多种协议(比如HTTP)的请求,并可以将请求的响应信息输出在终端/控制台上,因此对于调试和测试REST请求非常方便。
HTTPie(http://httpie.org)是和cURL非常类似的命令行工具,相比cURL有更良好的用户体验。
命令行工具的优点是简单方便,缺点是没有图形化界面。下面将介绍几款基于浏览器的扩展插件作为REST客户端调试工具的使用情况。
1.8.2 基于浏览器的图形化调试插件
cURL功能强大、易于在自动化脚本中使用,但cURL的每个请求都要通过码字来完成、没有图形界面的特点并不适于所有读者。下面将介绍几种基于浏览器的图形化调试插件,以方便读者在开发和测试REST服务时选择使用。基于Chrome浏览器的REST插件有很多,本节将介绍其中的3种。
1. Simple REST Client插件
Simple REST Client插件是基于Chrome浏览器的扩展,安装该插件后Chrome窗口的右上方会出现该插件的图标,以方便使用。该项目的地址是https://github.com/jeremys/Simple-Rest-Client-Chrome-Extension,插件的下载地址是http://chrome.google.com/extensions/detail/fhjcajmcbmldlhcimfajhfbgofnpcjmb()。Simple REST Client插件的界面如图1-2所示。

图1-2 Simple REST Client插件示意图
Simple REST Client插件的特点是简单易用,其界面分为请求信息录入和响应信息展示上下两部分。录入部分包括URL、HTTP请求方法和请求头3部分,见图1-2中上方的数字标识1~3。其中,HTTP请求方法支持HTTP的标准方法GET、POST、PUT、DELETE、HEAD和OPTIONS,Headers部分需要完全手工输入。响应信息部分包括响应状态、响应头和响应实体3部分。其中,Headers部分展示HTTP请求交互的响应头信息,Data中展示的是响应实体信息,语法高亮显示,见图1-2中下方的数字标识4~6。Simple REST Client插件总体上说是麻雀虽小,五脏俱全,但功能相比后面要讲的插件不够强大。
2. Advance REST Client插件
Advance REST Client可以看作是Simple REST Client的增强版。该项目的地址是https://code.google.com/p/chrome-rest-client,插件的下载地址是https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo。界面如图1-3所示。

图1-3 Advance REST Client插件示意图
Advance REST Client提供更为丰富的功能,除了Simple REST Client插件具备的输入和输出(见图1-3中的数字标识),Advance REST Client还支持带参数的请求和提交表单等更完整的请求功能。数据格式上,支持原生的格式(Raw)、XMl格式的响应(Response)信息。Advance REST Client支持对请求地址的保存和对最近使用地址的记忆,如果调试中需要多次测试同一个资源地址,可以将其保存下来为以后使用;而多个这样的地址也可以按照项目分别保存,方便区分使用。
3. Postman-REST Client插件
Postman-REST Client是基于Simple REST Client源代码编写的专门针对REST的插件。该项目的地址是https://github.com/a85/POSTMan-Chrome-Extension,插件的下载地址是http://www.getpostman.com。界面如图1-4所示。

图1-4 Postman-REST Client插件示意图
Postman-REST Client提供的功能更多,除了Advance REST Client具有的输入和输出(见图1-4的数字标识部分),还可以发起基于安全的请求。请求方法不仅包括HTTP的标准方法,还包括WebDAV标准的方法。其响应信息的展示和支持的格式也更丰富。如果读者希望深入和细致地调试REST服务,Postman-REST Client要比其他插件更合适。可以说3个插件的复杂度和功能性是递增的,使用哪一个要看读者的需求。类似的Chrome插件不胜枚举,如果有兴趣,可以通过https://chrome.google.com/webstore/category/extensions访问Chrome的网上商店,搜索更多的Chrome插件。
4. Firefox插件
相对于Chrome浏览器,Firefox的REST插件功能类似,其中常用的插件有REST-Easy和RESTClient。REST-Easy的项目地址是https://github.com/nathan-osman/REST-Easy,RESTClient的项目地址是http://restclient.net。类似的Firefox插件很多,读者如果有兴趣,可以在Firefox浏览器中录入about:addons进入Firefox的扩展,搜索更多的相关插件。
1.9 本章小结
本章主要讲述了REST服务的概念和实战。先后解读了REST、REST服务、JAX-RS2标准中的重要概念,对JAX-RS2的参考实现项目Jersey进行了简单而全面的概述。随后讲述了如何快速创建REST应用和REST服务,介绍了基于JAX-RS2标准的其他项目和其他非JAX-RS2标准的、著名的Java项目。
通过阅读本章,读者可以清楚地掌握Java领域开发REST服务中的基本概念。
本章主要的知识点如下。
REST是什么
一种架构风格。
HTTP+URI+XML是REST的基本实现形式。
REST服务的辨析
JAX-RS2标准
Jersey项目
JAX-RS的其他实现
其他的REST实现
REST调试工具
下一章将深入REST的设计,讲述如何创建更标准和健壮的REST服务。





第2章
REST API设计
设计和开发REST式的Web服务除了要掌握JAX-RS2标准,还要对统一接口、资源定位以及请求处理过程中REST风格的传输数据的格式、响应信息等有良好的认知。此外,设计良好的REST API应当对内容协商、资源地址信息(link)有良好的支持。
本章将详细讨论这些技术细节。
2.1 统一接口
REST服务和RPC服务在接口定义上的区别是:REST使用HTTP协议的通用方法作为统一接口的标准词汇,REST服务所提供的方法信息都在HTTP方法里,而RPC服务所提供的方法信息在SOAP/HTTP信封里(其封装的格式通常是HTTP或SOAP),每一个RPC式的Web服务都会公布一套符合自己商业逻辑的方法词汇。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.annotation.method。
每一种HTTP请求方法都可以从安全性和幂等性两方面考虑,这对正确理解HTTP请求方法和设计统一接口具有决定性的意义。换句话说,要定义严谨的REST统一接口,就需要真正理解HTTP方法的安全性和幂等性。
安全性是指外系统对该接口的访问,不会使服务器端资源的状态发生改变;幂等性(idempotence)是指外系统对同一REST接口的多次访问,得到的资源状态是相同的。
阅读指南
这里讨论的安全性对应的英文是Safety而不是Security,系统安全请参考第10章。以下,将从REST统一接口的定义角度,逐个讲述HTTP方法。
2.1.1 GET方法
REST使用HTTP的GET方法获取服务提供的资源。GET方法是只读的,那么它是幂等和安全的吗?答案马上揭晓。
1. 幂等性和安全性
HTTP的GET方法用于读取资源。GET方法是幂等的,因为读取同一个资源,总是得到相同的数据。GET方法也是安全的,因为读取资源不会对其状态做改动。JAX-RS2定义了@GET注解对资源方法定义,使得该方法用于处理GET请求。
值得注意的是,虽然GET方法的特性是幂等和安全的,但这不意味着任何一个定义为处理GET请求的方法都是幂等和安全的。换句话说,设计不良的API有可能违背GET的特性,将一个不该是GET的方法定义为之。
举个例子,在系统B中设计一个REST的API,在客户端调用时读取系统A中x类型的数据,然后将A.x与系统B内的y类型数据做比较,如果两个集合的内容、最后更新时间上有不同,需要执行同步数据,即将A.x追加或者更新到B.y中。最后,将同步结果信息返回给向系统B发起请求的客户端,如图2-1所示。

图2-1 请求资源示意图
从图2-1左侧部分乍看上去,这是一个获取同步信息的API,因此这个API的设计应该使用GET请求方法。但是,稍作分析后即可知道该场景并不具备使用GET的基本条件。因为同步过程中对系统B内的资源有写操作的可能,因此不具备安全性;而写的内容又不是每次相同,因此不具有幂等性。所以,这个例子应该定义的正确的请求方法是POST。
2. 资源方法命名
不妨一起探讨一下图2-1中的同步信息的API该如何命名?既然是同步功能,那就以sync一类的字根作为前缀,这样所有的同步API都具有相同的开头,字迹也很工整。遗憾的是,这样的设计并不符合REST风格。笔者的理解是,从字面上看有两个问题。第一,sync字根具有非名词性的含义,从ROA角度上看,sync是RPC风格的命名:动词、自定义方法名称。第二,这样命名后,资源名称从一个主语变成了宾语,从ROA角度上看,面向的不再是资源,而是要执行的动作。
因此,标准的命名方式应该是单数的同步操作以资源名称命名;批量的同步操作以资源名称的复数名称命名。比如这个API是用于同步设备的,那么命名可以使用device和devices。如果担心与普通查询业务资源地址混淆,可以在资源路径中增加查询或者路径参数,比如device/id=1&source=a_b、device/b/a/等。
3. 抽象层注解资源
JAX-RS2的HTTP方法注解可以定义在接口和POJO中,置于接口中的方法名更具抽象性和通用性。示例代码如下。
@Path("book")
public interface BookResource {
//关注点1:GET注解从抽象类上移到接口 
@GET
    public String getWeight();
}
public class EBookResourceImpl implements BookResource {
    //关注点2:实现类无须GET注解 
    @Override
    public String getWeight() {
        return "150M";
    }
}
public class GETTest extends JerseyTest {
    @Override
    protected Application configure() {
//关注点3:加载的是实现类而不是接口 
        return new ResourceConfig(EBookResourceImpl.class);
    }
    @Test
    public void testGet() {
        Response response = target("book").request().get();
        Assert.assertEquals("150M", response.readEntity(String.class));
    }
}
在这段代码中,资源接口BookResource定义了一个GET方法getWeight(),这个方法使用了HTTP方法注解@GET,见关注点1。资源接口BookResource的实现类EBookResourceImpl实现了getWeight()方法,但没有再次使用@GET注解。也就是说,在接口中抽象地定义了资源的请求方法类型后,其全部实现类都无须再定义。这使得编码更整洁和抽象,见关注点2。最后,需要注意的是,在测试类GETTest中注册的是实现类EBookResourceImpl类型而不是接口BookResource类型,见关注点3。
另外,我们一并介绍下HEAD方法和OPTIONS方法。HEAD方法和GET方法相似,只是服务器端的返回值不包括HTTP实体。因此,HEAD方法是安全的和幂等的。JAX-RS2定义了@HEAD注解来定义相关资源方法。OPTIONS方法和GET方法相似,是安全的和幂等的。OPTIONS用于读取资源所支持的(Allow)所有HTTP请求方法。JAX-RS2定义了@OPTIONS注解来定义相关资源方法。
2.1.2 PUT方法
PUT方法是一种写操作的HTTP请求。REST使用HTTP的PUT方法更新或添加资源。下面讲解一下PUT方法的作用和操作时的媒体类型。
1. 更新资源
因为REST只是风格,不是技术规范或标准,所以有些实现REST的细节没有明确的定义,这对实践而言,不可避免会产生某些误解。比如在创建和更新某个资源的时候,开发者比较迷茫的是何时该用HTTP的PUT方法,何时该使用POST方法。为了解决这一问题,我们首先应该知道PUT方法的特性。PUT方法是幂等的,即多次插入或者更新同一份数据,在服务器端对资源状态所产生的改变是相同的。PUT方法不是安全的,有写动作的HTTP方法都不是安全的。 我们知道,由于使用同一份数据向服务器请求更新某一资源,得到的结果应该总是相同的,因此对于更新操作,使用PUT是没有疑问的。可能读者会想到最后更新时间字段每次提交会不同,但那已经不是同一份数据了。
2. 添加资源
创建操作通常每次得到的结果是不同的,因为服务器端的业务层逻辑通常要求数据的主键字段要么来自于业务平台自增一个逻辑值,要么来自于数据库的主键自增。因此,相同的数据每一次提交到服务器端,都会为数据添加一个新的主键值,也就是创建一个主键值不同的新资源(如果没有业务或者外键冲突)。所以,创建操作通常应当设计为POST方法的API。唯有一种场景应当使用PUT方法来设计API,即客户端在发起创建请求时,在同一份数据中总可以提供唯一的主键值,服务器不会对其进行修改,这样的创建请求确保了幂等性,不应再使用POST方法。JAX-RS2定义了@PUT注解来定义相关资源方法,示例代码如下。
@Path("book")
public interface BookResource {
//关注点1:PUT方法 
    @PUT
//关注点2:资源方法定义了Produces注解和Consumes注解 
    @Produces(MediaType.TEXT_PLAIN)
    @Consumes(MediaType.APPLICATION_XML)
    public String newBook(Book book);
}
 
public class PutTest extends JerseyTest {
    public static AtomicLong clientBookSequence = new AtomicLong();
    @Test
    public void testNew() {
        final Book newBook = new Book(clientBookSequence.incrementAndGet(),
"book-" + System.nanoTime());
        MediaType contentTypeMediaType = MediaType.APPLICATION_XML_TYPE;
        MediaType acceptMediaType = MediaType.TEXT_PLAIN_TYPE;
        final Entity<Book> bookEntity = Entity.entity(newBook, contentTypeMediaType);
        final String lastUpdate = target("book").request(acceptMediaType)
.put(bookEntity, String.class);
//关注点3:资源方法定义了Produces注解和Consumes注解 
        Assert.assertNotNull(lastUpdate);
        LOGGER.debug(lastUpdate);
    }
}
在这段代码中,资源接口BookResource使用@PUT注解定义了newBook()方法,即该方法用于处理相对资源路径为"book"的PUT请求,见关注点1。单元测试类PutTest对其功能性进行了验证,对lastUpdate使用非空断言,lastUpdate是更新方法newBook()的返回实体的值,代表最后更新时间戳,见关注点3。我们注意到,newBook()方法上,同时定义了@Produces(MediaType.TEXT_PLAIN)注解和@Consumes(MediaType.APPLICATION_XML)注解,见关注点2,下面我们来介绍一下与关注点2相关的媒体类型知识。
3. 媒体类型
PUT方法执行写操作的非安全的HTTP方法,需要考虑请求实体媒体类型和响应实体媒体类型。请求实体媒体类型使用HTTP头的Content Type定义,响应实体媒体类型使用HTTP头的Accept定义。
在服务器端,@Consumes(MediaType.APPLICATION_XML)定义了服务器端要消费的媒体类型,即消费客户端请求实体的媒体类型。@Produces(MediaType.TEXT_PLAIN)定义了服务器端生产的媒体类型,即服务器产生的响应实体的媒体类型。客户端在提交非安全性HTTP请求方法前,在Entity类的实例中,定义该Entity实例的媒体类型,即客户端请求实体的媒体类型。request方法用于定义可接受的HTTP方法的返回媒体类型,即服务器的响应实体的媒体类型。
测试资源方法newBook(),将得到如下所示的请求头信息,从中可以看到请求媒体类型。
public final static String TEXT_PLAIN = "text/plain";
public final static String APPLICATION_XML = "application/xml";
public final static MediaType TEXT_PLAIN_TYPE = new MediaType("text", "plain");
public final static MediaType APPLICATION_XML_TYPE = new MediaType("application", "xml");
 
1 > PUT http://localhost:9998/book 
1 > Accept: text/plain
1 > Content-Type: application/xml
在这段代码中,javax.ws.rs.core.MediaType类是JAX-RS2提供的媒体类型定义类,其中定义了包括示例中使用的MediaType.TEXT_PLAIN,其值为"text/plain"。在MediaType类中,对应的响应实体媒体类型定义为Accept: text/plain;MediaType.APPLICATION_XML值为"application/xml",对应的请求实体媒体类型定义为Content-Type: application/xml。
2.1.3 DELETE方法
DELETE方法是幂等的,即多次删除同一份数据(通常请求中传递的参数是数据的主键值),在服务器端产生的改变是相同的。JAX-RS2定义了@DELETE注解来定义相关资源方法。下面来看看具体示例。
执行删除的资源方法,其返回值可以定义为void,即该方法没有返回值。之所以在删除资源的场景中可以采用这样的方式定义,是因为删除的前提是对该资源信息已经充分了解,没有必要再将其从服务器上传递回来,示例代码如下。
@Path("book")
public interface BookResource {
    @DELETE
    public void delete(@QueryParam("bookId") final long bookId);
}
在这段代码中,无返回值的资源方法delete()返回的响应实体为空,HTTP状态码为204。该定义可以参考Jersey的源代码中的Response类,示例代码如下。
package javax.ws.rs.core;
public abstract class Response {
    public interface StatusType {
    public enum Status implements StatusType {
        NO_CONTENT(204, "No Content"),
接下来是删除资源方法的单元测试,示例代码如下。
public class DeleteTest extends JerseyTest {
    @Test
    public void testGet() {
        final Response response =target("book").queryParam("bookId", "9527")
.request().delete();
        int status = response.getStatus();
        LOGGER.debug(status);
        Assert.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), status);
    }
}
在这段代码中,对REST请求的测试断言不是针对删除资源的实体,而是响应中HTTP状态码。也就是说,删除资源方法的返回值类型可以定义为void,业务逻辑更关注删除操作的结果状态。
2.1.4 POST方法
POST方法是一种写操作的HTTP请求。RPC的所有写操作均使用POST方法,而REST只使用HTTP的POST方法添加资源。
1. 既不幂等也不安全
定义为POST的REST接口用于写数据,POST方法的特性是既不幂等也不安全。由于请求会改变服务器端资源的状态,因此它是不是安全的;由于每次请求对服务器端资源状态的改变并不是相同的,因此它不是幂等的。
2. 两种分类
REST中使用的POST可以称之为POST(a),即用于创建、添加资源的HTTP方法。这是相对于RPC式的Web服务中对POST的使用而言的。
在RPC中使用的POST可以称之为POST(p),即通过重载的POST用于处理某种操作。服务器接收POST(p)的请求后,不是直接处理POST请求,由于真正的方法信息位于信封头或实体主体里,因此需要先解析出执行方法。
JAX-RS2定义了@POST注解来定义相关资源方法。示例代码如下。
@Path("book")
public interface BookResource {
//关注点1:POST方法 
    @POST
    @Produces(MediaType.APPLICATION_XML)
    @Consumes(MediaType.APPLICATION_XML)
    public Book createBook(Book book);
 
public class PostTest extends JerseyTest {
    @Test
    public void testCreate() {
        final Book newBook = new Book("book-" + System.nanoTime());
        MediaType contentTypeMediaType = MediaType.APPLICATION_XML_TYPE;
        MediaType acceptMediaType = MediaType.APPLICATION_XML_TYPE;
        final Entity<Book> bookEntity = Entity.entity(newBook, contentTypeMediaType);
        final Book book =target("book").request(acceptMediaType).post(bookEntity,
 Book.class);
//关注点2:测试POST方法的断言 
        Assert.assertNotNull(book.getBookId());
        LOGGER.debug("Server Id="+book.getBookId());
    }
}
在这段代码中,资源接口BookResource定义了createBook()方法,该方法使用@POST注解,表示该方法处理"book"路径下的POST请求,见关注点1。在测试方法testCreate()中,关注请求结果实体的主键是否为空。这是因为在POST请求提交的添加资源操作中,主键的设置是在服务器端完成的,因此客户端成功请求添加资源后,应关注服务器端返回的实体结果是否有主键信息,见关注点2。
到此,我们完成了对HTTP的基本方法的讲述。除了HTTP协议定义的标准方法,还存在来自其他协议中的HTTP方法。接下来,我们一起探讨这些方法对REST服务的影响。
2.1.5 WebDAV扩展方法
WebDAV(Web-based Distributed Authoring and Versioning,基于Web的分布式创作与版本控制)是IETF的RFC4918规范(RFC2518规范的替代规范地址是http://tools.ietf.org/html/rfc4918),是对HTTP1.1协议的一组扩展,该协议允许用户以协作方式编辑和管理远程Web服务器上的文件。WebDAV在HTTP方法的基础上,增加了如下方法(详见RFC4918第9章)。
PROPFIND方法:用于从Web资源中查询存储为XML格式的属性数据,或者重载为从一个远程系统中查询目录结构的数据。
PROPPATCH方法:用于原子地更改和删除一个资源的多个属性。
MKCOL方法:用于创建目录。
COPY方法:用于将资源从一个URI资源地址复制到另一个URI资源地址。
MOVE方法:用于将资源从一个URI资源地址移动到另一个URI资源地址。
LOCK 方法:用于锁定一个资源。WebDAV支持共享锁和独占锁。
UNLOCK方法:用于解锁一个资源。
虽然WebDAV对HTTP方法做出了功能性扩展,使之提供更强大服务,但是从ROA角度讲,因为WebDAV在HTTP标准方法的基础上增加了特殊的方法名称,WebDAV破坏了统一接口的原则。因此,对是否应该在REST式的Web服务中支持WebDAV,业内的观点并不一致。
笔者的观点是如果遵从ROA,那么就不使用HTTP标准方法之外的方法。如果业务需求确实超出了标准方法所及,那么可以使用如下注解实现对WebDAV的支持。JAX-RS2规范没有阐述对WebDAV提供支持的文字,但是JAX-RS2定义了@HttpMethod注解来定义相关的资源方法。在Jersey应用中,可以使用@HttpMethod注解定义HTTP标准方法之外的方法名称来支持WebDAV,示例代码如下。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod(value = "MOVE")
@Documented
public @interface MOVE {
}
这段代码是对@MOVE注解的定义,使用@HttpMethod注解定义了名为MOVE的HTTP扩展方法。有了扩展方法注解,我们就可以在资源类中定义新的方法来支持扩展方法的请求了,示例代码如下。
@Path("book")
public interface BookResource {
    @MOVE
    public boolean moveBooks(Books books);
}
在这段代码中,资源类BookResource定义了moveBooks方法,该方法使用@MOVE注解定义,表示用于处理"book"路径下的MOVE请求。下面我们来看看相关的测试代码。
public class HttpMethodTest extends JerseyTest {
    @Override
    protected Application configure() {
        ResourceConfig resourceConfig = new ResourceConfig(EBookResourceImpl.class);
        return resourceConfig;
    }
    @Override
    protected void configureClient(ClientConfig clientConfig) {
//关注点1:定义Grizzly连接器 
    clientConfig.connectorProvider(new GrizzlyConnectorProvider());
    super.configureClient(clientConfig);
    }
    @Test
    public void testWebDav() {
//关注点2:HTTP MOVE请求 
        final Response response = target("book").request().method("MOVE");
        Boolean result = response.readEntity(Boolean.class);
//关注点3:Move方法测试断言 
        Assert.assertEquals(Boolean.TRUE, result);
    }
}
在这段代码中,测试方法testWebDav()在请求中定义了MOVE请求,见关注点2;断言是针对MOVE方法的返回值,见关注点3;可以看出,使用Jersey实现对WebDav的支持并不困难。
需要注意的是,Jersey默认的连接器只支持HTTP标准方法,因此要使用HTTP的扩展方法就不能直接使用默认的连接器,这里使用了Grizzly连接器。对应的代码行是:clientConfig.connectorProvider(new GrizzlyConnectorProvider()),即为客户端配置实例提供Grizzly连接器,见关注点1。这行代码是Jersey2.5+后的写法,Jersey2.5之前的写法是clientConfig.connector(new GrizzlyConnector(clientConfig))。从中可以看出,Jersey在不断优化中,包括API。这一好处是活跃的社区为用户带来越来越便捷、高效的使用体验,缺点是破坏了向下兼容性。
到此,我们全面掌握了HTTP方法在REST统一接口定义中的作用和实现。明白了REST接口该使用什么样的请求方法非常重要,这决定了其性质。但是这还不够,一个接口如何被请求唯一定位还需要深入掌握REST的资源定位。接下来一节将详述资源定位的细节。
2.2 资源定位
REST使用URI实现资源定位,从这个角度上讲,对外提供REST式的Web服务的接口就是公布一系列的URI及其参数,这使得REST的实践过程简单到了极致。但是URI形式上的简单并不意味着我们可以将URI的定义信手拈来,正所谓“没有规矩,不成方圆”。
在设计REST式的Web服务过程中,资源地址的设计是非常严谨的,如果设计不得体,不仅REST接口的风格无法统一,使系统的扩展性和易用性降低,也很难实现资源准确地被定位。
资源地址的设计过程是面向资源的,资源名称应是准确描述该资源的名词,资源地址应具有直观的描述性。比如一个班级的资源地址可以是:学校/学院/学级/班级。值得注意的是一个URI资源地址唯一对应一个资源,但是一个资源可以拥有多个URI资源地址。比如Jersey最新版本的文档地址和Jersey2.7版本的文档地址指向同一个资源(本书写作时)。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.annotation.param。
2.2.1 资源地址设计
资源地址的设计对整个REST式的Web服务至关重要,涉及系统的可用性、可维护性和可扩展性等诸多方面的表现。因此,本节关注如何对资源地址进行设计。
1. 资源路径概览
资源地址的路径变量是用来表达逻辑上的层次结构的,资源和子资源的形式是自左至右、斜杠分割的名词。它们的关系可以是从整体到局部,比如学校到班级,城市到乡镇;也可以是从一般到具体,比如一个生物的“门、纲、目、科、属、种”的资源路径。资源地址具体可以分为5个部分,以scheme://host:port/path?queryString为例,如表2-1所示。
表2-1 资源地址路径分解
元素 描  述
scheme 协议名称,通常是HTTP和HTTPS
host (DNS)主机名称或者IP地址
port 服务端口
path 资源地址,使用“/”符号来分隔逻辑上的层次结构
? 用来分隔资源地址和查询字符串符号
queryString 查询字符串,方法作用域信息
使用“&”符号来分隔查询条件
使用逗号分隔有次序的作用域信息
使用分号分隔无次序的作用域信息

一个典型的URI如表2-1所示,包括协议名称、主机名称、服务端口、资源地址和查询字符串等5个部分。其中资源地址部分,根据具体部署的不同或有差别,如图2-2所示。

图2-2 资源地址示例
图2-2中,通常使用ContextPath、ServletPath和PathInfo来细分资源地址。ContextPath是上下文名称,通常和部署服务器的配置或者REST服务的web.xml配置有关。ServletPath是Servlet的名称,与REST服务中定义的@ApplicationPath注解或者web.xml的配置有关。JAX-RS2定义了@Path注解来定义资源地址。PathInfo是资源路径信息,与资源类、子类以及类中的方法定义的@Path注解有关。
现在我们对资源地址的层次结构有了认识,此时需要思考一个问题:资源地址是否可以唯一定位一个资源?
答案是否定的。资源地址相同,但HTTP方法不同的两个方法是两个不同的REST接口。HTTP方法和资源地址结合在一起才可以完成对资源的定位。细心的读者也许已经从3.1节的示例中看出端倪。示例中,GET方法用于读取/检索、查询/过滤一个资源,PUT方法用于修改/更新资源、创建客户端维护主键信息的资源,DELETE方法用于删除资源,POST方法用于创建资源。但这些方法的资源地址是相同的,都是"book"。
当上述的标准HTTP方法无法满足业务需求时,比如对于图书资源,除了基本的CRUD之外,若需要公布像借阅、折旧、电子版下载等实际生活中的更新操作的接口时,单单公布一个PUT方法就不够用了。这些操作是动词性的,无法简单地使用一个book名词定位。在路径变量难以准确描述的情况下,一种方案是可以考虑使用动词作为查询参数;另一种方案是可以在REST设计过程中引入RPC风格的POST方法,辅助完成复杂业务的接口设计,这就是REST和RPC混合型的Web服务了。
2. 资源地址和作用域
在路径变量里可以使用标点符号以辅助增强逻辑清晰性。这些辅助符号用在表2-2中的查询字符串,作为资源地址的查询变量,用来表达算法的输入,实现对方法的作用域的约束。下面来逐一讲述这些对资源地址设计至关重要的符号。
(1)问号(?)是用来分隔资源地址和查询字符串的,与符号(&)是用来分隔查询条件的参数的。示例代码如下。
GET /books?start=0&size=10
这行代码中的作用是查询图书列表,开始行参数为0,条目参数为10,即从第0行开始取10条并返回该图书列表。
(2)逗号(,)是用来分隔有次序的作用域信息。需要注意的是逗号分隔的逻辑上的顺序信息,这种顺序可以是约定俗成的,比如先写经度后写纬度;也可以是系统约定的,比如月、日、年的顺序等。举例来说,按时间区间查询图书,日期信息在资源地址中是采用月、年顺序,示例如下。
GET /books/01,2002-12,2014
这行代码中的作用是查询2002年1月到2014年12月这个时间段(出版)的图书。这个例子中还使用了连字符(-),有时候也可以使用下横线(_)来做逻辑上的辅助分隔。
(3)分号(;)是用来分隔无次序的作用域信息。通常这些信息是逻辑上并列存在的,比如并列的查询条件,示例如下所示。
GET /books/restful;program=java;type=web
这行代码中的作用是查询满足图书内容为restful的、使用的编程语言是Java的、讲述的类型是Web的图书列表。这样的逻辑没有顺序,互换顺序的查询条件不会影响资源的表述。
基于上述理论,这里抛砖引玉,列出常用的资源地址设计示例如表2-2所示。
表2-2 资源地址设计
功能 资源地址
添加/创建 POST /books
PUT /books/{id}
删除 DELETE /books/{id}
修改/更新 PUT /books/{id}
查询全部 GET /books HTTP1.1
主键查询 GET /books/{id} HTTP1.1
GET /books?id=12345678
分页作用域查询 GET /books?start=0&size=10
GET /books/01,2002-12,2014
GET /books/restful;program=java;type=web
GET /books?limit=100&sort=bookname

如果读者可以轻松领会表2-2列出的这些典型的REST接口和资源定位的设计,就可以放手实现了,否则建议回顾本节内容。接下来,我们完成从设计到实现的跨越,看看JAX-RS2标准是如何通过注解来支持资源定位的,并使用Jersey完成上述设计的实践。
2.2.2 @QueryParam注解
查询条件决定了方法的作用域,查询参数组成了查询条件。JAX-RS2定义了@QueryParam注解来定义查询参数,本节使用@QueryParam演示3个REST查询接口的实现示例如表2-3所示。
表2-3 @QueryParam示例列表
接口描述 资源地址
分页查询列表数据 /query-resource/yijings?start=24&size=10
排序并分页查询列表数据 /query-resource/sorted-yijings?limit=5&sort=pronounce
查询单项数据 /query-resource/yijing?id=8

1. 分页查询
分页查询是使用@QueryParam解析参数的基本示例,实现代码如下所示。
public Yijings getByPaging(@QueryParam("start")final int start,
@QueryParam("size")final int size){//关注点1:资源方法入参 
...
    int listSize = globalList.size();
    final int max = size > listSize ? listSize : size;
//关注点2:分页迭代逻辑 
    for(int i = 0, index = start; i < max; i++) {
        final Yijing yijing = globalList.get(index + i);
//关注点3:添加Link以保证REST的连通性 
        final URI location = ub.clone().queryParam("id", yijing.getSequence()).build();
        final Link link =
new Link("detail", location.toASCIIString(), MediaType.APPLICATION_XML);
        links.add(link);
        yijings.add(yijing);
    }
    result.setLinks(links);
    result.setGuas(yijings);
    return result;
}
在这段代码中,getByPaging()方法的输入参数包含了2个使用@QueryParam注解定义的查询参数,分别是起始条目参数"start"和条目数量参数"size",参数的类型是整型,见关注点1。在查询的迭代中使用这两个参数获取图书列表,见关注点2。在迭代中,每个图书资源条目的URI都存储在返回值中,以保证资源的联通性,见关注点3。该URI被封装到Link实例中,在单项查询时使用。
另外,参数的定义使用了final,符合Checkstyle的编程风格,即输入参数只作为逻辑算法的依据使用,其本身不会在这过程中被修改。也许这种不变的变量对提高执行效率并没有多少影响,但跬步积千里、蚁穴溃长堤。推荐Java开发者在REST开发中引入SonarQube平台或者单纯使用Checkstyle工具对静态代码进行质量检测,以帮助我们改进代码的质量。
2. 排序查询
排序查询是在解析参数的基础上,额外处理结果集顺序的示例,代码如下。
public Yijings getByOrder(@QueryParam("limit") final int limit,
@QueryParam("sort") final String sortName) {//关注点1:资源方法入参 
...
Collections.sort(list, new Comparator<Yijing>() {
    @Override
//关注点2:排序中的比较算法 
    public int compare(final Yijing o1, final Yijing o2) {
        switch (sortName) {
            case "sequence":
                return o1.getSequence().compareTo(o2.getSequence());
            case "name":
                return o1.getName().compareTo(o2.getName());
            case "pronounce":
                return o1.getPronounce().compareTo(o2.getPronounce());
        }
        return 0;
    }
});
在这段代码中,limit参数的用途同分页查询示例,而sortName参数则用于排序,见关注点1;排序接口需要额外解析sortName传递的排序字段,并将其作为数据库查询语句中的排序参数使用。这里实现了Comparator接口的compare()方法来完成根据不同字段对集合的排序,见关注点2。
3. 单项查询
客户端在获得结果集的基础上,根据表述中链接信息,向服务器发起单项查询的示例,代码示例如下所示。
public Yijing getByQuery(@QueryParam("id") final int seqId) {
    return ParamCache.find("" + seqId);
}
在这段代码中,使用@QueryParam定义了"id"参数,该参数来自分页查询中返回的URI信息。
注解QueryParam可以和注解DefaultValue一起使用。注解DefaultValue的作用是预置一个默认值,当请求中不包含此参数时使用,示例如下。
@DefaultValue("100") @QueryParam("size") final Integer pageSize
这句话的意思是当请求中不包含分页参数pageSize时,分页参数pageSize的默认值为100。
2.2.3 @PathParam注解
JAX-RS2定义了@PathParam注解来定义路径参数—每个参数对应一个子资源,本节使用@PathParam完成如表2-4所示的REST查询接口。
表2-4 @PathParam示例列表
接口描述 资源地址
基本路径参数 /path-resource/Eric
结合查询参数 /path-resource/Eric?hometown=Buenos Aires
带有标点符号的资源路径 /path-resource/199-1999
/path-resource/01,2012-12,2014
子资源变长的资源路径 /path-resource/Asia/China/northeast/liaoning/shenyang/huangu
/path-resource/q/restful;program=java;type=web
/path-resource/q2/restful;program=java;type=web

1. @Path注解
JAX-RS2定义了@Path注解来定义资源路径,@Path接收一个value参数来解析资源路径地址。该参数除了前面示例中的books这种静态定义的方式外,也可以使用动态变量的方式,其格式为:{参数名称:正则表达式}。这个接口的功能和查询参数实现的/query-resource/yijings?start=24&size=10相似,也是用于分页查询,其资源地址形如:/path-resource/199-1999,参考示例如下。
@GET
@Path("{from:\\d+}-{to:\\d+}")
public String getByCondition(@PathParam("from") final Integer from,
@PathParam("to") final Integer to) {
...
在这段代码中,使用@PathParam注解定义的两个参数from和to用以定义查询区间,正则表达式部分是\d+,表示数字。两个参数中间的连接符(-)是路径的格式信息。稍显复杂的例子是:/path-resource/01,2012-12,2014,引入了逗号(,)作为有顺序的日期分隔符号,那么对应的正则表达式为:@Path("{beginMonth:\\d+},{beginYear:\\d+}-{endMonth:\\d+},{endYear:\\d+}")
2. 正则表达式
正则表达式的讲述超出了本书范围,这里只简述示例中用到的正则表达式。刚刚的例子中的\\d+,代表参数应为数字并且至少出现一次。第一个反斜杠是Java中的转义字符,第二个反斜杠是正则表达式的起始,加号(+)是至少出现一次的意思,星号(*)则代表出现至少零次,句号(.)是匹配任何字符,d是匹配数字,w是匹配数字和字母。我们有的放矢,示例中使用的正则表达式如表2-5所示,读者掌握所列的路径含义即可,我们的目的是学习REST API设计,而非正则本身。
表2-5 正则表达式示例
正则表达式 含  义
[a-zA-Z][a-zA-Z_0-9]* 以字母开头,后面是零到多个“字母_数字”格式的字符组合
{region:.+}/{district:\w+} region变量至少包含一个任意字符。
district变量至少包含一个为数字或者字母的字符

3. 路径配查询
查询参数和路径参数在一个接口中配合使用,可以更便捷地完成资源定位,这很像战场上的多兵种协同作战。前述的图书资源的复杂设计就需要两者结合来完成,示例代码如下。
@Path("{user: [a-zA-Z][a-zA-Z_0-9]*}")
@Produces(MediaType.TEXT_PLAIN)
public String getUserInfo(@PathParam("user") final String user,
@DefaultValue("Shen Yang")@QueryParam("hometown") final String hometown) {
    return user + ":" + hometown;
}
在这段代码中,路径参数user中使用了通配符,方法参数中同时使用@PathParam注解和@QueryParam,定义了user和hometown两个参数。以资源地址:/path-resource/Eric?hometown=Buenos Aires为例,REST容器会将该请求匹配到getUserInfo()方法,其中Eric是路径变量user的值,Buenos Aires作为查询变量hometown的值。
4. 路径区间
路径区间(PathSegment)是对资源地址更灵活的支持,使资源类的一个方法可以支持更广泛的资源地址的请求。我们从下面定义的资源地址列表来走近PathSegment。
/path-resource/Asia/China/northeast/liaoning/shenyang/huangu
/path-resource/China/northeast/liaoning/shenyang/tiexi
/path-resource/China/shenyang/huangu
如上所示的资源地址中含有固定子资源(shenyang)和动态子资源两部分。对于动态匹配变长的子资源资源地址,PathSegment类型的参数结合正则表达式将大显身手,示例代码如下。
@GET
@Path("{region:.+}/shenyang/{district:\\w+}")
public String getByAddress(@PathParam("region") final List<PathSegment> region,
@PathParam("district") final String district) {
    final StringBuilder result = new StringBuilder();
    for (final PathSegment pathSegment : region) {
        result.append(pathSegment.getPath()).append("-");
    }
    result.append("shenyang-" + district);
...
}
在这段代码中,getByAddress()方法用来匹配表的这些资源地址。该方法的region变量是PathSegment类型的数组,以匹配至少出现一个字符的正则表达式(+)。PathSegment如其名字所示,是路径的片段,是子资源的集合。遍历PathSegment集合,对于每一个PathSegment实例,可以通过调用其getPath()方法获取子资源名称。
对于查询参数动态给定的场景,可以定义PathSegment作为参数类型,通过getMatrix-Parameters()方法获取MultivaluedMap类型的查询参数信息,即可将参数条件作为一个整体解析,示例代码如下。
@Path("q/{condition}")
public String getByCondition3(@PathParam("condition") final PathSegment condition) {
...
  final MultivaluedMap<String, String> matrixParameters = condition.getMatrixParameters();
   final Iterator<Entry<String, List<String>>>
iterator = matrixParameters.entrySet().iterator();
    while (iterator.hasNext()) {
        final Entry<String, List<String>> entry = iterator.next();
        conds.append(entry.getKey()).append("=");
        conds.append(entry.getValue()).append(" ");
    }
    return conds.toString();
}
在这段代码中,getByCondition3()方法只有一个PathSegment类型的参数condition,该参数包含了查询条件中携带的全部参数列表。举例来说,资源地址为path-resource/q/restful;program=java;type=web的请求可以匹配到getByCondition3()方法,其中,MultivaluedMap类型的实例matrixParameters的值为[program=[java], type=[web]]。
5. @MatrixParam注解
上例中,通过编程方式,调用PathSegment类的getMatrixParameters()方法来获取查询参数信息。还有一种方式是通过@MatrixParam注解来逐一定义参数,即通过声明方式来获取,示例代码如下。
@Path("q2/{condition}")
public String getByCondition4(@PathParam("condition")
final PathSegment condition, @MatrixParam("program") final String program,
    @MatrixParam("type") final String type) {
    return condition.getPath() + " program=[" + program + "] type=[" + type + "]";
}
在这段代码中,使用@MatrixParam注解分别定义了"program"和"type"两个参数。与上例相比,这段代码更能清晰地表达可接收的参数名称和类型,缺点是缺乏对请求资源地址更灵活的支持。
2.2.4 @FormParam注解
JAX-RS2定义了@FormParam注解来定义表单参数,相应的REST方法用以处理请求实体媒体类型为Content-Type: application/x-www-form-urlencoded的请求,示例代码如下。
@Path("form-resource")
public class FormResource {
@POST
public String newPassword(
    @DefaultValue("feuyeux") @FormParam(FormResource.USER) final String user,
    @Encoded @FormParam(FormResource.PW) final String password,
    @Encoded @FormParam(FormResource.NPW) final String newPassword,
    @FormParam(FormResource.VNPW) final String verification) {
在这段代码中,newPassword()方法是@FormParam注解定义了user等4个参数,这些参数是容器从请求中获取并匹配的。相关的客户端测试如图2-3所示。


图2-3 表单示例
图2-3所示的客户端工具是POSTMAN(详见2.6节),使用POSTMAN定义的基本表单信息与newPassword()方法一致。
newPassword()方法的测试代码片段,示例代码如下。
@Test
public void testPost2() {
    final Form form = new Form();
    form.param(FormResource.USER, "feuyeux");
    form.param(FormResource.PW, "北京");
    form.param(FormResource.NPW, "上海");
    form.param(FormResource.VNPW, "上海");
    final String result = target("form-resource").request().
post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), String.class);
    FormTest.LOGGER.debug(result);
    Assert.assertEquals("encoded should let it to disable decoding",
"feuyeux:%E5%8C%97%E4%BA%AC:%E4%B8%8A%E6%B5%B7:上海", result);
}
在这段代码中,Form类实例是请求实体,请求实体的类型为MediaType.APPLI-CATION_FORM_URLENCODED_TYPE,即application/x-www-form-urlencoded。这里还需要注意的是@Encoded注解和@DefaultValue注解的使用。
JAX-RS2定义了@Encoded注解用以标识禁用自动解码。示例的测试结果中“%E4%B8%8A%E6%B5%B7”是newPassword()方法的参数值“上海”的编码值,当对newPassword使用@Encoded注解,REST方法得到的参数值就不会被解码,如果将其直接返回,那么客户端得到的值就会是处于编码状态的字符串。
JAX-RS2定义了@DefaultValue注解,用以为客户端没有为其提供值的参数提供默认值。本例的user参数的默认值为feuyeux。
2.2.5 @BeanParam注解
JAX-RS2定义了@BeanParam注解用于自定义参数组合,使REST方法可以使用简洁的参数形式完成复杂的接口设计。@BeanParam注解的使用示例如下所示。
@GET
@Path("{region:.+}/shenyang/{district:\\w+}")
//关注点1:资源方法入参 
public String getByAddress(@BeanParam Jaxrs2GuideParam param) {
//关注点2:参数组合 
public class Jaxrs2GuideParam {
    @HeaderParam("accept")
    private String acceptParam;
    @PathParam("region")
    private String regionParam;
    @PathParam("district")
    private String districtParam;
    @QueryParam("station")
    private String stationParam;
    @QueryParam("vehicle")
    private String vehicleParam;
 
public void testBeanParam() {
...
    final WebTarget queryTarget = target(path).path("China").path("northeast")
    .path("shenyang").path("tiexi")
.queryParam("station", "Workers Village").queryParam("vehicle", "bus");
    result = queryTarget.request().get().readEntity(String.class);
    //关注点3:查询结果断言 
    Assert.assertEquals("China/northeast:tiexi:Workers Village:bus", result);
}
 
//关注点4:复杂的查询请求 
http://localhost:9998/ctx-resource/China/shenyang/tiexi?station=Workers+Village&vehicle=bus 
在这段代码中,getByAddress()方法只用了一个使用@BeanParam注解定义的Jaxrs2GuideParam类型的参数,见关注点1;Jaxrs2GuideParam类定义了一系列REST方法会用到的参数类型,包括示例中使用的查询参数"station"和路径参数"region"等,从而使得getByAddress()方法可以匹配更为复杂的资源路径,见关注点2;在变长子资源的例子基础上,增加了查询条件,但测试方法testBeanParam()发起的请求的资源地址见关注点4;可以看出这是一个较为复杂的查询请求。其中路径部分包括China/shenyang/tiexi,查询条件包括station=Workers+Village和vehicle=bus。这些条件均在Jaxrs2GuideParam类中可以匹配,因此从关注点3的测试断言中可以看出,该请求响应的预期结果是"China/northeast:tiexi:Workers Village:bus"。
2.2.6 @CookieParam注解
JAX-RS2定义了@CookieParam注解用以匹配Cookie中的键值对信息,示例如下。
@GET
public String getHeaderParams(@CookieParam("longitude") final String longitude,
    @CookieParam("latitude") final String latitude,
    @CookieParam("population") final double population,
    @CookieParam("area") final int area) {//关注点1:资源方法入参 
    return longitude + "," + latitude + " population=" + population + ",area=" + area;
@Test
public void testContexts() {
    final Builder request = target(path).request();
    request.cookie("longitude", "123.38");
    request.cookie("latitude", "41.8");
    request.cookie("population", "822.8");
    request.cookie("area", "12948");
    result = request.get().readEntity(String.class);
    //关注点2:测试结果断言 
    Assert.assertEquals("123.38,41.8 population=822.8,area=12948", result);
}
在这段代码中,getHeaderParams()方法包含4个使用@CookieParam注解定义的参数,用于匹配Cookie的字段,见关注点1;在测试方法testContexts中,客户端Builder实例填充了相应的cookie键值对信息,其断言是对cookie字段值的验证,见关注点2。
2.2.7 @Context注解
JAX-RS2定义了@Context注解来解析上下文参数,JAX-RS2中有多种元素可以通过@Context注解作为上下文参数使用,示例代码如下。
public String getByAddress(
    @Context final Application application,
    @Context final Request request,
    @Context final javax.ws.rs.ext.Providers provider,
    @Context final UriInfo uriInfo,
    @Context final HttpHeaders headers){
在这段代码中,分别定义了Application、Request、Providers、UriInfo和HttpHeaders等5种类型的上下文实例。从这些实例中可以获取请求过程中的重要参数信息,示例代码如下。
final MultivaluedMap<String, String> pathMap = uriInfo.getPathParameters();
final MultivaluedMap<String, String> queryMap = uriInfo.getQueryParameters();
final List<PathSegment> segmentList = uriInfo.getPathSegments();
final MultivaluedMap<String, String> headerMap = headers.getRequestHeaders();
在这段代码中,UriInfo类是路径信息的上下文,从中可以获取路径参数集合getPath-Parameters()和查询参数集合getQueryParameters()。类似地,我们可以从HttpHeaders类中获取头信息集合getRequestHeaders()。这些业务逻辑处理中常用的辅助信息的获取,要通过@Context注解定义方法的参数或者类的字段来实现。
到此,统一接口和资源定位的设计和实现已经讲述完毕。但是,设计REST接口还需要在此基础上,掌握请求实体和响应实体的传输格式。接下来让我们看看Jersey都支持哪些类型的传输格式。
2.3 传输格式
本节要考虑的就是如何设计表述,即传输过程中数据采用什么样的数据格式。通常,REST接口会以XML和JSON作为主要的传输格式,这两种格式数据的处理是本节的重点。那么Jersey是否还支持其他的数据格式呢?答案是肯定的,让我们逐一掌握各种类型的实现。
2.3.1 基本类型
Java的基本类型又叫原生类型,包括4种整型(byte、short、int、long)、2种浮点类型(float、double)、Unicode编码的字符(char)和布尔类型(boolean)。
阅读指南
本节的前4小节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.response。
Jersey支持全部的基本类型,还支持与之相关的引用类型。前述示例已经呈现了整型(int)等Java的基本类型的参数,本例展示字节数组类型作为请求实体类型、字符串作为响应实体类型的示例,示例代码如下。
@POST
@Path("b")
public String postBytes(final byte[] bs) {//关注点1:测试方法入参 
    for (final byte b : bs) {
    LOGGER.debug(b);
    }
    return "byte[]:" + new String(bs);
}
@Test
public void testBytes() {
    final String message = "TEST STRING";
    final Builder request = target(path).path("b").request();
    final Response response = request.post(
Entity.entity(message, MediaType.TEXT_PLAIN_TYPE), Response.class);
    result = response.readEntity(String.class);
//关注点2:测试断言 
    Assert.assertEquals("byte[]:" + message, result);
}
在这段代码中,资源方法postBytes()的输入参数是byte[]类型,输出参数是String类型,见关注点1;单元测试方法testBytes()的断言是对字符串"TEST STRING"的验证,见关注点2。
2.3.2 文件类型
Jersey支持传输File类型的数据,以方便客户端直接传递File类实例给服务器端。文件类型的请求,默认使用的媒体类型是Content-Type: text/html,示例代码如下。
@POST
@Path("f")
//关注点1:测试方法入参 
public File postFile(final File f) throws FileNotFoundException, IOException {
//关注点2:try-with-resources 
    try (BufferedReader br = new BufferedReader(new FileReader(f))) {
        String s;
        do {
            s = br.readLine();
            LOGGER.debug(s);
        } while (s != null);
        return f;
    }
}
@Test
public void testFile() throws FileNotFoundException, IOException {
//关注点3:获取文件全路径 
    final URL resource = getClass().getClassLoader().getResource("gua.txt");
//关注点4:构建File实例 
    final String file = resource.getFile();
    final File f = new File(file);
    final Builder request = target(path).path("f").request();
//关注点5:提交POST请求 
    Entity<File> e = Entity.entity(f, MediaType.TEXT_PLAIN_TYPE);
    final Response response = request.post(e, Response.class);
    File result = response.readEntity(File.class);
    try (BufferedReader br = new BufferedReader(new FileReader(result))) {
        String s;
        do {
            s = br.readLine();//关注点6:逐行读取文件 
            LOGGER.debug(s);
        } while (s != null);
    }
}
在这段代码中,资源方法postFile()的输入参数类型和返回值类型都是File类型,见关注点1;服务器端对File实例进行解析,最后将该资源释放,即try-with-resources,见关注点2;在测试方法testFile()中,构建了File类型的"gua.txt"文件的实例,见关注点3;作为请求实体提交,见关注点4;并对响应实体进行逐行读取的校验,见关注点5;需要注意的是,由于我们使用的是Maven构建的项目,测试文件位于测试目录的resources目录,其相对路径为/simple-service-3/src/test/resources/gua.txt,获取该文件的语句为getClass().getClassLoader().getResource("gua.txt"),见关注点6。
另外,文件的资源释放使用了JDK7的try-with-resources语法,见关注点2。
2.3.3 InputStream类型
Jersey支持Java的两大读写模式,即字节流和字符流。本示例展示字节流作为REST方法参数,示例如下。
@POST
@Path("bio")
//关注点1:资源方法入参 
public String postStream(final InputStream is) throws FileNotFoundException, IOException {
//关注点2:try-with-resources 
    try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
        StringBuilder result = new StringBuilder();
        String s = br.readLine();
        while (s != null) {
            result.append(s).append("\n");
            LOGGER.debug(s);
            s = br.readLine();
        }
        return result.toString();//关注点3:资源方法返回值 
    }
}
@Test
public void testStream() {
//关注点4:获取文件全路径 
    final InputStream resource = getClass().getClassLoader().getResourceAsStream("gua.txt");
    final Builder request = target(path).path("bio").request();
    Entity<InputStream> e = Entity.entity(resource, MediaType.TEXT_PLAIN_TYPE);
    final Response response = request.post(e, Response.class);
    result = response.readEntity(String.class);
//关注点5:输出返回值内容 
    LOGGER.debug(result);
}
在这段代码中,资源方法postStream()的输入参数类型是InputStream,见关注点1;服务器端从中读取字节流,并最终释放该资源,见关注点2;返回值是String类型,内容是字节流信息,见关注点3;测试方法testStream()构建了"gua.txt"文件内容的字节流,作为请求实体提交,见关注点4;响应实体预期为String类型的"gua.txt"文件内容信息,见关注点5。
2.3.4 Reader类型
本示例展示另一种Java读写模式,以字符流作为REST方法参数,示例如下。
@POST
@Path("cio")
//关注点1:资源方法入参 
public String postChars(final Reader r) throws FileNotFoundException, IOException {
//关注点2:try-with-resources 
    try (BufferedReader br = new BufferedReader(r)) {
        String s = br.readLine();
        if (s == null) {
            throw new Jaxrs2GuideNotFoundException("NOT FOUND FROM READER");
        }
        while (s != null) {
            LOGGER.debug(s);
            s = br.readLine();
        }
        return "reader";
    }
}
@Test
public void testReader() {
//关注点3:构建并提交Reader实例 
    ClassLoader classLoader = getClass().getClassLoader();
    final Reader resource =
new InputStreamReader(classLoader.getResourceAsStream("gua.txt"));
    final Builder request = target(path).path("cio").request();
    Entity<Reader> e = Entity.entity(resource, MediaType.TEXT_PLAIN_TYPE);
    final Response response = request.post(e, Response.class);
    result = response.readEntity(String.class);
//关注点4:输出返回值内容 
    LOGGER.debug(result);
}
在这段代码中,资源方法postChars()的输入参数类型是Reader,见关注点1;服务器端从中读取字符流,并最终释放该资源,返回值是String类型,见关注点2;测试方法testReader()构建了"gua.txt"文件内容的Reader实例,将字符流作为请求实体提交,见关注点3;响应实体预期为String类型的"gua.txt"文件内容信息,见关注点4。
2.3.5 XML类型
XML类型是使用最广泛的数据类型。Jersey对XML类型的数据处理,支持Java领域的两大标准,即JAXP(Java API for XML Processing,JSR-206)和JAXB(Java Architecture for XML Binding,JSR-222)。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.media.xml。
1. JAXP标准
JAXP包含了DOM、SAX和StAX 3种解析XML的技术标准。
DOM是面向文档解析的技术,要求将XML数据全部加载到内存,映射为树和结点模型以实现解析。
SAX是事件驱动的流解析技术,通过监听注册事件,触发回调方法以实现解析。
StAX是拉式流解析技术,相对于SAX的事件驱动推送技术,拉式解析使得读取过程可以主动推进当前XML位置的指针而不是被动获得解析中的XML数据。
对应的,JAXP定义了3种标准类型的输入接口Source(DOMSource,SAXSource,StreamSource)和输出接口Result(DOMResult,SAXResult,StreamResult)。Jersey可以使用JAXP的输入类型作为REST方法的参数,示例代码如下。
@POST
@Path("stream")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public StreamSource getStreamSource(
javax.xml.transform.stream.StreamSource streamSource) {
//关注点1:资源方法入参 
    return streamSource;
}
@POST
@Path("sax")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点2:支持SAX技术 
public SAXSource getSAXSource(javax.xml.transform.sax.SAXSource saxSource) {
    return saxSource;
}
@POST
@Path("dom")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点3:支持DOM技术 
public DOMSource getDOMSource(javax.xml.transform.dom.DOMSource domSource) {
    return domSource;
}
@POST
@Path("doc")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
//关注点4:支持DOM技术 
public Document getDocument(org.w3c.dom.Document document) {
    return document;
}
在这段代码中,资源方法getStreamSource()使用StAX拉式流解析技术支持输入输出类型为StreamSource的请求,见关注点1;getSAXSource()方法使用SAX是事件驱动的流解析技术支持输入输出类型为SAXSource的请求,见关注点2;getDOMSource()方法和getDocument()方法使用DOM面向文档解析的技术,支持输入输出类型分别为DOMSource和Document的请求,见关注点3和关注点4。
2. JAXB标准
JAXP的缺点是需要编码解析XML,这增加了开发成本,但对业务逻辑的实现并没有实质的贡献。JAXB只需要在POJO中定义相关的注解(早期人们使用XML配置文件来做这件事),使其和XML的schema对应,无须对XML进行程序式解析,弥补了JAXP的这一缺点,因此本书推荐使用JAXB作为XML解析的技术。
JAXB通过序列化和反序列化实现了XML数据和POJO对象的自动转换过程。在运行时,JAXB通过编组(marshall)过程将POJO序列化成XML格式的数据,通过解编(unmarshall)过程将XML格式的数据反序列化为Java对象。JAXB的注解位于javax.xml.bind.annotation包中,详情可以访问JAXB的参考实现网址是https://jaxb.java.net/tutorial。
需要指出的是,从理论上讲,JAXB解析XML的性能不如JAXP,但使用JAXB的开发效率很高。笔者所在的开发团队使用JAXB解析XML,从实践体会而言,笔者并不支持JAXB影响系统运行性能这样的观点。因为计算机执行的瓶颈在IO,而无论使用哪种技术解析,XML数据本身是一样的,区别仅在于解析手段。而REST风格以及敏捷思想的宗旨就是简单—开发过程简单化、执行逻辑简单化,因此如果连XML数据都趋于简单,JAXP带来的性能优势就可以忽略不计了。综合考量,实现起来更简单的JAXB更适合做REST开发。
Jersey支持使用JAXBElement作为REST方法参数的形式,也支持直接使用POJO作为REST方法参数的形式,后一种更为常用,示例代码如下。
@POST
@Path("jaxb")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public Book getEntity(JAXBElement<Book> bookElement) {
    Book book = bookElement.getValue();
    LOGGER.debug(book.getBookName());
    return book;
}
@POST
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_XML)
public Book getEntity(Book book) {
    LOGGER.debug(book.getBookName());
    return book;
}
以上JAXP和JAXB的测试如下所示,其传输内容是相同的,不同在于服务器端的REST方法定义的解析类型和返回值类型。
1 > Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book bookId="100" bookName="TEST BOOK"/>
2 < Content-Length: 79
2 < Content-Type: text/html
<?xml version="1.0" encoding="UTF-8"?><book bookId="100" bookName="TEST BOOK"/>
从测试结果可以看到,POJO类的字段是作为XML的属性组织起来的,详见如下的图书实体类定义。
@XmlRootElement
public class Book implements Serializable {
//关注点1:JAXB属性注解 
    @XmlAttribute(name = "bookId")
    public Long getBookId() {
        return bookId;
    }
    @XmlAttribute(name = "bookName")
    public String getBookName() {
        return bookName;
    }
    @XmlAttribute(name = "publisher")
    public String getPublisher() {
        return publisher;
    }
}
(1)property和element
本例的POJO类Book的字段都定义为XML的属性(property)来组织,POJO的字段也可以作为元素(element)组织,见关注点1。如何定义通常取决于对接系统的设计。需要注意的是,如果REST请求的传输数据量很大,并且无须和外系统对接的场景,建议使用属性来组织XML,这样可以极大地减少XML格式的数据包的大小。
(2)XML_SECURITY_DISABLE
Jersey默认设置了XMLConstants.FEATURE_SECURE_PROCESSING(http://javax.xml.XML Constants/feature/secure-processing)属性,当属性或者元素过多时,会报“well-formedness error”这样的警告信息。如果业务逻辑确实需要设计一个繁琐的POJO,可以通过设置MessageProperties.XML_SECURITY_DISABLE参数值为TRUE来屏蔽。服务器端和客户端,示例代码如下。
@ApplicationPath("/*")
public class XXXResourceConfig extends ResourceConfig {
    public XXXResourceConfig() {
       packages("xxx.yyy.zzz");
       property(MessageProperties.XML_SECURITY_DISABLE, Boolean.TRUE);
    }
}
ClientConfig config = new ClientConfig();
config.property(MessageProperties.XML_SECURITY_DISABLE, Boolean.TRUE);
2.3.6 JSON类型
JSON类型已经成为Ajax技术中数据传输的实际标准。Jersey提供了4种处理JSON数据的媒体包。表2-6展示了4种技术对3种解析流派(基于POJO的JSON绑定、基于JAXB的JSON绑定以及低级的(逐字的)JSON解析和处理)的支持情况。MOXy和Jackon的处理方式相同,它们都不支持以JSON对象方式解析JSON数据,而是以绑定方式解析。Jettison支持以JSON对象方式解析JSON数据,同时支持JAXB方式的绑定。JSON-P就只支持JSON对象方式解析这种方式了。
表2-6 Jersey对JSON的处理方式列表
解析方式\JSON支持包 MOXy JSON-P Jackson Jettison
POJO-based JSON Binding
JAXB-based JSON Binding
Low-level JSON parsing & processing

下面将介绍MOXy、SON-P、Jackson和Jettison这4种Jersey支持的JSON处理技术在REST式的Web服务开发中的使用。
1. 使用MOXy处理JSON
MOXy是EclipseLink项目的一个模块,其官方网站http://www.eclipse.org/eclipselink/moxy.php宣称EclipseLink的MOXy组件是使用JAXB和SDO作为XML绑定的技术基础。MOXy实现了JSR 222标准(JAXB2.2)和JSR 235标准(SDO2.1.1),这使得使用MOXy的Java开发者能够高效地完成Java类和XML的绑定,所要花费的只是使用注解来定义它们之间的对应关系。同时,MOXy实现了JSR-353标准(Java API for Processing JSON1.0),以JAXB为基础来实现对JSR353的支持。下面开始讲述使用MOXy实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的MOXy示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-1.simple-service-moxy。
(1)定义依赖
MOXy是Jersey默认的JSON解析方式,可以在项目中添加MOXy的依赖包来使用MOXy。
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-moxy</artifactId>
</dependency>
(2)定义Application
使用Servlet3可以不定义web.xml配置,否则请参考1.6节的讲述。
MOXy的Feature接口实现类是MoxyJsonFeature,默认情况下,Jersey对其自动探测,无须在Applicaion类或其子类显式注册该类。如果不希望Jersey这种默认行为,可以通过设置如下属性来禁用自动探测:CommonProperties.MOXY_JSON_FEATURE_DISABLE两端禁用,ServerProperties.MOXY_JSON_FEATURE_DISABLE服务器端禁用,ClientProperties.MOXY_JSON_FEATURE_DISABLE客户端禁用。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends ResourceConfig {
    public JsonResourceConfig() {
        register(BookResource.class);
        //property(org.glassfish.jersey.CommonProperties.MOXY_JSON_FEATURE_DISABLE, true); 
    }
}
(3)定义资源类
接下来,我们定义一个图书资源类BookResource,并在其中实现表述媒体类型为JSON的资源方法getBooks()。支持JSON格式的表述的资源类定义如下。
@Path("books")
//关注点1:@Produces注解和@Consumes注解上移到接口 
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
    private static final HashMap<Long, Book> memoryBase;
...
    @GET
    //关注点2:实现类方法无需再定义@Produces注解和@Consumes注解 
    public Books getBooks() {
        final List<Book> bookList = new ArrayList<>();
        final Set<Map.Entry<Long, Book>> entries = BookResource.memoryBase.entrySet();
        final Iterator<Entry<Long, Book>> iterator = entries.iterator();
        while (iterator.hasNext()) {
            final Entry<Long, Book> cursor = iterator.next();
            BookResource.LOGGER.debug(cursor.getKey());
            bookList.add(cursor.getValue());
        }
        final Books books = new Books(bookList);
        BookResource.LOGGER.debug(books);
        return books;
    }
}
在这段代码中,资源类BookResource定义了@Consumes(MediaType.APPLICATION_JSON)和@Produces(MediaType.APPLICATION_JSON),表示该类的所有资源方法都使用MediaType.APPLICATION_JSON类型作为请求和响应的数据类型,见关注点1;因此,getBooks()方法上无须再定义@Consumes和@Produces,见关注点2。
如果REST应用处于多语言环境中,不要忘记统一开放接口的字符编码;如果统一开放接口同时供前端jsonp使用,不要忘记添加相关媒体类型,示例如下。
@Produces({"application/x-javascript;charset=UTF-8", "application/json;charset=UTF-8"})
在这段代码中,REST API将支持jsonp、json,并且统一字符编码为UTF-8。
(4)单元测试
JSON处理的单元测试主要关注请求的响应中JSON数据的可用性、完整性和一致性。在本章使用的单元测试中,验证JSON处理无误的标准是测试的返回值是一个Java类型的实体类实例,整个请求处理过程中没有异常发生,测试代码如下。
public class JsonTest extends JerseyTest {
    private final static Logger LOGGER = Logger.getLogger(JsonTest.class);
@Override
    protected Application configure() {
        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);
        return new ResourceConfig(BookResource.class);
    }
    @Test
    public void testGettingBooks() {
//关注点1:在请求中定义媒体类型为JSON 
        Books books = target("books").request(MediaType.APPLICATION_JSON_TYPE).
get(Books.class);
        for (Book book : books.getBookList()) {
            LOGGER.debug(book.getBookName());
        }
    }
}
在这段代码中,测试方法testGettingBooks()定义了请求资源的数据类型为MediaType.APPLICATION_JSON_TYPE来匹配服务器端提供的REST API,其作用是定义请求的媒体类型为JSON格式的,见关注点1。
(5)集成测试
除了单元测试,我们使用cURL来做集成测试。首先启动本示例,然后输入如下所示的命令。
curl http://localhost:8080/simple-service-moxy/api/books
curl -H "Content-Type: application/json" http://localhost:8080/simple-service-moxy/api/books
返回JSON格式的数据如下。
{"bookList":{"book":[{"bookId":1,"bookName":"JSF2和RichFaces4使用指南","publisher":"电子工业出版社","isbn":"9787121177378","publishTime":"2012-09-01"},{"bookId":2,"bookName":"Java Restful Web Services实战","publisher":"机械工业出版社","isbn":"9787111478881","publishTime":"2014-09-01"},{"bookId":3,"bookName":"Java EE 7 精髓","publisher":"人民邮电出版社","isbn":"9787115375483","publishTime":"2015-02-01"},{"bookId":4,"bookName":"Java Restful Web Services实战II","publisher":"机械工业出版社"}]}}
2. 使用JSON-P处理JSON
JSON-P的全称是 Java API for JSON Processing(Java的JSON处理API),而不是JSON with padding(JSONP),两者只是名称相仿,用途大相径庭。JSON-P是JSR 353标准规范,用于统一Java处理JSON格式数据的API,其生产和消费的JSON数据以流的形式,类似StAX处理XML,并为JSON数据建立Java对象模型,类似DOM。而JSONP是用于异步请求中传递脚本的回调函数来解决跨域问题。下面开始讲述使用JSON-P实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的JSON-P示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-2.simple-service-jsonp。
(1)定义依赖
使用JSON-P方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-processing</artifactId>
</dependency>
(2)定义Application
使用JSON-P的应用,默认不需要在其Application中注册JsonProcessingFeature,除非使用了如下设置。依次用于在服务器和客户端两侧去活JSON-P功能、在服务器端去活JSON-P功能、在客户端去活JSON-P功能。
CommonProperties.JSON_PROCESSING_FEATURE_DISABLE
ServerProperties.JSON_PROCESSING_FEATURE_DISABLE
ClientProperties.JSON_PROCESSING_FEATURE_DISABLE
JsonGenerator.PRETTY_PRINTING属性用于格式化JSON数据的输出,当属性值为TRUE时,MesageBodyReader和MessageBodyWriter实例会对JSON数据进行额外处理,使得JSON数据可以格式化打印。该属性的设置在Application中,见关注点1,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends ResourceConfig {
    public JsonResourceConfig() {
        register(BookResource.class);
//关注点1:配置JSON格式化输出 
        property(JsonGenerator.PRETTY_PRINTING, true);
    }
}
(3)定义资源类
资源类BookResource同上例一样定义了类级别的@Consumes和@Produces,媒体格式为MediaType.APPLICATION_JSON,资源类BookResource的示例代码如下。
@Path("books")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
...
    static {
        memoryBase = com.google.common.collect.Maps.newHashMap();
        //关注点1:构建JsonObjectBuilder实例 
        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
        //关注点2:构建JsonObject实例 
        JsonObject newBook1 = jsonObjectBuilder.add("bookId", 1)
            .add("bookName", "Java Restful Web Services实战")
            .add("publisher", "机械工业出版社")
            .add("isbn", "9787111478881")
            .add("publishTime", "2014-09-01")
            .build();
...
    }
    @GET
    public JsonArray getBooks() {
        //关注点3:构建JsonArrayBuilder实例 
        final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
        final Set<Map.Entry<Long, JsonObject>> entries =
        BookResource.memoryBase.entrySet();
        final Iterator<Entry<Long, JsonObject>> iterator = entries.iterator();
        while (iterator.hasNext()) {
...
        }
        //关注点4:构建JsonArray实例 
        JsonArray result = arrayBuilder.build();
        return result;
    }
}
在这段代码中,JsonObjectBuilder用于构造JSON对象,见关注点1;JsonArrayBuilder用于构造JSON数组对象,见关注点2;JsonObject是JSON-P定义的JSON对象类,见关注点3;JsonArray是JSON数组类,见关注点4。
(4)单元测试
JSON-P示例的单元测试需要关注JSON-P定义的JSON类型,测试验收标准在前一小节MOXy的单元测试中已经讲述,示例代码如下。
public class JsonTest extends JerseyTest {
    private final static Logger LOGGER = Logger.getLogger(JsonTest.class);
@Override
    protected Application configure() {
        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);
        return new ResourceConfig(BookResource.class);
    }
    @Test
    public void testGettingBooks() {
//关注点1:请求的响应类型为JsonArray 
        JsonArray books = target("books").request(MediaType.APPLICATION_JSON_TYPE).
get(JsonArray.class);
        for (JsonValue jsonValue : books) {
//关注点2:强转JsonValue为JsonObject 
            JsonObject book = (JsonObject) jsonValue;
            LOGGER.debug(book.getString("bookName"));//关注点3:打印输出测试结果 
        }
    }
}
在这段代码片段中,JsonArray是getBooks()方法的返回类型,get()请求发出后,服务器端对应的方法是getBooks()方法,见关注点1;JsonValue类型是一种抽象化的JSON数据类型,此处类型强制转化为JsonObject,见关注点2;getString()方法是将JsonObject对象的某个字段以字符串类型返回,见关注点3。
(5)集成测试
使用cURL对本示例进行集成测试的结果如下所示,JSON数据结果可以格式化打印输出。
curl http://localhost:8080/simple-service-jsonp/api/books
 
[
    {
        "bookId":1,
        "bookName":"Java Restful Web Services实战",
        "publisher":"机械工业出版社",
        "isbn":"9787111478881",
        "publishTime":"2014-09-01"
    },
    {
        "bookId":2,
        "bookName":"JSF2和RichFaces4使用指南",
        "publisher":"电子工业出版社",
        "isbn":"9787121177378",
        "publishTime":"2012-09-01"
    },
    {
        "bookId":3,
        "bookName":"Java EE 7精髓",
        "publisher":"人民邮电出版社",
        "isbn":"9787115375483",
        "publishTime":"2015-02-01"
    },
    {
        "bookId":4,
        "bookName":"Java Restful Web Services实战II",
        "publisher":"机械工业出版社"
    }
]
curl http://localhost:8080/simple-service-jsonp/api/books/book?id=1
{
    "bookId":1,
    "bookName":"Java Restful Web Services实战",
    "publisher":"机械工业出版社",
    "isbn":"9787111478881",
    "publishTime":"2014-09-01"
}
curl -H "Content-Type: application/json" -X POST \
-d "{\"bookName\":\"abc\",\"publisher\":\"me\"}" \
http://localhost:8080/simple-service-jsonp/api/books
 
{
    "bookId":23670621181527,
    "bookName":"abc",
    "publisher":"me"
}
3.使用Jackson处理JSON
Jackson是一种流行的JSON支持技术,其源代码托管于Github,地址是:https://github.com/FasterXML/jackson。Jackson提供了3种JSON解析方式。
第一种是基于流式API的增量式解析/生成JSON的方式,读写JSON内容的过程是通过离散事件触发的,其底层基于StAX API读取JSON使用org.codehaus.jackson.JsonParser,写入JSON使用org.codehaus.jackson.JsonGenerator。
第二种是基于树型结构的内存模型,提供一种不变式的JsonNode内存树模型,类似DOM树。
第三种是基于数据绑定的方式,org.codehaus.jackson.map.ObjectMapper解析,使用JAXB的注解。
下面开始讲述使用Jackson实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的Jackson示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-3.simple-service-jackson。
(1)定义依赖
使用Jackson方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
</dependency>
(2)定义Application
使用Jackson的应用,需要在其Application中注册JacksonFeature。同时,如果有必要根据不同的实体类做详细的解析,可以注册ContextResolver的实现类,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends ResourceConfig {
    public JsonResourceConfig() {
        register(BookResource.class);
        register(JacksonFeature.class);
        //关注点1:注册ContextResolver的实现类JsonContextProvider 
        register(JsonContextProvider.class);
    }
}
在这段代码中,注册了ContextResolver的实现类JsonContextProvider,用于提供JSON数据的上下文,见关注点1。有关ContextResolver详细信息参考3.2节。
(3)定义POJO
本例定义了3种不同方式的POJO,以演示Jackson处理JSON的多种方式。分别是JsonBook、JsonHybridBook和JsonNoJaxbBook。第一种方式是仅用JAXB注解的普通的POJO,示例类JsonBook如下。
@XmlRootElement
@XmlType(propOrder = {"bookId", "bookName", "chapters"})
public class JsonBook {
    private String[] chapters;
    private String bookId;
    private String bookName;
 
    public JsonBook() {
        bookId = "1";
        bookName = "Java Restful Web Services实战";
        chapters = new String[0];
    }
...
}
第二种方式是将JAXB的注解和Jackson提供的注解混合使用的POJO,示例类JsonHybridBook如下。
//关注点1:使用JAXB注解 
@XmlRootElement
public class JsonHybridBook {
    //关注点2:使用Jackson注解 
    @JsonProperty("bookId")
    private String bookId;
 
    @JsonProperty("bookName")
    private String bookName;
 
    public JsonHybridBook() {
        bookId = "2";
        bookName = "Java Restful Web Services实战";
    }
}
在这段代码中,分别使用了JAXB的注解javax.xml.bind.annotation.XmlRootElement,见关注点1,和Jackson的注解org.codehaus.jackson.annotate.JsonProperty,见关注点2,定义XML根元素和XML属性。
第三种方式是不使用任何注解的POJO,示例类JsonNoJaxbBook如下。
public class JsonNoJaxbBook {
    private String[] chapters;
    private String bookId;
    private String bookName;
    public JsonNoJaxbBook() {
        bookId = "3";
        bookName = "Java Restful Web Services使用指南";
        chapters = new String[0];
    }
...
}
这样的3种POJO如何使用Jackson处理来处理呢?我们继续往下看。
(4)定义资源类
资源类BookResource用于演示Jackson对上述3种不同POJO的支持,示例代码如下。
@Path("books")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
    @Path("/emptybook")
    @GET
//关注点1:支持第一种方式的POJO类型 
    public JsonBook getEmptyArrayBook() {
return new JsonBook();
    }
    @Path("/hybirdbook")
    @GET
//关注点2:支持第二种方式的POJO类型 
    public JsonHybridBook getHybirdBook() {
return new JsonHybridBook();
    }
    @Path("/nojaxbbook")
    @GET
//关注点3:支持第三种方式的POJO类型 
    public JsonNoJaxbBook getNoJaxbBook() {
return new JsonNoJaxbBook();
    }
……
在这段代码中,资源类BookResource定义了路径不同的3个GET方法,返回类型分别对应上述的3种POJO,见关注点1到3。有了这样的资源类,就可以向其发送GET请求,并获取不同类型的JSON数据,以研究Jackson是如何支持这3种POJO的JSON转换。
(5)上下文解析实现类
JsonContextProvider是ContextResolver(上下文解析器)的实现类,其作用是根据上下文提供的POJO类型,分别提供两种解析方式。第一种是默认的方式,第二种是混合使用Jackson和Jaxb。两种解析方式的示例代码如下。
@Provider
public class JsonContextProvider implements ContextResolver<ObjectMapper> {
    final ObjectMapper d;
    final ObjectMapper c;
    public JsonContextProvider() {
        //关注点1:实例化ObjectMapper 
        d = createDefaultMapper();
        c = createCombinedMapper();
    }
    private static ObjectMapper createCombinedMapper() {
        Pair ps = createIntrospector();
        ObjectMapper result = new ObjectMapper();
        result.setDeserializationConfig(
        result.getDeserializationConfig().withAnnotationIntrospector(ps));
        result.setSerializationConfig(
        result.getSerializationConfig().withAnnotationIntrospector(ps));
        return result;
    }
    private static ObjectMapper createDefaultMapper() {
        ObjectMapper result = new ObjectMapper();
        result.configure(Feature.INDENT_OUTPUT, true);
        return result;
    }
    private static Pair createIntrospector() {
        AnnotationIntrospector p = new JacksonAnnotationIntrospector();
        AnnotationIntrospector s = new JaxbAnnotationIntrospector();
        return new Pair(p, s);
    }
    @Override    public ObjectMapper getContext(Class<\?> type) {
//关注点2:判断POJO类型返回相应的ObjectMapper实例 
        if (type == JsonHybridBook.class) {
            return c;
        } else {
            return d;
        }
    }
}
在这段代码中,JsonContextProvider定义并实例化了两种类型ObjectMapper,见关注点1;在实现接口方法getContext()中,通过判断当前POJO的类型,返回两种ObjectMapper实例之一,见关注点2。通过这样的实现,当流程获取JSON上下文时,既可使用Jackson依赖包完成对相关POJO的处理。
(6)单元测试
单元测试类BookResourceTest的目的是对支持上述3种POJO的资源地址发起请求并测试结果,示例如下。
public class BookResourceTest extends JerseyTest {
    private static final Logger LOGGER = Logger.getLogger(BookResourceTest.class);
    WebTarget booksTarget = target("books");
    @Override
    protected ResourceConfig configure() {
//关注点1:服务器端配置 
        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);
        ResourceConfig resourceConfig = new ResourceConfig(BookResource.class);
//关注点2:注册JacksonFeature 
        resourceConfig.register(JacksonFeature.class);
        return resourceConfig;
    }
    @Override
    protected void configureClient(ClientConfig config) {
//关注点3:注册JacksonFeature 
        config.register(new JacksonFeature());
        config.register(JsonContextProvider.class);
    }
    @Test
//关注点4:测试出参为JsonBook类型的资源方法 
    public void testEmptyArray() {
        JsonBook book = booksTarget.path("emptybook").request(MediaType.APPLICATION_JSON).get(JsonBook.class);
        LOGGER.debug(book);
    }
    @Test
//关注点5:测试出参为JsonHybridBook类型的资源方法 
    public void testHybrid() {
        JsonHybridBook book = booksTarget.path("hybirdbook").request(MediaType
.APPLICATION_JSON).get(JsonHybridBook.class);
        LOGGER.debug(book);
    }
    @Test
//关注点6:测试出参为JsonNoJaxbBook类型的资源方法 
    public void testNoJaxb() {
        JsonNoJaxbBook book = booksTarget.path("nojaxbbook").request(MediaType.
APPLICATION_JSON).get(JsonNoJaxbBook.class);
        LOGGER.debug(book);
    }
……
在这段代码中,首先要在服务器端注册支持Jackson功能,见关注点2;同时在客户端也要注册支持Jackson功能并注册JsonContextProvider,见关注点3;该测试类包含了用于测试3种类型POJO的测试用例,见关注点4到6;注意,configure()方法是覆盖测试服务器实例行为,configureClient()方法是覆盖测试客户端实例行为,见关注点1。
(7)集成测试
使用cURL对本例进行集成测试,结果如下所示。
curl http://localhost:8080/simple-service-jackson/api/books
 
{
  "bookList" : [ {
    "bookId" : 1,
    "bookName" : "JSF2和RichFaces4使用指南",
    "isbn" : "9787121177378",
    "publisher" : "电子工业出版社",
    "publishTime" : "2012-09-01"
  }, {
    "bookId" : 2,
    "bookName" : "Java Restful Web Services实战",
    "isbn" : "9787111478881",
    "publisher" : "机械工业出版社",
    "publishTime" : "2014-09-01"
  }, {
    "bookId" : 3,
    "bookName" : "Java EE 7 精髓",
    "isbn" : "9787115375483",
    "publisher" : "人民邮电出版社",
    "publishTime" : "2015-02-01"
  }, {
    "bookId" : 4,
    "bookName" : "Java Restful Web Services实战II",
    "isbn" : null,
    "publisher" : "机械工业出版社",
    "publishTime" : null
  } ]
}
curl http://localhost:8080/simple-service-jackson/api/books/emptybook
 
{
  "chapters" : [ ],
  "bookId" : "1",
  "bookName" : "Java Restful Web Services实战"
}
curl http://localhost:8080/simple-service-jackson/api/books/hybirdbook
 
{"JsonHybridBook":{"bookId":"2","bookName":"Java Restful Web Services实战"}}
curl http://localhost:8080/simple-service-jackson/api/books/nojaxbbook
 
{
  "chapters" : [ ],
  "bookId" : "3",
  "bookName" : "Java Restful Web Services实战"
}
4. 使用Jettison处理JSON
Jettison是一种使用StAX来解析JSON的实现。项目地址是:http://jettison.codehaus.org。Jettison项目起初用于为CXF提供基于JSON的Web服务,在XStream的Java对象的序列化中也使用了Jettison。Jettison支持两种JSON映射到XML的方式。Jersey默认使用MAPPED方式,另一种叫做BadgerFish方式。
下面开始讲述使用Jettison实现在REST应用中解析JSON的完整过程。
阅读指南
2.3.6节的Jettison示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.3.6-4.simple-service-jettison。
(1)定义依赖
使用Jettison方式处理JSON类型的数据,需要在项目的Maven配置中声明如下依赖。
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jettison</artifactId>
</dependency>
(2)定义Application
使用Jettison的应用,需要在其Application中注册JettisonFeature。同时,如果有必要根据不同的实体类做详细的解析,可以注册ContextResolver的实现类,示例代码如下。
@ApplicationPath("/api/*")
public class JsonResourceConfig extends ResourceConfig {
    public JsonResourceConfig() {
        register(BookResource.class);
        //关注点1:注册JettisonFeature和ContextResolver的实现类JsonContextResolver
        register(JettisonFeature.class);
        register(JsonContextResolver.class);
    }
}
在这段代码中,注册了Jettison功能JettisonFeature和ContextResolver的实现类JsonContextResolver,以便使用Jettison处理JSON,见关注点1。
(3)定义POJO
本例定义了两个类名不同、内容相同的POJO(JsonBook和JsonBook2),用以演示Jettison对JSON数据以JETTISON_MAPPED(default notation)和BADGERFISH两种不同方式的处理情况。
@XmlRootElement
public class JsonBook {
    private String bookId;
    private String bookName;
    public JsonBook() {
        bookId = "1";
        bookName = "Java Restful Web Services实战";
    }
    ...
}
(4)定义资源类
资源类BookResource为两种JSON方式提供了资源地址,示例如下。
@Path("books")
public class BookResource {
...
    @Path("/jsonbook")
    @GET
    //关注点1:返回类型为JsonBook的GET方法 
    public JsonBook getBook() {
        final JsonBook book = new JsonBook();
        BookResource.LOGGER.debug(book);
        return book;
    }
    @Path("/jsonbook2")
    @GET
    //关注点2:返回类型为JsonBook2的GET方法 
    public JsonBook2 getBook2() {
        final JsonBook2 book = new JsonBook2();
        BookResource.LOGGER.debug(book);
        return book;
    }
 }
在这段代码中,资源类BookResource定义了路径不同的两个GET方法,返回类型分别是JsonBook和JsonBook2,见关注点1和2。有了这样的资源类,就可以向其发送GET请求,并获取不同类型的JSON数据,以研究Jettison是如何处理JETTISON_MAPPED和BADGERFISH两种不同格式的JSON数据的。
(5)上下文解析实现类
JsonContextResolver实现了ContextResolver接口,示例如下。
@Provider
public class JsonContextResolver implements ContextResolver<JAXBContext> {
    private final JAXBContext context1;
    private final JAXBContext context2;
    @SuppressWarnings("rawtypes")
    public JsonContextResolver() throws Exception {
        Class[] clz = new Class[]{JsonBook.class, JsonBook2.class, Books.class, Book.class};
        //关注点1:实例化JettisonJaxbContext
        this.context1 = new JettisonJaxbContext(JettisonConfig.DEFAULT, clz);
        this.context2 = new JettisonJaxbContext(JettisonConfig.badgerFish().build(), clz);
    }
    @Override
    public JAXBContext getContext(Class<\?> objectType) {
        //关注点2:判断POJO类型返回相应的JAXBContext实例
        if (objectType == JsonBook2.class) {
            return context2;
        } else {
            return context1;
        }
    }
}
在这段代码中,JsonContextResolver定义了两种JAXBContext分别使用MAPPED方式或者BadgerFish方式,见关注点1。这两种方式的参数信息来自Jettision依赖包的JettisonConfig类。在实现接口方法getContext()中,根据不同的POJO类型,返回两种JAXBContext实例之一,见关注点2。通过这样的实现,当流程获取JSON上下文时,既可使用Jettision依赖包完成对相关POJO的处理。
(6)单元测试
单元测试类BookResourceTest的目的是对支持上述两种JSON方式的资源地址发起请求并测试结果,示例如下。
public class BookResourceTest extends JerseyTest {
    private static final Logger LOGGER = Logger.getLogger(BookResourceTest.class);
    @Override
    protected ResourceConfig configure() {
        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);
        ResourceConfig resourceConfig = new ResourceConfig(BookResource.class);
        //关注点1:注册JettisonFeature和JsonContextResolver 
        resourceConfig.register(JettisonFeature.class);
        resourceConfig.register(JsonContextResolver.class);
        return resourceConfig;
    }
    @Override
    protected void configureClient(ClientConfig config) {
        //关注点2:注册JettisonFeature和JsonContextResolver 
        config.register(new JettisonFeature()).register(JsonContextResolver.class);
    }
    @Test
    public void testJsonBook() {
        //关注点3:测试返回类型为JsonBook的GET方法 
        JsonBook book = target("books").path("jsonbook")
        .request(MediaType.APPLICATION_JSON).get(JsonBook.class);
        LOGGER.debug(book);
        //{"jsonBook":{"bookId":1,"bookName":"abc"}} 
    }
    @Test
    public void testJsonBook2() {
        //关注点4:测试返回类型为JsonBook2的GET方法 
        JsonBook2 book = target("books").path("jsonbook2")
        .request(MediaType.APPLICATION_JSON).get(JsonBook2.class);
        LOGGER.debug(book);
        //{"jsonBook2":{"bookId":{"$":"1"},"bookName":{"$":"abc"}}} 
    }
...
}
在这段代码中,首先要在服务器和客户端两侧注册Jettison功能和JsonContextResolver,见关注点1和2。该测试类包含了用于测试两种格式JSON数据的测试用例,见关注点3和4。
(7)集成测试
使用cURL对本例进行集成测试,结果如下所示。可以看到Mapped和Badgerfish两种方式的JSON数据内容不同。
curl http://localhost:8080/simple-service-jettison/api/books
 
{"books":{"bookList":{"book":[{"@bookId":"1","@bookName":"JSF2和RichFaces4使用指南","@publisher":"电子工业出版社","isbn":9787121177378,"publishTime":"2012-09-01"},{"@bookId":"2","@bookName":"Java Restful Web Services实战","@publisher":"机械工业出版社","isbn":9787111478881,"publishTime":"2014-09-01"},{"@bookId":"3","@bookName":"Java EE 7 精髓","@publisher":"人民邮电出版社","isbn":9787115375483,"publishTime":"2015-02-01"},{"@bookId":"4","@bookName":"Java Restful Web Services实战II","@publisher":"机械工业出版社"}]}}}

Jettison mapped notation
curl http://localhost:8080/simple-service-jettison/api/books/jsonbook
 
{"jsonBook":{"bookId":1,"bookName":"Java Restful Web Services实战"}}

Badgerfish notation

curl http://localhost:8080/simple-service-jettison/api/books/jsonbook2
{"jsonBook2":{"bookId":{"$":"1"},"bookName":{"$":"Java Restful Web Services实战"}}}
最后简要介绍一下Atom类型。
Atom是一种基于XML的文档格式,该格式的标准定义在IETF RFC 4287(Atom Syndication Format,Atom联合格式),其推出的目的是用来替换RSS。AtomPub是基于Atom的发布协议,定义在IETF RFC 5023(Atom Publishing Protocol)。
Jersey2没有直接引入支持Atom格式的媒体包,但Jersey1.x中包含jersey-atom包。这说明Jersey的基本架构可以支持基于XML类型的数据,这种可插拔的媒体包支持对于Jersey本身更具灵活性,对使用Jersey的REST服务更具可扩展性。
2.4 连通性
REST的一个重要的特性就是连通性。Web Link和HATEOAS以不同方式实现了REST式服务的联通性。
Web Link定义在IETFRFC 5988(Web Linking),是通过在HTTP头中定义链接信息,以描述当前页面与链接页面之间的关系。Web Link是一种过渡型链接(Transitional Links)。JAX-RS 2.0引入了javax.ws.rs.core.Link类,用来处理Web Link的表述。
HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用程序状态引擎)。HATEOAS的形式是包含链接信息的超媒体文档,HATEOAS的核心是“引擎”,该引擎的目的是通过请求的响应实体将超媒体信息返回给客户端,超媒体信息可以告诉用户,如果接下来选择去往某个链接(或者链接列表中的某个链接),应用的状态就会如超媒体描述的那样发生转变。HATEOAS是一种结构型链接(Structural Links)。Jersey2中可以使用XML实现HATEOAS的结构要求。
下面讲述在Jersey中如何实现Web Link和HATEOA这两种REST连通性实践方式。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.link。
2.4.1 过渡型链接
Web Link通过使用HTTP的头信息来传递操作链接,在Jersey中使用javax.ws.rs.core.Link类可以非常简洁地实现支持Web Link的资源类,示例代码如下。
@Path("weblink-resource")
public class WebLinkResource {
  @Context
  UriInfo uriInfo;
 
  @POST
  @Produces(MediaType.APPLICATION_XML)
  @Consumes({ MediaType.APPLICATION_JSON,
    MediaType.APPLICATION_XML, MediaType.TEXT_XML })
  public Response saveBook(final Book book) {
      final long newId = System.nanoTime();
      book.setBookId(newId);
      LinkCache.map.put(newId, book);
      //关注点1:通过UriInfo实例获取资源路径 
      final UriBuilder ub = uriInfo.getAbsolutePathBuilder();
      final URI location = ub.path("" + newId).build();
      //关注点2:通过模板获取资源路径 
      final String uriTemplate = "http://{host}:{port}/{path}/{param}";
      final URI location2 = UriBuilder.fromUri(uriTemplate)
      .resolveTemplate("host", "localhost").resolveTemplate("port", "9998")
      .resolveTemplate("path", "weblink-resource")
      .resolveTemplate("param", newId).build();
      //关注点3:通过模板方法获取资源路径 
      final UriBuilder ub3 = uriInfo.getAbsolutePathBuilder();
      final URI location3 = ub3.scheme("http").host("localhost").port(9998)
      .path("weblink-resource").path("" + newId).build();
      //关注点4:为响应实例添加路径信息 
      return Response.created(location).link(location2, "view1")
      .link(location3, "view2").entity(book).build();
    }
}
在这段代码中,使用了3种方式构建URI实例。第一种方式是通过调用UriInfo实例的getAbsolutePathBuilder()方法可以获取当前请求的绝对路径,然后基于此路径添加资源id信息,见关注点1;第二种方式是为UriBuilder提供路径模板,然后链式调用resolveTemplate()方法传递并解析模板参数,最后通过UriBuilder的build方法生成URI实例,见关注点2;第三种方式和第二种类似,不同的是模板信息被具体方法替代。最后,这3个与Link相关的URI实例由Response构建,作为返回值响应给客户端,见关注点4。
2.4.2 结构型链接
HATEOAS用以代替聚集数据并避免描述膨胀,通常使用 Atom格式在实体字段中提供链接信息。本例使用XML格式来支持HATEOAS,折中的设计是在POJO中额外定义一个链接字段。支持HATEOAS的资源类示例如下。
@Path("hateoas-resource")
public class HATEOASResource {
  @Context
  UriInfo uriInfo;
 
  @POST
  @Produces({ MediaType.APPLICATION_XML })
  @Consumes({ MediaType.APPLICATION_XML })
  public BookWrapper saveBook(final Book book) {
      final long newId = System.nanoTime();
      book.setBookId(newId);
      LinkCache.map.put(newId, book);
      //关注点1:通过UriInfo实例获取资源路径 
      final UriBuilder ub = uriInfo.getAbsolutePathBuilder();
      final URI uri = ub.path("" + newId).build();
      BookWrapper b = new BookWrapper();
      b.setBook(book);
      //关注点2:将资源路径赋给资源实体 
      b.setLink(uri.toString());
      return b;
  }
}
在这段代码中,URI实例由上下文UriInfo中获取的绝对路径和资源ID组成,见关注点1;该链接信息被赋值到POJO实例的link属性中,以实现HATEOAS,见关注点2。
阅读指南
REST连通性的实践手段非常多,推荐读者从成熟的产品中学习其设计。如果有可能,这里推荐Jenkins和RallyDev两个敏捷开发中常用的平台,它们提供了比较舒适的连通性设计。比如在RallyDev中,为一个测试用例结果添加测试用例属性(该属性是必输项),其内容并不是对应测试用例的实例,而是该测试用例的引用地址字符串。这样的设计不但减少了网络传输的负载,还方便在调试和维护时排错。
2.5 处理响应
REST的响应处理结果应包括响应头中HTTP状态码,响应实体中媒体参数类型和返回值类型,以及异常情况处理。JAX-RS2支持4种返回值类型的响应,分别是无返回值、返回Response类实例、返回GenericEntity类实例和返回自定义类实例。如下,逐一讲述这4种返回值类型。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.response。
2.5.1 返回类型
1. void
在返回值类型是void的响应中,其响应实体为空,HTTP状态码为204。在前面的DELETE方法讲述中已经介绍过,再来看一下这种类型的资源方法。
@DELETE
@Path("{s}")
//关注点1:无返回值的DELETE方法 
public void delete(@PathParam("s") final String s) {
    LOGGER.debug(s);
}
因为delete操作无须返回更多的关于资源表述的信息,因此该方法没有返回值,即返回值类型为void,见关注点1。
2. Response
在返回值类型为Response的响应中,响应实体为Response类的entity()方法定义的实体类实例。如果该内容为空,则HTTP状态码为204,否则HTTP状态码为200 OK,示例代码如下。
@POST
@Path("c")
public Response get(final String s) {
    LOGGER.debug(s);
    //Response.noContent().build(); 
//关注点1:构建无返回值的响应实例 
    return Response.ok().entity("char[]:" + s).build();
}
在这段代码中,Response首先定义了HTTP的状态码为ok,然后填充实体信息,最后调用build()方法构建Response实例,见关注点1。
3. GenericEntity
通用实体类型作为返回值的情况并不常用。其形式是构造一个统一的实体实例并将其返回,实体实例作为第一个参数、该实体类型作为第二个参数,示例代码如下。
@POST
@Path("b")
public String get(final byte[] bs) {
    for (final byte b : bs) {
        LOGGER.debug(b);
    }
    return "byte[]:" + new String(bs);
}
/*
public GenericEntity<String> get(final byte[] bs) {
    for (final byte b : bs) {
        LOGGER.debug(b);
    }
  //关注点1:构建GenericEntity实例
    return new GenericEntity<String>("byte[]:" + new String(bs), String.class);
}
*/
在这段代码中,GenericEntity的第一个是由byte数组实例作为参数构建的字符串实例,第二个参数是字符串类,见关注点1。
4. 自定义类型
JDK中的类(比如File、String等)都可以作为返回值类型,更常用的是返回自定义的POJO类型,前述多个例子就是这样做的,再来看一个示例。
@POST
@Path("f")
//关注点1:GET方法的返回类型为File 
public File get(final File f) throws FileNotFoundException, IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(f))) {
        String s;
        do {
            s = br.readLine();
            LOGGER.debug(s);
        } while (s != null);
        return f;
    }
}
@POST
@Consumes({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
@Produces(MediaType.APPLICATION_XML)
//关注点2:POST方法的返回值是自定义类Book 
public Book getEntity(Book book) {
    LOGGER.debug(book.getBookName());
    return book;
}
@POST
@Consumes({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
@Produces(MediaType.APPLICATION_XML)
//关注点3:POST方法的返回值是自定义类Book 
public Book getEntity(JAXBElement<Book> bookElement) {
    Book book = bookElement.getValue();
    LOGGER.debug(book.getBookName());
    return book;
}
在这段代码中,返回值类型有来自JDK的File类型,见关注点1,也有自定义的POJO类型,见关注点2和关注点3。
2.5.2 处理异常
实现REST的资源方法时应使其具有良好的异常处理能力,这包括异常的定义和错误状态码的正确返回。
1. 处理状态码
首先通过表2-7了解下REST中常用的HTTP状态码,应当在处理异常的同时,为REST请求的客户端提供对应的错误码。
表2-7 HTTP常用状态码列表
状态码 含  义
200 OK 服务器正常响应
201 Created 创建新实体,响应头Location指定访问该实体的URL
202 Accepted 服务器接受请求,处理尚未完成。可用于异步处理机制
204 No Content 服务器正常响应,但响应实体为空
301 Moved Permanently 请求资源的地址发生永久变动,响应头Location指定新的URL
302 Found 请求资源的地址发生临时变动
304 Not Modified 客户端缓存资源依然有效
400 Bad Request 请求信息出现语法错误
401 Unauthorized 请求资源无法授权给未验证用户
403 Forbidden 请求资源未授权当前用户
404 Not Found 请求资源不存在
405 Method Not Allowed 请求方法不匹配
406 Not Acceptable 请求资源的媒体类型不匹配
500 Internal Server Error 服务器内部错误,意外终止响应
501 Not Implemented 服务器不支持当前请求

JAX-RS2规定的REST式的Web服务的基本异常类型为运行时异常WebApplicationException类。该类包含3个主要的子类分别对应如下内容。
HTTP状态码为3xx的重定向类RedirectionException;
HTTP状态码为4xx的请求错误类ClientErrorException;
HTTP状态码为5xx的服务器错误类ServerErrorException。
它们各自的子类对照HTTP状态码再细分,比如常见的HTTP状态码404错误,对应的错误类为NotFoundException,如图2-4所示。
除了Jersey提供的标准异常类型,我们也可以根据业务需要自定义相关的业务异常类,示例如下。
//关注点1:定义WebApplicationException接口实现类 
public class Jaxrs2GuideNotFoundException extends WebApplicationException {
    public Jaxrs2GuideNotFoundException() {
//关注点2:定义HTTP状态 
        super(javax.ws.rs.core.Response.Status.NOT_FOUND);
    }
    public Jaxrs2GuideNotFoundException(String message) {
        super(message);
    }
}
在这段代码中,Jaxrs2GuideNotFoundException类继承自JAX-RS2的WebApplication-Exception类,见关注点1。其默认构造子提供了HTTP状态码,其值为Response.Status.NOT_FOUND,见关注点2。

图2-4 Jersey定义的异常类型
2. ExceptionMapper
Jersey框架为我们提供了更为通用的异常处理方式。通过实现ExceptionMapper接口并使用@Provider注解将其定义为一个Provider,可以实现通用的异常的面向切面处理,而非针对某一个资源方法的异常处理,示例如下。
@Provider
public class EntityNotFoundMapper
implements ExceptionMapper<Jaxrs2GuideNotFoundException>{
    //关注点1:定义ExceptionMapper接口实现类 
    @Override
    public Response toResponse(final Jaxrs2GuideNotFoundException ex) {
      //关注点2:拦截并返回新的响应实例 
      return Response.status(404).entity(ex.getMessage()).type("text/plain").build();
    }
}
在这段代码中,EntityNotFoundMapper实现了ExceptionMapper接口,并提供了泛型类型为前述刚定义的Jaxrs2GuideNotFoundException类,见关注点1;当响应中发生了Jaxrs2GuideNotFoundException类型的异常,响应流程就会被拦截并补充HTTP状态码和异常消息,以文本作为媒体格式返回给客户端,见关注点2。
2.6 内容协商
一个资源可以有不同格式的表述,表述(即响应实体)的内容是人类可识别的信息,服务器很难使用一种表述来适应所有用户。conneg(HTTP Content Negotiation,内容协商)是指在服务器提供的多种表述中,为特定的请求选择最好的一种表述的处理过程。那么什么是最好,又怎样做到最好呢?服务器和客户端/浏览器之间往复通信来协商用于交换数据的内容格式等信息,达成一致即为最好。内容协商定义在RFC2616的第12节(http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html)。
客户端/浏览器通过使用 HTTP Accept、Accept-Charset、Accept-Language和Accept-Encoding头来定义接收头的信息,将其所期待的格式或MIME类型告知服务器,服务器根据协商算法,返回客户端/浏览器可接受的数据信息。内容协商不只是数据格式协商,还包括语言、编码、字符集等信息。Accept用于数据类型协商;Accept-Language用于语言协商;Accept-Charset用于字符集协商;Accept-Encoding用于压缩算法协商。
JAX-RS2对内容协商的支持,是通过@Produces实现的,其他协商没有从架构上提供支持,可以通过编码从请求头中获取信息并处理。
阅读指南
本节示例源代码地址:https://github.com/feuyeux/jax-rs2-guide-II/tree/master/2.simple-service-3。
相关包:com.example.conneg。
2.6.1 @Produces注解
注解@Produces用于定义方法的响应实体的数据类型,可以定义一个或多个,同时可以为每种类型定义质量因素(qualityfactor)。质量因素是取值范围从0到1的小数值。如果不定义质量因素,那么该类型的质量因素默认为1。我们将结合示例深入了解@Produces注解对媒体类型的影响,示例代码如下。
@Path("conneg-resource")
public class ConnegResource {
  @GET
  @Path("{id}")
  //关注点1:媒体类型为XML 
  @Produces(MediaType.APPLICATION_XML)
  public Book getJaxbBook(@PathParam("id") final Long bookId) {
      return new Book(bookId);
  }
 
  @GET
  @Path("{id}")
  //关注点2:媒体类型为JSON 
  @Produces(MediaType.APPLICATION_JSON)
  public Book getJsonBook(@PathParam("id") final Long bookId) {
      return new Book(bookId);
  }
}
在这段代码中,getJaxbBook()和getJsonBook()是同等质量因素、资源地址相同的两个GET方法,一个定义响应实体格式为XML,一个定义响应实体格式为JSON,见关注点1和2。那么对同一个资源的访问,JAX-RS2该如何选择处理方法呢?如果请求中明确定义可接受的数据类型为两者之一,处理方法应该是定义相应数据类型的方法。如果两者都定义了,处理方法应该是质量因素高的方法。如果两者都定义,而且数据类型的质量因素是相等的或者没有定义Accept,XML的方法会被优先选择。
客户端明确表述格式为XML,Jersey通过内容协商,会选择getJaxbBook()作为相应的资源方法来处理该请求。其测试代码如下所示。
WebTarget path = target("conneg-resource").path("123");
Builder request = path.request(MediaType.APPLICATION_XML_TYPE);
Book book = request.get(Book.class);

1 > GET http://localhost:9998/conneg-resource/123
1 > Accept: application/xml
2 < Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book bookId="123"/>
接下来,测试一个稍微复杂的内容协商。客户端明确表述格式的质量因素JSON高于XML,Jersey会选择资源方法getJsonBook()来处理请求。示例代码如下所示。
WebTarget path = target("conneg-resource").path("123");
Builder request = path.request();
request.header("Accept", "application/xml;q=0.1,application/json;q=0.2");
Book book = request.get(Book.class);
...1 > GET http://localhost:9998/conneg-resource/123 1 > Accept: application/xml;q=0.1,application/json;q=0.2 2 < Content-Type: application/json {"bookId":123}
现在我们清楚了两个同等方法的场景,再来看一个方法中多种数据类型的场景。示例代码如下。
...java
@GET
@Produces({ "application/json; qs=.9", "application/xml; qs=.5" })
@Path("book/{id}")
public Book getBook(@PathParam("id") final Long bookId) {
    return new Book(bookId);
}
在这段代码中,getBook()方法定义了XML和JSON两种表述数据类型,XML的质量因素是0.5(0可以省略),JSON的是0.9。
因此,可以推断,如果客户端请求中,明确接收的数据类型是两者之一,响应实体使用指定类型。如果没有定义或者两者都定义且JSON的质量因素大于或者等于XML,则返回JSON类型。还有一种用例是,两者都定义但JSON的质量因素小于XML,该如何处理请求方法呢?答案是:内容协商的结果按照客户端的喜好选择响应实体的数据类型,即选择XML格式。
其测试代码如下所示,客户端明确表述格式XML优于JSON,虽然服务器端定义的资源方法中JSON的质量因素高,但Jersey会根据客户端的喜好,选择了XML格式作为表述的格式返回。
WebTarget path = target("conneg-resource").path("book").path("123");
Builder request = path.request();
request.header("Accept", "application/xml;q=0.7,application/json;q=0.2");
Book book = request.get(Book.class);

1 > GET http://localhost:9998/conneg-resource/book/123
1 > Accept: application/xml;q=0.7,application/json;q=0.2
2 < Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book bookId="123"/>
2.6.2 @Consumes注解
注解@Consumes用于定义方法的请求实体的数据类型,和@Produces不同的是,@Consumes的数据类型的定义只用于JAX-RS2匹配请求处理的方法,不做内容协商使用。如果匹配不到,服务器会返回HTTP状态码415(Unsupported Media Type),示例代码如下。
@POST
//关注点1:@Consumes注解定义了XML和JSON两种格式 
@Consumes({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
@Produces(MediaType.APPLICATION_XML)
public Book getEntity(Book book) {
    LOGGER.debug(book.getBookName());
    return book;
}
final Builder request = target(path).request();
//关注点2 
final Book result = request.post(
Entity.entity(book, MediaType.APPLICATION_XML), Book.class);
在这段代码中,getEntity()方法定义了@Consumes媒体类型为XML格式和JSON格式,见关注点1;那么,在客户端请求中,如果请求实体的数据类型定义是两者之一,该方法会被选择为处理请求的方法,否则查找是否有定义为相应数据类型的方法,如果没有抛出javax.ws.rs.NotSupportedException异常,则使用该方法处理请求,见关注点2。
2.7 本章小结
本章是REST理论和Jersey实践的核心章节,详细讲述了HTTP方法与REST API的统一接口设计、URI的REST风格设计,并逐个讲述了JAX-RS2定义的注解如何支持资源定位,还对Jersey对各种表述类型的支持和实现、Jersey对REST连通性的两种实现、REST资源方法对响应的处理以及Jersey对内容协商的支持和实现进行了讲述。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接