概述
有人说SOA就像雪花——没有两片是相同的。情况确实如此,因为面向服务架构的主要目的是为企业集成提供一种松散耦合的架构,而企业的内部情况却是千差万别。另外,SOA是从业务角度来设计接口的,而业务角度向来是由开发人员决定的。
因此,对于一名试图阐述实现选择和最佳实践的作者而言,这带来了一定的挑战。在过去几年中,出现了许多有关SOA的书籍,这些书籍从架构师或管理人员的视角来介绍SOA的总体概念,它们给出的是SOA的概念图景,但不是从实践前沿的角度来进行说明。
那些从架构师角度介绍SOA的书籍往往只是提供一些长的项目单,列出重要的和即将出现的WS-*规范,尽管这些书籍成功地梳理出了构建SOA的某些抽象方式,但并没有告诉程序员/架构师如何来进行实际工作。也就是说,许多SOA书籍可能会告诉读者要做什么,但没有告诉读者如何去做。
例如,它们可能指出应该构建组合服务。这听起来很有说服力,也很重要,但是,接下来当你回到办公桌打开集成开发环境时,才意识到自己不知道要输入什么。有些书籍可能会更进一步,提供了基于XML的语言(如BPEL)的语法概述,但在告诉你如何实际使用它们之前就结束了。很难对这些书籍求全责备,因为SOA的本质就意味着你可以用各种工具来构建它。
编写本书的目的是要告诉读者如何实际使用SOA的一些基础构件:Web服务、编排、策略等,本书意在为开发人员填充现实世界中的空白。但要做到这一点,我必须从实际出发。要使本书的篇幅可以控制,我必须将重点放在最重要的内容上,同时省去某些内容。
本书的重点如下。
基于SOAP的Web服务
.NET或Java EE 5中的SOAP Web服务是一个组件,其注释生成一个它所提供服务的XML描述(称为WSDL)。此描述并不特定于编写组件时所处的平台,因此,用其他语言编写的客户端可以调用SOAP Web服务。
这使得基于SOAP的服务成为SOA的一个重要组成部分,本书的大量内容用于讲述如何使用XML、SOAP Web服务以及支持它们的Java API。
与其他技术(如POX over HTTP)相比,刚开始使用SOAP可能很复杂,这导致了人们对SOAP的批评。供应商们为各种优化功能(包括可靠性、安全性、位置透明性等)实现了SOAP标准,让用户以标准、可互操作的方式获得这些功能是非常有价值的。
虽然用户可能回想起了几年前出版的许多Web服务书籍,但事情已经有了很大改变。在Java SE 6和EE 5中创建SOAP Web服务变得完全不同,本书将包括最新的资料,并且不会止步于创建Web服务,还将告诉用户如何将它们实际组合起来。
REST式(RESTful)Web服务
REST(Representational State Transfer,具象状态传输)是一种在互联网架构上构建应用的方式,它与SOAP相对(至少是在大众的心中)。在“REST式Web服务”一章中,我们将分析这一观点,然后相当全面地介绍如何以各种方式创建REST式Web服务,包括使用新的JAX-RS规范JSR 311(REST式Web服务的Java API)。本书还将介绍如何使用已经成为REST实际标准的主流API,如Atom发布协议(Atom Publishing Protocol)。
Java EE 5
虽然书中提供了通过Java之外的语言来使用Web服务的例子,但本书的重点绝对是用Java实现SOA。这样做的原因很简单:我的背景和专业领域是Java。
本书不会介绍如何用Java EE 5和Java SE 6之前的版本来编写Web服务,因为有许多书籍对此都有介绍。在最新的Java版本中,Web服务方面有了很大变化。仅在最近的一年中,Web服务在注释、新的API以及发展中的各种WS-*规范实现方面发生了真正变化。本书将只介绍最新的内容。
SOA
和O扲eilly的众多其他实战指南相比,本书有一个方面与众不同:解决方案并不总是给出代码示例,因为SOA的有关问题并不总是代码问题。本书的重点是如何实现Java EE 5 Web服务以及使用各种相关技术,如BPEL编排和WS-*规范。涉及这些主题的章节提供了具体、真实的代码示例来说明如何完成实际工作。在这方面,本书与O扲eilly的其他实战指南没什么区别。但是,只要有可能,本书也为SOA的“人员问题”提供了解决方案,如组织和投资回报率。这些章节不涉及代码解决方案,因为它们不是代码问题。本书努力将这限定于那些有明确解决方案、建议或最佳实践的问题,以遵从一般的实战指南格式。不过,并不总是能够成功这样做,许多这类主题都受到热烈争议。此外,我需要将这类主题留给常见SOA的许多优秀书籍。如果需要一本关于这类SOA问题的真正好书,我推荐Nicolai M. Josuttis的“SOA in Practice”一书(O扲eilly, http://www.oreilly.com/catalog/9780596529550)。
Glassfish和WebLogic
虽然有很多提供Web服务功能的优秀应用服务器,但Glassfish是第一个支持Java SE 6的应用服务器,并且以可互操作方式完全实现了新的Java Web服务标准。这里并没有像部分供应商那样认为RPC已经过时。尽管书中的许多示例在其他容器(如Oracle的WebLogic 10gR3或Axis2)中进行了测试和展示,但Glassfish v2是这些示例的默认容器。
本人在可移植性方面做了很大努力,因此,不用太费劲,用户应该能够让这些示例在自己的引擎中工作。
BPEL编排
随着公司在寻求采用适合于开发人员、系统分析人员和架构师使用的方式来组织、改善和表示其业务流程,WS-BPEL标准是近些年来的一个重要发展。通过使用BPEL,用户可以将多个Web服务结合在一个工作流中,向外界展示一个单一编排视图,从而创建组合服务。由于用户可以用一种非常松散的耦合方式来组合多个Web服务,编排也有助于促进服务的重用。
一些开源实现(如Apache ODE)以及许多商业产品都支持BPEL,本书将逐一介绍它们。
企业服务总线(ESB,Enterprise Service Bus)
严格来说,ESB并不是SOA所必需的,但它往往是成熟SOA体系的重要组成部分。ESB是服务集成点之间的中介、路由器和间接层。在早期的EAI(Enterprise Application Integration,企业应用集成)工作中,人们很快发现需要进行很多点对点集成,这要求为每个连接节点到任何其他节点的路由创建一个特定接口。ESB降低了这种复杂性。用户可以只将服务连接到ESB,而不是将每个服务都连接到所有其他服务。通过这种方式,ESB实现了与任何计算机总线一样的功能,它负责仲裁工作并将消息路由到相应的服务。
与本书中大多数主题不同,ESB没有标准;因此,各供应商对ESB的实现方式也各不相同。使用的术语也并不总是相同的;尽管概念是相似的,但一家供应商所称的管道(pipeline),另一家供应商则可能称其为通道(channel)。在某些情况下,ESB的功能会与SOA工具套件的其他元素类似,例如,一些ESB可以进行轻量级的存储工作或者维护内部规则引擎。
2005年,Sun公司开始推广Java业务集成(Java Business Integration,JBI)规范,并最终在OpenESB中创建了一个参考实现。不过,许多供应商认为这一规范是以Java为中心的,并且过于臃肿。另外,Sun规范没有利用已有的供应商工具。结果,JBI规范并未能统治SOA市场。
因此,本书提供了ESB市场概况以帮助用户理解各类流行供应商所提供的产品功能,从而勾勒出ESB在SOA中所扮演的角色,同时也帮助用户在选择供应商时做出明智的决策。
目标读者
本书是为有经验的Java开发人员和架构师编写的。Web服务领域庞大、复杂,并且正在快速变化中。作者假定读者具有用Java编写、部署和维护企业应用程序的背景。读者需要熟悉Java SE 5或6、Servlet和JSP、企业版容器(如Glassfish、WebLogic或Tomcat),另外还要熟悉互联网协议,如HTTP。读者应该能够熟练使用Enterprise JavaBean,熟悉所有标准企业版功能,包括JDBC、JNDI、EAR和WAR,还应该熟悉现代企业开发的基本原则和概念。如果读者使用过早期Java版本的Web服务,本书将有助于学习更新的API。这些API已经发生了巨变,本书能够帮助读者快速开始学习。
日常Java企业开发人员将能够最有效地使用本书。不过,SOA是一种特殊架构和组织策略,而不是一种开发风格或开发方法,因此,书中某些方案解决的是架构问题(如治理和模式),希望这有助于开发人员理解其开展工作时所处的较大环境。另一方面,架构师将发现各种API和功能可能会增加他们的工作。此外,现在比以前有了更多的实践型架构师,他们负责设计和编写各种应用程序,这些程序必须推动整个企业的架构议程。本书试图针对这些读者。书中对于商务问题只是略有触及,如SOA的投资回报率。在一本关于开发的书籍中,尽管这类主题可能看起来不太合适,但作者相信,作为一名开发人员,全面、综合地认识SOA很重要,以使自己不仅在代码方面遵从SOA要求,而且在认识方面理解SOA要求。
如果读者正在进行Web服务开发,我们认为读者对XML、XML Schema和处理它们的Java API具有基本理解。在某些情况下,读者可能多年一直使用一种技术,但尚未使用过最新的API。因此,关于XML的一章将着重说明如何使用StAX API来处理XML流,但并不涉及SAX和DOM。该章还介绍了能够对SOA开发有所帮助的各种工具。DTD完全被省略,而是选择了Schema。
为保持以解决方案为基础这一重点,本书省略了许多基本的Web服务概述材料,同时假定读者是一名未来的SOA实现者,因此至少应该听说过SOAP和WSDL。各章中的引言部分较好地概述了每个主题,并将这些介绍和目前存在的关于SOA实现的争论关联起来。然后,我们将很快进入正题。
作者努力对书中的内容进行了平衡,以便读者能够获得所有需要的东西,同时不会看到任何不需要的东西。当然,要取悦每个人是不可能的,但作者希望这种平衡对于读者来说是有用的。
由于SOA勾勒的是一种使不同平台能够互操作的方式,因此,书中的部分方案将涉及Java之外的语言,如Ruby、Python和.NET。这并不要求读者具备这些语言的背景,因为这些方案都很简单而且有限,展示它们只是为了告诉读者如何能够使不同平台相互通信。
主要内容
尽管Java以其可移植性而著称,许多Web服务人为结果也是可移植的,但为使用户更加方便(或者为了将用户锁定,取决于你的理解),供应商们往往会提供很多其他产品。考虑到涉及的内容是如此广泛,要介绍清楚特定平台上操作方式的细小变化是完全不可能的。要在一本书(比如本书)中涉及各种产品(如WebLogic、Tomcat、WebSphere、Glassfish、ServiceMix、CXF、Mule、Active Endpoints、OpenESB)以及用来解决不同SOA问题的许多其他供应商的产品,将会遇到内容倍增的问题。因此,作者在很大程度上选择了中间路线的态度,将重点放在大多数人可以使用的工具上。以下是具体内容。
第一部分 SOA基础
第1章 SOA入门
本章对相关术语进行了定义,介绍了引导SOA发展的相关架构主题。本章涉及的要点并不详尽,后面的章节也不会按顺序对其进行介绍,它只是对SOA概念的介绍。
第2章 XML Schema和SOA数据模型
由于SOA和Web服务在很大程度上要依赖XML,因此本章介绍如何使用XML、XPath和Schema来支持用户的面向服务工作。重点介绍了一些有用工具以及如何使用JAXB在Java和XML之间进行转换。本章将涉及一些关键的SOA主题,如规范数据模型和基于模式的验证。
第3章 使用XML和Java
第2章的内容主要集中在XML模式、设计和验证这些主题上,本章则将对Java和XML的讨论扩展到文档实例处理领域,主要介绍新的StAX API、JAXB和XML目录。
第二部分 Web服务
第4章 准备工作
作为Java程序员,我们习惯于使用各种有用的方法和类,本章将介绍一些有用的内容。更确切地说,用户将熟悉一些不同的容器,以便能够在一些不同的主流环境中执行客户端程序和服务。用户将创建一个“Hello World”服务,本章将介绍WSDL、讨论服务客户端程序并向用户展示一些在开发过程中监视和调试服务的工具,然后我们就能在后面的章节中转向更特定的API工作。
第5章 使用SAAJ的Web服务
本章介绍SOAP with Attachments API for Java(SAAJ),它主要涉及如何以编程方式来构建SOAP消息。SAAJ是底层API,可以在服务和客户端程序中直接处理XML。
第6章 用JAX-WS创建Web服务应用程序
Java API for XML Web Services(JAX-WS)建立在旧的JAX-RPC API基础之上,与使用SAAJ相比,用它创建和调用Web服务要轻松得多。本章介绍如何以更精练、更抽象的方式来进行许多用SAAJ完成的工作。不过,如果读者以前常常使用JAX-RPC API,则要注意在JAX-WS中,内容已经发生了很大改变。
第7章 提供基于SOAP的Web服务
既然我们已经知道了如何使用基于SOAP的Web服务,那么,本章将介绍如何用各种高级特性来提供服务,包括二进制内容、头等。
第8章 REST式Web服务
通常被称为REST的具象状态传输(Representational State Transfer)不使用SOAP,因此,用户不使用SAAJ或JAX-WS API来处理基于REST的SOA。本章将介绍REST,讨论它之所以流行的原因,并探索如何利用Web固有协议来创建有意义的Web服务,同时又不产生基于SOAP服务可能带来的某些开销。
第三部分 业务流程
第9章 使用BPEL编排服务
本章介绍如何使用WS-BPEL(业务流程执行语言,Business Process Execution Language)2.0来创建业务流程或工作流。BPEL允许用户对多个Web服务的调用进行编排并对输入和输出执行各种有力操作,包括XSL转换和结构化编程的其他常见操作。
第10章 高级BPEL编排
BPEL庞大而复杂,本章将深入介绍使用编排时的一些更高级方面,包括处理错误、并行活动、延时和关联。
第11章 SOA管理
用户需要有一些度量、标准和工具来对SOA进行监视。没有这些,用户可能会花费大量时间和精力却创建出一堆乱糟糟的低质、重复的服务和未知的客户端程序。加上可能出现的糟糕文档、低可见性和不一致性,用户手中得到的是一个非常昂贵的“艺术品”项目。SOA治理是IT治理的延伸,对于用户的SOA,它能够增强结构、提高可见性和一致性,甚至能够优化策略。
第四部分 互操作性和服务质量
第12章 Web服务的互操作性
数十年来,集成对于架构师和开发人员来说一直是个难题,而Web服务则勇敢地以解决有关集成的许多问题为目标。虽然对于非常简单的Web服务来说,可以很容易就实现互操作,但在现实世界中,问题可能要更棘手一些。本章将介绍增强Web服务功能的一些标准方法,以使其能够与其他平台上的服务和客户端程序互操作。书中介绍了WS-Addressing规范以及如何用各种语言(如Ruby和.NET)来实现客户端程序。用户还可以找到关于如何处理规范中灰色地带的建议和技巧。
第13章 服务质量
基于SOAP的服务可以通过相关规范如WS-ReliableMessaging来提高其可靠性,本章阐述了一些具体的标准方法。
第14章 企业服务总线
本章抛开了常见的方案格式以便向用户提供企业服务总线(ESB)的概况,ESB往往是灵魂SOA的支柱。由于ESB通常是作为一组模式来实现的,因此,对于什么是ESB并没有标准的规范,本章总体介绍了一些领先的ESB。虽然ESB一般都是用作消息中介并提供路由、安全和规则,但其工作方式差别非常大。在这一领域,没有一个明确的市场领导者,但我们将介绍一些最流行的产品,包括Oracle Service Bus、TIBCO的ActiveMatrix ESB、OpenESB和Apache ServiceMix。
遗憾的是,我们没有时间来介绍更多与SOA有关的主题,如用WS-Security来实现安全。尽管在一些方案中涉及到了相关内容,如认证,本书并未对Web服务安全进行全面介绍。
如何阅读本书
本书是一本实战指南,这意味着书中主题(在很大程度上)都是以统一的问题/解决方案格式来呈现的,每组问题/解决方案都有简洁的表述。必要时,讨论区中将对解决方案给出详细说明。可以逐页阅读本书,不过读者可能不会用这种方式来使用它。通常,后面的章节都要基于前面章节中的有关知识,但本书在结构上被组织成有用的参考书形式,用户可以在构建SOA的不同阶段参考本书,或者将其作为自己工作的起点。
使用代码示例
本书的目的是帮助用户完成工作。一般而言,用户可以在自己的程序和文档中使用本书中的代码。除非要复制很大一部分代码,否则不需要联系我们获得许可。例如,编写一个使用本书中多段代码的程序不需要许可,不过,销售或发布包含O扲eilly书中示例的光盘则需要许可。引用本书内容和示例代码来解答问题不需要许可,但是,将本书中大量示例代码加入用户自己的产品文档则需要许可。
我们感谢但不要求用户在引用本书内容和代码时将著作权归属于我们,著作权归属通常包括书名、作者、出版商和ISBN,例如:“Java SOA Cookbook”,作者Eben Hewitt,版权所有2009,Eben Hewitt,书号978-0-596-52072-4。
如果觉得对代码示例的使用不在正当使用或以上给出的许可范围之内,请随时发邮件至permissions@oreilly.com进行询问。
笔者努力做到让书中的示例尽可能地保持完整独立。笔者发现,阅读包含一些短小玩偶式代码(toy code)片段的计算机图书会让人沮丧,在用户需要知道有关内容的地方遍布着//...;或是提醒用户参考其他位置的可下载代码,而在讨论中却省略了相关代码。
据说没有什么比Web服务更占用源代码了。即使是一个简单的WSDL就可能横跨好几页,再加上其引用的模式,在开始讨论之前,用户可能就有10或20页的源代码,而大部分代码并不适用于当前的主题,这会降低书的可读性。作者努力对这一点进行了适当平衡,以便用户在需要时可以获得所需的内容。
当每个示例自由使用作者的环境脚本和应用程序,而这些应用程序这又包含初学者无法通过表面解释来区别API和作者自己建立的有用类时,也是比较棘手的。这使得示例难于阅读和理解。这类示例会使作者的工作变得更轻松,却不利于读者。因此,笔者尽力不这样做。
展示代码示例的另一种方法是采用一个足可以作为一本书的“实例研究”,将所有的示例包含在其中。有些读者喜欢这种方法,但笔者发现在某个时间点后,实例研究的内容不可避免地变成开始控制,对示例进行扭曲,试图将每个条目放入一个虚假的环境中。有些系统并不需要以头的方式传递基本验证数据;为什么要扭曲示例或添加大量的无用内容到虚构的实例研究中呢?本书采用一种更简单的方法。
关于有效示例,笔者认为它们必须通常是完整独立的,而且能够尽可能轻松地被复制到读者自己的环境中。此外,它们必须不是“玩偶式代码”,需要具有真实用途。诚然,当谈及某个具有众多移动部件的主题时,要实现这些目标是很困难的。构建Web服务会包含很多中间层,这意味着即使是一个简单的示例,也可能会涉及大量的不同文件和设置。如上所述,笔者花了很多心思考虑如何为本书中的解决方案实现所有上面这些目标,希望本书已经找到了一种好的平衡方式。
本书的代码示例可以从http://examples.oreilly.com/9780596520724获得。
坚持住!
此外,不要泄气!这方面的内容是很难的,的确很难。现有的规范比较杂乱,新规范的出炉速度超前于行业发展的速度。供应商落后于它们,而开发人员就更落后。这是一项惊人的复杂任务。现在,有关SOA的宣传也是满天飞,但并没有让人们更轻松掌握它。在这种背景下,很难说哪些内容是眼下时新的,哪些是过时的,哪些又是未来的。
所幸的是,近来在Java API方面取得的成就已经使创建Web服务比过去更容易。但是,世界总是朝前看,倾向于使用更复杂的软件,单纯依赖Web服务是不够的。Web服务需要与BPEL编排、代理的ESB、业务活动监控工具、SCA以及新层的所有方面共同运行于某个系统中。不过,SOA是眼下流行的技术,这方面的内容没有过时。虽然SOA的基础概念是基于已有几十年历史的技术,但SOA真正体现了企业在如何开展业务方面的一种转变。
非常感谢你选择了本书,希望它能对你要做的工作有所帮助。虽然不可能将一位SOA从业者所需的所有内容包含在单本书中,我本人还是尽了最大努力将有关重点内容收纳进来,为读者在SOA方面提供扎实的基础。衷心希望你能喜欢本书。
排版约定
本书中使用如下排版惯例。
斜体
表示新的术语、URL、电子邮件地址、文件名和文件扩展名。
等宽字体(Constant width)
表示程序清单以及段落中的程序元素,比如变量名或函数名、数据库、数据类型、环境变量、语句和关键字。
等宽粗体(Constant width bold)
用于强调代码清单中的有关内容。
提示: 用来表示秘诀、建议或通用注解。
警告: 用来表示警告或注意事项。
联系我们
有关本书的任何建议和疑问,可以与下面的出版社联系。
美国:
O扲eilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
中国:
北京市西城区西直门南大街2号成铭大厦C座807室(100035)
奥莱利技术咨询(北京)有限公司
我们为本书提供了一个网页,其中给出了勘误表、示例和所有的附加信息。可以通过以下地址访问该网页:
http://www.oreilly.com/catalog/9780596520724
要对本书发表评论或询问技术问题,请发电子邮件到:
bookquestions@oreilly.com
有关我们的书籍、会议、资源中心以及O扲eilly网络,可以访问我们的网站:
http://www.oreilly.com
http://www.oreilly.com.cn
献辞
本书献给我在DTC的所有好友和同事,特别是Architecture和Programming小组。Barney Marispini、Bob LaChapelle、Bob Lemm、Brian Lee、Brian Mericle、Chris Servis、Deryl Heitman、John O払rien、Kevin Williams、Mike Moore、Phillip Rower、Scott Ramsey、Steve Miller、Tom Schaeffer——本书献给你们。很感谢和你们一起在这么棒的团队工作,你们总是互相提携,帮对方做到最好。与这样一群具有献身精神的顶尖人才一起工作,是一种享受。
致谢
特别感谢Steve Miller、Deryl Heitman、John O払rien和Rich Kuipers对本项目的大力支持,如果不是你们那十足的慷慨、理解、耐心和超凡的视野,本书就不会存在。谢谢你们对该项目的坚定不移。能够在世界上最好的公司工作,我感到无比高兴。
谢谢技术评审Jason Brittain和Nicolai Josuttis,你们指出不当的地方并帮助我阐明问题的重要方面。
非常感谢Barney Marispini,辛苦地读完了初稿,给出了许多有益的更正和建议,实实在在地提高了本书的质量。
谢谢Simon St.Laurent对该项目的编辑指导,和他一起工作非常愉快。十分谢谢我的制作编辑Loranah Dimant、索引员Lucie Haskins以及O扲eilly的所有职员,感谢他们的敬业、献身精神和对细节的关注,与你们一起共事真的很快乐。
谢谢Alison Brown在完善方面所做的努力,和我幸福生活中许多事情一样,你在这方面功不可没。
第一部分
SOA基础
第1章
SOA入门
1.1 概述
本章简要介绍面向服务架构(SOA)的前景和说明SOA可能会遇到的一些组织挑战。读者可能熟悉各种SOA定义,但本书中给出的解决方案——甚至本书自身的结构——在某种程度上取决于对如何定义各种SOA要素部件的具体理解。
当今IT具有异质性。市面上有成百上千的编程语言,它们都用来编写当代企业使用的各种应用程序;这些应用程序有时需要互相对话,而这顷刻就成为一个需要慎重对待的问题。
用较少平台支持的语言编写的老系统需要和各种不同现代语言编写的应用程序一起工作,这些系统可能具有许多明显的瑕疵和缺点,使人很想简单地将它们禁用并替换。不过,这样做代价太大了,而且很少会像开始设想的那样容易,同时,从操作的角度来说,非常容易受到攻击。这些应用程序经过了时间的考验,人们通过日常使用对它们进行审查。虽然我们希望我们的系统看起来尽可能的简洁、精要,但很多时候没有经验的开发者认为,要处理真实世界所要求我们的应用程序解决的许多杂乱和异常情况,代码的蔓延就不可避免。
当然,程序中实际有大量劣质代码不值得保存。许多老应用程序编写时遵守的规则已经发生了很大的变化,有时,一些特定用意的程序编写时采用的方式比较奇特,以至于在其他应用程序中很难重新使用这些程序。但是,对于那些作为某个企业支柱的关键应用程序,可以采用一种现代化的方式,同时要能够让程序重复使用和功能得以提升。
通过用某种面向服务架构支持框架中运行的Web服务将传统系统包装起来,用户可以争取时间。争取的时间表现在供应商的中立,这使得用户可以将移植决策延期,也就是只让老化系统的生命得以延长而不移植具体软件。
合并与收购很容易导致集成问题。不同种类的平台可能具有多余或重复的功能,这会对一个组织的灵活性、响应能力和服务分发能力产生影响。在基于Web的全球商务领域,停止移植是不可能的,而且,“星期天凌晨3点”这类切换机会的成本比以前要高。
SOA代表的是一种方法,通过将不同的内容包含在组织中而不是排斥在外,为组织抓住新的商业机会和缩短上市时间。可能不必禁用和替换那些旧的应用程序,也可能不必受供应商的支配,为避免再次遇到同类问题而无可奈何地使用完整的统一堆栈。面向服务架构使得用户可以用一种改良而不是革命的方式解决自己的集成问题。
面向服务方面的进展寻求的是将IT工作与商业目标更紧密地结合,确保企业能响应各种变化和迅速抓住新的机会。
本章中给出的问题和解决方案是为用户刚开始进行SOA学习时提供的一些说明性解决方案,用户可以根据自己的环境需要进行适当修改。这些方案比用户以前可能在某本实战指南中看到的方案更常规和主观。因为SOA代表的是一种架构,因此,它有助于开发者理解所创建服务将来运行时所处的更大业务环境。为了体现SOA的优点和编写能够具有真正业务价值的有用服务,任何给定服务实现需要有助于达到制定的上层业务目标。
本章中的方案没有给出基于代码的解决方案,它们是作为各种实现会遇到的概念问题来介绍的。尽管对于Java开发者来说,可以完全跳过本章,直接进入下一章的编码学习,但是,在SOA中,开发者像架构师一样去思考是非常重要的,反之亦然。实际上,开发者和架构师两种角色有时甚至无法分割开,这取决于组织的规模和结构。无论你所在的组织是不是这种情况,都是可以使用本章中的方案来定义本书其余内容所基于的常见术语。
1.2 定义服务
问题
什么是服务?
解决方案
服务就是具有以下属性的软件组件:
由某个接口定义,该接口可以是独立于平台的。
可以通过网络进行访问。
接口中定义的操作通过操纵业务对象来实现业务功能。
接口及其实现可以在运行时被扩展。
服务还可以代表其他内容。服务的定义有时比较宽,例如,包含其接口由当前业务指定的分布式组件。有时又比较窄,例如基于SOAP的Web服务。
讨论
起初难于接近SOA的困难之一是:人们总是对它的基础内容——服务——所代表的含义有不同的理解。有关服务的许多定义都包含难以琢磨或非常抽象的概念,例如“适应性”和“灵活性”,这些概念对开发者来说太含糊了。如果可以选择的话,大概没有人愿意生产不符合业务需要的无法改编的劣质软件,但事实是,尽管经过很大努力,这还是经常发生。认识到SOA应该具有“适应性”和“灵活性”是值得称赞的,但这并不有助于状况的改变。下面将解释这些术语。
其他定义对真实世界来说太窄了。例如,将服务指定成必须是使用SOAP的软件组件会比较吸引人,但这种限制是人为的,如今有许多使用REST的从业者坚信他们也在使用服务。或许我们的定义应该包含消息基于XML方面的内容,因为这是REST式Web服务和基于SOAP的服务的共有特性,但这对常规定义来说,限制太多了。
因此,为了实用,服务的定义应该足够明确;而为了能够包含真实世界中人们采用的各种方法,服务的定义又应该足够全面,那么,这种定义是怎样的呢?
在接着往下进行之前,让我们来看看有关服务定义的各个方面。
接口独立于平台
服务所能执行的任务必须通过一个外部接口来描述。有些架构允许这类接口可以实际与某个特定平台关联,如用来描述远程会话EJB上操作的Java接口。通过这种方式当然可以接近服务,而且能够在给定的商务环境中调整这类接近方法。如果规定用户必须通过某种平台(例如Java或.NET)来使用服务,则会节省大量的时间和金钱,并会大大简化实现过程。
在公司里,许多SOA是在一个相对静态的知名环境下运行的,而且是在防火墙后运行的。作者本人和这类环境的一些架构师交谈过,他们的整个SOA是通过EJB定义的。这是一个非常特定的平台,这些架构师认为自己正在做的就是SOA,他们非常聪明并富有经验。
他们的观点是:在给定的商务环境下,使用SOAP及其许多附带规范会使开发者付出不应有的努力,操作更复杂却没有得到更多的收益,而且带来不必要的运行开销。因此,尽管许多作者试图说明服务必须按照某种没有规定平台的方式进行定义,而且这类服务是本书的核心主题,但是,我本人认为的服务定义可以允许特定于平台的实现。
不过,创建服务通常是为了解决系统集成问题,或者至少关注集成问题。WSDL是描述功能的一种方法,在该方法中,没有规定实现。虽然WSDL通常基于HTTP使用SOAP,但这不是必要条件;其他的协议可以用来绑定,同样抽象的WSDL可以解决各种绑定,从而提供了更大的灵活性。
对于基于Web的服务来说,HTTP或许不是理想的传输层,但简单和普遍性使其成为了默认选择对象。因此,这类服务实现受到了广泛支持。
REST式Web服务使用XML和其固有的HTTP方法定义服务接口,许多认为基于SOAP的服务太复杂和过于臃肿的用户支持该方法。我本人希望避免这种争论,因而只指出REST充分满足平台独立条件,但并没有明确说明它是否符合可以作为服务合同一部分进行运行的特殊接口。
由于服务不仅仅是一项功能,在用来约束用户的合同中,可以进行有关描述,因此,清楚定义的接口能在合同自动化中正常发挥作用是很重要的。没有清晰的接口,服务的重用机会就会减少。如果不具备平台独立性,潜在的用户会减少,而且服务组件会变得更加紧耦合。
当然,事物都有两面性,平台独立对性能和复杂性有一些影响。
可通过网络访问
当今世界充满了各种非常有用的程序,这些程序的函数只能从同一虚拟计算机或同一物理计算机进行调用,这些程序不能简单地称为服务。就像在现实世界中一样,客户必须能够获得有关联系信息才能订约清洗服务、咨询服务或饮食服务。如果没有提供电话号码,就不会显示相关的联系信息。假如不能远程访问相应的函数,它就不是一项服务。
操纵业务对象
服务的第三条标准是通常执行业务功能,而且是常常通过操纵业务文档来实现的。这是什么意思?将两个数加起来的计算器可能不能被很好地称为服务,因为整数不是业务对象。你的组织永远不会用一种特定的方式定义这些它们。
“业务对象”指的是所处业务领域中的有关实体,例如,“顾客”、“产品”、“发货单”、“学生”和“雇用申请”。因为它们是将用户的业务与其他人业务区别开来的实体,用户可以自己决定这些对象的含义。对add操作的进一步思考需要质疑加运算是否是用户所在组织所特有的。不是。世界上的任何人都可以整天执行加运算,而且,假如基本掌握这一任务,每个人进行加运算的方法是一样的。此外,用户没有机会革新进行加运算时采用的方法,无法在自己的操作实现过程中设想和使用某种定制方案来获得有竞争性的市场优势。除非具有一个特别有创造力的会计部门,否则,用户自己无法决定整数和加法的含义。
的确,有许多流行的服务可供人们公开访问,这些服务对“实况”进行操作。它们包括确定两个位置之间的距离,执行计算(如根据重量计算运费),以及查找最近商店的位置。虽然邮政编码或以盎司为单位的重量本身可能看起来不像业务对象,但这类服务确实执行业务操作,不同于将两个数相加的纯粹一般操作。
这只是一般原则。让我们来考察货币转换服务。这类可公开使用的Web服务非常多,而且欧元对大家来说都是一样的,因此,这些服务看起来不像是业务对象,然而,这通常是观点问题。与宠物食品商店里不同,在银行里,货币概念被进一步细分,使它成为银行的一种业务对象。
提示: 这一原则通常有例外。此处的思想是根据服务是否交换业务文档进行考虑,而不是根据服务是否接受简单价值清单。
如果不具有某一尺寸的业务,至少是某一历史长度的业务,可能就不需要SOA。虽然程序员可能会出于自己的个人用途而喜欢编写视频游戏,但没有人会在星期六下午躲在车库里构建SOA。谈论SOA就要谈论某一尺度的业务,而且,如果各要素服务以适当粒度水平运作来达到最大化重复使用,SOA将实现其可能的ROI(投资回报率)。当然,这要看什么是“适当程度”,将取决于定义服务的业务流程以及哪些相关的流程可能重复使用这些服务。
需要强调的是这一原则让服务有多种不同的解释,用户可以将它作为起点,自己再指定一些特性。用户希望不要这么模糊,从而更简单地进行操作。实际上,也有只运行于技术领域的实用服务,1.5小节介绍了这些服务。
可装饰
这指的是用户需要能够按照某种方式装饰服务操作,以满足功能性和非功能性要求。这一方面将服务提升到SOA的有意义层面。
如果使用SOAP,各种WS-*规范(例如WS-ReliableMessaging或WS-SecureConversation)将向服务添加功能,这些功能不影响核心业务功能,但会使服务更好地适用于SOA中。比如,添加HTTP头这项功能就可以装饰基于REST的服务实现。EJB进行削减,因为虽然该机制不是基于WS-*标准,但拦截器允许装饰like功能。
此处的要点是用户想要借助治理(后面我们将讨论)的技术方法能够提供SOA及其必需的要素服务。希望能够包括运行时策略,来增强服务的使用方式。安全交互、允许事务操作以及在某些情况下接受监控手段都是非功能性要求,这些要求就是理论上纯粹实现“清单处理”服务和真实世界中的差别,后者涉及到需要满足这些外部约束条件的特殊网络。将这类要求放入服务中会大大减弱重复使用的可能性,甚至会丧失重复使用的可能性,尤其是在联合环境中。不过仍然需要处理这些要求;如果可能,可以将它们移到装饰器中。
这方面的要求可以指导用户选择平台或实现。软件组件中有这么多的非功能性要求如此频繁地出现(以及众多要求是通过WS-*规范来指定的),这是作者本人为什么选择在本书中(以及在构建实际SOA时)关注基于SOAP服务的主要原因。借助这些规范按照一种标准的方式处理这类常见要求,将会增强互操作性和可维护性,使得开发人员可以将精力放在自己的业务问题上。
其他考虑事项
要使一项操作成为一项服务,除了上面提到的一些特性外,还需要具备哪些条件?似乎只要服务将被重复使用,那么,定义和创建该服务时再困难也是值得的。SOA的主要目标是其组件是自包含的,没有冗余功能。组件应该具有一定程度的可重复使用性。也就是说,它们必须不局限于企业中与其他对象很少甚至没有关联的某个狭窄业务环境。用户可能会编写不重复使用的服务;用户所在的企业可能会改变方向;或者某项服务可能定义得太不完美了,要么太精细,要么太宽泛,以至于不能适用于其他环境。发现这些问题就是架构师的价值所在。
有些架构师会要求服务必须是没有状态的。我承认通常来说,“确保服务操作是无状态的”是一个不错的主意,但不要求这是服务的一项必备功能。如果服务必须维护状态,可以通过数据库或其他临时序列化机制来外部实现。如果可能,通常最好避免会话状态,不过,当服务合成中需要维护会话状态时,可以通过一种规范(比如WS-SecureConversation和WS-Conversation)进行处理。这种规范已经问世好些年了,而且容器(如WebLogic 10gR3)中实现了该规范。
1.3 定义SOA
问题
天花乱坠的广告、各种夸词和模糊的陈述笼罩着术语“SOA”。用户需要的是一种实用的定义。
解决方案
在设定相应目标之前,需要小心定义SOA。近些年关于SOA的广告满天飞,这给定义SOA带来了一定的难度,不过还是可以做到的。SOA的定义可以有很多种,下面是我给出的定义:
SOA是一种架构,它使用服务作为构建块,通过松散耦合来促进企业集成和组件重复使用。
让我们对该定义稍作分解,怎么样?
SOA是一种架构
虽然这可能存在明显的夸张,但是,此处是一条小的经验。简单地创建一组服务不会自动形成一种架构,尽管架构应该指导服务的创建,但它不同于服务自身的实现。
在术语“SOA”中,“面向服务”作为修饰语修饰“架构”。也就是说,SOA描述的是一种可操作的架构,它不是一种开发模式。不能简单地对某个EJB进行注释,来指出它是一个Web服务和认为既然现在拥有了Web服务,就一定可以顺利开始进行SOA的有关操作了。如果不是十分注意方式方法,则实际上很可能是在开始非常精心的而代价却巨大的瞎忙活。
需要指出的一点是不能简单购买SOA,不能期望只要贵公司的CIO提供支票,某位供应商就可以移交某种SOA。构建SOA涉及到大量的分析和综合。
SOA是用服务构建的
这是“面向服务架构”中的“服务”部分,此处基于的是1.2小节中的定义。如果不是一种架构,或者该架构没有使用服务作为普通度量单元(就像面向对象系统使用对象或类作为普通单元一样),那么,它就不是SOA。
没有服务,就无法进行构建、监控和治理,也无法进行执行操作来体现业务价值。不过另一种极端是,用户长时间的辛苦工作,设计出一组有趣的完美架构图,但这些图非常不实用。如果只关注完成一组服务实现而忘记了架构,或者更可能的是,在摒弃“宇航员架构”时心不在焉,故意不考虑架构,则不仅仅是没有实现SOA,而且将会使情况更糟。将会花费大量的时间和金钱为错误问题超工程寻找解决方案。这类狂暴工作有一个实际名称:反模式,通俗地讲,就是“只是一堆杂乱无章的Web服务”(Just a Bunch of Web Services,JaBoWS)。此外,如果莽撞的开发人员在没有架构方针的指导下进行操作,一不留神,就可能会导致业务陷入另一种SOA反模式:劣种服务,这类服务在网络上运行,它们不属于目录中已知的治理服务。因此,没有架构或治理的一组服务并不是SOA。
在我看来,完全位于架构环境外部的某个“服务”是否实际上真正称为服务是有待确定的。支持架构中的环境角色在一定程度上有助于将操作提升到服务层面。在我给出的服务定义中,最后一条标准包含了这一状态,但没有提供直接的陈述:它可以通过运行时扩展进行装饰,来处理某些非功能性的技术要求。
SOA促进集成
SOA代表一种思考系统集成的方法,通过将多个系统连接在一起,SOA帮助提供改进的或新的业务功能和机会。它相当直截了当的形式让用户可以更快地执行B2B工作。
数十年来,在CORBA、连接器架构、EDI以及点对点EAI方面的成就已经完全说明了我们有能力集成不同系统,但这些技术成本昂贵,而且不够强大。它们会分散开发人员和IT部门解决真正业务问题的精力,因为需要花费几个月来让系统进行对话。这些解决方案都定义了它们自身的消息交换格式。通常情况下,SOA重复使用XML作为标准的消息格式,这使得集成工作能够更容易进行。
有些CIO受挫于以前的EAI项目,再加上为了更快更果断地解决问题,他们放弃自己努力了,而是与他们自己中意的供应商联系,签约让供应商完成全部内容,试图避免所有的集成问题。但这实际上可能会是一个花费非常昂贵的决定,还会让他们所在的组织受控于供应商,需要根据供应商的时间表来进行操作,而且将只能有权规定外部功能。
因此,需要集成时,SOA试图解决集成工作需要费心的地方。
SOA通过松散耦合促进重复使用
“SOA通过松散耦合促进重复使用”这一思想是对“只支持企业集成工作”想法的重要修改。SOA提出两种不同的思想:一种是首先促进重复使用,另一种是通过创建松散耦合组件后再促进重复使用。
早期的企业集成或先前的服务供应商所做的工作没有十分特别关注重复使用这一思想。不过现在,只要给定新的标准和使用某种一般消息交换格式,比如XML,就能够充分意识到企业服务可以被重复使用。
这类重复使用可以表现为一般的互操作性。如果两个软件组件可以互操作,并不表示它们是松散耦合的。但是,松散耦合的组件更可能进行互操作。
提示: SOA的一个基本方面是,创建服务时就预想到它们会和某些未知组件进行互操作。这与传统的集成工作有很大的不同,后者的目标可能就是一组已知接口。
虽然确保服务松散耦合会实现重复使用,但是,没有必要将重复使用作为每项服务的目标。可能存在这样一些服务,用户知道它们几乎不可能重复使用于某个合成或未知的业务环境。这类服务可能已作为试验项目创建;或者,有时有这样的需要:将某一功能创建为服务以便形成更大的架构基础,使其可视性更好,例如,通过某种服务编目;也可能是为了利用集中的架构;还可能是为了更快获取SOA监控工具提供的运行时规格。
对于任意给定服务来说,重复使用不是绝对必需的。但是,完全由非重复使用组件形成的SOA根本不可能是SOA,而是一堆异样的、肿胀的、超工程点对点EAI“恐龙骨骼”。
调用服务
在允许一项服务直接调用另一项服务(也就是说,一项服务本身是另一项服务的客户端)时要小心,因为这会显著减少能够重复使用该服务的业务环境。有时,似乎需要实现时让一项服务成为另一项服务的客户端,不过,这会导致一定程度的耦合,而用户后来可能会不希望这样。如果是这种情况,可以考虑使用编排技术,比如BPEL(有关介绍请参见第9章和第10章)。
此外,如果不适当使用服务水平协议(SLA)监控软件和规格,就会很难跟踪服务从不同地方接收流量的情况。如果服务跨越多个业务域,则需要特别注意考虑由于当前主题以外的合成所带来的突然性的意外流量猛增。直接调用的另一个问题是会使外形更复杂,无法通过工具提高可见性。大型SOA套件供应商可以提供相应的工具来显示企业存储库中存储的服务相关资源之间的依赖,不过这些工具非常贵。
这个问题的结构解决方案是当一项服务需要直接调用另一项服务时,考虑是否可以创建编排(或流程服务,就像1.5小节中定义的那样)来替代。正如服务包装某个系统或子系统一样,编排包装形成某种流的一组服务。不过,编排外部本身像服务,因此,它看起来就像客户端的任意其他服务。
其他考虑事项
对不同的组织而言,SOA意味着不同的内容,鼓励用户根据自身的业务环境及其附带的有关条件来考虑具体的定义和确定重点要素。
1.4 识别服务候选对象
问题
用户需要识别某个提议的软件项目是否能成为一个好的服务候选对象。这一过程有时称为“服务发现”。
解决方案
从上到下或从下到上开始瞄准一组可能的候选对象,而且,要知道根据业务流程要求,可能会从中间进行开发。
回想起术语“服务”不包含任意特定实现平台,如果能够至少肯定回答以下规则中的一些问题,提议的软件项目可能就是一个好的服务候选对象:
1. 功能能否被相应设计成符合本章中所陈述的服务定义?
2. 该服务是否能够适用于多个平台?它是否需要互操作?外部业务合作伙伴会使用该服务吗?它是否需要穿越功能屏障或防火墙?它是否能够使用流行协议,比如HTTP或SMTP?
3. 将该功能作为服务实现是否有助于攻克集成障碍?它是否包装或替代某个专用点对点接口?它是否运行于ERP、CRM、金融应用程序或其他系统之前?
4. 它是否清楚映射到一个或多个业务流程?或者它是否就是一个程序?(通常来说,服务可以映射到单个流程,不过,映射到多个流程会更容易重复使用)
5. 提议服务所具有的功能是否跨越多个业务域?这会导致更大的重复使用机会。
6. 服务是否交换业务文档或很容易将其归属到1.5小节给出的三种常见服务类别中的某一种?
7. 业务人员是否有兴趣报告该服务的输出?IT是否有兴趣监控服务的生命周期或运行过程?或者,它代表某项长期会提升综合业务灵活性的实用服务。(如果都没有,则有可能是操作太小,无法在服务目录中争得一席之地)
8. 它是否具有业务价值?是否某项已有服务已经执行了这项功能?
9. 提议服务是否具有合适的粒度?(如果过分精细,将会影响合同的接口方面,降低重复使用的可能性,增加不适当的闲谈内容。倘若过分宽泛,该服务可能会是一项潜在的流程服务,将被分解成其他实体或实用服务)
10. 该功能成为服务后是否允许它进行组合?也就是说,它是否能作为无缝黑匣子参与某项统一合同(比如编排)背后的其他服务?或者,它是否仅代表某项单独的功能,与其他系统组件无关?
11. 该候选对象是否代表可确认生命周期的某种思想?该业务是否能定义接口?
12. 如果能够动态找到该软件(比如通过注册/存储库),是否比较有利?
13. 它是否具有某种机会帮助实现提供“唯一真实版本”这一目标?
14. 根据实现情况,使用XML呈现数据所带来的好处是否胜过对XML来回进行排列和打乱排列所导致的性能降低?交换文档的可读性是否是一种因素?
15. 将其作为服务来实现是否降低了未来集成项目的成本?它是否促进了新的产品或业务服务?
再次指出,这些问题只是需要考虑,在这方面没有硬性规定。此处的观点是用户应该确保自己没有被弄得精疲力竭,不要将每件事放入一项服务中。我知道有些公司多年之前就是这样做的,现在,他们拥有多达5 000项“服务”,痛苦不堪。将这作为一条指导原则就好。
讨论
并不是用户所在企业的任何事都能够或都应该成为服务。不要陷入“工具陷阱”:拥有一把锤子后,什么东西都看起来像钉子。有许多实际且重要的问题是服务和SOA完全不能解决的。用户一旦将SOA作为企业的一个目标建立后,如果宣称所有出现的新项目都应该编写为服务,将会非常吸引人,但是,这不会有助于创建能够带来长期实际投资收益的实用SOA,而是将开始创建“只是一堆杂乱无章的Web服务”,即前面提到的反模式。
不过,事实是确实要创建一些服务,形成某种合适的SOA结构,因此,问题仍然是:“如何辨别所见对象是服务候选对象?”当开始扩建服务目录时,使用可靠的原则来确定对象应不应该是服务是很重要的。
与业务各部分领导进行会谈和使用其他信息收集技术来理解业务模型,这也是非常重要的,业务模型将用作服务发现分析的基础。这些模型最后将是甄别服务候选对象的实际钥匙。为了确定是否为服务定义了合适的边界以及服务是否符合相应的粒度级别,可以对该模型反复重构。
当开始填充服务目录时以及在创建SOA过程中,为了发现服务候选对象,有两种基本的方法可以用来分析和评估企业。
虽然前面显示的清单给出了许多有用问题,但是,只应该将它作为指导原则。用户作为开发人员或架构师,当要确定某一提议软件是否应该作为服务进行编写时,可以回过头来参看该清单,这样用户就不会偏离目标。用户将可能及时开始定制自己的一组标准。
接下来,让我们转向如何发现组织中的服务机会这一问题。
从上到下或从下到上方法
从上到下方法是从高层业务视图开始。使用这种方法,用户可以检查企业制定的蓝图,其中给出了给定时间框架内的已建立业务目标。接着,用户可以评估这些目标以创建潜在的服务。这个过程不是简单地将一组具有优先次序的业务项目直接映射成服务,而是要求用户使用现有结构文档和业务流程建模技术来确定作为服务编写时,企业中的哪些对象将更有意义。
架构师、CTO、CIO或企业的其他负责方应该拥有直接基于企业蓝图的IT蓝图。该蓝图可以具有不同的粒度级别,还可以跨越不同的时间或不同的IT部门。但是,这类文档可以作为基础来识别那些能够成为服务候选对象进行检验的热点主题。该蓝图如果存在,对于服务设计来说,蓝图本身的级别太高。不过,一旦蓝图上的项目经核准可用,就可以开始识别涉及的业务流程。映射项目所需的业务流程通常会针对某个外部系统和多个内部系统,这个外部系统可以通过服务集成,内部系统可以通过服务进行包装。
提示: 许多IT项目包含面向用户的表示层。通常来说,只有项目的某些方面在后端应该编写为服务。面向服务架构师需要与多个项目或产品的项目经理进行交流来协调他们的工作。这对于专注的项目团队来说,是具有挑战性的。
如果企业相对年轻,具有较少的异类系统,或者通常思想十分前卫,则非常适合使用这种方法。在这种方案中,企业是比较容易支持SOA工作的,因为他们让预期的新产品得以交付。
从下到上方法是从传统技术视图开始。它将现有的功能包装成服务,为的是达到更全面的业务目标,比如增强灵活性、提高互操作性或更易于集成。这有时称为“服务激活”。基本的功能已经存在,但在面向服务结构中,可以将其变成“一等公民”。
这种方法是从识别企业中能够作为服务被有效处理的“费心对象”开始的。这是一种常见方法,适用于用户没有相应的架构来促进计划好的系统集成工作,而且发现自己处于无节制、无文档、独占或通常丑陋无比点对点通信的“附属结构”。如果是上面这种情况,需要小心进行此类工作,注意理解已经使用该紧密耦合功能的那些流程。
企业认识到该方法的价值是,通过解开以前技术中的一些复杂缠绕以获得更大的灵活性。这样会直接缩短上市的时间。
这种方法比较适合于这样一些公司:已经有数十年的历史,具有众多老系统,使用了各种不同的平台(可能是由于多次合并或收购)或者具有许多现有的点对点连接。不过,要注意只采用这些老系统的现有接口,脱离开这些系统创建Web服务。在HTTP之上运行SOAP的不好接口仍是一个坏接口。设计可用的接口通常意味着需要对当前状态业务模型进行检修,可能是出于其他目的已使用从上到下方法对这些模型进行定义。
从下到上的方法使用得比较少。IT可以结束“驾驶员”这一角色,而用户却不希望。企业需要了解SOA并帮助治理和定义流程。未来的接口是不可能在过去的代码中找到的,过去的代码可以用新的接口封装。此外,根据业务流程的需要,用户通常既不是从顶部开始,也不是从底部开始,而是从中间开始。接着,用户可以找到相应的方法来重复使用现有的系统,根据流程进行相应修正。
参见
1.5小节。
1.5 识别不同种类的服务
问题
用户需要识别能够创建的不同种类服务,以至于采用合适的粒度和相应的劳动强度来设计它们。
解决方案
有三种基本的服务:实体服务、功能服务和流程服务。
讨论
下面是服务候选对象将属于的三种基本类型:
实体服务
实体服务代表的是一个或多个业务实体。实体是一个名词,它组成了企业中的对象,有关实体的示例有“客户”、“发货单”、“职工”、“产品”等。对于从根本上分解的基本实体来说,实体服务实现CRUD(Create、Read、Update和Delete)操作。不能只因为某个对象是名词就意味着它是实体服务,例如,CustomerAccount可以由许多不同系统之间的交互来创建。在这种情况下,需要将账目的创建提升到工作流流程服务。可以将服务自治作为一种目标,这应该有助于用户选择合适的服务类型。
由于许多实体在整个企业中都有引用,因此很容易识别它们,基于它们的服务也就具有较好的重复使用前景。在主要的数据管理方案中,这些可以作为“单一数据源”策略使用。
功能服务
功能服务不代表业务流程和业务实体,它在业务模型中没有相应的表示。不过,可以通过顺序图来表示它。功能服务是面向技术服务,而不是面向业务服务,它的目的是提供其他服务依赖的可重复使用的集中功能。
可以要求功能服务执行某种给定功能,比如发送电子邮件,或者,以一种能够支持记录或通知功能的标准方式处理异常。功能服务可以用作集中规则引擎或安全服务。
这类服务尽可能自治是很重要的。
流程服务
流程服务代表的是一系列的相关任务,这些任务可以是单个业务域内执行的任务,也可以是跨越多个业务域甚至跨越组织的任务。流程服务可以通过ESB调用的编排来表示,编排中包含粗线条合同,这样,对客户来说,流程服务看起来像一个统一的整体。
处理发货单就是一个例子。流程组合就变得更加复杂,将组合其他流程。这方面的一个例子是某项New Hire服务,该服务定义吸纳新员工的过程,它是一个实体服务。但这类服务还可能会访问某个IT机构提供服务来为新员工准备新的工作站,此外,它还可能使用某项功能服务发送电子邮件以及使用另一项功能服务处理错误。
尽管上面这些种类不能将用户的实际服务总量和开发项目计划组织起来,但是,了解当前使用哪种类型的服务是很有价值的,这样会有助于组织操作方法以及控制与其他开发人员和架构师的交流。架构师需要在自己的模型上注意这些不同服务。
1.6 为服务建模
问题
确定服务候选对象后,需要开始为服务建模。
解决方案
使用已有的结构建模技术,例如泛化、分解和聚合。
讨论
确定合理的候选服务后,就可以使用一组技术为该服务建模,对服务的定义进行重构,直到知道它属于哪种类型的服务并对它的定义和粒度级别满意,而且确信它仍旧是一项服务。
使用下面这些信息建模技术可以更好地进行建模:
泛化
概括地说,就是分析服务以从概念的角度确定它所代表的内容。在面向对象(OO)编程中,这称为发现IS-A关系。用户需要为服务找到合适的泛化级别。可以将“客户”确定为“人”,也可以将“员工”确定为“人”,对于用户所在的企业,这种泛化程度可能是合适的;但是,依据“人”是“智人”这一论据进行泛化或许就是不合适的。更常见的问题可能是用户的设计不够常规,没有弄清真正排列不同组件的哪些方面以及对它们进行区分。设计服务时,如果太具体,就会降低它们重复使用的可能性,容易形成非常复杂的接口和编排,阻挠SOA工作真正目的的实现。
使用哪种类型的服务是不重要的。前面提到的“人”示例看作是一个实体,但对流程服务也是一样的。
分解
分析服务以确定服务包含的其他元素。这种分析将揭示本身可以独立存在的功能服务或实体服务。在某些情况下,当前流程包含其他独立的流程。值得注意的是,比较精细的服务也比较容易组合和重复使用,比较粗泛的服务也比较容易互操作,也就更适合面向客户终端。
聚合
分析服务以确定它可以作为哪些其他元素的一部分。这些元素本身可以是现有的流程、服务或服务候选对象。在面向对象编程中,这称为发现HAS-A关系。使用这项技术有助于发现可合成的服务。
以上是一些基本技术,通过使用这些技术,用户就会做得很好。但是,对于服务建模来说,用户需要超越这些基本原则,还要考虑其他一些方面:
重用性
在面向对象设计中,通常要考虑重用性,但在服务模型中,不是必须要考虑重用性。为了确定如何编写合同,用户需要进行系统分析,以充分了解服务在整个企业中的重用情况。如果不确定服务将如何重用,则让服务保持尽可能的一般化,以提升重复使用的可能性。
安全性
安全性通常是在对象的应用层处理的,但需要在创建服务时对单个服务层考虑安全性模型。可以为SOA建立完整的安全性模型,包括SAML、策略执行点、传输安全性、数字签名、Kerberos或其他相关措施。为了支持合同,通常是通过工具执行的策略来给出这些措施。
互操作性
Java对象只需要与其他Java对象交互,而服务是用于互操作的。为服务建模时,需要从互操作角度进行考虑,识别将要使用的其他平台。遵守“基本轮廓”(后面将讨论)会对此有帮助。
SLA
像它们所包装的系统一样,服务应该定义服务水平协议。服务参与的业务流程可能需要在100毫秒内进行响应,否则就应该给出一种标记。在SOA中,这特别重要,因为设计好的服务最终将被许多流程重复使用,而且,对于帮助企业了解预期目标,进行流程分析将是很重要的。
粒度
用户的服务将参与由许多服务形成的规划。采用合适的粒度级别是确保重复使用某项服务的关键,也是确保将服务成功展示给业务合作伙伴的关键,还是确保合适安全性约束的关键。
合同
识别整个服务合同将需要模型中的哪些元素。部分元素可能是服务的直接属性,其他元素可以用作元数据,在WS-MetadataExchange环境中使用。有些元素是非功能性的,商业供应商提供的各种SOA运行时环境声明和实现了其中的一部分。
流程
流程分析本身就是一个学科,许多OO开发人员对该学科可能不熟悉。由于业务流程可以表示为服务,再加上业务流程将使用服务,因此,从该角度了解IT世界是非常重要的。有关实际讨论超出了本书的范围,不过,如果用户所在的公司拥有业务分析人员以及Lean或Six Sigma专员,则这些流程会是非常有价值的资源。
在为一项新服务建模时,除了运用面向对象分析技巧外,请考虑上面这些事情。
提示: 此处只是简要涉及SOA建模这一主题。正如前面提到的那样,SOA的有关概念、服务建模、流程建模等等可以用一整本书来介绍。本章的目的是对部分核心概念进行基础介绍,如果用户对SOA建模感兴趣,可以参阅Michael Bell编写的“Service-Oriented Modeling:Service Analysis, Design, and Architecture”一书(Wiley出版社)。
服务文档
用企业视图记录选出的服务候选对象。对于启用服务的企业,为了直观化,请采用一种标准格式将这项工作作为一个反复过程来进行。该文档最后必须可以组合到更大的企业服务目录中。
要当心试图预先为整个业务建模。在用户完成这类工作之前,环境可能发生了改变。如果采用较模块化的方法来处理单个业务域、系统设置或问题,并使用一种迭代方法进行建模,将会是比较合适的。所建的各个模型最终将相遇于域或功能边界。
1.7 使服务可组合
问题
用户希望确保服务是按照一种可重复使用该服务进行组合的方式设计的。
解决方案
不要将服务的接口或实现与任何具体业务流程关联在一起,而是将业务流程的特定代码移到编排或新的流程服务中。
讨论
组合服务就是其他现有服务的集合。虽然用户可能会意识到通过不同应用程序调用同一服务来重复使用某一服务,但是,可组合服务是指按照如下方式定义的服务:其他服务的实现中,inclusion可以重复使用当前服务。
此处有两个问题:服务接口必须不能负面影响服务的可组合性;另外,服务的实现也必须不能无意中这样做。为了具有业务价值,服务必须执行某一业务用例所特有的任务。不过,用户需要考虑特定任务单元的执行(服务调用)和该操作所用工作流之间的差别,后者通过某一给定业务应用的形式,使服务对业务来说是可用和有价值的。
在软件中,可组合性一般概念不是完全不同于它的通俗使用。由于它们本身的设计,Lego砖形小片可以用来形成各种有趣的形状。Java Address类既可用于组合Employee对象,也可用于组合Company对象。同样,服务的可组合性也是设计过程中刚开始就必须考虑的内容。
试想一个软件公司,三个程序员为系统的不同特定方面编写代码,比如用户界面、数据库等,这些可粗略地比拟成成员服务。接下来是编程管理员,他们不在IDE中键入任何内容,但协调他们底下员工的工作,确保所有程序员的工作最后可以合并,来实现客户感兴趣的单个软件产品中的功能。各个程序员自身完成了重要工作,而管理员的加入提供了协调工作的业务接口,这可以作为一个流程服务。这个流程服务没有做任何独立工作:它的唯一功能就是指导其他服务的工作,并在这一过程中创造新的价值。
让我们来看一个更面向服务的例子,假设你所在的公司是一个零售公司,需要编写一项服务,销售点应用程序在检验过程中将调用该服务来对客户的信用进行预审。如果客户是值得信任的,售货员会优先考虑让该客户填写私有标识信用卡申请。公司的要求规定必须先查看客户数据库来确定该客户是否已经拥有信用卡,如果客户已经拥有信用卡,售货员就不会再向该客户发放信用卡。
假设已经存在一项名为Customer的查找服务,除了确定其他一些事情外,该服务确定客户是否拥有用户所在公司的私有标识卡,它可以被CreditCheck服务重新调用,而且可以组合到CreditCheck服务中。这假定的是Customer查找服务是为一般目的设计的,它包含所有相关数据。同样,用户希望执行基本CreditCheck的服务只进行信用检查,不进行其他操作,不希望该服务本身查看本地客户数据库,哪怕是通过重复使用现有的客户查找服务来这样做,这样做会减弱CreditCheck的可组合性。在面向对象的编程中,这种思想称为“高内聚”——一种设计限制,规定一个类的操作必须是紧密相关的。
客户查找和信用查看似乎是紧密相关,却是受到争议的。获取客户数据是一个简单的操作,除了需要知道客户的相关材料外,不需要其他外在业务情况。同样,执行信用检查这一操作完全没有指定需要首先查看本地客户数据库来确定客户是否已经拥有相应的卡。具体的IT项目必须履行这项要求,但它与信用查看操作无关。将这些想法合并就会犯类别错误,而且会限制SOA的可重用性和灵活性。
因此,现在有三个独立的想法,从而也就有三项独立的服务:
已经存在的Customer查找服务,它设计成免于任何特殊用例,从而可用于进一步的组合。
基本的信用查看服务,只通过姓名和地址查询信用中心并返回结果。
由业务分析人员描述的整个信用查看流程服务,除了协调成员服务操作之外,该流程服务本身不做任何其他事。
第三个组合服务的任务是按照实现业务目的的方式协调操作,也就是说,该工作流的任务是在浪费时间调用信用查看服务之前,知道用户需要查看客户是否已经拥有相应的卡。在这个工作流中,它是面向客户的服务,客户完全不知道在屏幕后方他们的结果是如何产生的。
这是非常有用的,因为它有助于将这一特殊用例伴有的业务要求放入该用例特有的一个客户视图中,从而使得成员服务保留它们的独立性。例如,用户可能需要以某种新的方式重复使用CreditCheck服务。业务可能需要查看潜在的新雇员的信用情况,而这些人员不在客户数据库里,是很难在服务级别进行客户查看的。在面向对象编程中,一个常见的目标是隔离可能发生改变的内容,此处也是一样。
概要是:业务流程服务必须在员工服务以外定义,其中的员工服务可以是其他流程服务、实体服务或功能服务。服务通常不是可组合的,但在设计服务时,用户应该记住可组合性是要追求的目标,至少要查看正在进行的设计是有助于还是有损于组合,哪怕当时没有紧急需求。
注意,运行时进行服务组合是特别困难的。重要的是确保一直符合SLA,当一组服务组合成各种基本服务,而这些基本服务也组合成不同等级的另一组服务时,这就变得有点棘手。
第9章介绍如何从技术上定义和创建编排。
1.8 支持SOA工作
问题
需要开发一组方针和原则来设计并实现面向服务结构,它不仅应该记录用户所做的决策,还应该用作组织中他人的教育工具。
解决方案
创建一种代表实际实现的参考架构。在内部,可以将这构建成包含一系列规划图、概念蓝图、指导原则、标准和惯例,它们将作为面向服务结构的基础。如果建立了卓越中心(Center of Excellence,CoE)或SOA团队(这是非常好的想法),他们将是这些资源的管理者,并可能出于教育目的与更大的开发团队共享这些资源。
用户可以将参考架构文档用作一种在线资源基础来帮助教育他人,这可以是内部网站、维客或门户网站,可通过它们访问结构蓝图和其他支持资源。
提示: 在2008年4月,OASIS发布了一篇名为“Reference Architecture for Service Oriented Architecture Version 1.0”的文章,用户可以通过访问http://docs.oasis-open.org/soa-rm/soa-ra/v1.0/soa-ra-pr-01.pdf来阅读相应的PDF文档。这篇文章是为企业架构师编写的,其中介绍了治理、社会结构、服务建模及所有权、安全性等,该文档非常抽象,但值得一读。用户将发现许多这类在线文档,为了内部使用,可能需要对它们进行筛选和置于上下文中研究。
讨论
就像统一软件过程中定义的那样,参考架构(RA)是一组预定义好的架构模式,是为特定业务和技术环境设计的并进行了相应的使用检验,同时还带有说明其使用的支持文档。可以通过已经存在的架构文档、标准和惯例来有机生成这些模式,但需要针对SOA对它们进行相应处理。
参考架构既可以用作单个项目的蓝图,也可以用作多个项目共有的蓝图。架构师和开发人员可以验证:后续的实现符合已建立的方针且会提高实现业务目标的可能性。
提示: 可以将参考架构类比拟成“美国宪法”。它用于建立各种抽象的机关,比如政府的三个分支机构,各个机构具有不同的责任,同时还带有一系列的可修订方针,来确保人们根据支持国家上层目标的原则进行行事。(当然,所有类推都具有局限性,并可能形成误导,用户实际面临的情况可能会有所不同)
参考架构的目标
参考架构可以通过各种方式有助于确保架构工作成功:
作为蓝图
参考架构可以捕获IT结构、代码和模型之间的交集。由于这些不同的方面是由不同的团队来实现的,因此,他们可能无意识地分散工作,使得企业无法利用潜在的现有工作资源。
该蓝图可以帮助解决方案架构师不仅从代码或系统角度审视SOA,而且可以采用更全面的视图来确保达到服务质量级别。
SOA参考架构可能会链接到其他现有的企业架构文档,例如网络和硬件IT结构,并将服务层思想添加到链接到的对象。例如,在Oracle/BEA中,它定义了包含以下服务层的SOA RA:
数据服务和连接服务
它们提供了到下层数据层和企业应用程序的灵活访问。
面向业务服务
它是数据服务之上的逻辑层,可以在其中管理业务流程。
表示服务
包装业务功能服务,为各种通道而改写。
虽然用户可能没有选择Oracle/BEA功能清单(或许未利用数据和连接服务,并可能连表示服务也抛在一边,或者添加了自己定义的另一个服务层),但重要的是要将服务在该级别进行概念化,而不仅仅是停留在实体服务、流程服务或功能服务级别,因为它们是补充的类别。
提升最佳实践
RA可以声明服务和客户端程序必须按照某些原则进行创建和使用。可能在有些情况下,用户需要一贯实现一些设计模式;用户也可能希望要求用一组合意的方法来进行服务创建、消息传递、编码等。
通过声明原则、标准和惯例,RA有助于确保整个服务实现过程的一致性。如果实际公用文档清楚给出了有关原则,架构师在执行这些原则时就会比较轻松。一旦每个人都习惯按照一种给定方式来一贯实现服务,将意味着产品能更快上市。开发人员也就能以所重用的服务将会很好地配合在一起这种自信姿态创建新组件。
RA应该解决如何处理横切关注点,例如安全性和规则引擎,它们本身有时是作为服务实现的。
明晓平衡
我们不可能兼顾各个方面,有些要求通常相互冲突,我们总是需要在完美性能与高安全性之间进行协调。
激活治理
SOA治理高度依赖于RA的创建,RA用作SOA卓越中心或治理委员会遵守的类别章程。
参考架构的网站
SOA参考架构可以是一组实际的可交付资源,这些资源可以用于各种目的,而不是仅仅作为代码的一种附属文档。作为一种独立的可交付对象,它非常适合于以某种易于更新的网络方式实现,例如维客。SOA团队成员、治理委员会成员或企业中参与的架构师可以创建一个站点,将架构模型、标准和惯例文档以及与宣传和教育有关的资源发布在其中,该站点或许可以作为一个入口来让人们了解运行时工具。
提示: RA网站的大小和形式将由开发团队的大小、体制和分布决定。
作为一种建议,我们认为下面介绍的条目在参考架构中非常重要。
惯例。惯例使通信更清晰,刚开始时,它们取决于创建者的主观判断,但是,一经建立,就需要遵守这些惯例来更容易地查找和理解事物。用户可以考虑为下面这些惯例创建相应的原则:
文档名称,包括模式、WSDL和捆绑的自定义文件
服务实现项目的文件位置和实际项目结构
标准。标准比原则更严格,更少地依赖于主观判断。它们具有一定的功能性目的,因此,如果不遵循标准,性能或功能可能会受到影响。
提示:如果用户对WSDL或Schema设计模式不熟悉,可以参阅第2章。
它可以用于标准化以下内容:
允许使用绑定的自定义文件,例如针对JAXB或JAX-WS的(参阅第3章)
无论是否允许自定义,进行内联或外联(参阅第3章)
导入WSDL绑定文件与内联定义这些文件
导入WSDL模式类型与内联定义这些类型
默认协议选项以及可以接受其他协议时
选择何种“开始”方法(参阅第2章)
适当地使用模式设计式样(参阅第2章)
使用接口
使用最优化方法,例如FastInfoset和MTOM/XOP
使用头
使用附件
如果使用SOAP,可以规定可接受的样式、用途和参数式样值,例如document/literal wrapped
使用单向方法
使用异步客户端应用程序
为二进制数据编码
集中化的模式
这些原则理论上应该包含各个条目的正确用例和错误用例。
根据用户的实现选项、系统成熟度和环境,还有许多条目要考虑。尽管本书的核心放在Java上,但是,可能需要确定在RPG、.NET或用户自己所用环境中创建服务的有关标准。该清单给出了服务实现和SOA的一些相应原则,不遵守这些规则会导致互用性问题和严重的性能下降,而且会浪费开发时间。
提示: 要不断更新标准,在制定原则时,一定要让团队成员都参加,开放地接受他们的反馈,并要适当地综合他们的建议。创建新的条目,去掉过时或障碍性的条目,并根据需要进行更新,确保原则是时新的、恰当的和有用的。
规范。在SOA和基于Java的Web服务领域,已经有几十种规范能够快速起作用。采用这些规范可以有助于确保每个人快速为问题找到确定的答案,而且有助于暗中引导人们认识到要想在SOA环境中多产出,必须理解哪些内容。下面是相关的Java规范:
JAX-WS
JAXB
SAAJ
JAXR
Web服务
WS注释
DOM,SAX,StAX
以上这些仅是Java特有的规范。以下是一些重要的SOAP和WS-*规范(后面将详细介绍其中的部分规范):
XML Schema
XSLT
SOAP
WSDL
OASIS UDDI
WS-Policy
BPEL
WS-Security
WS-Trust
WS-ReliableMessaging
WS-Transaction
WS-MetadataExchange
集中的书签。创建一个维客,鼓励开发人员公开标记网络资源,以便于每个人查找和使用这些资源。可以邀请其他人参与。
行业标准。除了要使用的各种规范,还要包含一些链接,指向了解后每个人都会受益的一组行业标准。这些标准包括:
Basic Profile 1.1
OASIS XML Catalog
W3C Web服务架构
OASIS Web服务实现方法
此处应该指定要遵守哪些BP 1.1原则,或者声明对这些原则的立场。用户可以根据认可的行业最佳实践,为自己的企业定义一组最佳实践,并将它们合并到自己的设计中。有些最佳实践可以从诸如WS-I Basic Profile之类的标准提炼得来,例如,BP 1.1要求WSDL使用目标命名空间。可以利用这个机会明确地声明这类惯例,使得企业中的其他开发人员能够很快遵守它们。
安全性。让网站的一部分致力于维护安全性。在Web服务领域,这是一个庞大且复杂的话题,已经有许多标准用于实现这个目标。不同实现阶段的供应商都遵守这些标准,为自己所在的团队挑选合适的标准将是非常重要的。此外,这些原则对于确保互操作性也是很重要的,可以指定如下条目:
使用特定的密码套件
使用数字签名
传输层安全
认证方法
随着SOA的扩大,需要针对各项服务提供角色合计清单,因为这将有助于管理流程编排。
实用示例。根据团队的经验情况,用户可以包含一些实用的代码示例,来说明如何创建服务和附带的组件。
提示: 考虑在制定试验项目的同时建立架构。在没有实际看见工具是如何协同工作和标准是如何实现的情况下,完全预先开发某种架构会对最终期限以及最终产品带来负面影响,特别是用户处于学习阶段,例如试验期间。制定一个一般的计划,并知道有可能无法总是按照该计划来执行,进行一部分工作,然后修订计划。架构也可以是灵活的,没必要一开始就是完美无缺的,能够一直指导人们按照正确的方向工作,也可以像Frederick Brooks所写的那样:“让用户进行修订”。
词汇。考虑创建重要术语清单并为其给出用户将要依照的一致定义,使得自己的团队对这些术语及其定义都很清楚。在SOA中,有些概念是有争议的、含糊的或过分宣传的,这使得很难知道每个人实际是在谈论同一事物。使用由术语(例如服务、合同、治理等)组成的词汇可以有助于进行有意义的沟通。
文档。此处用户可以链接到JavaDoc、项目文档、供应商文档、要求、用例文档、模型分析文档和其他类似条目。
如果用户具有IT规划图、业务规划图或对构建SOA有用的其他企业架构文档,也可以在此处给出它们的链接。
人为结果。如果结构允许,用户可以考虑在SOA中提供有关运行时人为结果的视图,可以包含的内容有集中化模式、通过建模工具得到的业务流程建模文档、BPEL编排的可视表示以及其他条目。让相应的使用者能够了解监控工具,从而使他们拥有必要的信息为新的服务候选对象、SLA、新的组合或结构改变做出好的决策。
新信息。用户可以考虑建立一种传播机制,将各种与SOA有关的网站汇总到用户自己的参考架构门户网站,这有助于让每个人紧跟行业潮流,而且使团队能够很好地彼此告知。
小结
尽管本节介绍的有些条目似乎有点过度,但要记住参考架构的作用是多方面的:它可以用于指导希望在企业中使用Web服务的新手,也可以帮助有经验的开发人员找到和使用企业已经编写或认可的Web服务,还可以帮助治理委员会或卓越中心就有关服务候选对象以及未来结构方向方面做出决策。
1.9 选择试验项目
问题
用户现在已经准备好开始进行SOA有关工作,因此需要选择第一个项目,但不清楚如何进行选择。
解决方案
根据下面的标准来衡量某个特定项目是否可以作为试验项目:
该项目是否创造业务价值?
该项目是否具有有限的范围?
该项目是否使用某项好的服务?
团队成员是否对该项目解决的问题有很好的理解?
该项目是否有用但并不是关键任务?
讨论
考虑以上这些标准应该会帮助用户就哪个项目可以用作第一个项目做出好的决策。在本节中,我们将进一步说明各条标准。
该项目是否创造业务价值?
SOA的许多方面都是有关IT和业务的相结合问题,因此,这两者之间没有明确的界线,其形成方式将十分取决于用户所在的企业及其文化。不过,SOA需要相当多的支持,它代表了一种对系统进行思考的新方式,而且需要学习非常多的API。尽管有一些免费的开源工具可以用作应用服务器、企业服务总线、业务流程执行等,许多企业还是会购买昂贵的软件。这一过程会占用企业大量的时间和投入,企业必须在培养团队并将其扩展到更大的IT小组方面做出投资,这意味着所花的实际时间和金钱最初不会带来新的产出。它通常被看作是结构的隐没成本,企业希望通过长期得到回报。
基于这些原因,争取这项业务是很重要的,否则,对SOA团队所做的投资就不会有收益,争取业务的方法就是给出要投入的金钱。选择某个具有一定程度快速回报的项目,从而使企业维持支撑。清楚说明SOA的实际回报是通过若干年实现的,这样一来,IT可能更高效地解决变化的业务需求。
该项目是否具有有限的范围?
项目应该具有有限的范围,它应该代表有点独立于未知系统的功能,不过,如果项目跨越多个系统,用户将从试验项目获得更多的东西。它不能仅仅是有关Web服务概念的试验,而必须是成熟的实际项目,不然,用户就会低估可伸缩性要求和整体复杂性。
提示: 有些从业者建议从基本的服务集开始,后来再引入ESB和编排服务。在我看来,用户需要选择某个具有有限业务范围的项目,从而一开始就有时间并能清晰地同时引入ESB。用户不希望对解决方案进行超工程操作,但是,如果计划将自己的工作与ESB在某点进行结合,就没有必要再次访问ESB。而且,由于ESB不是标准化的,因此,产品会提供很不相同的功能或以不同的方式提供同一功能,用户需要及早了解架构的重要部分。
对开发人员来说,引入SOA会带来许多新的API、新的应用服务器、新的语言以及新的结构,它代表的是一种新的开发方式。面向服务设计不同于面向对象设计,在功能或过程编程与面向对象编程之间进行转换会花费时间,而且对开发人员来说,是一种巨大的改变。我本人见过超过15,000行代码的Java类,单个方法就有700或800行长,仅因为是在Java中编写代码,并不意味着编译和运行的程序是面向对象的,不会出现不可思议的成功。也不要低估服务给OO开发人员带来的不同,用户需要花时间培养团队的每个人(虽然不必是同时),这会占用开发时间,如果项目的范围非常大,加上有这么多需要外部考虑的事项,就可能会无法完成项目。
用户希望能够将未知的部分独立出来,从而可以在项目的开发和质量保证阶段更轻松地识别问题。选择使用其他暂新产品的某种产品时,要小心。在使用新的结构和API时,团队就已经需要处理许多神秘事物。将所做改动独立开来,从而使SOA启用工作成为主要焦点。这时咨询相关人士将会比较有帮助。
需要考虑初始服务的目标客户。通过选择业务的内部客户,而不是直接面向客户,将会降低风险。这使得用户有时间在外部世界卷入之前构建一个小的结构,外部世界的介入会导致无法预料的新问题。内部使用的服务将有助于用户关注正确实现初始服务,并会一段时间保护用户免受更高级、更复杂问题(比如联合安全)的困扰。内部服务还可能提供了一种机会,让用户更有信心地测量流量。
该项目是否使用某项好的服务?
这个问题是显而易见的;可以考虑功能性是否实际满足1.2小节中给出的标准。选择某个试验项目时,如果提议的项目没有实际使用某项好的服务,要注意不要有外部压力。这种情况是存在的——SOA团队成立,老板坚持必须进行某个试验项目,而不管它实际上是不是一个有用的服务候选对象。刚开始编写服务时,会给项目增加很大的复杂性和开销,当然希望长期收益会超过这些成本。如果服务不再被重新使用,其他平台也不调用该服务,则说明当前不是非常好的服务候选对象,这意味着用户可能无法实现高的ROI。
要确保所选服务能让自己有机会探究整个开发生命周期。例如,如果计划使用“从模式开始”方法,需要在试验项目中正面解决这个问题,因为它使工作围绕模式管理人展开,会修改构建流程等。
企业的一个总体SOA目标可能是按照某种中性结构方向迁移工作,存在这种情况的公司通常是几十年的传统数据都依赖于某个特定平台。找到封装了某些重要实体表示的数据服务将会有助于减少ETL或数据迁移操作,ETL是数据抽取(Extract)、转换(Transform)和加载(Load)的简写。
团队成员是否对问题领域有很好的理解?
如果给定的问题领域存在相应的主题专家,用户就会降低很大的风险。在引入服务和SOA的同时,引入的新系统、新软件、新流程和新问题领域会形成一种危险的调和物。如果用户和供应商或顾问一起工作,要确保他们对自己要解决的纵向业务领域(金融、零售等)具有一定背景。
该项目是否有用但并不是关键任务?
尽管在创建SOA过程中及早认识到业务价值是很重要的,但是注意不要在初始阶段过分关注这一点。SOA试验项目通常会无法满足最终期限或无法实现某些要求,也有可能这两者都无法满足。选择的项目应该是:即使需要花费更多的努力来启动该项目并将其作为一项服务来运行,也不会影响业务。
这使得在现有难点处启用服务成为一个吸引人的试验项目。已经存在相应的流程或功能,我们回过头来说说它是否应该是必不可少的。显然,我本人不建议用户刚开始就打算失败,但任何事都有可能发生,提前认识到这一点比较好。
提示: 实际上服务是很难的,它们不仅在概念上很难,而且实际实现也很难。为了使服务可移植、可重用、可组合和可互操作,环境的复杂性就会大大增加。这对企业和IT来说,都很难接受。
可以对架构框架、方法和设计模式做一些研究,然后选择最适合于给定环境的某种项目。
第二个试验项目
一旦完成最初的试验项目后,第二个项目就变得非常重要,该项目或许应该被看作是试验扩展。第二个试验项目的主要目标是建立SOA基础,了解技术牵连,使用新约束和新策略创建有用的内容。
第二个项目提供了一个重要的机会来扩建SOA促成的一些组织方面和横切关注点,例如治理(参见1.10小节)。基于这个原因,第二个试验项目需要与第一个试验项目具有类似的有限业务范围,不用对第二个SOA项目重新编写支付结构。用户需要时间学习新工具的行为模式,还需要时间训练和日常运行要嵌入的产品服务环境。
还可以使用第二个项目来构建初始架构。可以添加功能服务或元服务,例如规则服务或安全服务。这一阶段可以引入BAM工具,也可以通过WS-Policy或一般的安全装置深化工作。可以阐明和改进服务版本的处理方式,而这不是该试验项目的核心焦点。可以利用一些更强大的编排功能,在刚开始试验时,用户可能没有时间或者没有需求要使用这些功能。
SOA规划图中应该已经安排了这些事情。该规划图会改变,需要不断进行更新,而且要记住SOA是一项长期投资。计划要宏伟一点,但要从点滴做起。
1.10 建立治理机制
问题
需要一种可靠的组织机制来传递知识、实施标准、提供有关生长和改变的结构,以及大体上提供面向服务架构有关的战略指导。
解决方案
建立卓越中心,有时称为“能力中心”或“SOA治理委员会”,由企业的股东和机构的技术人才组成,其工作是向企业提供有关支持。
讨论
用户可能已经根据公司的成熟度和规模制定了适当的IT治理标准,SOA治理是对IT治理的扩展,专门针对特定的SOA需求。由于SOA代表一种战略倡导,一系列的工具和技术只是其中的一部分,因此,用户需要通过一种方法将战略逐步引入企业。
提示: 本书不解决SOA众多的组织和商务方面,虽然它们是SOA非常实质和重要的组成部分,但已经有许多不错的书籍从这个角度阐释了SOA,本书主要是解决技术方面的问题。不过,作为开发人员/架构师,清楚知道SOA不仅仅是Web服务的集合是非常重要的。
SOA需要一些新的流程,企业必须清楚如何设计基于SOA的解决方案、筛选服务候选对象以及协调新的开发周期来支持正在形成的SOA。
可以将解决方案架构师、系统架构师以及企业股东包含在卓越中心里,这有助于确保IT与企业同步,也确保IT可以访问当前紧急事物和未来计划。
提示: 让治理团队有所倾向。只要SOA治理团队具有充分资源来了解企业的工作并能实施和发展参考架构,就应该够了。对它进行合理发展,用户应该不希望SOA因委员会而夭折吧。
可以指定给这类治理人员的责任有:
建立业务用例
这可以在SOA使用过程的早期由企业架构师和其他人员执行。SOA的业务用例包含设想、SOA将实现的目标以及用于资助该项倡导的可能计划。
描绘角色
SOA将在IT中进入新角色,需要对这些角色进行清楚描述。用户需要相应人员来负责执行治理工作、做出架构决策、建立参考架构、审查服务、处理服务候选对象以及建立和发展新的结构,例如ESB。用户还可以决定在服务开展后,由哪些人负责支持和维护服务,还需要确保知识传递计划说明了这一点。
帮助设定规划图
治理团队具有特殊的视野,这使得他们成为帮助企业架构师或技术执行者设定IT规划图的关键分子。设定的规划图可以将定义的各项事物规划到较大的目标,还可以将这些目标规划到企业设想的一些更大方面。
维护参考架构
CoE还可以根据其视野来维护参考架构,这会有助于构建流程模型并将这些模型结合到进一步的工作中。
促进设计开发
该环境中的架构师充当开发人员向导,实施参考架构并提供相应支持,他们可以选择工具、框架和结构应用程序,并采用和修改有关方法来支持面向服务事业。
促进运行时服务
SOA治理问题涉及到开发人员和服务,应该采用适当的运行时监控工具来确保符合SLA。治理团队需要跟踪这方面并为未来策划,这样就能很快地实现各项需求。
推动服务生命周期
治理团队可以知晓何时需要对服务进行版本控制并评估版本控制的效果。与传统开发相比,SOA中的版本控制是一个更为复杂的问题,这是由它的分布式特性决定的。开发某个给定服务的人员或团队无法清楚了解当前组合该服务的进一步流程,治理团队需要知晓这一点并为其做准备,指导进行合适的实现。
进行知识传递
CoE可以支持知识传递工作。有关SOA以及Web服务API和结构的学习曲线是值得考虑的。即使团队的一些成员当前没有在构建服务,为了作为后备人员,他们也应该知晓有关语言并熟悉相应的概念。这些开发人员最后可以转向创建和使用服务,他们需要为理解和执行参考架构中制定的原则做准备。
帮助避免的常见隐患
如果不采用适当的治理就进行SOA尝试,企业会面临各种困扰,包括SOA的全盘失败。有一些常见的、可以避免的隐患。
无法治理不知道的内容,因此,为了确保所有与服务有关的代码和文档都是可见的,应该建立一个中心存储库,给出每项服务所有版本的记录文档。这会有助于在设计和运行时过程中跟踪这些重要文档。
服务冗余。一种常见隐患是服务冗余或重叠,这在大企业或分散的企业中尤其普遍。企业中的不同小组重复创建相同或类似的服务,这样会费用浩大,也是浪费的,而且会令人困惑,结果用户会发现维护成本急剧增加。CoE应该预防这种情况的发生,方法是监控要开发的内容,鼓励实施团队相互协作,并建立和使用集中的存储库来发现服务。
各式各样的人为结果。要实际确保采用适当的治理来管理SOA引入的众多新型人为结果,例如WSDL、模式、SCA配置、策略等。如果留给设备来处理,开发人员会将这类文档像其他内容一样看作就是代码的一部分,并将它们直接放入项目文件。人为结果将会在企业中满天飞,版本控制和客户依赖管理会很快失去控制。
DBA(缩写词致死)虽然缩写词致死是IT中的一种综合病症,也不是SOA所特有,但SOA从业人员却是最容易因其而受伤。缩写词致死就是错误地相信某人理解了缩写词所代表的含义。由于缩写词代表的是一些非常复杂的概念,它们简短精悍,使得我们只彼此断言相关概念的最直接、最表面和最常规含义。结果导致通信不畅,损伤了协同工作。最不好的一点是,那些指望获得某个新术语市场份额的公司会列出一组仅与该术语沾边的产品。SOA易受这类问题的影响,
学习是唯一已知的解决方法。对于SOA来说,这意味着不轻信任何单一资源,反复核对各种参考资料。
恢复统调
这是事实。要当心开发人员和架构师为了提升他们自己的经历而向SOA中加入无关紧要的功能或超工程解决方案,要识别这类问题,可以检查他们是否对已经解决的问题不断地给出新答案。
可以通过治理来处理该问题,方法是将所有与服务有关的开发工作透明化并管理解决方案实施。
兔子服务
院子里有一只兔子是令人愉快的,两只兔子更好,能看见它们到处相互打闹。许多兔子就会有问题,只是因为某件事情是好事,并不意味着更多的同类事情就一定更好。正在烘培的蛋糕可能需要一茶匙盐,加入一大汤匙就会让蛋糕成为废品。
用户擅长构建服务后,将会试图构建众多的服务。请回顾自己的规划,并不是任何内容都应该构建成服务。容许服务像兔子繁殖一样在企业中增生,同时要预料到性能、维护和管理问题。请记住这句老话:只是因为你能够那样做,并不意味着你应该那样做。
本章小结
在本章中,我们给出了一些核心术语的实用定义,例如面向服务架构和服务本身。我们是从非常一般的层面开始的,主要解决一些组织问题,比如如何建立参考架构和选择试验项目。此外,还清楚地介绍了用于确定有价值服务候选对象的特定标准,以及用来在组织中支持SOA目标的待建特定原则。
尽管我们将解决其他设计注意事项,但本书其余章节的大部分内容是非常实用的,采用编码解决方案解决编码问题,而且,编码问题具有多种解决方案。不过,对于本章中的主题,有许多其他的合理观点,许多人可能不同意我做出的某些结论。
市面上有好多不错的书籍进一步介绍了这些主题以及其他一些内容,如果用户对高级服务设计和组织注意事项感兴趣,可以阅读Thomas Erl编著的“SOA:Principles of Service Design”(Prentice Hall出版社)和Nicolai M. Josuttis编著的“SOA in Practice”(O扲eilly出版社)(http://www.oreilly.com/catalog/9780596529550)。
第2章
XML Schema和SOA数据模型
2.1 概述
XML是SOA的混合语言,它适用的对象包括消息有效载荷、应用配置、部署、发现和运行时策略,而且越来越多地用于描绘一些可执行语言,比如SOA规划中核心的BPEL。Web服务接口也是用XML描绘的,采用的形式是WSDL,XML用作SOA中的主要数据传输机制。由于XML与Web服务开发联系紧密,再加上其内在的灵活性和表现力使XML具有强大的功能,它自然非常适用于设计SOA中使用的数据模型。
通过使用XML设计数据模型,你可以建立一个独立于实施的有价值基础。XML使你可以更关注实体(确定哪些对象是存在的),较少受限于供应商约束。由于库变得更加完善且更易于使用,作为开始处理SOA数据模型的一种工具,XML比以前更有吸引力。
权威性评论者和类似的能手总是指出,我们使用新的通信方法——无论是手机、短信、IM、电子邮件,还是PowerPoint——不是仅仅改变通信方式,而是不可避免地需要根本性增加或重排通信的内容。1967年,Marshall McLuhan将这总结为“媒介是消息”,意思是消息的形式将包含消息本身的表面内容。将XML用作SOA基础层时,思考方式上会出现细微(也可能是深远)变化,作为架构师,知晓这一点非常重要。当然,这种变化的程度通常完全取决于你所在组织的环境。
XML的一个优点——实际上是它的主要目标——是灵活。世界上的许多IT商店发现自己陷入维护众多的应用程序枯井,每一个都涉及到某种严格的数据模型,而这个数据模型需要包含各种新的更新或转出。许多这类商店目前指望通过SOA来寻求答案。在SOA中使用XML进行数据交换有助于形成更模式化的方法,建立与环境有关的数据视图,这些视图可以在各种服务和层之间有目的的流动、转换以及相互影响。
本章不是要介绍XML,也不打算覆盖XML、Schema和数据绑定方面的内容。我本人假定读者不再对这方面的内容感到困惑,已经基本了解XML和Schema,知道什么是数据绑定,不然就不会阅读这本书。如果不是这样,当今有许多这方面的书籍和在线教程供读者学习。
我们将通过SOA的特定角度介绍XML Schema,它要求一定的公开性和灵活性,简而言之,这意味着让事物适当简单。现在,如果读者特别希望编写超级复杂的多形态Schema,这是读者的自由,但我不推荐这样做。保持简单将会最大化互操作性。Schema越复杂,你就越容易养成依赖于供应商或工具的习惯,这对于SOA程序员/架构师来说不是什么好事。不过,保持简单对面向对象程序员来说,似乎是违反直觉的。例如,Martin Fowler编写了有关“贫血模型”反模式方面的内容,这种反模式对象只包含数据,不包含行为,从而就不具有使用面向对象语言时的一项主要优势。对来自面向对象世界的用户来说,第一个冲动可能是试图使用Schema来捕获Java模型中可以捕获的一切内容,而且,还有工具帮助用户这样做,例如,可以找到相应插件让JAXB生成hashcode和equals实现,在默认情况下是不会完成这些实现的。这刚一听起来实在不错,而且对设计面向对象的系统来说是至关重要的。从直观的Java角度来说,你希望内容被完全封装,会考虑用serialVersionUID等说明对象。但在SOA范畴,这些都没什么用处,因为它们不是常规内容。
XML Schema非常强大,但不是面向对象的,用于捕获数据模型而不是对象模型。Schema真的允许设计者创建看起来非常像对象的模型;例如,JAXB使得你可以生成Java 5枚举类型,而且可以通过Schema实现一定程度的多态。不过,最终的要点是服务必须足够常规,以至于可以进行多语言通信。因此,我建议你采用保守一点的方法。
让接口保持简单将有助于确保以后不会陷入困境,它会鼓励你按照合适的粒度级别来考虑服务设计,不过,从长远来看,不同的设计Schema对你的影响不一样,可以是正面的,也可以是负面的,我们将在本章中介绍这些。
尽管告诫要保持简单,但是,在没有扎实理解命名空间和Schema的情况下,要阅读和调试一系列调用多个WSDL的BPEL编排,而WSDL又导入多个Schema,这时是非常困难的。XML已被我们熟知10多年了,它的普遍存在掩饰了其简单外表下潜藏的实际复杂性。
本章将向你介绍处理XML Schema时使用的实际工具,不过,由于Schema允许你采用各种方式来表示同一数据结构,本章中的部分小节还关注了一些设计选项,这些设计选项不仅有助于确保数据模型是按照呈现所需实例的方式构建的,而且有助于确保SOA从长远来看是灵活的。
2.2 为SOA设计Schema
问题
需要借助XML Schema定义SOA中使用的数据类型,但Schema太灵活,你不知道如何实现,希望采用Schema中最适合SOA环境的模式和最佳实践。
解决方案
按照下面讨论的某种Schema设计模式:Russian Doll、Salami Slice或Venetian Blind。
通常来说,创建XML Schema时,可以采用几种公认的设计模式。XML Schema具有内在的灵活性,这意味着:要按照一种同时实现表现力强、灵活性高、输入完美的一贯清晰方式开始编写Schema,可能会比较难。如果目标是按照一种既允许生成Java代码又可作为原始XML使用的方式来为SOA设计Schema,情况就更困难。
Schema定义了一些基本构建块来说明实体:简单类型、复杂类型、元素和属性。除了这些以外,还有许多有关全局类型、局部类型、命名空间限定等的选项供选择。不根据情况而胡乱做出选择会大大损伤SOA,无意当中限制了它的灵活性。如果不进行仔细的Schema设计,很难为不同领域使用编排和中间ESB来组合生成松散耦合服务,只会猛然发现由于Schema设计中的错误选择,自己SOA中的服务实际上一开始就是非常紧的耦合。这时,一点简单的Schema变化就会导致重新部署所有的服务组合。
不过,XML是SOA的核心,你希望利用Java的强大功能,同时又要不失XML带来的灵活性。可以两者兼得,然而就需要考虑Schema设计选项门类。在本节中,我们将介绍用来构建Schema的三种知名设计模式:Russian Doll、Salami Slice和Venetian Blind。人们有时也采用另外两种模式:Garden of Eden和Chameleon,由于它们会带来较大的麻烦,我们会在后面的小节中对其进行讨论。
模式通常是按照一种规则进行区分的:元素和类型是否是全局定义的。全局元素或类型是指作为Schema节点子对象的元素或类型,而局部元素或类型是指嵌入在其他元素或类型中的元素或类型,局部元素不能在其他地方重复使用。
现在我们来介绍这些模式。
Russian Doll
在现实中,俄罗斯玩偶具有一层木制外壳,这层外壳内包含多个其他外观一致的玩偶,每个第二层玩偶外壳又包含更小一层的玩偶,打开最外层玩偶的盖子就可以看见下一层玩偶。
该设计模式正如其名称所示的那样,因此很容易记住。它用于创建单个全局根元素,像同名的俄罗斯玩偶一样,该元素包含所有其下分支的类型,除这个全局元素外,所有其他元素都是局部元素。所有的元素声明都嵌入在这个根元素中,因此,在这种环境中,这些声明只能被使用一次。根元素是唯一可以使用全局命名空间的元素。
Russian Doll具有如下特点:
具有单个全局根元素。
所有类型都是局部类型,即嵌入在根元素中。
只支持用单个文件完整设计的Schema。
它具有高内聚低耦合。
由于类型被隐藏,Schema是完全封装的。
它是最易于阅读和编写的模式。
如果你需要重复使用类型,就不要使用Russian Doll。
示例2-1演示了Russian Doll设计模式。
示例 2-1:使用了Russian Doll Schema设计模式的Book.xsd文件
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/russiandoll"
xmlns:tns="http://ns.soacookbook.com/russiandoll"
elementFormDefault="unqualified">
<xsd:annotation>
<xsd:documentation>
Book Schema as Russian Doll design.
</xsd:documentation>
</xsd:annotation>
<xsd:element name="book">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="price" type="xsd:decimal"/>
<xsd:element name="category" type="xsd:NCName"/>
<xsd:choice>
<xsd:element name="author" type="xsd:string"/>
<xsd:element name="authors">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="author"
type="xsd:string"
maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:Schema>
正如你看到的那样,这个Schema定义了单个全局元素“book”,该元素中嵌入了创建一个book元素所需的全部类型。不能引用元素和类型,命名空间被局部化,“authors”元素重新定义了子“author”元素,这会存在潜在的维护问题。
Russian Doll具有一些优点,对于简单的Schema来说,它易于阅读和编写,因为没有使用混合、引用,也没有追踪类型定义;该设计没有引入灵活性,这使得其结果可以预期;很容易理解作者的意图,可以确切知道生成的文档实例将是什么样子。
由于它是完全自包含的,按照这种模式设计的Schema是独立于其他Schema的,改变类型将不会影响其他Schema。
对于纯粹XML工作来说,Russian Doll的明显缺点是精心定义的类型无法在别处重复使用,如果是非常大的Schema,它会变得很笨重。
包装来自某种相当静态资源的遗留数据时,比如,用来存储隔离记录的中端上的已修改DB2文件系统表,Russian Doll或许是可以使用的合适设计。在主要的数据管理工作中,借助生成工具可以快速表现这类定义。
Salami Slice
Salami Slice代表的是与Russian Doll相对的另一种设计。
通过使用这种模式,你可以将所有的元素声明为全局元素,但将所有的类型声明为局部类型,所有的元素都放入全局命名空间,这使得其他Schema可以重复使用当前Schema。每个元素就好比是“切片”这种单个定义,它可以和其他元素组合。
借助Salami Slice,你可以拥有许多组件,这些组件都是分开定义的,接着是在全局元素下合并在一起。Russian Doll生成的设计相当严格,十分固定,向你给出了所定义的单个元素的明确说明,而Salami Slice是完全开放的,允许各种可能的组合。实际俄罗斯玩偶的物理结构只允许按照一种方式将玩偶组合起来,而意大利腊肠切片没有指示该如何将它们放在三明治中。
Salami Slice具有如下特点:
所有元素都是全局元素。
所有元素都在全局命名空间中定义。
所有类型都是局部类型。
元素声明从不嵌套。
元素声明可以重复使用。Salami Slice使你最有可能重复使用所有Schema设计模式。
很难确定目标根元素,因为存在许多潜在选项。
示例2-2给出了重新用Salami Slice设计后的book Schema。
示例 2-2:使用了Salami Slice Schema设计模式的Book.xsd文件
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/salami"
xmlns:tns="http://ns.soacookbook.com/salami"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation>
Book Schema as Salami Slice design.
</xsd:documentation>
</xsd:annotation>
<xsd:element name="book">
<xsd:complexType>
<xsd:sequence>
<xsd:element ref="tns:title" />
<xsd:element ref="tns:author" />
<xsd:element ref="tns:category" />
<xsd:element ref="tns:price" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="title"/>
<xsd:element name="price"/>
<xsd:element name="category"/>
<xsd:element name="author"/>
</xsd:Schema>
这种模式的优点是由于元素是全局声明的,生成的Schema可以被重复使用;不过,改变某个元素会影响组合的元素,因此Salami Slice Schema被认为具有紧密耦合。
采用这种模式的Schema会比较长,事物都被透明排列,而且采用平面方式。
还要注意,对服务来说,Schema的重复使用通常意味着紧密耦合。有关这个问题的更多说明,请参阅本小节结尾处的讨论。
Russian Doll和Salami Slice是两种完全相对的设计模式,由于它们要么只关注严格性,要么只关注灵活性,因而具有明显的优缺点。下一种模式,Venetian Blind比较折中,结合了这两者的优点。
Venetian Blind
Venetian Blind是对Russian Doll的扩展,它只包含单个全局根元素,与Russian Doll不同的是,它允许重复使用所有类型以及全局根元素。
通过使用Venetian Blind,你可以定义单个用于实例化的全局根元素,并将它和外部定义的类型组合起来,这样就具有最大化重复使用所带来的好处。
Venetian Blind具有如下特点:
具有单个全局根元素。
混合有全局和局部声明。这与Russian Doll和Salami Slice形成对比;Russian Doll中的所有类型都是局部的,Salami Slice中的所有类型都是全局的。
既具有高内聚,又具有高耦合。由于其组件是耦合的,不是自包含的,它可以不时地和其他Schema耦合。
它最大化了重复使用,所有类型和根元素都可以重新组合。
由于类型是可见的,因此封装是有限的。
允许你使用多个文件来定义Schema。
比较冗长。将每个类型拆分,使你可以对各个单个方面或元素进行非常有选择性的粒度控制,不过这会使得键入的内容非常多。
在示例2-3中,有5个可重复使用的类型:TitleType、AuthorType、CategoryType、PriceType和book元素本身。
示例 2-3:使用了Venetian Blind设计模式的Book Schema
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/venetianblind"
xmlns:tns="http://ns.soacookbook.com/venetianblind"
elementFormDefault="unqualified"
attributeFormDefault="unqualified">
<xsd:annotation>
<xsd:documentation>
Book Schema as Venetian Blind design.
</xsd:documentation>
</xsd:annotation>
<!-- Single global root element exposed -->
<xsd:element name="book" type="tns:BookType" />
<!-- The root is given a type that is defined here,
using all externally defined elements.-->
<xsd:complexType name="BookType">
<xsd:sequence>
<xsd:element name="title" type="tns:TitleType"/>
<xsd:element name="author" type="tns:AuthorType"/>
<xsd:element name="category" type="tns:CategoryType"/>
<xsd:element name="price" type="tns:PriceType" />
</xsd:sequence>
</xsd:complexType>
<!-- Each type used by the global root is defined below,
and are potentially available for reuse depending on
the value of the 'elementFormDefault' switch
(use 'qualified' to expose, 'unqualified' to hide) -->
<xsd:simpleType name="TitleType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="AuthorType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CategoryType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="LITERATURE"/>
<xsd:enumeration value="PHILOSOPHY"/>
<xsd:enumeration value="PROGRAMMING"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PriceType">
<xsd:restriction base="xsd:float" />
</xsd:simpleType>
</xsd:Schema>
如果需要最大化重复使用和灵活性以及要让命名空间可见,可以选择Venetian Blind。
参见
2.3小节。
Garden of Eden
Garden of Eden Schema设计模式由Sun Microsystems确立,它是Salami Slice和Venetian Blind的组合。要使Schema采用这种模式,可以在全局命名空间中定义所有的元素和类型,然后根据需要引用相应元素。
在所有的Schema设计模式中,Garden of Eden让你可以最大限度地实现重复使用,你可以自由地重复使用所有元素和类型。在这种Schema中,元素没有封装,可能会存在许多根元素,因为元素都是全局的,采用这种模式的文档阅读起来要困难一些。在Russian Doll中,根元素是明确的,而在Garden of Eden中则不同,作者的意图被掩盖。你可以推断的唯一意图是:在基于该Schema创建XML实例文档时,使你具有最大的灵活性。当然,作为Schema作者,可以自由定义元素,将所需的各种类型组合起来,从而向你说明采用这种模式的原因。
示例2-4中给出了前面已经生成的Book Schema,这次是使用Garden of Eden重新改写的。
示例2-4:采用Garden of Eden设计模式的Book Schema
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/eden"
xmlns:tns="http://ns.soacookbook.com/eden"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation>
Book Schema as Garden of Eden design.
</xsd:documentation>
</xsd:annotation>
<xsd:element name="book" type="tns:bookType"/>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="author" type="xsd:string"/>
<xsd:element name="category" type="xsd:string"/>
<xsd:element name="price" type="xsd:double"/>
<xsd:complexType name="bookType">
<xsd:sequence>
<xsd:element ref="tns:title"/>
<xsd:element ref="tns:author"/>
<xsd:element ref="tns:category"/>
<xsd:element ref="tns:price"/>
</xsd:sequence>
</xsd:complexType>
</xsd:Schema>
在这个Schema中,实例文档可以直接引用和使用Book类型,还可以使用元素组合。在OAGi行业Schema中,混合模型宽松地采用这种模式,它使你更容易长期维护SOA中的Schema,因为它允许你根据需要重新组合现有Schema的不同方面。对于SOA中希望多次重复使用或者不属于某个明确单一业务领域的常规或核心类型,这或许是可以考虑的不错选择。
当然,你不必将自己局限于单一模式,也可以使用上面介绍的所有不同模式,这取决于给定定义需要具有何种程度的封装、范围或灵活。
2.3 创建规范的数据模型
问题
正如Woolf和Hophe编写的“Enterprise Integration Patterns”(Addison-Wesley Professional出版社)中讨论的那样,你需要为SOA创建规范的数据模型,但不知道如何实现。
解决方案
请阅读下面的讨论。可以选择不这样做。如果确实选择使用规范的数据模型,你可能希望在生成许多服务之前,根据从主要数据管理角度所做的企业数据详细分析,让数据架构预先创建该模型。
简而言之,实际情况可能会发生变化,但解决方案或许是定义服务范畴的Schema,服务重复使用一层单独的Schema,而这些Schema是在企业范围内按照独立于服务的方式定义的。
讨论
创建规范的数据模型是比较艰苦的工作。你可能每件事都做对,但如果不是这样,就会陷入需要花费相当多的努力并进行重新构造才能摆脱的困境。我知道有一家拥有许多相互关联产品的软件公司是花了10个月处理Product的Schema定义。我们要小心行事。
执行Schema分析,在不同于服务分析的另一个优先流程中进行设计。服务将交换业务文档,这些业务文档由跨域实体组成,例如,跨越不同业务领域的各种服务可能需要使用某些核心类型,比如Customer、Product或Invoice。到目前为止,一切似乎正常。
但实际上,这会立即面临Schema重复使用问题。可以有以下选择:
将类型的重复使用最大化,去除多余的类型定义。采用某种允许重新组合的Schema模式来定义Customer、Product和Invoice Schema。编写Schema来表示可能会在许多地方使用的核心或常规类型,比如Address、SSN或CreditCard。编写将重复使用这些核心类型的特定于服务的Schema。现在拥有了一个完美的原始设计,接下来,倘若服务的需求没有发生改变,你需要做的全部工作就是将双手手指交叉起来放好。如果某件事的发生导致Schema发生改变,你就会发现自己需要重新创建、重新测试以及重新部署许多服务。我知道有家公司拥有2,500个Web服务,正如“海绵宝宝”说的那样,祝他们好运。
将服务的灵活性最大化,而将相互依赖最小化,为各个服务单独定义Customer、Product和Invoice。现在拥有了完全针对各个服务定制的合同,没有任何妥协。这可能意味着存在大量冗余(也称为“非规范化Schema”),但服务可以独立发展。还允许构建更完全封装的服务,例如,如果定义了一个Customer服务来作为所有与客户相关操作的入口,那么,该服务是Schema的所有者以及其依赖关系仅限于这一服务范围是比较有意义的。当然,这可能会存在很多冗余,取决于你所在公司拥有或计划拥有的服务数量。如果许多不同Schema中定义的某种核心类型的需求没有发生改变,就不要做任何事情,因为随后会有大量的易出错手动工作等着你。
这两种方法都不是特别吸引人。
或许可以通过一种方式获得这两者的长处,那就是使用一种基本的计算机学科行为模式:抽象层。此时可以将它应用为规范的数据模型,有时称为规范的Schema模式。
定义规范的数据模型
规范的数据模型指的是这样一种实践:定义单个Schema来表示全局类型,然后在为服务设计的局部Schema中通过引用来重复使用这些类型。当将企业服务总线作为介质时,任意数量的Web服务在无需相互之间创建特定点对点联系就可以彼此通信,通过这种方式,规范的数据模型使得你可以为企业中的类型定义黄金版Schema;任何需要涉及类型的服务可以使用服务总线或类似的机制将其版本转换成规范版本。
规范的Schema是黄金标准,它能够表示实体的一切有关内容,引用某个实体的各项服务需要对实体进行某些定制以执行其局部工作。不过,这些不同之处特定于服务所对应的Schema,任一特定Schema总是可以变成规范的Schema。由于实体服务Schema和规范的Schema是两个截然不同的文档,你可以同时最大程度地利用灵活性和重复使用。
当然,你不希望就为了能够足够灵活以容纳任意数据而将Schema设计成只包含一串字符串。例如,实体服务可能会表示某个实际数据库,这个数据库要求某个域具有特定长度。你希望自己的服务能够反应需要处理的实际约束,约束越具体,就可以进行更多基于Schema的确认。
你已经定义了一个规范的Schema,它代表的是你所在公司希望表示Address、Name等之类的条目。规范的Schema比局部实体Schema更灵活、更常规,它能适应更多的环境,因为不是需要结合的每个应用程序的每条地址限制都将根据相同的约束进行完美定义。可以从任何局部实体Schema生成规范的表示,而从规范的Schema又可以生成任何其他表示。在总线上,你可以在编排或侦听服务中使用XSLT来根据需要将一种类型转换成另一种类型,还可以使用默认值填充任何未定义的字段等。
现在,假如你所在的公司刚收购另一家公司,两家公司将具有不同的Customer实体服务,对Address和Name之类的事情采用不同的表示。如果使用规范的数据模型,你只需要将两个服务映射到这个规范的模型,而不必相互映射或分别改写(这和集成的目的背道而驰)。因此,现在有三种客户Schema:规范的客户Schema、服务客户Schema以及所收购公司定义的Schema。
随着时间的推移,你可以使用治理来转移服务Schema以更好地匹配规范的Schema,从而减少潜在的昂贵的运行时转换。
建议
我本人提倡将Schema设计作为SOA中一项重要任务来开展,下面是我的一些建议:
尽可能在创建服务约束之前,以独立于服务设计的方式为规范的数据模型设计Schema。
让规范Schema中的类型保持一定的灵活性和一般性,因为它们可能需要吸收各种服务实体定义。例如,在服务Schema中使用枚举可能是比较合适的,而在规范的Schema中可能需要使用字符串来表示同一类型。我们实际具有相同意见的地方是少得让人惊讶的,或许USState枚举类型具有50个条目,但是,需要结合的服务所对应的Schema中的枚举条目可能只包含Guam和Puerto Rico。
将实体服务Schema作为WSDL中要包含的独立文档来设计,不要在WSDL中定义局部类型。
为某项服务定义Schema时,尽可能从规范的模型导入。
将Schema设计与服务设计独立开来。
对所有Schema设计应用企业标准,严密监视规范模型的相关治理。不要试图将核心业务实体类型(比如Customer或Invoice)集中化,除非具有合适的治理结构来管理Schema变化。
将规范的Schema放入企业存储库中。如果要节省费用,可以通过LDAP服务器或Web服务器来实现。可以采用文档控制软件或更理想的SOA特定产品来解决所有的SOA人为结果。Schema应该支持网络运行。
考虑定义一些“基本的”类型,这些类型是所在命名空间和所在Schema中集中使用的类型。例如,美国电话号码、社会保障卡号或其他不由企业定义的数据结构都可以作为集中化类型的不错候选对象,许多Schema都重复使用这些集中化类型。
不要强行将规范的Schema用作任何数据库的前端,它们的存在是用来定义概念和作为理想的介质。
模式限制更适用于服务Schema,而不是规范的Schema,这些限制在验证中发挥着重要作用。
避免使用某些高级Schema结构,比如多态、选择或联合,这些结构会极大破坏互操作性。许多平台要么没有为所生成代码提供相应的工具支持,要么就是非常差。
要严格控制基本类型的范围,不要允许自己为这些类型给出宽泛定义。例如,尽管地址似乎像邮局定义的那样,也不是你公司所特有的,但这不是实际实例的常见情形。一项新运送服务需要的粒度或类型限制可能与你近20年销售点使用的有所不同。让所有遗留数据具有条理性是一项重要的工作,这样就可以定义一种两个系统都使用的地址类型,为企业带来少许潜在益处。
在本书中,我提倡从数据模型开始,并据此形成服务合同思想,以促进文档交换。尽可能地延迟编写代码。Schema明朗后,可以从它生成合同,这两者都可以进行反复修改。接着可以生成服务要使用的Java代码,也可以让开发人员来实现Java代码,这有助于确保你没有被实现紧紧束缚住,因为它模拟客户需要执行的操作。
当然,你现在被局限在Schema和Java代码中,Schema发生改变就需要生成新的代码。不过,此处要注意两点:任何Schema变化意味着强制进行合同改变;它们是合同的一部分。因此,重建是最小的问题。完善应该是构建过程的一部分,不要将生成的对象交付给存储库,也不要变得依靠服务中特定于实现的功能。从Java开始会让你被泛型以及服务合同中不重要的其他多余语言功能吸引。
此时没有灵丹妙药,因此不要太拖延——将出现一些冗余或某种程度的耦合。请抓一副药,选择冗余而不是耦合。你无法确切知道需求和业务联系将如何发展以及随着时间的推移将如何变化。
更新规范的数据模型
规范的Schema是“不可思议的大型火箭”。有些SOA从业者称它为反模式,了解这比较好;不过许多其他人不仅将它看作是具有积极意义的模式,而且将它作为创建可靠SOA的一种最重要的基础方面。我所交谈过的一些架构师指出,在着手进行SOA构建时,他们最开始做的一件事是预先定义规范的数据模型。这是本体和分类工作。
如果对概念进行大量分析和说明,规范的数据模型应该不是一种反模式。确保自己所在的企业包含实体可能需要的任何内容,而且,为了解决问题,应该让内容足够常规。请使用一种能给设计带来灵活性的Schema模式。
然后,像定义实体服务一样,针对当前服务定义Schema。通过导入,尽可能地重复使用规范模型中的类型。
对规范模型所做的任何改变都必须进行SOA治理,而且必须是改变不大。服务代表业务实体,这些东西不会每天改变,你将需要最终对规范模型进行更新。然而,可以对这些Schema明确地进行版本控制,如果在命名空间中指定版本,就不必同时改变所有的服务。创建一种新版本的Schema,在命名空间中指定该版本,接着,就可以只改变需要更新成使用新版本规范模型的那项服务。根据自己的时间更新其他服务,不过一定要尽快更新。如果一个规范的Schema同时存在多种版本,那么,它就不再是规范的Schema:只不过是另一种杂乱无章的层,通过它建立的SOA必定好景不长。应该管理好这一过程,这样就不会有问题。
参见
2.5小节。
2.4 使用Chameleon命名空间设计
问题
希望找到一种巧妙的方法来解决Schema中的名称空间依赖,想到在SOA中使用某种Chameleon模式将会奏效。
解决方案
请不要这样做。Chameleon模式非常灵活,它的方式是让你在没有命名空间的Schema(称为Chameleon)中设计常见类型,然后定义一个主Schema,这个Schema在自己的命名空间中通过<include>将第一个Schema包含进来,Chameleon中的常见类型被强制赋予命名空间,使用的是主Schema中定义的命名空间。示例2-5和2-6说明了这种情况。
示例 2-5:Chameleon客户
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<!-- Defined without namespace -->
<xsd:complexType name="CustomerType">
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:Schema>
示例 2-6:Invoice主Schema将命名空间强制赋予Chameleon
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:Invoice"
xmlns="urn:Invoice"
elementFormDefault="qualified">
<xsd:include SchemaLocation="CustomerChameleon.xsd"/>
<!-- Invoice has a Product and a Customer -->
<xsd:element name="Invoice">
<xsd:complexType>
<xsd:sequence>
<!-- Define product here -->
<xsd:element name="product">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="sku" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<!-- Pull from Chameleon -->
<xsd:element name="customer" type="CustomerType"
minOccurs="1" maxOccurs="1" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:Schema>
这里的主Schema定义了Invoice类型,该类型包含Chameleon Schema中定义的Product,这是命名空间在XML Schema中发挥有力杠杆作用的一种方式,乍一看非常吸引人,看来你能够绕开前一小节有关Schema设计中引入的一些依赖关系问题。如果没有声明命名空间并在所有可能的地方使用Chameleons来定义常见类型,则当后来替换类型时不会打扰客户。
然而,要知道Chameleon设计模式的使用是饱受争议的。Chameleon依赖于Schema规范中的一些方面,而对于这些方面的解释,供应商们并不是完全赞同。SOA的一项主要目的是实现可互操作的一般服务,你使用只有某些时候才被支持的内容时要当心。
此外,Chameleon通常会在验证过程中降低性能,哪怕是从支持它的供应商进行验证,这是因为命名空间解决方案的延迟妨碍了分析器基于命名空间来缓存Schema的组件。
Chameleon还使XPath身份约束的使用受到限制。XPath不使用默认的命名空间,因此命名空间的使用必须预先处理,这首先与Chameleon的要点存在冲突。
Chameleon使得组件名称冲突的可能性增大,因为类型没有受到命名空间的约束。
另外,像包一样,命名空间的存在是有一定原因的,它用来对一组类型进行有逻辑地组合,并根据某种给定的常规思想将它们分开。如果不能将类型放入命名空间,你可能需要重新考虑自己的设计。如果可以将类型放入命名空间,但却为了能够获得灵活性而选择不将类型放入命名空间,那么,互操作和冲突问题的发生是迟早的事。
2.5 对Schema进行版本控制
问题
有许多不同的方法可以指定Schema的版本,对某种SOA环境来说,如果存在相应的方法,你不确定哪一种是最合适的。
解决方案
强烈建议考虑使用命名空间的某些部分来指定Schema版本,这支持规范的数据模型模式。不过,必须要按照不损害代码生成工具的某种方式来实现。
讨论
XML Schema会随着时间的推移而发展,甚至规范模型中希望不怎么改变的那些Schema也是这样,因此,需要使用一种策略来对SOA中实际支持工作的Schema进行版本控制。有几种不同的方法,下面我们将对这些方法进行讨论。
使用version属性
指定Schema版本的最直接方法是使用就是为该目的而存在的内置属性,如下所示:
<xs:Schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
version="1.0.0">
这种版本控制方法的主要优点是简单,不需要任何附加劳动。它也是比较有利的,因为如果现有Schema或导入某一指定版本Schema的WSDL与新版本兼容,则无需进行改变。
不过,就像许多简单的事情一样,它无法真正给我们提供想要的结果。不利方面是不能通过工具来实现,而且,从导入对象(比如WSDL或其他Schema)来看,无法清楚知道使用的是哪种版本,除非还用实际文档所处位置来给出版本。这带来了维护问题,因为它违反了DRY(Don抰 Repeat Yourself,不要做重复性的工作)原则。它还回避了这个问题:如果本身不足以说明版本,为什么要为用该属性指定版本而烦恼?
利用根元素
定义Schema中的根元素时,可以让它自身带有版本属性,这允许通过工具实现验证,但不是一个真正的解决方案。强制执行验证需要额外的自定义预处理。你不希望数据模型渗杂有特定于实现的条目(比如Schema版本号),在Web服务的使用中,这是完全不合适的。
改变Schema文档的名称或URI
可以只改变文件的名称或位置,这使得导入Schema文档或WSDL能够清楚表示所使用的版本,如下所示:
<customerxmlns="http://www.soacookbook.com/Customer"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://www.soacookbook.com/Customer
http://www.soacookbook.com/Customer-v1.0.0.xsd">
这种方法与前一种方法相似。SchemaLocation属性不是必须的,意味着它对处理程序来说是一种提示,可以被忽略,这对WSDL文档来说是合适的,因为这些文档是基于实际位置执行明确导入。不过,这会使工具带来相当多的不一致和问题,因为虽然版本发生变化,但命名空间没有改变,从而告知客户WSDL与常规命名空间是兼容的,而不仅仅是与特定的版本兼容。范围外的客户使用的生成代码将不是同步代码。
使用命名空间和文档名称
最好的解决方案是使用命名空间本身来指定Schema的版本,这对于Web服务中我们使用的许多规范来说是常见的。考虑以下命名空间:
SOAP 1.2的命名空间是http://www.w3.org/2003/05/soap-envelope。
WS-Addressing 1.0的命名空间是http://www.w3.org/2005/08/addressing。
XML Schema 1.0的命名空间是http://www.w3.org/2001/XMLSchema,它使用的实例命名空间是http://www.w3.org/2001/XMLSchema-instance。
这些命名空间都对Web服务极为重要,可以通过在命名空间本身中包含发行的年月来指定版本。关于是查看SOAP 1.1信封还是SOAP 1.2信封,是不存在混乱的。不过,你不要随意在定义用于WSDL的Schema中直接简单复制该结构,这是因为Web服务依赖工具生成的代码。对于通过WSDL使用命名空间生成的结果,工具(比如wsimport和WSDL2Java)会为其生成包名称,而且在Java中,包名称以数字开头是不合法的。
这很容易补救,可以如下处理:
<definitions name="MyService"
targetNamespace="http://ns.soacookbook.com/sales/v2009Q1"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://www.soacookbook.com/sales/v2009Q1"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
可以使用前缀(比如v)表示“版本”来处理客户端生成问题,当然,不一定非要使用日期,可以使用更标准的版本号。
这有利于使WSDL、导入Schema和所生成的正要使用的客户端代码更清晰,还允许这些条目继续使用兼容的版本直到准备好进行更新。
由于已经改变了命名空间,应该基于新版本创建新的文档,并同时保留两个版本直到每个人都更新。不是任何内容都能够或应该立即移植到新Schema中。
确保预先决定一种惯例,将其发布在开发人员文档中,对原则进行治理,并强制实施所选结构。
2.6 参考Schema
问题
内心怀疑某人于某处已经为Address之类的基本实体设计了XML Schema,希望借助由优秀人员组成的标准机构或工作组的有关成果,这些人具有大量的时间来考虑这类事物,而且希望不要重复他人做过的工作。
解决方案
查看http://www.openapplications.org中开放应用程序组(Open Applications Group,OAGi)定义的许多行业Schema,他们针对各种不同方面定义了XML Schema,注册基本意味着给出自己的电子邮件地址,然后就可以免费下载他们编写的各种Schema。可以直接使用这些Schema,这会简化与其他也直接使用这些Schema的公司的整合;也可以将它们作为起点,将其中的一些合适内容吸收到自己的设计中。
开放应用程序组花了很多年根据最佳实践对创建Schema展开了大量工作,尤其是为了支持B2B集成。有关如何定义Schema的白皮书也已经发布,你也可以下载这些资料。这些白皮书有着各自的目的,不过也应该有助于鼓励你在自己的企业中完成类似操作。
创建一系列的标准、最佳实践以及作为参考架构一部分来内部发布的要求,并将这个任务纳入治理计划。回顾OAGi文档将有助于指导自己如何实现这一目标。
要注意OAGi给出的Schema和白皮书可能会比较长和复杂,因此,根据自己的目的对它们进行相应的精简会是比较合适的。
2.7 常见Schema类型
问题
希望定义一些典型的正则表达式类型约束以供Schema使用,而且希望不要重复他人已做过的工作。
解决方案
尝试以下某种通常使用的正则表达式。尽管正则表达式的使用可能会因平台不同而不同,但这些表达式都是已经通过Java进行测试的,将有助于对SOAP有效载荷的验证。
人名
这种类型使用两个独立的元素来分别表示姓和名,人名必须是2到30个字符,而且必须包含字母,可以包含-、'、.和空格。的确如此,我知道某个合法的人名包含“.”(嗨,J.!),我有一个表弟,他的合法名称为“M扡ee”:
<xs:element name="name">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:minLength value="2" />
<xs:maxLength value="30" />
<xs:pattern value="[A-Za-z\-. ']{2,30}" />
</xs:restriction>
</xs:simpleType>
</xs:element>
对人名进行限制未必是一种好方式,但至少可以这样处理。例如,航空业不允许使用任何特殊字符,包括非常常见的连字符-。就按照适合自己的方式定义人名。
美国电话号码
这种类型处理通常带有连字符的域分隔,有时也使用.或空格作为域分隔符。
<xs:element name="usPhone">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="\(?\d{3}\)?[ \-\.]?\d{3}[ \-\.]?\d{4}" />
<xs:minLength value="10" />
<xs:maxLength value="16" />
</xs:restriction>
</xs:simpleType>
</xs:element>
注意,该正则表达式有一个缺点,我听说有效美国电话号码的区号和前缀都不允许以0或1开头,而这个正则表达式没有关注这一点。不过,我从未见过这样的电话号码,也无法找到对应的电话规则。而且,我发现更严格形式的该表达式是不切实际的,给出了非常多的测试数据,而生成这些数据的人却不知道该规则。
电子邮件地址
这个正则表达式允许以字母、数字开头的典型电子邮件地址,还可以包含-和.,其后是@符号和符合同一规则的域名,域名后跟一重要后缀,长度为2到9个字符:
<xs:element name="email">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="(\w+\.)*\w+@(\w+\.)+[A-Za-z]{2,9}" />
<xs:minLength value="6" />
<xs:maxLength value="255" />
</xs:restriction>
</xs:simpleType>
</xs:element>
美国邮政编码
该正则表达式允许5位数字的邮政编码或85266-1234形式的邮政编码:
<xs:element name="usZip">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="\d{5}(-\d{4})?" />
<xs:minLength value="5" />
<xs:maxLength value="10" />
</xs:restriction>
</xs:simpleType>
</xs:element>
社会保障卡号
这是标准结构,允许漏掉连字符,因为用户输入的社会保障卡号可能带有连字符,但数据库存储时是不包含连字符的:
<xsd:simpleType name="SSN">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\d{3}(-)?\d{2}(-)?\d{4}" />
<xsd:minLength value="9" />
<xsd:maxLength value="11" />
</xsd:restriction>
</xsd:simpleType>
注意,这是一种非常简单、易懂的表示,但不完美。对于社会保障卡号,有这么一个规定:三个字段中的每一个字段都不允许全部是0,而这个正则表达式没有说明这一点。
加拿大邮政编码
加拿大邮政编码包含6个字符,它们由交替出现的字母和数字组成,前三个字符表示地区代码,后三个字符表示街道代码。有效示例代码有:M1A 1A1、H9Z 9Z9。
<xs:element name="caPostalCode">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[ABCDEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d" />
<xs:minLength value="7" />
<xs:maxLength value="7" />
</xs:restriction>
</xs:simpleType>
</xs:element>
加拿大省份
加拿大有14个省,像美国的州一样,它们使用两字符简称:
<xsd:simpleType name="CAProvince">
<xsd:restriction base="xsd:string">
<xsd:length value="2" />
<xsd:enumeration value="AB" />
<xsd:enumeration value="BC" />
<xsd:enumeration value="MB" />
<xsd:enumeration value="NB" />
<xsd:enumeration value="NL" />
<xsd:enumeration value="NS" />
<xsd:enumeration value="NT" />
<xsd:enumeration value="NU" />
<xsd:enumeration value="ON" />
<xsd:enumeration value="PE" />
<xsd:enumeration value="QC" />
<xsd:enumeration value="SK" />
<xsd:enumeration value="YT" />
</xsd:restriction>
</xsd:simpleType>
URL
该正则表达式用于匹配URL,URL中可以带有也可以不带有http://,https://也是允许的,因为它没有考虑协议,可以只给出www.domain.com之类的信息。端口号也是可选的,不过,如果指定了,则必须是2到5位数字长:
<xs:element name="url">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="(https?://)?[-\w.]+(:\d{2,5})?(/([\w/_.]*)?)?" />
</xs:restriction>
</xs:simpleType>
</xs:element>
因此,下面都是有效的示例:
http://www.domain.com
https://www.domain.com
http://www.domain.com:8080
http://www.domain.com:8080/some.xsd
http://www.domain.com:80/path/doc.xml
IP地址
对于IP地址,需要使用四个八位字节来表示0到255之间的数值:
<xs:element name="ip">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="(((\d{0,2})|(1(\d){0,2})|(2[0-4]\d)|(25[0-5]))\.){3}
((\d{0,2})|(1(\d){0,2})|(2[0-4]\d)|(25[0-5]))" />
</xs:restriction>
</xs:simpleType>
</xs:element>
该模式允许127.0.0.1、192.168.1.1、10.0.134.147等之类的地址。
当然,为你定义许多其他常见正则表达式类型也是一件容易的事,但上面这些表达式应该足以让你开始工作。
2.8 根据单个Schema验证XML文档
问题
拥有一个XML文档实例,希望根据XML Schema来验证它是否有效。
解决方案
扩展org.xml.sax.helpers.DefaultHandler并将它设定为DOM DocumentBuilder中的错误处理例程。
注意,为了方便起见,现已提供DefaultHandler类,因此,你不必实现不感兴趣的那些方法,不过,该类的所有方法都是空操作,为了弄清错误,你从而需要重新抛出异常、记录或打印堆栈。
示例2-7给出了一个有用示例。
示例 2-7:DomValidator.java
package com.sc.ch02.Schema;
import static java.lang.System.out;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Validates an XML document according to a Schema.
*/
public class DomValidator {
private static final String Schema_LANG_PROP =
"http://java.sun.com/xml/jaxp/properties/SchemaLanguage";
private static final String XML_Schema =
"http://www.w3.org/2001/XMLSchema";
private static final String Schema_SOURCE_PROP =
"http://java.sun.com/xml/jaxp/properties/SchemaSource";
//run the example
public static void main(String[] args) {
String Schema = "C:/repository/books/SOACookbook/code/" +
"soacookbook/bin/ch02/Catalog.xsd";
String xmlDoc = "bin/ch02/Catalog.xml";
boolean valid = validate(Schema, xmlDoc);
out.print("Valid? " + valid);
}
//do the work
private static boolean validate(String Schema, String xmlDoc) {
DocumentBuilder builder = createDocumentBuilder(Schema);
ValidationHandler handler = new ValidationHandler();
builder.setErrorHandler(handler);
try {
builder.parse(xmlDoc);
} catch (SAXException se) {
out.println("Validation Error: " + se.getMessage());
} catch (IOException ioe) {
ioe.printStackTrace();
}
return handler.isValid();
}
/**
* Convenience method sets up the validating factory and
* creates the builder.
*/
private static DocumentBuilder createDocumentBuilder(
String Schema) {
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(true);
factory.setAttribute(Schema_LANG_PROP, XML_Schema);
factory.setAttribute(Schema_SOURCE_PROP, Schema);
DocumentBuilder builder = null;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException pce) {
pce.printStackTrace();
}
return builder;
}
}
/**
* This class gets notified by the parser in the event of a
* problem.
*/
class ValidationHandler extends DefaultHandler {
private boolean valid = true;
private SAXException se;
/**
* The default implementation does nothing.
*/
@Override
public void error(SAXParseException se) throws SAXException {
this.se = se;
valid = false;
throw se;
}
/**
* The default implementation does nothing.
*/
@Override
public void fatalError(SAXParseException se)
throws SAXException {
this.se = se;
valid = false;
throw se;
}
public boolean isValid() {
return valid;
}
}
在正常情况下,该程序应该给出如下输出:
Valid? true
如果改动Schema或XML文档以使其无效,将看到如下类似的消息:
Validation Error: cvc-elt.1: Cannot find the declaration of element 'dude'.
Valid? false
Java SE 6内置的DOM分析器默认情况下不支持命名空间,因此需要在DocumentBuilderFactory中设置该属性。如果忘记设置该属性,就会没有显示;即使处理程序进行了有关设定,任何验证错误都会被忽略。
为了正确验证文档,需要在默认命名空间中声明元素的Schema。
2.9 根据多个Schema验证XML文档
问题
拥有一个XML文档实例,该实例使用多个命名空间中的元素,现在需要验证该实例。
解决方案
使用前一小节中的代码,只是创建由所需Schema名称组成的字符串数组,然后将它传递给DocumentBuilderFactory。
假如拥有多个Schema定义的如下XML文档:
<c:customer cid="99" xmlns:c="urn:ns:soacookbook:customer">
<gen:name xmlns:gen="urn:ns:soacookbook:general">
Indiana Jones
</gen:name>
<a:address xmlns:a="urn:ns:soacookbook:address">
<a:street>1212 Some Street</a:street>
<a:city>Washington,DC</a:city>
<a:state>VA</a:state>
</a:address>
</c:customer>
这是实际设计中更容易遇到的情况,验证这类文档几乎与根据单个Schema验证文档是一样的,只不过在这种情况下,是将代表所需Schema名称的字符串数组传递给DocumentBuilderFactory:
...
//create a string path to each Schema needed for Customer.xml
String SchemaAddress = ROOT + "a.xsd";
String SchemaGeneral = ROOT + "gen.xsd";
String SchemaCustomer = ROOT + "Customer.xsd";
String[] Schemas =
{SchemaAddress, SchemaGeneral, SchemaCustomer};
//change this method to accept vararg Schema
private static DocumentBuilder createDocumentBuilder(
String...Schemas) {
...
//business as usual
factory.setAttribute(Schema_SOURCE_PROP, Schemas);
值得注意的是,我是使用字符串来指代Schema文档的路径,但这并不是唯一的方法,你可以使用下面任何一种方式来指定传递给factory.setAttribute的值:
一个指向Schema URI的字符串
之前用来捕获Schema内容的InputStream
SAX InputSource
File对象
由这些类型中任意一种类型的元素组成的数组
2.10 使用正则表达式限制Schema类型
问题
需要在Schema中表示一些基本的简单类型,比如电话号码或社会保障卡号,希望采用比xs:string更严格的约束。
解决方案
在模式中使用正则表达式来定义约束,并通过<xs:restriction>来实现它。
在XML Schema中,元素可以通过作为字符串定义的简单类型来表示,但带有一个限制元素,该元素定义了相应的模式。该模式是一个正则表达式,给出了可接受值的范围。你还可以利用Schema中其他的内置限制元素,比如<minLength>和<maxLength>。
下面是一个示例:
<?xml version="1.0" encoding="UTF-8"?>
<xs:Schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/cart"
xmlns:tns="http://ns.soacookbook.com/cart"
elementFormDefault="qualified">
<xs:element name="usPhone" type="tns:USPhoneType" />
<xs:simpleType name="USPhoneType">
<xs:restriction base="xs:string">
<xs:pattern value="\(?\d{3}\)?[ \-\.]?\d{3}[ \-\.]?\d{4}"/>
</xs:restriction>
</xs:simpleType>
</xs:Schema>
一旦验证XML文档实例,就会拒绝不符合正则表达式的域元素的值。
下面是一个通过了此类约束验证的XML文档实例:
<?xml version="1.0" encoding="UTF-8"?>
<cart:usPhone xmlns:cart="http://ns.soacookbook.com/cart"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://ns.soacookbook.com/cart
Cart.xsd">(888) 999-0101</cart:usPhone>
你可以添加其他Schema约束来限制可接受值的长度(不过这不是正则表达式的一部分):
...
<xs:restriction base="xs:string">
<xs:pattern value="..."/>
<xs:minLength value="10"/>
<xs:maxLength value="16"/>
</xs:restriction>
...
表2-1概述了一些使用最频繁的正则表达式结构。
表2-1:常见正则表达式结构
\d 0到9之间的任何数字
\D 非0到9之间任何数字
\s 空格或制表符
\S 非空格或制表符
\w 大小写字母、数字或下划线
\W 非大小写字母、数字或下划线
. 任何单个字符
? 0或1次出现
* 0或多次出现
+ 1或多次出现
表2-2说明了一些重要的元字符,这些字符的含义是在构建正则表达式时获得的。
表2-2:正则表达式元字符
{} 指定前面模式出现的次数,例如,\d{3}表示一行中的任意三个数字
[] 表示括号中的任一模式出现一次且仅此一次,例如[AB]表示只能是单个字符A或B(区分大小写)
- 在[]中使用时,用于定义范围分隔符,例如,[1-5]表示1到5之间的任何单个数值。一个列表中可以使用多个范围,比如,[1-5A-E]表示1到5之间的任何单个数值或单个区分大小写的字符A、B、C、D或E
^ 在[]中使用时,表示对表达式取反,例如,[^0]表示0以外的任何内容
括弧中的空格是有一定含义的,括号用于表示分组,但如果是表示括号字符,则需要免除。下面是一些更简洁的示例:
capitali[zs]e表示美式“capitalize”或英式“capitalise”。
honou?r表示美式“honor”或英式“honour”。
\(?\d{3}\)?[\-\.]?\d{3}[ \-\.]?\d{4}表示888.999.1111、(888)999-1111、(888) 999.1111 或(888)-999.1111。让我们分析一些这个电话号码结构。先只研究这一部分:\(?\d{3}\)?,第一个斜杠表示免除左括号,说明它是代表括号字符,而不是一个分组。第一个?表示其前的左括号字符是可选的,可以没有左括号,也可以有一个左括号,\d{3}表示三个数字。对称结构\)?用于指定右括号,它也是可选的。现在我们来看紧接着的一个部分:[ \-\.]?,方括号表示一个范围,空格字符是有一定含义的,这一小部分表示区域代码后面可以是一个空格、连字符、句点,也可以不带有任何内容。电话号码正则表达式的其余部分重复使用了这些模式的不同形式。
如果知道某种数据类型的结构,使用正则表达式来约束数值或许是一个不错的主意,不过,为了获得尽可能好的互操作性,将正则表达式结构保持相当简单可能会比较好,而且不要使用不适用于各种平台的可选扩展。这包括用来进行位置处理的一些结构,比如,\<表示一个单词的起始位置。
参见
2.11小节。
2.11 使用Schema枚举
问题
希望Schema为元素定义一组有限数量的可用已知值。
解决方案
使用XML Schema枚举类型。
下面是一个有关将美国作为一个元素使用时的示例:
<xs:simpleType name="USState">
<xs:restriction base="xs:string">
<xs:length value="2" />
<xs:enumeration value="AL" />
<xs:enumeration value="AK" />
<xs:enumeration value="AR" />
<xs:enumeration value="AZ" />
...
</xsd:restriction>
</xsd:simpleType>
在文档实例中,只有枚举实例指定的那些字符才会通过验证。
2.12 从Schema生成Java类
问题
前面已经认真创建代表数据类型的可靠XML Schema,现在希望从其生成Java源代码。
解决方案
使用Java附带的XML/Java Compiler XJC工具将Schema编译成Java类。如果使用的是Ant,请使用Sun的<xjc>包装类任务。
提示: Sun没有将Ant任务和Java SE放在一起发布,而且XJC的有关任务没有异常。可以在glassfish/lib目录中找到Webservices-tools.jar,此外还需要类路径中使用Webservices-rt.jar。
使用JAXB
JAXB的目的是提供一种方便、轻松的方法将XML Schema用一系列的Java类给出相应的表示,它有两个主要功能:
编组(Marshaling)
从Java对象创建XML文档实例
解组(Unmarshaling)
从XML文档实例创建Java对象
JAXB以前要比现在较难使用,可以单独下载附带有Web Services Developer包的1.0版本,而且,因为仍没有使用Java 5注解,JAXB在编组和解组过程中生成非常多的代码,这些代码难于阅读,而且不好使用。首先,JAXB 1.0在解组过程中为任何事情生成Java接口,这使得代码库庞大而且复杂,有时会用反直觉的名称来表示类。
Java SE 1.6.0_05附带有2.1.3版本的JAXB,其bin目录中包含XJC工具,因此,只要路径中包含Java,而且应该包含,调用它就没有问题。
你将注意到的第一个不同是JAXB 2.1在解组过程中生成简单、清晰且带有注解的POJO(Plain Old Java Objects)。这些具体类的注解给出了有关提示来供JAXB在编组和解组过程中使用。我们将在下一节中介绍这些注解的使用和含义,现在让我们来看一个例子。
假定存在示例2-8所示的Schema。
示例 2-8:要绑定到Java类的XML Schema
<xs:Schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/suits"
xmlns:tns="http://ns.soacookbook.com/suits"
elementFormDefault="qualified">
<xs:simpleType name="Suit">
<xs:restriction base="xs:string">
<xs:enumeration value="SPADES" />
<xs:enumeration value="HEARTS" />
<xs:enumeration value="DIAMONDS" />
<xs:enumeration value="CLUBS" />
</xs:restriction>
</xs:simpleType>
</xs:Schema>
该Schema定义了玩纸牌时使用的四种花色,示例2-9给出了将根据这个Schema进行验证的XML文档实例。
示例 2-9:与上面Suit Schema匹配的XML文档
<?xml version="1.0" encoding="UTF-8"?>
<s:suit xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns:s='http://ns.soacookbook.com/suits'
xsi:SchemaLocation='http://ns.soacookbook.com/suits Suits.xsd'>HEARTS</s:suit>
这可以作为载荷文档添加到SOAP消息,不过,如果定义接受Java Suit枚举实例的Web服务,则无法编译服务类,除非类路径包含相应的Suit枚举。
因此,需要创建与该Schema匹配的Java源代码。为了实现这个目标,可以从命令行运行XJC来生成Java源代码。此时要改变控制台中项目根目录,希望将文件放入一个名为work/gen的目录,可以使用-D开关指定文件将生成到哪个目录。你既可以指定单个Schema文件名来只对这个Schema执行编译,也可以指定一个目录,XJC将编译这个目录中找到的所有Schema:
>>xjc -verbose -d work/gen src/xml/ch02/Suits.xsd
最后参数指定XJC应该在哪个目录中查找文件。通过这种用法,XJC将根据指定目录中找到的所有Schema进行运行。输出应该如下所示:
parsing a Schema...
compiling a Schema...
[INFO] generating code
com\soacookbook\ns\suits\ObjectFactory.java
com\soacookbook\ns\suits\Suit.java
>
为了与JAXB 1.0向后兼容,该版本提供了ObjectFactory。对于当前这个示例,它将是空的,因为生成的是枚举类型。得到的类如示例2-10所示。
示例 2-10:JAXB 2.1生成的Suit.java枚举类型
package com.soacookbook.ns.suits;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlType;
/**
* <p>Java class for Suit.
*
* <p>The following Schema fragment specifies the expected content contained within this class.
* <p>
* <pre>
* <simpleType name="Suit">
* <restriction base="{http://www.w3.org/2001/XMLSchema}string">
* <enumeration value="SPADES"/>
* <enumeration value="HEARTS"/>
* <enumeration value="DIAMONDS"/>
* <enumeration value="CLUBS"/>
* </restriction>
* </simpleType>
* </pre>
*
*/
@XmlType(name = "Suit", namespace = "http://ns.soacookbook.com/suits")
@XmlEnum
public enum Suit {
SPADES,
HEARTS,
DIAMONDS,
CLUBS;
public String value() {
return name();
}
public static Suit fromValue(String v) {
return valueOf(v);
}
}
这个源文件对应于Suits Schema,它已在suits命名空间反演所对应的包中生成,命名空间http://ns.soacookbook.com/suits被转换成包名com.soacookbook.ns.suits。
既然已具有基本概念,那就让我们来看一个更为复杂的实际例子。早期Catalog.xsd的声明方式如示例2-11所示。
示例 2-11:Catalog Schema声明
<?xml version="1.0" encoding="UTF-8"?>
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://ns.soacookbook.com/catalog"
targetNamespace="http://ns.soacookbook.com/catalog">
//...
此处我将不重复整个清单,而只关注其中的Book类型,如示例2-12所示。
示例 2-12:Catalog.xsd中声明的Book类型
<xsd:complexType name="Book">
<xsd:sequence>
<xsd:element name="isbn" type="ISBN"/>
<xsd:element name="author" type="Author"/>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="category" type="Category"/>
</xsd:sequence>
</xsd:complexType>
基于该类型定义,JAXB将生成一个Book.java文件,complexType对应于一个Java类,类型序列的子元素按照序列中的声明顺序各自对应于一个属性。由于没有元素被指定为nillable="true",因此,各个字段都被注释为@XmlElement(required = true)。这作为一种提示来告诉marshaler:没有为各个字段定义值的Java对象实例应该无法通过验证。请看示例2-13。
示例 2-13:JAXB生成的Book.java类
package com.soacookbook.ns.catalog;
import javax.xml.bind.annotation.*;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Book", propOrder = {
"isbn",
"author",
"title",
"category"
})
public class Book {
@XmlElement(required = true)
protected String isbn;
@XmlElement(required = true)
protected Author author;
@XmlElement(required = true)
protected String title;
@XmlElement(required = true)
protected Category category;
//... getters and setters ommitted
此处有不少事情要考虑。注意,生成的包是从Book.xsd在命名空间之后命名的,字段本身由类来表示,而这些类对应于它们的complexType定义。例如,Book类型是由一个Category对象部分组成的:
<xsd:simpleType name="Category">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="COOKING" />
<xsd:enumeration value="LITERATURE" />
<xsd:enumeration value="PHILOSOPHY" />
<xsd:enumeration value="PROGRAMMING" />
</xsd:restriction>
</xsd:simpleType>
JAXB从而生成对应的Category.java文件。Category恰巧是一个Schema枚举对象,而且,从Schema枚举到Java枚举的映射是非常直接的:
@XmlType(name = "Category")
@XmlEnum
public enum Category {
COOKING,
LITERATURE,
PHILOSOPHY,
PROGRAMMING;
public String value() {
return name();
}
public static Category fromValue(String v) {
return valueOf(v);
}
}
接着,可以像任何其他常规Java类一样编译和使用这些类。此处生成的ObjectFactory类只是为了与以前的JAXB版本向后兼容,因此,可以将该代码与JAXB 1.0编写的代码组合在一起,它们依然可以一起使用。
你会看到生成过程中丢失了simpleType字符串约束,这或许不是你所期望的,不过,如果使用JAXB生成Web服务模型,则在运行时使用Schema本身就能够验证这些类型的你输入。
对于SOA,可以考虑从为实体类型创建XML Schema开始,根据这些Schema生成源代码,然后在客户端将Schema用WSDL包装,这样就可以根据定义的任何模式约束来进行验证。
要查看XJC使用的其他选项,可以从命令行简单调用xjc,不需要带任何参数,将会给出有关使用信息。
警告: 要当心将生成的文件作为项目中的“一等公民”对待。虽然用来生成这些文件的Schema可能非常重要,但不应寄希望于生成的Java源代码也是这样。通常来说,不要将生成的代码放入CVS或SVN存储库。
JAXB注解
尽管对JAXB进行全面讨论超出了本书的范围,不过是值得花一点时间来介绍JAXB定义的那些最重要注解,这些注解都包含在javax.xml.bind.annotation包中。
JAXB注解可以添加到包、类、域或方法中,在这里,我们将介绍从XML Schema解组生成的Java对象时,XJC通常会添加到Java对象的那些注解。
XMLRootElement
这种注解位于类层次,用来说明从Java到XML编组时,Java类型可用作Schema的根元素。Book示例只定义了一个complexType,而不是一个元素,因此,解组时不会收到这种注解。
示例2-14是包含一个全局元素的Schema,该元素使用XMLRootElement。
示例 2-14:包含根元素的XML Schema
<xsd:Schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/book"
xmlns:tns="http://ns.soacookbook.com/book"
elementFormDefault="qualified">
<xsd:element name="author">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="firstName" type="xsd:string"/>
<xsd:element name="lastName" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:Schema>
如果发出如下命令:
>xjc -d . AuthorRootType.xsd
JAXB就会在当前目录中生成示例2-15所示的Java类型(包含相应的包)。
示例 2-15:包含XMLRootElement注解的Author类型
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"firstName",
"lastName"
})
@XmlRootElement(name = "author")
public class Author {
@XmlElement(required = true)
protected String firstName;
@XmlElement(required = true)
protected String lastName;
//... getters and setter methods ommitted.
上面的输出清单中省略了getter和setter方法,因为它们不会获得任何有关的注解。注意,此处使用了Russian Doll设计模式,这使得类获得@XMLRootElement注解。如果在元素的外部定义complexType并让其他内容保持不变,JAXB可以选择的根元素就可能有多个,从而不会包含XMLRootElement注解。换句话说,JAXB无法一定保证另一时间单独编译的其他Schema不使用该complexType,这会导致冲突。
试图对多个包含@XMLRootElement注解的嵌入对象进行编组是不合法的。
让我们快速浏览一下此处使用的一些注解。
XMLAccessorType。该注解给出将使用四个可能值中的哪一个来对子元素进行编组:
PUBLIC_MEMBER
告诉marshaler应该对每个Java bean属性(公有的getter和setter对)进行编组。
FIELD
告诉marshaler对类中的每个非静态、持久化字段进行编组。要防止某个字段被编组,可以使用XMLTransient为其添加注解。
PROPERTY
告诉marshaler对每个getter/setter对进行编组,而不用管它们的访问范围。
NONE
告诉marshaler默认情况下不对任何属性进行编组。必须明确指定需要编组以包含自身最重要注解的那些字段。
XMLElement。该注解指定它所注解的字段应该是XML Schema中的元素,共有三种可能的值:required、nillable和defaultValue,它们与所期望的Schema值相对应。
小结
重申一下,JAXB是一个非常大的主题,在这里,我们的目标不是对JAXB的各个方面进行完全探讨,而是只阐述在SOA环境中使用JAXB时你必须知道的那些内容。现在有许多在线的教程和其他资源,完整说明非常长(大约400页),不过它绝对是介绍JAXB各项功能的权威资料。
2.13 从Java生成Schema
问题
手头上有一个Java类,希望根据该Java类生成一个XML Schema。
解决方案
运行Java SE 6附带的命令行工具Schemagen,另外,可以在运行时使用SchemaOutputResolver来生成Schema。
XML Schema可以从Java类生成,在生成过程中,可以使用javax.xml.bind.annotations包中的注解来向marshaler给出提示。Schemagen实用程序是与JAXB的参考实现一起发布的,因此,也可以在Java SE 6工具中找到它。在命令行输入Schemagen就应该会给出有关使用信息。
使用Schemagen
一种最简单的使用Schemagen的方式是指定要为其创建Schema的类的名称。要在Windows中调用Schemagen,可以使用批处理文件,而在Linux中可以使用shell脚本。下面是最快方式的一个示例:
>Schemagen Product.java
提示: 在装有JDK 1.6.0的Windows XP Service Pack 2中使用Schemagen时,可能会出现问题,该问题被记录为bug 6510966,谢谢Brian Lee发现这一问题。
这将首先编译指定的Java类,接着,默认情况下会在当前目录中写入一个名为Schema1.xsd的新XSD文件。要覆盖Schemagen用来放置输出文件的目录,可以使用-d选项。此外,Product类没有任何外部依赖,不过,如果有,则需要使用-cp选项进行指定。Java源文件如示例2-16所示。
示例 2-16:不包含注解的简单Product.java类
package com.soacookbook.ch02.Schemagen;
import java.util.Date;
public class Product {
private static final long serialVersionUID = 12345L;
private String name;
private Date mfrDate;
public Product() {
}
public Date getMfrDate() {
return mfrDate;
}
public void setMfrDate(Date mfrDate) {
this.mfrDate = mfrDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
注意,Schemagen将忽略类中没有为其指定getter和setter方法的private字段。调用Schemagen后的输出结果如示例2-17所示。
示例 2-17:针对Product.java使用Schemagen命令生成的Schema
<xs:Schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="product">
<xs:sequence>
<xs:element name="mfrDate" type="xs:dateTime" minOccurs="0"/>
<xs:element name="name" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:Schema>
对于这个例子,有一些有趣的事情值得讨论。首先,为类创建了一个complexType,而不是一个元素,而且,该Schema没有目标命名空间。
其次,忽略了serialVersionUID,这直观表明它是私有的,而且表明不将静态字段作为常规Java序列化的一部分来对其进行序列化。因此,即使字段不是私有的,但其静态状态足以使它不生成到Schema中。
使用了标准Schema类型xs:dateTime来表示java.util.Date对象,而且Schemagen让字段保持原有顺序。
要想稍微重新排列结果,可以使用标准的JAXB注解来修饰Java类,能够做出的修改有:
指定不同的生成文件名
指定不同的类名
指定命名空间
创建全局根元素
保留静态字段并维持其值
示例2-18给出的是同一文件,只不过现在为了实现新目标,在其中包含了JAXB注解。
示例 2-18:包含有注解的用来生成Schema的Product Java类
package com.soacookbook.ch02.Schemagen;
import java.util.Date;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
@XmlRootElement(namespace="com.soacookbook.ch02.Schemagen",
name="Product2")
@XmlType(namespace="com.soacookbook.ch02.Schemagen")
public class ProductAnnotated {
private static final long serialVersionUID = 12345L;
@XmlElement(defaultValue="1.0")
static String VERSION = "1.0";
private String name;
private Date mfrDate;
public ProductAnnotated() { }
//... getters and setters omitted
}
这次运行Schemagen后的输出结果是如示例2-19所示的单个文件。
示例 2-19:使用JAXB注解生成的Schema文件
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:Schema version="1.0"
targetNamespace="com.soacookbook.ch02.Schemagen"
xmlns:tns="com.soacookbook.ch02.Schemagen"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Product2" type="tns:productAnnotated"/>
<xs:complexType name="productAnnotated">
<xs:sequence>
<xs:element name="VERSION"
type="xs:string" default="1.0" minOccurs="0"/>
<xs:element name="mfrDate"
type="xs:dateTime" minOccurs="0"/>
<xs:element name="name"
type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:Schema>
让我们花几分钟看看各个注解是如何为Schema的创建贡献力量的。首先,XMLRootElement注解定义了根元素的命名空间,不过,实际上,光对复杂类型声明命名空间是不够的,因为该注解只适用于根元素,因此,必须在XMLType注解中重复该命名空间,以便告诉Schemagen元素和复杂类型在同一命名空间,这就生成了此处所见的单个文件。如果没有使用XMLType注解,Schemagen就会进行保守猜测,让复杂类型没有命名空间,然后,不得不将它放入一个单独的Schema文件中,并将类型导入到定义了元素的Schema中。
注意,使用XMLElement域级修饰符是为了在Schema定义中保留静态字段,不过,却没有考虑到它会被忽略,Schema中是没有“静态”概念可以保留的。
使用SchemaOutputResolver
基于Java类创建Schema的第二种方法是不使用命令行工具,而是编写能够实现这一目标的Java代码。这种方法的优点是可以在运行较大程序时使用这种方法。示例2-20给出的程序清单就是为ProductAnnotated.java文件编写了一个Schema。
示例 2-20:在运行时生成Schema的Java程序
package com.soacookbook.ch02.Schemagen;
import static java.lang.System.out;
import java.io.File;
import java.io.IOException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
/**
* Generates a Schema from a Java class.
*/
public class SchemaMaker {
SchemaOutputResolver resolver;
//run the show
public static void main(String...arg){
try {
Class[] classes = {ProductAnnotated.class};
new SchemaMaker().execute(classes);
} catch (JAXBException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
/**
* Creates an instance of SchemaMaker with defaults.
*/
public SchemaMaker() {
resolver = new MySchemaOutputResolver(".", "MySchema.xsd");
}
public void execute(Class...classes)
throws JAXBException, IOException {
JAXBContext context = JAXBContext.newInstance(classes);
context.generateSchema(resolver);
out.println("All done.");
}
}
/**
* Extends the resolver.
*/
class MySchemaOutputResolver extends SchemaOutputResolver {
private File output;
public MySchemaOutputResolver(String dir, String fileName){
output = new File(dir, fileName);
}
public Result createOutput(String namespaceUri,
String suggestedFileName) throws IOException {
return new StreamResult(output);
}
}
运行这一文件产生的结果与前面使用Schemagen和JAXB注解来生成Schema所取得的结果是一样的,不过优点是该方法在运行时能够得到更灵活地运用。
2.14 在Ant中从XML Schema生成Java源文件
问题
希望从Ant脚本调用XJC。
解决方案
针对XJC使用Ant任务,sun-appserv-ant.jar中提供了该任务。
讨论
在2.12小节中,读者知道了如何从命令行使用XJC来基于XML Schema生成Java源文件,但是,这不是一种非常实际的做法。理想的情况是,可以随意改变Schema,而且能够在构建服务时重新生成Java源代码。如果使用Ant来进行构建,这是轻而易举的事。
在构建脚本中,将定义Sun XJC包装类任务,不过需要首先在类路径中添加Glassfish的几个JAR。将如下条目添加到构建脚本的属性文件中:
sun.app.ant.jar=${glassfish.jars.dir}/sun-appserv-ant.jar
sun.ws.tools.jar=${glassfish.jars.dir}/Webservices-tools.jar
sun.ws.rt.jar=${glassfish.jars.dir}/jaxws-rt.jar
xjc.task.path=${sun.app.ant.jar}${path.separator}${sun.ws.tools.jar}${path.separator}${sun.ws.rt.jar}
这将使得Ant找到XJC和Ant包装类任务。接下来,在build.xml中,定义如下内容:
<taskdef name="xjc"
classname="com.sun.tools.xjc.XJCTask">
<classpath path="${xjc.task.path}"/>
</taskdef>
然后,在编译服务代码之前,调用该任务:
<target name="Schema-to-java">
<echo message="-----Generating Java sources from Schema-----" />
<xjc destdir="./src/gen">
<Schema dir="./src/xml/META-INF/wsdl" includes="**/*.Schemalet, **/*.xsd"/>
</xjc>
</target>
destdir属性指定XJC应该将生成的源文件写入哪个目录,Schema元素指向Schema所处的位置并指定生成时要包含哪些文件。
这个例子中包含了一个Schemalet源文件,用来定制绑定。有关这方面的详细介绍,请参见7.15小节。
2.15 从Schema生成XML文档实例
问题
希望采用一种编程方式从XML Schema生成XML文档实例。
解决方案
当然,有一些工具可以解决这一问题,例如,可以使用NetBeans 6或XML Spy来生成XML文档,不过,如果需要以编程的方式实现同样的目标,或者没有使用这些工具,则可以尝试使用免费Java工具XIG。
首先,从Sourceforge下载XIG,网址为http://xm-xig.sourceforge.net,当前版本是xml-xig-0.1.1。
需要将Schema名称和要为其创建实例的根元素的名称传递给XIG。此处,将指向Catalog Schema并基于searchResults元素生成一个XML文档,在命令提示处,运行如下JAR:
> java -jar xml-xig-0.1.1.jar "src/xml/META-INF/wsdl/Catalog.xsd" search Catalog.xig
XIG: Generating Template src/xml/META-INF/wsdl/Catalog.xsd.xig...
XIG: Instantiating src/xml/META-INF/wsdl/Catalog.xsd.xml...
创建了两个文件:Catalog.xsd.xig和Catalog.xsd.xm,我们将稍后介绍XIG文件。
下面是生成的Catalog.xsd.xml文件:
<search>
<firstName>?{string}</firstName>
<lastName>?{string}</lastName>
</search>
接着可以使用相应的值替换?{string}占位符。
让我们对这个源文件稍作修改,使得可以通过验证器运行它以确保XIG正确执行,在其中放置带有前缀的命名空间。在此处,Catalog.xsd作为实例文档位于同一目录中:
<s:search xmlns:s="http://ns.soacookbook.com/catalog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://ns.soacookbook.com/catalog Catalog.xsd">
<firstName>James</firstName>
<lastName>Joyce</lastName>
</s:search>
对于这样一个小Schema,这似乎不会带来很大变化,不过,还是带来了好处,Schema的大小和复杂性发生了改变,而且可以使用XIG来生成一系列的实例。
该XIG文件是一个模板,可以用来基于该类型生成大量的XML实例,而不仅仅是一个实例。而且,可以在必要时将运行时数据传递给该模板以提供不同的值。有关详细信息,可以查阅Sourceforge XIG网站,不过,默认生成的.xig文件如下所示:
<xig:template document='search' Schema='src/xml/META-INF/wsdl/Catalog.xsd' xmlns:xds='http://xml-xsd.sourceforge.net/Schema/XmlXsd-0.1'>
<search>
<firstName>${xs:string}</firstName>
<lastName>${xs:string}</lastName>
</search>
<xig:generate>
<!-- Generate instance documents from template document above -->
<loop count='10'>
</loop>
</xig:generate>
</xig:template>
如果希望使用不同的索引数据多次调用该文件,现在就可以这样做,这与JUnit 4.4中提供的参数化测试功能类似。
Relaxer
能够完成同样任务的另一种流行工具是Relaxer,它适用于XML Schema、RELAX NG、Relax Core和DTD,该工具也是可以免费下载的。有关教程和文档,请查阅http://www.asahi-net.or.jp/~dp8t-asm/java/tools/Relaxer。
2.16 定制从Schema生成Java类的方式
问题
不希望按照默认的方式来让JAXB生成Java源文件,需要定制这一操作。
解决方案
定义一个Schemalet文件,将它添加到XJC Ant任务的Schema元素的includes属性中。另一种方法是,将特定于JAXB的注解内嵌到XML Schema中。
对于将Schema映射到Java源代码这一困难任务,JAXB完成得非常出色,Schema元素名称映射到类型名称,命名空间映射到包,枚举直接映射到Java枚举。不过,有时,你希望覆盖默认操作,指定自己所需的映射。你可能希望使用XMLGregorianCalendar之外的其他日期类型,而XMLGregorianCalendar是JAXB为xs:dateTime Schema类型生成的日期类型。本小节介绍如何进行这方面的操作。
表2-3给出了XML Schema类型与Java数据类型之间的默认映射。
表2-3:XML Schema类型与Java数据类型之间的默认映射
XML Schema类型 Java数据类型
xs:string java.lang.String
xs:integer java.math.BigInteger
xs:int int
xs:long long
xs:short short
xs:decimal java.math.BigDecimal
xs:float float
表2-3:XML Schema类型与Java数据类型之间的默认映射(续)
XML Schema类型 Java数据类型
xs:double double
xs:boolean boolean
xs:byte byte
xs:QName and xs:NOTATION javax.xml.namespace.QName
xs:base64Binary byte[]
xs:hexBinary byte[]
xs:unsignedInt long
xs:unsignedShort int
xs:unsignedByte short
xs:time, xs:date, xs:dateTime,xs:gDay, javax.xml.datatype. xs:gMonth, xs:gMonthDay, xs:gYear, xs:gYearMonth XMLGregorianCalendar
xs:anySimpleType java.lang.String
xs:duration javax.xml.datatype.Duration
如果希望绑定编译器生成默认Java类型以外的其他Java类型,可以采用以下两种方法中的一种:将所需的类型编写在一个传递给绑定编译器的外部文件中,或者对XML Schema进行注解。
通常来说,你不需要这样做。要记住,构建的是SOA,而不仅仅是一个Java应用程序。Web服务的客户端需要能够执行合同指定的任何操作,这一点必须牢记在心。使用多形态类型或对Schema的外围功能做深入研究或许并不有助于构建SOA。
有四种绑定自定义范围,它们分别指定各自适用于哪些Schema组件,表2-4列出了这4种范围。
表2-4:绑定范围
范围 描述
全局(使用<globalBindings>) 适用于当前Schema中的所有Schema元素,以及当前Schema递归导入或包含的所有Schema
Schema(使用<SchemaBindings>) 适用于当前Schema的目标命名空间中的所有Schema元素
定义 适用于该绑定指定的元素,以及引用该类型定义的所有其他Schema元素
组件 只适用于该绑定指定的元素
这些范围可以相互覆盖,较特殊的范围通常覆盖与其有关的更一般范围。
接下来,让我们看看两种使用这些绑定的方法。
使用内嵌注解
定制生成代码的第一种方法是使用内嵌注解。为了让应用程序能够定制自身的环境,XML Schema特地提供了<annotation>的<appinfo>子元素,请看示例2-21。
示例 2-21:定制包含Schemalet文件的JAXB
<xs:Schema elementFormDefault="qualified" version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
jaxb:version="2.0"
targetNamespace="urn:Schemalet:calendar">
<xs:annotation>
<xs:appinfo>
<jaxb:globalBindings mapSimpleTypeDef="false"
choiceContentProperty="true">
<jaxb:javaType name="java.util.Date" xmlType="xs:date"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
<jaxb:javaType name="java.util.Date" xmlType="xs:dateTime"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
</jaxb:globalBindings>
</xs:appinfo>
</xs:annotation>
//...rest of Schema here
</xs:Schema>
在这个例子中,我们将所需命名空间http://java.sun.com/xml/ns/jaxb的前缀指定为jaxb,接着在appinfo代码段中,添加全局定制,使得当前Schema及其重用的任何Schema中出现的所有dateTime或date类型采用定制类型。也可以在此处添加不同范围的其他注解,并可以在注解中添加常规注释。
name属性指定要使用的目标Java类型,xmlType属性指定Schema中哪个类型将转换成该目标Java类型。在这个特定的JAXB类中,还指定了方法名称parseMethod和printMethod,它们将在解析和打印过程中被替换成相应的值。
提示: parseMethod和printMethod中指定的值必须与目标数据类型相对应,例如,假设要定制短绑定,应该使用如下方式:
printMethod="javax.xml.bind.DatatypeConverter.printShort"
parseMethod="javax.xml.bind.DatatypeConverter.parseShort"
在下一节中,我们将介绍如何在外部文件中实现同样目标。
使用外部文件(Schemalet)
与放在Schema本身中相比,将绑定定制注解放在一个外部文件中或许是一种更可取的方法。像许多事情一样,这种方法也具有优点和缺点,优点是Schema不会因Java和JAXB所特有的代码而变得混乱,不会将实现泄露给其他实现不可知的Schema文档,它会造成或大或小的问题,这取决于所处的环境。如果没有有关约束来阻止使用外部文件方法,我推荐优先采用该方法。它使Schema更易于阅读,清晰性较好,不会让企业中使用其他语言的实现者踌躇。不过,使用该外部方法的不足是定制只在解析过程中运行时才被实施到Schema,源Schema的读者完全不知可能会对由该Schema生成的代码进行重大改变。这也会出现或大或小的问题,取决于定制的程度和范围。
示例2-22给出了一个文件,有时它被适当地称为“Schemalet”,该文件只包含JAXB定制。xs:dateTime对象的默认Schema映射是XMLGregorianCalendar,基于某种原因,读者可能希望自己的代码改为使用java.util.Calendar或java.util.Date。或许API比过去难于使用,或者读者不希望在不必要的时候泄露类的XML起源。读者可以在Schemalet中进行有关指定,Schemalet是XML文件,如示例2-22所示。
示例 2-22:用于XML Schema绑定定制的外部文件
<!-- Changes dates and dateTimes to java.util.Date -->
<jaxb:bindings version="2.0"
xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
jaxb:extensionBindingPrefixes="xjc">
<jaxb:globalBindings mapSimpleTypeDef="false"
choiceContentProperty="true">
<jaxb:javaType name="java.util.Date" xmlType="xs:date"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
<jaxb:javaType name="java.util.Date" xmlType="xs:dateTime"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
</jaxb:globalBindings>
</jaxb:bindings>
该文件将xs:date和xs:dateTime都映射到java.util.Date对象,而不是映射到XMLGregorianCalendar,并将xs:time(以及其他内容)保持默认不变。将该文件以.xjb扩展名保存,以便绑定编译器可以自动找到它。
提示: 在外部文件中指定绑定定制所用的代码与指定内嵌注解所用的代码非常相似,实际上,<jaxb:globalBindings>元素中的代码是一样的,不过根元素不同,因为在外部文件中是<jaxb:bindings>,而在Schema中是<xsd:Schema>。
假定现有一个Library Schema,它定义了一个包含标题和到期日的book。指向Schemalet,而且,通过使用-b开关,读者可以指定针对某个目录查找其中所有带有.xjb扩展名的文件,如果使用的是其他扩展名,则可以指定文件名。在这里,我将该Schemalet命名为dateTime.xjb,并将它与LibraryBook.xsd放在同一目录下,
现在,让我们运行它,用-d表示希望将生成的类写入目录,给出要读取的Schema名称,并将-b用作绑定开关,此处是要告诉XJC查找当前目录中的所有.xjb文件:
>xjc -d . LibraryBook.xsd -b .
下面是编译结果:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"title",
"dueDate"
})
@XmlRootElement(name = "book")
public class Book {
@XmlElement(required = true)
protected String title;
@XmlElement(required = true, type = String.class)
@XmlJavaTypeAdapter(Adapter2 .class)
@XmlSchemaType(name = "date")
protected Date dueDate;
public Date getDueDate() {
return dueDate;
}
public void setDueDate(Date value) {
this.dueDate = value;
}
/...etc
}
我去掉了生成代码中的注释,读者或许已经注意到XmlTypeAdapter注解说明已使用Adapter2.class文件修改了该类型,在此处,JAXB所做的工作就是为实现定制而创建一系列的Adapter类,这些定制扩展了XML适配器类,对两类值进行了参数化:一种是JAXB已经知道如何处理的类型,另一种是bound类型。它定义了两个方法:marshal和unmarshal,这两个方法都是在运行时执行实际转换。
最后要注意的是,有时读者可能希望定制JAXB源代码生成,如果封装某些类型能够更好地封装Web服务层,则这样做是有意义的,但是,过度定制会使代码更难维护,而且可能导致互操作性问题,因此,尽管定制的作用非常大,而且易于使用,常常需要使用它来获得所需的结果,但是,要谨慎使用,不要在这条路上走得太远。要确保努力让程序保持灵活并最大化互操作性的实现机会,毕竟,这是Web服务的要点。
2.17 在编组和解组过程中根据Schema进行验证
问题
将Java对象编组到XML或将XML文档解组成Java对象时,希望在提出请求前验证所得到的XML是否与指定的Schema相符。
解决方案
使用JAXP验证,方法是在JAXB Marshaler中设置javax.xml.validation.Schema实例,
请看示例2-23。
示例 2-23:在解组过程中根据Schema验证XML实例
package com.soacookbook.ch02.jaxb;
import java.io.File;
import java.io.StringReader;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.*;
/**
* Sets a Schema onto the unmarshaller to validate.
*/
public class ValidateUnmarshal {
private static final String Schema =
"/home/ehewitt/soacookbook/repository/code/catalog/ws/" +
"src/xml/ch02/BookVenetianBlind.xsd";
public static void main(String...arg) {
try {
//Create context
JAXBContext ctx = JAXBContext.newInstance(Book.class);
//Create marshaller
Unmarshaller um = ctx.createUnmarshaller();
//Create instance of Schema
SchemaFactory factory =
SchemaFactory.newInstance(
XMLConstants.W3C_XML_Schema_NS_URI);
//Create factory, add options if necessary
Schema Schema = factory.newSchema(
new StreamSource(new File(Schema)));
//This sets us up for validation
um.setSchema(Schema);
//Read in the XML from anywhere
//In this case it is a complete XML book as string.
StringReader sr = new StringReader(getBookXml());
//Get XML from object.
//Now that we have a Schema set, this throws
//MarshalException if XML doesn't match XSD
JAXBElement<Book> b = um.unmarshal(
new StreamSource(sr), Book.class);
//We never get this far with invalid XML
//Start working with object
Book book = b.getValue();
System.console().printf("Title: %s", book.getTitle());
} catch (JAXBException ex) {
ex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//NOTE: THIS XML FAILS VALIDATION!
private static String getBookXml(){
return "<com.soacookbook.ch02.xstream.Book>" +
"<title>On Friendship</title>" +
"<price>39.95</price>" +
"<author><firstName>Jacques</firstName>" +
"<lastName>Derrida</lastName>" +
"</author><category>PHILOSOPHY</category>" +
"</com.soacookbook.ch02.xstream.Book>";
}
}
此处的验证是失败的。因为其中的XML最初是从XStream生成的,这使得包名预先考虑生成的XML中的根元素名,而此处的根元素是com.soacookbook.ch02.xstream.Book,这是不合法的,因此,会抛出javax.xml.bind.UNMarshalException,处理过程立即结束。下面是输出结果:
javax.xml.bind.UnmarshalException
- with linked exception:
[org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element 'com.soacookbook.ch02.xstream.Book'.]
所以,让我们继续,修正前面的XML内容,使其得以验证。注意,仅仅去掉包名并小写“book”元素是不够的,要想实际有效,该XML需要指定这些元素和类型的命名空间,就像Schema中指定的那样。不过,其中还有一些错误的内容。还需要将元素按正确的顺序放置,Schema中使用的<sequence>元素表示内容需要按指定的顺序排列,这是JAXB XmlType映射注解提供顺序属性的原因,因此,需要对该XML进行重新排列以符合正确的顺序。此外,author标记是无效的,因为这个特定Schema已将它指定为simpleType,因此它必须只包含一个字符串,而不包含子元素。
为了修正该XML,需要替换下部的getBookXml方法,该方法获得一个book XML实例,该实例根据前面定义的Venetian Blind Schema示例进行验证:
//This is valid according to the Schema
private static String getBookXml(){
return "<b:book xmlns:b='http://ns.soacookbook.com/venetianblind'>" +
"<title>On Friendship</title>" +
"<author>Jacques Derrida</author>" +
"<category>PHILOSOPHY</category>" +
"<price>39.95</price>" +
"</b:book>";
}
正如你可能希望的那样,无论编组还是解组,这个过程都是一样的。因此,使用简单Schema验证就可以了。在验证过程中,读者也可以使用更为高级的选项,比如解析器发出的收集事件,这样就能够获取更多信息,而不仅仅是知道是否通过验证,然后将该数据提供给调用程序。我们将在下一节中介绍这些高级选项中的部分选项,不过,一般性的使用只需要上面这些就够了。
如果读者希望探索关于创建Schema factory方面的更多选项,可以查阅下面这些方法:
factory.setErrorHandler(errorHandler)
factory.setFeature(string, value)
factory.setResourceResolver(resourceResolver)
factory.setProperty(prop, obj)
参见
7.13小节详细说明了一项读者可以部署并用来从其Schema生成JAXB类型的Web服务,接着就可以使用本节中的技术来验证调用该服务时所使用的Java对象。
2.18 在编组和解组过程中收集Schema验证事件
问题
希望在解组中了解更多有关Schema验证错误方面的信息,使得能够将更翔实的消息报告给用户,能够在验证失败事件出现时接收到这些事件,而且通常能够更好地控制验证过程。
解决方案
实现javax.xml.bind.ValidationEventHandler接口以执行定制操作并将它设置到marshaler或unmarshaler对象中,还可以尝试使用ValidationEventCollector实用程序。
该接口定义了一个方法:handleEvent,使得读者有机会执行自定义事件处理。要使用它,可以实现该接口,然后向marshaler或unmarshaler注册这一实现,marshaler或unmarshaler将使用注册的处理程序来收集验证事件。
提示: 验证处理程序不适合用来修改XML目录树,虽然这么做可能会吸引人。将处理程序看作一种只读操作器,如果有任何严重错误或运行时异常被抛出,它就必须返回false。
为了方便使用,javax.xml.bind包提供了ValidationEventHandler的两种实现:DefaultValidationEventHandler和ValidationEventCollector。如果读者没有通过调用setEventHandler方法自己在un/marshaler中设置处理程序,就会使用默认处理程序。当接收到验证警告时,该实现将继续进行处理,不过,一旦发现第一个错误,就会立即停止执行。
在验证过程中,handleEvent方法使用ValidationEvent对象来给出验证警告或错误,该对象给出事件的验证程度(WARNING、ERROR或FATAL_ERROR),来自处理器的消息以及抛出的任何异常。此外,还可以通过它的getLocator方法访问ValidationEventLocator对象,该对象使你可以获得大量数据,这些数据是有关导致事件发生的文件、URL或DOM节点的。同时还可以获得行号和列号,从而提供了非常具体的细节来帮助你查找何处存在错误。让我们将一些代码放在一起来说明有关使用。
编写一个处理程序实现,然后修改2.17小节中使用的类,使其接受该处理程序。最后,稍微打乱XML文档,以便使其发出警告或错误。在这里,我们将“b”前缀声明了两次:
<b:book xmlns:b='http://ns.soacookbook.com/venetianblind'
'xmlns:b='http://ns.soacookbook.com/x'> ...
示例2-24给出了处理程序的实现,该程序仅仅是收集一些数据并用某些信息设定了一个字符串的格式。也可以将信息收集到某个数据库中或在此处执行某种其他操作。
示例 2-24:自定义验证处理程序
class MyHandler implements ValidationEventHandler {
public boolean handleEvent(ValidationEvent event) {
int severity = event.getSeverity();
if (severity == ValidationEvent.WARNING) {
String msg = event.getMessage();
ValidationEventLocator vel = event.getLocator();
int line = vel.getLineNumber();
String warn = "**** WARNING! Msg is %s." +
"See source line %d.";
System.console().printf(warn, msg, line);
//print warning, but proceed.
return true;
} else {
String err = "**** Got an Error of type %s.";
System.console().printf(err,
event.getLinkedException().getClass().getName());
//ERROR--quit parsing
return false;
}
}
}
执行实际解组的main方法基本上是一样的,除了需要设置处理程序:
//Create marshaller
Unmarshaller um = ctx.createUnmarshaller();
//Create instance of Schema...
//Create instance of our custom handler
MyHandler myHandler = new MyHandler();
//Add it to unmarshaller
um.setEventHandler(myHandler);
//...
使用该处理程序会得到如下输出,指出不能两次指定同一前缀:
**** Got an Error of type org.xml.sax.SAXParseException.javax.xml.bind.UnmarshalException
- with linked exception:
[org.xml.sax.SAXParseException: Attribute "xmlns:b" was already specified for element "b:book".]
提示: 每次调用ValidationEventLocator可能不会得到全部这些数据。 读者可能有权或无权访问某些域,这取决于验证的种类。在解组过程中进行验证会产生有关XML源数据的一些信息,而在编组过程中进行特别验证或一般验证会给出有关Java对象的定位数据。例如,在这种情况下,不要期望能从getLineNumber方法获得太多信息。
ValidationEventCollector
正如前面提到的那样,该验证处理程序的工作方式是当遇到错误或致命错误时,就会立即停止处理。为了优化性能和避免不必要的工作,这或许就是读者想要的。当第一个错误出现时,你可以拒绝某个无法控制的文档,指示用户修正内容后再将它添加到SOAP消息主体来发送。
另一方面,你可能在跟踪用户是如何调用服务的,而且希望能够收集更多的后台信息,或许希望评估所有的警告和错误后再决定采取相应的措施,你可能使用自校正代码,这种代码根据接收到的载荷来发送数据,这意味着需要继续处理,即使已经接收到警告和错误,这就是需要使用ValidationEventCollector的地方。
它的工作方式是:执行处理,不过在对文档进行解析时,继续收集警告和错误,处理将最终完成。一旦有关验证、编组或解组的调用返回,就会调用getEvents方法将收集到的事件归总,它们将成为ValidationEvent对象组成的数组。
本章小结
在本章中,我们将XML Schema作为SOA消息设计的基础进行介绍的。对于使用WSDL实现Web服务接口的SOA来说,Schema非常重要。由于WSDL使用XML Schema,Schema就成为服务接口的一个重要方面。我们介绍了多种设计和使用Schema的方法,还介绍了各种工具,比如XJC和XIG,以及如何使用它们来简化Web服务开发周期。
从下一章开始,我们将研究用于XML文档处理的新工具,这会让读者拥有坚实的基础来在创建SOA过程中处理消息载荷。
第3章
使用XML和Java
3.1 概述
本章提供一些有用的技术来按照各种不同的方式处理XML数据,对于创建SOA来说,它们特别重要。我们还将介绍最新Java API提供的一些XML处理方法。作为Java编程人员,我们已经在过去的多年里采用各种不同的方法来处理XML,因此,我将略去一些基本内容。我假定读者以前使用过SAX和DOM,从而能够开始学习新的内容。此外,本章的目标是解释如何有效使用XML和数据绑定,尤其是在SOA中。
不过,并没有假定每个架构师都将采用XML来设计SOA。如果读者决定将使用EJB或RMI来在纯粹的Java环境中实现所有服务,也是可以的,不过,将XML用作消息交换模式是一种非常流行的方法,而且这是很合理的。SOA是有关灵活集成的,尽管遗留数据可以采用各种格式,但是,如果可以将它转换成XML,就能够将遗留数据移到任何其他地方,对遗留数据进行转换以及赋予它新的生命。在我看来,XML说明了什么是SOA的核心消息:接受不同。IBM的Midrange iSeries计算机的正职就是运行20年之久的COBOL程序,现在附带有可以直接使用的SAX解析器。纯粹的XML数据库得到了一定程度的流行,比如Xindice和Berkeley XML DB。支持SQL 2003后,Microsoft SQL Server、Oracle和JDBC 4.0将XML作为一等公民对待,不再要求需要将XML数据转换成CLOB。将XML用作转换层后,就可以总是选择合适的工具来开展工作,而不用将自己局限于如何在一堆无法交流的应用程序进行选择,也不用听从于某个供应商有关自己IT堆栈应该是什么样的言论。因此,使用XML来进行消息处理是本书采用的常规方法。
此外,有许多选项可用于XML与Java之间的生成,我们将介绍其中的几个,包括JAXB和XStream。还有其他流行的框架,包括Castor和Apache的XMLBeans项目。不过,在处理Java堆栈时,使用JAXB是比较方便的,因为它已经增加了Java SE和JAX-WS参考实现,这样可以自动继承性能改进和功能。令人鼓舞的是,这些技术将一起发展,虽然会出现一些小的冲突,但仍和谐共存。通常来说,将依赖关系尽可能地减少看起来是一个不错的主意,但这不意味着不应该使用最好的工具来完成必须要做的工作。每个人都有异常的时候,我(以及我知道的其他所有人)仍使用Log4J,尽管Java从1.4就已经具有自己的日志API。“接受不同”导致的必然结果是使用合适的工具来完成工作。
3.2 读取XML数据流
问题
希望通过快速流访问XML文档,由于数据集太大而无法实现DOM,希望提供比SAX更有选择性的API。
解决方案
使用Java SE 6中的StAX API来对文档进行一种“拉”机制的解析。
讨论
Java已向我们提供了多种处理XML文档的方法,包括流行的DOM和SAX。最近增加了StAX方法,即Streaming API for XML,它主要是由Oracle/BEA提出。尽管这三种解析XML的方法都有优点,但它们也都有缺点。
StAX是当前最有效的XML处理方法,因此特别适合于处理复杂流程,比如数据绑定和SOAP消息。就像Glassfish v2一样,Oracle/BEA的WebLogic 9和10在应用服务器内部使用该分析器。
DOM提供了一个易于使用的API,与SAX和StAX相比,它的优势在于支持XPath,不过,它也迫使将整个文档读入存储器中,这对于小文档来说没什么,但会影响大文档的性能,而对于非常大的文档来说,这是根本禁止的。一家欧洲银行网络通常在自己的SOA中传输多个XML大文件,这些文件不使用DOM进行处理。
在另一方面,SAX通过作为一种“推”机制的解析器来处理该问题,也就是说,对于该解析器在文档中遇到的每种结构,都会生成相应的事件,程序员可以选择自己感兴趣的事件进行处理,不足之处在于SAX通常生成的大量事件是程序员并不关心的。而且,SAX API不提供迭代文档处理,从头至尾摧毁整个事件。在这种模型中,解析器控制文档处理。
StAX API提供的控制与Java I/O RandomAccessFile类似——我们可以跳过文档的某些部分,处理文档的片段,暂停和恢复处理,或随时停止处理。使用这种“拉”处理模式,应用程序控制文档的处理方式,并通过指定自己感兴趣处理的条目来发挥这种控制,接着,解析器将这些条目从事件流中拉出来。
流解析器可以处理自由格式的文档,但读者需要事先知道希望处理的内容:需要告诉解析器你所希望“拉”的内容。
不过,StAX创建的信息集是非常小的,可以直接作为垃圾收集的候选对象。这让XML处理任务占用较小的空间,使得它不仅适用于小型堆设备,比如移动电话,而且适用于长期运行的服务器端应用程序。
与SAX不同,StAX能够对XML文档进行写操作,这减少了需要处理的API数量。除了读写XML数据外,StAX还使用两种不同的解析数据模型:光标模型和迭代器模型。
使用StAX光标模型:XMLStreamReader
在这里,XMLStreamReader接口是主角。使用该接口,我们可以读取与文档结构以及事件流内容有关的任何信息。为了接收事件,可以使用hasNext方法来确定是否还有其他事件要读取,并使用next方法来获得下一事件的整型标记。使用该标记,可以切换不同类型的解析事件,如果当前事件所代表的内容就是自己感兴趣的,就执行相应的操作。
可以捕获以下XMLEvent子接口的有关事件,其中的每个子接口代表文档结构的一个不同方面:
CDATA
CHARACTER
COMMENT
DTD
START_DOCUMENT
END_DOCUMENT
START_ELEMENT
END_ELEMENT
ENTITY_DECLARATION
NAMESPACE
NOTATION_DECLARATION
PROCESSING_INSTRUCTION
SPACE
光标在文档中从开始处至末尾处向前移动,不断指向沿路遇到的各个条目。如果读者曾使用SAX,对此应该比较熟悉。
让我们从一个非常简单同时又定义得相当不完善的XML文件示例开始,并将该XML文件作为解析的基础:
<catalog>
<book sku="123_xaa">
<title>King Lear</title>
<author>William Shakespeare</author>
<price>6.95</price>
<category>classics</category>
</book>
<book sku="988_yty">
<title>Hamlet</title>
<author>William Shakespeare</author>
<price>5.95</price>
<category>classics</category>
</book>
<book sku="434_asd">
<title>1984</title>
<author>George Orwell</author>
<price>12.95</price>
<category>classics</category>
</book>
<book sku="876_pep">
<title>Java Generics and Collections</title>
<authors>
<author>Maurice Naftalin</author>
<author>Phillip Wadler</author>
</authors>
<price>34.99</price>
<category>programming</category>
</book>
</catalog>
这样,我们就有了一个包含一系列图书的目录,它可能用于销售。每本书都包含一个标识符,一个标题,一位或多位作者等信息。catalog.xml文件最复杂的部分在于author元素是可选的,只有存在多位作者时才使用。
示例3-1中的程序说明了如何使用StAX中的光标解析方法。就像实际查找作者一样,在此处,author对象存储在一个TreeSet中,Tree部分用于正常排序,Set部分用来确保唯一性。
示例 3-1:使用StAX中的光标解析法
package com.sc.ch02.stax;
import static java.lang.System.out;
import java.io.InputStream;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.XMLEvent;
public class StaxCursor {
private static final String db = "/ch02/Catalog.xml";
//we'll hold values here as we find them
private Set<String> uniqueAuthors;
public static void main(String... args) {
StaxCursor p = new StaxCursor();
p.find();
}
//constructor
public StaxCursor() {
uniqueAuthors = new TreeSet<String>();
}
//parse the document and offload work to helpers
public void find() {
XMLInputFactory xif = XMLInputFactory.newInstance();
//forward-only, most efficient way to read
XMLStreamReader reader = null;
//get ahold of the file
final InputStream is =
StaxCursor.class.getResourceAsStream(db);
//whether current event represents elem, attrib, etc
int eventType;
String current = "";
try {
//create the reader from the stream
reader = xif.createXMLStreamReader(is);
//work with stream and get the type of event
//we're inspecting
while (reader.hasNext()) {
//because this is Cursor, we get an integer token to next event
eventType = reader.next();
//do different work depending on current event
switch (eventType) {
case XMLEvent.START_ELEMENT:
//save element name for later
current = reader.getName().toString();
printSkus(current, reader);
break;
case XMLEvent.CHARACTERS:
findAuthors(current, reader);
break;
}
} //end loop
out.println("Unique Authors=" + uniqueAuthors);
} catch (XMLStreamException e) {
out.println("Cannot parse: " + e);
}
}
//get the name and value of the book's sku attribute
private void printSkus(String current, XMLStreamReader r) {
current = r.getName().toString();
if ("book".equals(current)) {
String k = r.getAttributeName(0).toString();
String v = r.getAttributeValue(0);
out.println("AttribName " + k + "=" + v);
}
}
//inspect author elements and read their values.
private void findAuthors(String current, XMLStreamReader r)
throws XMLStreamException {
if ("author".equals(current)) {
String v = r.getText().trim();
//can get whitespace value, so ignore
if (v.length() > 0) {
uniqueAuthors.add(v);
}
}
}
}
reader的getText方法给出事件值,getAttributeValue方法使用时用一个整数指定希望获得其值的属性的索引。
运行该程序将获得如下结果:
AttribName sku=123_xaa
AttribName sku=988_yty
AttribName sku=434_asd
AttribName sku=876_pep
Unique Authors=[George Orwell, Maurice Naftalin, Phillip Wadler, William Shakespeare]
在这个示例中,我们感兴趣的是作者(它们自身就是元素)和SKU值(它们是book元素的属性)。保存该循环每次迭代的当前节点的名称,这样就可以将两种处理方法中的情况进行对照。
提示: 通常来说,只需使用Java SE 6附带的StAX实现就够了,不过,值得注意的是,Sun已经提供了可以单独下载的StAX实现,网址为http://sjsxp.dev.java.net/。该实现基于Xerces 2,而且非常懒散(对于解析器来说是好事)。市面上还有其他的StAX实现,比如Oracle提供的StAX实现。
使用StAX迭代器模型
在两种模型中,迭代器API比较灵活,而且易于扩展。
让我们用另一种StAX模型——迭代器——来解析刚才定义的Catalog.xml文档,如示例3-2所示。
示例 3-2:使用StAX迭代器读取XML
public class StaxIterator {
public void find() {
XMLInputFactory xif = XMLInputFactory.newInstance();
//forward-only, most efficient way to read
XMLEventReader reader = null;
//get ahold of the file
final InputStream is =
StaxIterator.class.getResourceAsStream(db);
try {
//create the reader from the stream
reader = xif.createXMLEventReader(is);
//work with stream and get the type of event
//we're inspecting
while (reader.hasNext()) {
XMLEvent e = reader.nextEvent();
if (e.isStartElement()){
e = e.asStartElement().getAttributeByName(
new QName("sku"));
if (e != null){
out.println(e);
}
}
} //end loop
} catch (XMLStreamException e) {
out.println("Cannot parse: " + e);
}
}
}
执行该程序将给出如下输出结果:
sku='123_xaa'
sku='988_yty'
sku='434_asd'
sku='876_pep'
正如你所看到的那样,这两种解析模型非常相似,只是处理事件的方法稍有不同。
3.3 编写XML数据流
问题
希望使用一种快速的新API来编写XML文档。
解决方案
使用Java SE 6中的StAX API。
本节说明如何使用XMLStreamWriter和XMLOutputFactory来快速将一些XML信息放在一起并通过StAX光标API将其写入到一个文件中。请看示例3-3。
示例 3-3:使用XMLEventWriter编写XML文件
package com.sc.ch02.stax;
import static java.lang.System.out;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
public class WriteStax {
private static final String REPAIR_NS = "javax.xml.stream.isRepairingNamespaces";
private static final String NS = "http://ns.example.com/books";
public static void main(String... args) {
XMLOutputFactory factory = XMLOutputFactory.newInstance();
// autobox
factory.setProperty(REPAIR_NS, true);
try {
//setup a destination file
FileOutputStream fos =
new FileOutputStream("result.xml");
//create the writer
final XMLStreamWriter xsw = factory.createXMLStreamWriter(fos);
xsw.setDefaultNamespace(NS);
//open the document. Can also add encoding, etc
xsw.writeStartDocument("1.0");
xsw.writeEndDocument();
xsw.writeComment("Powered by StAX");
//make enclosing book
xsw.writeStartElement("book");
xsw.writeNamespace("b", NS);
xsw.writeAttribute("sku", "345_iui");
//make title child element
xsw.writeStartElement(NS, "title");
xsw.writeCharacters("White Noise");
xsw.writeEndElement(); //close title
xsw.writeEndElement(); //close book
//clean up
xsw.flush();
fos.close();
xsw.close();
out.print("All done.");
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
} catch (XMLStreamException xse) {
xse.printStackTrace();
}
}
}
该API非常灵活,允许按照不同程度的规范化和合法性来编写XML。可以快速、清晰地生成这样的XML片段:适合于传输到SOAP主体的有效载荷中或其他任何希望粘贴某种标记的地方。
执行该程序将给出如下结果:
<?xml version="1.0"?>
<!--Powered by StAX-->
<book xmlns="http://ns.example.com/books" xmlns:b="http://ns.example.com/books"
sku="345_iui">
<b:title>White Noise</b:title>
</book>
还可以使用setPrefix方法来指定元素使用的前缀。将factory的javax.xml.stream.isRepairingNamespaces设置成true,这样就可以将默认命名空间添加到根元素:
<book xmlns="http://ns.example.com/books" xmlns:b="http://ns.example.com/books"
sku="345_iui">
如果没有设置factory的javax.xml.stream.isRepairingNamespaces,或者将它设置成false,就会省去默认命名空间,使得book元素如下所示:
<?xml version="1.0"?>
<!--Powered by StAX-->
<bookxmlns:b="http://ns.example.com/books" sku="345_iui">
//...
一般来说,在两种模式中进行抉择时,如果希望能够修改事件流和采用更灵活的API,就选择迭代器。如果希望得到更快的可行性能和更小的空间,就使用光标API。
3.4 过滤XML流中的数据
问题
在读取XML文件时,希望通过过滤StAX事件流来滤去不感兴趣的事件,从而实现效率的最大化。
解决方案
对于数据流,只实现javax.xml.stream.StreamFilter接口并将它传递给文件输入流的XMLStreamReader构造对象,使用FilteredReader封装XMLStreamReader。对于事件流,使用EventFilter实现封装XMLEventReader。此处可以采用Decorator设计模式,就像典型的标准Java I/O库。
前面的StAX光标示例不错,不过可以更有效率一些。接收时,它捕获了大量的我们不感兴趣的事件。可以使用过滤器来提高应用程序的性能和清晰度,方法是指示解析器只提供我们所感兴趣的事件。
当使用光标时,要做的所有事情就是实现StreamFilter接口的accept方法,然后使用它构造XMLStreamReader。当使用EventReader时,要做的所有事情就是实现EventFilter接口的accept方法。示例3-4给出了这是如何完成的。
示例 3-4:StaxFiltered.java
package com.sc.ch02.stax;
import static java.lang.System.out;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.HashMap;
import java.util.Map;
import javax.xml.stream.*;
import javax.xml.stream.events.XMLEvent;
public class StaxFiltered {
private static final String fdb = "path/ch02/Catalog.xml";
private Map<String, Double> expensiveBooks;
private String lastTitle;
//constructor
public StaxFiltered() {
expensiveBooks = new HashMap<String, Double>();
}
public static void main(String[] args) {
StaxFiltered p = new StaxFiltered();
p.findByEvent();
}
/*
* Here our aim is to find book prices over $10. So we use a
* filter to give us only start elements so we have already
* filtered out items we know don't help us.
*/
public void findByEvent() {
try {
XMLInputFactory xif = XMLInputFactory.newInstance();
FileReader fr = new FileReader(fdb);
// wrap the XMLStreamReader with FilteredReader
XMLEventReader reader =
xif.createFilteredReader(
xif.createXMLEventReader(fr),
new StartElementEventFilter());
// work with stream and get the type of event
// we're inspecting
while (reader.hasNext()) {
XMLEvent event = (XMLEvent) reader.next();
int eventType = event.getEventType();
switch (eventType) {
case XMLEvent.START_ELEMENT:
findHighPrices(reader, event);
}
} // end loop
out.println("Expensive books=" + expensiveBooks);
} catch (FileNotFoundException fnfe) {
out.println("Cannot find source: " + fnfe);
} catch (XMLStreamException e) {
out.println("Cannot parse: " + e);
}
}
private void findHighPrices(XMLEventReader reader,
XMLEvent event) throws XMLStreamException {
String currentElem = event.asStartElement().toString();
// save off the title so we can match the price with it
if ("<title>".equals(currentElem)) {
lastTitle = reader.getElementText();
}
// get the current price and add to collection if high
if ("<price>".equals(currentElem)) {
double price;
try {
price = Double.parseDouble(reader
.getElementText());
if (price > 10.0D) {
expensiveBooks.put(lastTitle, price);
}
} catch (NumberFormatException nfe) {
nfe.printStackTrace();
} catch (XMLStreamException xse) {
xse.printStackTrace();
}
}
}
}
/**
* Get only start elements for efficiency. If we returned only
* attributes, for example, we wouldn't be able to read the data
* we're interested in here (title and price values).
*/
class StartElementEventFilter implements EventFilter {
// only req'd method to implement
public boolean accept(XMLEvent event) {
return event.isStartElement();
}
}
这会导致如下输出结果:
Expensive books={Java Generics and Collections=34.99, 1984=12.95}
3.5 从XML文档选择值
问题
解析后,需要从XML文档中只选择要处理的某些值。
解决方案
使用Java内置的XPath API来搜索符合条件的元素和属性。
在以前版本中,提供该语言是为了执行XPath操作,不过,Java 5添加了更新的XPath API,这些库现在更易于使用,而且更稳定。
对于SOA工作来说,XPath很重要,因为它在两个关键方面非常有用:当在服务器端供应器中使用SAAJ API和在服务编排中使用BPEL指定时,对SOAP消息载荷搜索某些值。我们将在后续章节中详细讨论,眼下重要的是对XPath能够处理的事情种类有比较好的认识。
示例3-5中的程序清单给出了如何解析XML文档和编译各种XPath表达式来查找不同的值。此处将使用本章前面介绍的Catalog.xml文件。
示例 3-5:BasicXPath.java
package com.sc.ch02.xpath;
import static java.lang.System.out;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
/**
* Accepts an XPath expression to perform searching against
* the Catalog.xml document.
*/
public class BasicXPath {
public static void main(String...args) throws Exception {
String xmlSource = "src/xml/ch02/Catalog.xml";
//Get all titles with price between $5 and $9.99
xpath = "//book[price > 5.00 and price < 9.99]/title";
/* Prints:
* Value=King Lear
Value=Hamlet
*/
search(xmlSource, xpath);
}
public static void search(String fileIn, String xpathExp)
throws IOException {
// Set up the DOM parser
DocumentBuilderFactory docFactory =
DocumentBuilderFactory.newInstance();
try {
//Parse XML document
DocumentBuilder docBuilder =
docFactory.newDocumentBuilder();
Document doc = docBuilder.parse(fileIn);
//Create XPath instance
XPath xpath = XPathFactory.newInstance().newXPath();
//Evaluate XPath expression against parsed document
NodeList nodes = (NodeList) xpath.evaluate(xpathExp,
doc, XPathConstants.NODESET);
//We could return these instead to let caller deal
for (int i = 0, len = nodes.getLength(); i < len; i++) {
Node node = nodes.item(i);
String value = node.getTextContent();
out.println("Value=" + value);
}
} catch (XPathExpressionException xpee) {
out.println(xpee);
throw new IOException("Cannot parse XPath.", xpee);
} catch (DOMException dome) {
out.println(dome);
throw new IOException("Cannot create DOM tree", dome);
} catch (ParserConfigurationException pce) {
out.println(pce);
throw new IOException("Cannot create parser.", pce);
} catch (SAXException saxe) {
out.println(saxe);
throw new IOException("Error parsing XML document.", saxe);
}
}
}
多数情况下,这是你在SOA中需要做的事情。不过,这非常有用,因为可以通过用户提供的值或其他运行时环境供应对象(比如某个系统属性)来创建表达式字符串。
XPath表达式中的注入攻击
允许用户直接填充XPath表达式时要极为小心。像SQL语句一样,XPath表达式非常容易受到注入攻击。通过直接破坏结构,或通过揭示远超于原本打算返回的节点,注入攻击可以颠覆数据库(无论是基于SQL的数据库还是XML文档数据库)。
最基本的一种注入攻击形式是在主表达式中插入一个永远为true的表达式。例如,假定XPath表达式字符串允许用户提供一个值,好比以前用JDBC语句执行该操作:
"//book[price <" + uservalue + "]/title"
这看起来一点问题都没有,用户可以提供“6”来获得低于6美元的所有书籍的标题,但是,这是一个糟糕的想法,因为它让你易于受到注入攻击。设想一下,如果用户提供的值是or 1 = 1。
将返回所有的书籍。眼下,你的书籍标题示例或许不是一个大买卖,然而,当处理金融数据、用户凭据或其他敏感信息时,这可能是灾难性的。
修正方法就是认真验证输入值。例如,在上面的示例中,如果对用户提供的值调用Double.parseDouble,就会抛出NumberFormatException。
XPath不仅适用于SOAP消息数据提取和BPEL指定,它还是处理各种其他XML规范的基础,包括XPointer、XQuery和XSLT。
讨论
本讨论为那些不太熟悉XPath的读者提供XPath的一些进一步特征,详细说明如何创建高级XPath表达式。
在该解决方案示例中,我们主要对元素和属性值感兴趣。除了这些以外,XPath还能够处理以下文档节点:
根
文本
注释
处理说明
命名空间
名称XPath反映了XPath的用途:它定义一种表达式语言,用来创建指向XML文档树结构中任意一组节点的路径。使用XPath表达式,可以产生如下类型的结果:
节点集(与表达式匹配的一组无序独特节点)
字符串
布尔值
浮点数
XPath定义了一些基本的导航构建块,可以将这些构建块组合起来创建地址表达式。表3-1列出了一些最基本的选择器。
表3-1:XPath选择器
/ 当作为表达式的首字符时,它选择根节点,根节点是文档元素的父元素。位于后续位置时,它作为元素分隔符
. 选择当前节点
.. 选择当前节点的父节点
@ 选择一个或多个属性
// 选择所有后续元素,无论这些元素在文档结构中的位置如何。在路径中使用时,表示元素是前面指定元素的直接或非直接子孙
XPath还提供一组操作符,其用途正如我们所希望的那样(表3-2)。
表3-2:XPath操作符
and,or 根据布尔含义选择
=,!= 等于,不等于
<,>,<=,>= 小于,大于,小于等于,大于等于
| 二选一
使用这些元素和一些基本的操作符,可以创建各种搜索。让我们来看一些示例:
//Find the book titled 'Hamlet' and select its price.
String xpath = "/catalog/book[title='Hamlet']/price";
//Prints: Value=5.95
//Find titles of books with multiple authors
xpath = "/catalog/book[authors]/title";
//Prints:
Value=Java Generics and Collections
//Find all title AND price elements
xpath = "//title | //price";
//Prints:
Value=King Lear
Value=6.95
Value=Hamlet
Value=5.95
Value=1984
Value=12.95
Value=Java Generics and Collections
Value=34.99
//Get the author of the second book on the list
xpath = "//book[2]/author";
//Prints:
Value=William Shakespeare
//Get the SKU attrib value of the last book on the list
xpath = "//book[last()]/@sku";
//Prints:
Value=876_pep
//Get the entire book node for Hamlet
xpath = "//book[title='Hamlet']";
//Prints the entire Hamlet node
//Get the penultimate (one before the last) book that is a 'classic'
//whose price is between $5 and $10.
xpath = "//book[category='classics' and (price > 5 and price < 10)][last()-1]";
//Prints the entire King Lear node
//Get the title node with the value of Hamlet
xpath = "//title[.='Hamlet']";
//Get the title of the first book whose author starts with 'William'
xpath = "//book[1][author[starts-with(., 'William')]]/title";
//Prints: Value=King Lear
//Gets the categories of the books after the 2nd one in the tree
xpath = "//book[position() > 2]/category";
//Gets any authors that are co-authors,
//that is, all <author> nodes under an <authors> node
xpath = "//authors//author";
//Prints:
Value=Maurice Naftalin
Value=Phillip Wadler
正如你看到的那样,操作符的优先级可以使用括号指定。虽然对XPath的深入讨论超出了本书的范围(而且需要花费很多时间),但是,读者可以阅读http://www.w3.org/TR/xpath中提供的完整规范,或只参阅其中的更多字符串功能。
3.6 更新XML文档中的值
问题
希望改变XML文档元素或属性的值,XML文档放在文件系统中,需要保留这种改变。
解决方案
解析文档,使用XPath来查找希望改变的值,改变节点的文本内容,然后使用带有Transformer的StreamResult将结果写出到文件。
解决方案如示例3-6所示。
示例 3-6:UpdateXMLValue.java
package com.sc.ch02.xpath;
import static java.lang.System.out;
import java.io.FileWriter;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class UpdateXMLValue {
public static void main(String[] args) throws Exception {
String xmlSource = "src/xml/ch02/Catalog.xml";
//find the book titled 'Hamlet' and select its price.
String xpath = "/catalog/book[title='Hamlet']/price";
//this is the new price
String value = "8.95";
//we're throwing any exception out
updateValueInXmlFile(xmlSource, xmlSource, xpath, value);
out.println("All done.");
}
public static void updateValueInXmlFile(String fileIn,
String fileOut, String xpathExpression,
String newValue) throws IOException {
// Set up the DOM evaluator
final DocumentBuilderFactory docFactory =
DocumentBuilderFactory.newInstance();
try {
final DocumentBuilder docBuilder =
docFactory.newDocumentBuilder();
final Document doc = docBuilder.parse(fileIn);
final XPath xpath =
XPathFactory.newInstance().newXPath();
NodeList nodes =
(NodeList) xpath.evaluate(xpathExpression,
doc, XPathConstants.NODESET);
// Update the nodes we found
for (int i = 0, len = nodes.getLength(); i < len; i++) {
Node node = nodes.item(i);
node.setTextContent(newValue);
}
// Get file ready to write
final Transformer transformer =
TransformerFactory.newInstance()
.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT,
"yes");
transformer.setOutputProperty(OutputKeys.ENCODING,
"UTF-8");
StreamResult result =
new StreamResult(new FileWriter(fileOut));
transformer.transform(new DOMSource(doc), result);
// Write file out
result.getWriter().flush();
result.getWriter().close();
} catch (XPathExpressionException xpee) {
out.println(xpee);
throw new IOException("Cannot parse XPath.", xpee);
} catch (DOMException dome) {
out.println(dome);
throw new IOException("Cannot create DOM tree", dome);
} catch (TransformerConfigurationException tce) {
out.println(tce);
throw new IOException("Cannot create transformer.",
tce);
} catch (IllegalArgumentException iae) {
out.println(iae);
throw new IOException("Illegal Argument.", iae);
} catch (ParserConfigurationException pce) {
out.println(pce);
throw new IOException("Cannot create parser.", pce);
} catch (SAXException saxe) {
out.println(saxe);
throw new IOException("Error reading XML document.",
saxe);
} catch (TransformerFactoryConfigurationError tfce) {
out.println(tfce);
throw new IOException("Cannot create trx factory.",
tfce);
} catch (TransformerException te) {
out.println(te);
throw new IOException("Cannot write values.", te);
}
}
}
对于非常大的文档来说,这或许不合适,但可以快速、轻松地引入到某个需要立即更新实际文件中相应值的程序中。
3.7 将Java对象转换成XML文档实例
问题
现有一个希望转换成XML文档的Java对象。
解决方案
使用JAXB Marshaler类并调用它的静态方法。
无论是从头开始编写Java类还是从Schema生成Java类,都可以使用JAXB来将填充的Java对象改写成XML表示。这种XML表示可以采取以下某种形式:
javax.io.Writer
javax.xml.stream.XMLStreamWriter
javax.xml.stream.XMLEventWriter
javax.xml.transform.Result
org.xml.sax.ContentHandler
org.w3.dom.Node
各种处理编组的方法赋予我们很大灵活性,可以将XML结果写出到文件,使用StAX写出到流,作为转换结果写出,或作为DOM节点写出。使用Node目标是非常有意思的,因为它允许你将编组的对象作为指定节点的子节点来创建,将它作为该对象的一部分来使用。在为某项Web服务请求组合SOAP主体时,这会特别有用。
不能从一个不带有注解的常规POJO(Plain Old Java Object,简单Java对象)着手并开始编组,需要具有相应的JAXB XML注解修饰Java类之后才能这样做。请回顾一下,当从Schema开始生成Java类时,这些是自动添加的。如果拥有一个POJO且不想从Schema构建,则只需使用XMLRootElement注解来对主组合对象进行修饰就可以了。
有两种方法可以初始化JAXBContext:使用一组包名或使用一个类数组。第一种构造方法接受一个字符串,该字符串以冒号分隔的形式列出包名。列表中的各个名称必须包含JAXB注解修饰的类,或者必须包含通过JAXB从Schema得到的类,它还可以包含用于处理的包级别注解。第二个构造方法接受一个类数组,其中给出希望JAXB编组或解组的类。注意,不必指定复合中的所有类,指定正在复合的类就够了,JAXB将编组剩余的所有类。
示例3-7是一个基本示例,给出了如何执行编组以及打印到控制台以查看结果。为了说明JAXB可以独特地处理对象复合,我们将创建一个要编组的示例Book类,该类包含一个Author类型、一个Category类型、一个title字符串和一个double类型的price。
示例 3-7:要编组成XML的Book类
package com.soacookbook.ch02.jaxb;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Book {
private String title;
private double price;
private Author author;
private Category category;
//... getters and setters omitted.
}
class Author {
private String firstName;
private String lastName;
//... getters and setters omitted.
}
enum Category {
LITERATURE,
PHILOSOPHY,
PROGRAMMING
;
}
示例3-8说明了如何将Book类实例编组成XML。结果被发送到输出流,在本例中,就是标准输出。
示例 3-8:使用JAXB将Book对象编组成XML并将它打印到控制台
package com.soacookbook.ch02.jaxb;
import static java.lang.System.out;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
public class MarshalToConsole {
public static void main(String...arg) {
try {
Book book = new Book();
Author a = new Author();
a.setFirstName("Jacques");
a.setLastName("Derrida");
book.setAuthor(a);
book.setPrice(34.95);
book.setTitle("Of Grammatology");
book.setCategory(Category.PHILOSOPHY);
Class[] c = {Book.class};
JAXBContext ctx = JAXBContext.newInstance(c);
Marshaller m = ctx.createMarshaller();
//could also use System.out here
m.marshal(book, out);
out.println("\nAll done.");
} catch (JAXBException ex) {
ex.printStackTrace();
}
}
}
下面是打印到控制台的结果(为了增加可读性,我添加了空格和换行符):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book>
<author>
<firstName>Jacques</firstName>
<lastName>Derrida</lastName>
</author>
<category>PHILOSOPHY</category>
<price>34.95</price>
<title>Of Grammatology</title>
</book>
All done.
注意,此处可以获得Book类实例句柄并将其传递给JAXBContext来创建编组环境。创建该环境还有另外一种方法,需要将一个字符串传递给构造方法,用以指定包含要处理类型的包的名称。
在示例3-9中,为了简单起见,我们将结果打印到控制台。让我们看看编组代码行的输出目标位置的一些随意替换。在各种情况下,编组(即生成的内容)是一样的,要改变的只是生成的位置。
示例 3-9:编组到DOM节点
//Create Document
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
//Send marshal result to Document
m.marshal(book, doc);
//Find a value
String title = doc.getDocumentElement().
getElementsByTagName("title").item(0).getTextContent();
System.console().printf("Read %s now!", title);
/*
Prints: Read Of Grammatology now!
All done.
*/
有时,我们希望将对象编组成XML并直接将结果写出到某个文件,示例3-10给出了这种情况,该示例与示例3-9非常相似,只是稍微改动了一下。
示例 3-10:编组到新文件
m.marshal(book, new FileOutputStream(new File("aBook.xml")));
这会在当前目录的文件系统中创建一个新文件。
提示: 对marshaler实例设置以下属性,以缩进的形式优美打印XML结果:
m.setProperty("jaxb.formatted.output", true);
该属性默认情况下为false。
此外,示例3-11使用StringWriter类将XML类型编写成字符串。
示例 3-11:使用StringWriter将编组结果存储在字符串中
StringWriter sw = new StringWriter();
m.marshal(book, sw);
System.out.println(sw);
JAXBContext是一个抽象类,因此,供应商可以按照自己认为合适的方式实现它。JAXB不仅灵活,而且易于使用。不过,还有其他工具可用于做这类事情,包括XStream、XMLBeans和Castor。在这些工具中,XStream非常快,不要求对类使用注解,因此,在某种程度上说,它比JAXB更灵活。Castor是一种更常规的框架,支持永久映射,但对严格绑定方面限制稍微多一些。
XMLBeans最初是由BEA编写,最终捐献给Apache项目。最新发行的2.3版诞生于2007年6月,该版本包含XQuery支持。
要了解更多的其他方法,还可以查阅Betwitx以及使用XML的常规Java序列化。
3.8 将XML文档实例转换成Java对象
问题
现有一个希望转换成Java对象的XML文档实例。
解决方案
使用JAXB的包含JAXBElement<T>的Unmarshaller类。
通过JAXBElement<T>,可以指定在不必事先编写任何映射的情况下希望将XML内容解组的类。也就是说,使用JAXBElement时,在解组过程中,不必对XML文件使用注解,可以将希望在其中存放结果的对象类指定为类型参数。
可以从各种资源获得XML,而不仅仅是依靠最初从JAXB编组的那些对象或那些有机会注解的对象。例如,你可能希望允许相当宽松的对象结构解释,使得指定的根元素名称不同于实际类中的名称。
在XML序列化过程中,XStream在包名的前面生成一个根元素,因此,最终得到了<com.soacookbook.ch02.xstream.Book>的根元素,而不是将Book对象序列化到<book>元素中。虽然你和我知道XML文档将成为一个Book对象,但实际执行操作的工具很难知道这一点。幸运的是,这些工具相当聪明。
提示: 序列化和编组之间存在截然不同。术语“序列化”用于表示对象到输出流的简单转换,而此处的输出流恰巧是XML格式。编组也生成XML格式,它是通过绑定独特地完成操作的,是以注解映射的形式通过JAXB实现的。
示例3-12使用JAXBElement<T>来解组完全在JAXB外部生成的XML文档。此处没有使用XML注解,该Book就是一个POJO。此外,甚至首先没有使用JAXB来进行编组,使用的是XStream。
示例 3-12:使用JAXBElement解组XML字符串
package com.soacookbook.ch02.jaxb;
import java.io.StringReader;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.Document;
public class UnmarshalWithElement {
public static void main(String...arg) {
try {
//Create context
JAXBContext ctx = JAXBContext.newInstance(Book.class);
//Create marshaller
Unmarshaller um = ctx.createUnmarshaller();
//Read in the XML from anywhere
//In this case it is a complete XML book as string.
StringReader sr = new StringReader(getBookXml());
//Get XML from object
JAXBElement<Book> b = um.unmarshal(
new StreamSource(sr), Book.class);
//Start working with object
Book book = b.getValue();
System.console().printf("Title: %s", book.getTitle());
} catch (JAXBException ex) {
ex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static String getBookXml(){
return "<com.soacookbook.ch02.xstream.Book>" +
"<title>On Friendship</title>" +
"<price>39.95</price>" + //etc...
}
}
输出结果正如你所希望的那样:
Title: On Friendship
就像看到的那样,JAXBElement使你能够非常灵活地处理XML和Java对象。
3.9 从XML文档生成Schema
问题
现有一个XML文档实例,希望快速为其生成一个有效的Schema。
解决方案
使用Trang,可以从http://www.thaiopensource.com免费下载该工具。
下载的文件经过解压后,可以按照如下方式调用Trang:
>java -jar C:/programs/trang/trang.jar C:/repository/src/xml/ch02/Catalog.xmlCatalog.xsd
将会运行Trang程序并要求传入两个输入值:希望为其生成Schema的XML文件的名称和希望Trang创建的输出Schema文件的名称。
假定使用3.3小节中的Catalog.xml文件作为输入,Trang生成如下Schema:
<?xml version="1.0" encoding="UTF-8"?>
<xs:Schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="catalog">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" ref="book"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="book">
<xs:complexType>
<xs:sequence>
<xs:element ref="title"/>
<xs:choice>
<xs:element ref="author"/>
<xs:element ref="authors"/>
</xs:choice>
<xs:element ref="price"/>
<xs:element ref="category"/>
</xs:sequence>
<xs:attribute name="sku" use="required" type="xs:NMTOKEN"/>
</xs:complexType>
</xs:element>
<xs:element name="title" type="xs:string"/>
<xs:element name="authors">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" ref="author"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="price" type="xs:decimal"/>
<xs:element name="category" type="xs:NCName"/>
<xs:element name="author" type="xs:string"/>
</xs:Schema>
讨论
正如你看到的那样,Trang擅长于从XML源推断相应的值。一本书可以有一位作者,也可以有多位作者,Trang解决了如何使用choice元素和添加有关参考。
值得注意的一件事是Trang中使用的Schema设计模式可能不是你刚好想要的,这取决于使用的情况。上面示例中的元素都是全局的,而且位于默认的命名空间。
处理SOA时,的确需要尽力确保互操作性。因此,可能需要使用选项来禁用抽象类型,如下所示:
-o disable-abstract-elements
通常来说,如果可以,你希望从Schema开始。有时没有那么奢侈,而且出于SOAP载荷或要处理的对象的需要,有时是更快、更易于编写XML实例,然后生成一个可以稍作(或大幅度)调整的Schema,以便使其符合所需的设计模式。
还可以使用Trang生成DTD和Relax NG——这实际上是它的最初用途。不过,在这里,我们没有过多关注XML Schema,因为它是使用得最为广泛的表示有效XML结构的方法。
从单个XML文档生成一组Schema
或许你现有的一个XML文档定义了多种命名空间,你希望Trang为各个命名空间生成相应的Schema。也可以这样做。
所不能做的是只为元素添加前缀,然后期望Trang提供一些样板默认值。例如,如下消减是不合适的:
<a:address>
<a:street>1212 Some Street</a:street>
<a:city>Washington,DC</a:city>
<a:state>VA</a:state>
</a:address>
答案非常明确简单。只需要为前缀提供命名空间定义,然后正常使用Trang,就像前面示例所做的那样:
<a:address xmlns:a="urn:ns:soacookbook:address">
<a:street>1212 Some Street</a:street>
<a:city>Washington,DC</a:city>
<a:state>VA</a:state>
</a:address>
这样做将会为定义的各个前缀命名一个新XSD文档。
参见
2.9小节。
3.10 不使用JAXB将XML转换成Java
问题
前一小节中的示例强制你指定要处理的类型以及需要包含XML注解形式映射的类型,如果在SOA中是使用JAXB来从Schema开始生成Java,让JAXB提供映射等,那么,这不会有问题。但是,或许你不是这种情况,或者希望采用一种更常规的解决方案来实现Java和XML之间的转换。
解决方案
试试XStream,它是一种开源项目,可从http://xstream.codehaus.org得到。它非常易于使用,可定制,而且它对完整的对象图执行序列化。
可以使用示例3-13中的代码来实现一般意义的Java和XML之间的转换。它包含两个方法:第一个方法接受任意类型的对象,只要该对象实现了java.io.Serializable就行,并使用XStream将该对象转换成XML字符串。第二个方法接受之前序列化成XML的字符串并根据它创建一个Java对象。
示例 3-13:使用XStream实现Java与XML之间的转换
package com.soacookbook.ch02.xstream;
import com.soacookbook.ch02.jaxb.*;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.Serializable;
import static java.lang.System.out;
/**
* Shows Java to XML back to Java with no mapping
* using XStream.
*/
public class XMLStreamRoundTrip<T extends Serializable> {
public static void main(String...arg){
//Create a complex object to work with
Book book = new Book();
Author a = new Author();
a.setFirstName("Jacques");
a.setLastName("Derrida");
book.setAuthor(a);
book.setPrice(39.95);
book.setTitle("Glas");
book.setCategory(Category.PHILOSOPHY);
//Put the book into XML
XMLStreamRoundTrip<Book> x = new XMLStreamRoundTrip<Book>();
String bookXml = x.toXml(book);
//Print entire XML
System.console().printf("XML:\n%s\n", bookXml);
//Create a new object by rehydrating the XML
Book newBook = x.fromXml(bookXml);
//Show values
System.console().printf("Object:\n%s costs $%s\n",
newBook.getTitle(), newBook.getPrice());
}
public String toXml(T model) {
return new XStream().toXML(model);
}
@SuppressWarnings("unchecked")
public T fromXml(String modelAsString) {
XStream xstream = new XStream(new DomDriver());
T model = (T)xstream.fromXML(modelAsString);
return model;
}
}
这无法更简单。代码的大部分是设置book对象本身,类底部的两个方法实现所有的XStream操作。输出结果就像你能期望的那样:
XML:
<com.soacookbook.ch02.xstream.Book>
<title>Glas</title>
<price>39.95</price>
<author>
<firstName>Jacques</firstName>
<lastName>Derrida</lastName>
</author>
<category>PHILOSOPHY</category>
</com.soacookbook.ch02.xstream.Book>
Object:
Glas costs $39.95
注意,在XSteam构造方法中,传递了一个DomDriver对象。也可以将其省去,在类路径中放入XPP。XPP代表ThoughtWorks XML Pull Parser(也可以从http://xstream.codehaus.org免费下载),它已进行效率优化。
3.11 在JAXB中自定义代码生成
问题
希望在解组过程中,对于JAXB如何生成Java代码获得高级控制。例如,想要JAXB添加hashcode和equals方法到生成的类中,或者是toString方法。
解决方案
查阅https://jaxb2-commons.dev.java.net中提供的JAXB插件,或者自己编写。
在编写的时候,JAXB 2 commons项目提供了16种插件和应用程序,它们扩展了JAXB功能。这些插件和应用程序位于jaxb2-commons站点,如果你要自己编写对象而不是生成这些对象,使用这些插件和应用程序会有助于你完成编写对象过程中通常要做的一些有用事情。例如,有一个插件是用于通过驼峰匹配生成标识符,还有一个插件用于创建有关字段的属性监听器。
这些插件易于使用。只要从网站下载相应的JAR,然后将它和必要的插件选项一起添加到XJC的类路径中就可以了。下面是一个有关使用属性监听器注入插件的调用示例:
>xjc -cp property-listener-injector.jar -Xinject-listener-code Book.xsd
如果所列的插件没有提供你想要的功能,可以尝试自己编写。为此,首先查阅需要基于的JAXB实现源代码,然后扩展com.sun.tools.xjc.Plug-in以编写插件类,该类定义一系列的方法,为了与XJC交互,编写的插件需要实现这些方法。
接下来,需要将类包装成JAR并使用Service Provider Interface (SPI)向运行时环境说明其存在,Java 1.4内部引入了Service Provider Interface,不过到Java 6才被公开提供。在JAR的META-INF/services目录中创建一个名叫com.sun.tools.xjc.Plugin文本文件,在该文件中,键入实现插件接口的类的名称。运行时,它将作为一个实现类来执行。
然后,需要添加注解或外部绑定来指定Schema应该使用这种自定义,有关这方面的信息,请参见2.16小节。
最后,像上面显示的那样调用XJC,或者,如果使用的是Ant,像往常一样调用它,不过要进行两处改动。首先,需要将XJC任务定义为使用该插件;其次,调用时将插件的有关参数传递给XJC任务:
<taskdef name="xjc" classname="com.sun.tools.xjc.XJCTask">
<classpath>
<pathelement path="/path/jaxb-xjc.jar"/>
<pathelement path="/path/plugin.jar" />
</classpath>
</taskdef>
...
<xjc ...>
<arg value="-Xinject-listener-code" />
</xjc>
3.12 在Linux上查找包含给定类的JAR
问题
你正在Linux平台上开发,知道需要放到类路径中的类名称,但不知道它位于哪个JAR。典型的Java EE 5应用服务器附带有许多JAR,使得这成为一种困难情形。
解决方案
执行示例3-14中的命令,它将检查当前目录中的所有JAR文件并将其内容打印到一个名为tmp的文件。
示例3-14:在JAR中查找Java类
>find . -name "*.jar" -print -exec jar -tvf {} \; > tmp
得到的文件如下所示:
./appserv-deployment-client.jar
0 Tue Apr 24 07:21:00 MST 2007 META-INF/
449 Tue Apr 24 07:20:58 MST 2007 META-INF/MANIFEST.MF
0 Tue Apr 24 06:45:36 MST 2007 com/
0 Tue Apr 24 06:45:34 MST 2007 com/sun/
0 Tue Apr 24 06:45:34 MST 2007 com/sun/enterprise/
0 Tue Apr 24 06:45:42 MST 2007 com/sun/enterprise/admin/
0 Tue Apr 24 06:45:42 MST 2007 com/sun/enterprise/admin/util/
1821 Tue Apr 24 06:45:28 MST 2007 com/sun/enterprise/admin/util/HostAndPort.class
上面列出了其中一部分内容。每个JAR将被命名,后跟包含的所有包和类。接着就可以使用less工具来快速跳到相应的类名称处。
讨论
单单对需要处理的JAR开始采用Web服务时,这是非常有用的。
你只希望将实际需要的JAR放在类路径中,而不包含众多可能要用又可能不用的经过修改的bundled版内容。这样的项目比较凌乱,很难维护。例如,如果build时调用xjc任务,却报告说无法找到类“com/sun/enterprise/cli/framework/InputsAndOutputs.class”,你就无法继续工作。需要找到这个类,可以辨别该名称,它是由Sun提供的,因此可以开始浏览Glassfish库目录,使用强制方法逐一打开JAR并对其进行目测。
但是,如果打开Glassfish(也可以是WebLogic 10或WebSphere)的库目录,将会看到许多JAR。确定哪个JAR包含所需的类将是一项令人厌烦、耗时的任务。你不知道该类实现什么操作,甚至根本不关心该类,只是需要XJC Ant任务正常工作,其中的某个类显然需要另外的一个类,而后者又需要上面查找的类。不过,将存在的所有JAR都放在类路径中不是一种好的做法——这会影响速度并引起误解,无法有助于你了解内在发生的事情。在我看来,无论怎样,依赖IDE都不太好。你应该能够从命令行执行Ant,build仍应该成功。
就像前面提到的那样,每个JAR将被命名,后跟包含的所有包和类。因此,然后可以使用less工具找到相应内容。在less工具中,键入:
>/InputsAndOutputs
这将搜索InputsAndOutputs类的内容,查找并突出显示相应的文本(假定包含所查找类的JAR实际位于运行该命令的目录中)。
或者,可以使用Ctrl+G到达文件的底部,然后搜索该文件中第一次出现“.jar”的位置。它将是包含所查找类的JAR文件的名称。运行:
>?.jar
将会突出显示./admin-cli.jar,它实际上就是包含InputsAndOutputs.class的Glassfish JAR文件,因此,现在可以将其放入类路径中并继续build。
如果你不知道哪个JAR文件包含要查找的类,这种方法比较容易在Linux上找到相应的文件。假如你不希望将“tmp”文件遗留,不要忘记将其删除。“tmp”文件可以非常大,这取决于运行命令时所处的目录。
3.13 透明替换XML文件
问题
代码通过远程地址引用XML文档,比如WSDL或XML Schema,但你希望代码自动透明替换本地保存的远程资源。或者,你已经为某个WSDL定义了占位符位置,希望运行时替换实际值。
解决方案
使用XML目录。
讨论
XML目录包含一个或多个定义逻辑结构的文件,而逻辑结构映射一组XML实体。XML目录涉及两种基本方案:将外部实体的public或系统标识符映射到URI和将一个URI引用映射到另一个URI引用。
XML目录的使用具有以下几个原因:
无连接访问
即使无法连接到定义远程资源的网络,XML目录允许应用程序继续工作。如果你在无法上网时使用膝上型电脑,并且正在开发使用办公场所支持的Schema的Web服务,则可以使用XML目录来将本地Schema用远程Schema替换掉。接着就可以继续开发,而不用改变应用程序中指向这些远程资源的代码。
性能
在性能方面,XML目录也是占有一席之地。应用程序可以使用目录来避免昂贵的远程WSDL文档调用。
软件开发生命周期
当在开发、QA、分段运输和生产过程中移动代码时,你会发现XML目录有助于解析映射到现有生产资源的新QA实体。XML目录可能还会使大团队受益,这取决于如何配置环境。
提示: 在某些情况下,让XML Schema位于中心存储库是一种SOA最佳实践。通过使用XML目录,使我们在实际使用这类方法时具有灵活性,因为XML目录添加了一个间接层,有助于我们透明移动、维护和缩放应用程序组件。
为了支持XML catalogs 1.1(它是OASIS发布的一种规范),需要提供JAX-WS实现。JAX-WS支持以处理引擎的形式出现,它读取目录并解析位置映射。当JAX-WS部署遇到一个XML文件引用,目录中又存在该文件的一个实体映射时,运行时就会用引用名称替换映射的名称。换句话说,引擎接收输入,比如应用程序代码引用的WSDL文档的远程URL,接着检查目录中是否存在该URL的映射条目,如果找到,就静静地替换作为映射目标的实际文档。在Web服务中,它们通常就是这样使用的,不过它们也可以用于映射外部实体的替换文本。
在JAX-WS部署中使用XML目录需要以下几个步骤:
1. 下载希望替换的远程文件到本地。
2. 创建用于将远程资源名称映射到本地替代名称的XML目录文件。
3. 将该文件保存为jax-ws-catalog.xml。
4. 用部署外层包装该文件。如果是WAR,就将目录文件直接放入Web-INF。如果是EAR,则直接放入META-INF。
提示: 你可以阅读http://www.oasis-open.org/committees/download.php/14809/xml-catalogs.html中提供的XML目录规范,它不是太长。
XML目录实体
XML目录独立于应用程序和供应商,它们不是特定于Java的,不过它们是按照适用于任何平台来定义的。
提示: 不要将同一条目映射到不同的资源,引擎将永远使用找到的第一个资源。
下面是一个示例。假定你有一个WSDL,位于http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl,现有一个单独的项目实现了基于该WSDL的客户端程序,而你不希望自己的应用程序每当客户端代码遇到该WSDL时就调用相应的远程资源。定义一个XML目录来将该WSDL位置映射到你所存储的带有客户端部署外层包装的本地WSDL。可以首先将本地WSDL(及其导入的任何外部Schema)存储到一个名为的src/xml目录中,然后部署时让编制脚本包含该WSDL和Web-INF目录中的Schema。
提示: 此处我使用的是一个将远程文档映射到本地文档的示例,但这不是使用目录的唯一方法,还可以将一个XML文档映射到另一个重新定义它的文档。
生成的Web服务客户端程序将定义注解,如下所示:
@WebServiceClient(name="CatalogServiceSN",
targetNamespace = "http://ns.soacookbook.com",
wsdlLocation = "http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl")
public class CatalogServiceSN extends Service { ... }
因此,将要使用的目录文件会替换WSDL文件及其导入的单个Schema(该Schema定义了getTitle和getTitleResponse元素),如示例3-15所示。
示例 3-15:使用system的XML目录
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog" prefer="system">
<system systemId="http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl"
uri="src/xml/CatalogServiceSN.wsdl"/>
<system systemId="http://localhost:8080/soaCookbookWS/Library.xsd"
uri="src/xml/Library.xsd"/>
</catalog>
该目录文档具有几个重要元素,接下来我们将介绍这些元素。
<system>元素是主角,它把通过systemID属性值引用的文档映射成uri属性的值,这样,在该示例中,“http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl”的任何应用程序引用将被替换成“src/xml/CatalogServiceSN.wsdl”。
提示: 目录文件中定义的相对URI通常是相对于目录文件本身的位置而言的。
<public>元素的构造与<system>非常相似,它将自己publicId属性指定的给定公有标识符与自己uri属性的值相映射:
<public publicId="somePublicId" uri="someUri"/>
<uri>元素将一个URI映射到另一个URI,而且这样做时不考虑是system还是public标识符解决方案,它具有如下形式:
<uri name="http://www.oasis-open.org/committees/docbook/"
uri="file:///projects/oasis/docbook/Website/"/>
根元素catalog声明命名空间并提供一个prefer属性,有效值可以是public或system,该属性指定当两者都存在时,应该优先选择哪种类型的标识符。让我们看看两种可行的方案。如果目标文档同时包含public和system标识符,而目录只包含public映射,且prefer=public,则将使用public映射。但是,如果目标文档只包含system标识符,而目录只包含public映射,且prefer=public,则没有任何效果。
在wsimport中使用XML目录
wsimport工具定义-catalog <filename>选项,你可以通过该选项在导入过程中指定XML目录文件的路径,这会导致生成的类使用该目录来解析外部实体引用。
Ant任务包装器具有一个相应的属性,如下所示:
<wsimport
wsdl="${wsdl.url}"
catalog="jax-ws-catalog.xml"
destdir="${gen.classes.dir}"
sourcedestdir="${src.gen.dir}"
keep="true"
extension="false"
verbose="true" >
<binding dir="${binding.dir}" includes="${binding.file}" />
</wsimport>
参见
Norman Walsh的文章,地址是https://jax-ws.dev.java.net/nonav/2.1.1/docs/catalog.html。
本章小结
在本章中,我们介绍的是XML文档,在基于XML的SOA中,XML文档组成了基本的交换单元。在前一章中,我们讨论了如何设计和使用Schema,而在本章中,我们将研究扩展到了通过XPath查询XML文档,使用StAX读写XML文档,以及使用JAXB来实现XML实例和Java对象之间的转换。我们还介绍了各种有助于按照多种有用方式处理XML的行业工具和开源工具。
从下一章开始,我们将转向Web服务,介绍如何编写Web服务客户端程序。
第二部分
Web服务
第4章
准备工作
4.1 概述
JAX-WS,即Java API for XML Web Services,提供了三种基本的方法,用于连接到Web服务:Dynamic Invocation、Proxy和SAAJ。前两个隐藏了内在处理XML管道的复杂性,通过第三种方法,你可以完全访问XML视图中的SOAP信封,而且可以采用几种不同的方法来控制SOAP请求调用。
鉴于这种灵活性,可以轻松地使用JAX-WS客户端来调用Web服务,但是,首先需要克服的困难是识别三种API之间的不同以及确定它们各自适用的场合。基本的需求和响应存在许多普通变体,它们可以变得相当复杂。一旦需要修饰某种需求(参见6.12小节),或者希望向SOAP消息添加MIME附件或自定义头,或者异步调用客户端程序,就需要完成一些操作。
关于Web服务,最初令人迷惑的事情是命名方式。例如,你或许认为Service类用于创建Web服务实现,但不是这样的,相反,它表示客户端的Service Endpoint Interface,用作你实际希望调用的服务端点的代理。另一个可能令人迷惑的地方是存在大量的类和注解,根据所采用的方法,你通常不会直接使用这些类和注解。如果你只是浏览Java EE 5 API中的JavaDocs,或许不知道自己是否打算对Provider注解的同一个类编写@WebService注解(不打算),或者如果Services对客户端执行操作,Provider实现是否也这样(不这样)。不过,这就是本章所要介绍的内容。
本章是为提供各种各样的便利而编写的,它是有关如何设置一些工具以及了解公开的Web服务前景的。在第一章中,我们介绍了一些常见SOA概念和术语,而本章关注的是Web服务所特有的一些概念,比如SOAP和WSDL。我们收集了部署和使用服务时需要使用的一些工具,创建了第一个Hello World Web服务,最后简短介绍了有助于调试的监控工具。
如果你有兴趣阅读支持当今Java Web服务开发的一些规范,向你介绍如下几种:
JSR 181
Web Services Metadata for the Java Platform。提供了许多注解,可将这些注解组合起来并用于部署Web服务。
JSR 224
Java API for XML-Based Web Services (JAX-WS 2.1)。替代JAX-RPC作为当今创建Web服务的方法,与SAAJ相比,它操作的是更高级别的对象视图。
JSR 109
Web Services for Java EE。定义Web服务的编程模型。
JSR 67
SOAP with Attachments API for Java (SAAJ 1.3)。与JAX-WS提供的对象视图相对,该规范描述如何在更低级的XML视图创建和使用Web服务。
本书介绍了各种其他规范,比如有关SOAP、OASIS XML Catalogs和JBI的规范,不过,它们是Sun的Web服务代码规范。本章假定你对SOAP已有基本了解。图4-1说明了基本API是如何组合在一起的。
Java Web服务的世界大且复杂,而且十分迷人。让我们赶紧进入吧!
4.2 使用公开的Web服务进行测试
问题
希望通过使用已建立的现有实际Web服务WSDL来进行测试,去除复杂Web服务开发中的一些不定因素。这使得眼下只关注客户端部分。
解决方案
使用某些场所(比如StrikeIron.com或XMethods.com)公开提供的一些免费Web服务。
你可以在以下这些场所中查找WSDL来测试自己的客户端:
图4-1:JAX-WS的世界
Web Service X
从该场所开始查找会相当不错,它所提供的Web服务都是免费的,还带有部分文档,不需要注册,而且这些服务的调用都非常简单和直接。此外,它们都是用.NET编写的,因此,你将立即体会到Web服务的实际好处(并发现一些有待完善的地方)。可以通过http://www.Webservicex.net获得它们。
StrikeIron.com
如果提供电子邮件地址,你可以调用某项服务五次。如果给出完整注册信息,你可以获得25次;此后,他们希望你购买服务。注意,StrikeIron不是试验台,他们通过让开发者和组织购买他们的服务用于生产来挣钱。这些服务具有实际的内在实现并能完成实际工作,比如计算出某个确定地址的地理编码(纬度和经度),但是,此处提供的服务是真实的,带有需要通过头传入认证信息的重要接口,而且Schema定义许多复合对象。所有这些使得开始使用StrikeIron提供的服务要难一些。图4-2显示了一个StrikeIron.com服务文档页面样例。
图4-2:StrikeIron Web服务文档页面
XMethods.com
此处不需要注册,而且许多服务的使用都很简单,不过,这些服务不是XMethods本身编写的,XMethods只是提供它们,因此,各项服务具有自己的使用条件,而且它们的实现和说明方式也是多种多样。此处的服务是由各个开发者以及盈利性公司(比如CDyne和StrikeIron)提供的。图4-3显示了XMethods所提供服务的列出方式。
CDyne.com
与StrikeIron类似的另一家Web服务提供商,也提供一些免费的服务供开发者使用,如果你提供0作为凭据密钥,就会像在StrikeIron处一样获得有限使用。供开发者免费使用的服务位于http://www.cdyne.com/free/default.aspx。
FedEx.com
注册后,你可以试用Ship Manager Web服务。
图4-3:XMethods的Web服务列表
Amazon.com
Amazon提供的Web服务放在http://aws.amazon.com中。在这个页面中,你可以作为开发者进行注册,浏览解决方案目录,阅读白皮书等。提供的服务包括存储服务(S3)、虚拟服务器(EC2)和数据库使用(SimpleDB)。
通过http://code.google.com提供了各种API,不再给出基于SOAP的Web服务的密钥,所提供的服务主要是关于JSON和类似技术。
讨论
Web服务非常复杂,尽管它们是实现跨平台集成的强大工具,而且Sun和Microsoft等公司在使用这些服务实现实际互操作性方面取得重大进展,但Web服务仍然非常难。它们包含过多的细节,需要非常仔细。如今,Web服务的现实是平台以及平台版本之间存在太多的细小差别,而且存在不同的引擎和IDE实现。在这样一种环境中,如何开始呢?
要想使众多Web服务这一药丸更易于吞咽的一种方法是减掉一半的工作量。可以使用一些免费提供的公开Web服务,并且只关注让客户端正常工作,而不是自己编写服务然后再使用它。这样,你就有机会从一个方面了解错综复杂的情况,看看客户端是如何处理已经测试的服务,从而至少可以开始对某一部分比较自信。
公开服务注册中心遭遇了什么?
SAP、Microsoft和IBM都提供了公开服务注册中心。在本世纪第一个十年的前期,这些公司和其他一些公司都曾规划过发展UBR,即Universal Business Registry,其观念与EJB的初始意图类似。回想一下,EJB的景象是开发者创建组件,然后以“现货供应”的方式公开提供这些组件以供其他人购买和使用。比如,我们打算创建Cart EJB并出售,使得前端开发者可以轻松使用我们的组件,我们的组件在他们的Java应用程序中就能管用。UBR的景象与这非常相似。SAP、Microsoft和IBM都提供了公开注册中心,在其中,开发者可以创建服务,上传包含WSDL和一些文档的服务,接着,前端开发者或合成者可以浏览和购买自己所需的服务。你可以选择某个开发者的Cart Web服务,另外一个人提供的Tax Calculator Web服务以及另一方提供的Shipping Calculator服务。然后,只需要生成调用这些服务的客户端代码,很快,你就处于电子商务之中。
不知什么原因,这方面发展得不是很顺利。Microsoft和IBM一起撤销了其公开的统一描述、发现和集成(UDDI,Universal Description, Discovery, and Integration)注册中心。SAP的注册中心还保留,如图4-4所示,在一个被明显忽视的服务器上蹒跚着前行,遗留下来提醒大家从未实现全球商务招蜂引蝶的显赫业务前景。
这些注册中心遭遇了什么?简而言之,没有人观察鸡舍。善意的开发者将自己新近创建的Web服务上传到这些公开注册中心,主要是为了测试JAX-R(Java API for XML Registries)的使用。该API让你发布一个Web服务,或者浏览UDDI或ebXML注册中心提供的Web服务。随着somedude@yahoo.com提供的成百上千名为“test2”的服务,这些注册中心变得混乱。不足为奇,兴趣就减退了。而且,越来越明显的是,UDDI规范中的某些倾向使得企业很难验证大部分自动的“服务发现”流程。虽然运行时仍选择服务,但通常是基于已通过审查的私有注册中心中的一个服务集。因此,这些公开注册中心结束了它们的命运,就像以前昙花一现的EJB现货供应市场。
最后,企业变得更久经世故,设立自己的注册中心供内部使用,在防火墙后发布自己的服务供自己的团队或选定的业务伙伴浏览和使用。这就是SOA中注册中心如今的主要使用方式。
不过,像Amazon、Google和eBay等公司提供了一些强大的服务作为自己平台的一部分,包含的功能不仅涉及到业务核心,而且是对其业务平台的展示。或许我们将来会看到更多这种模式。
图4-4:SAP的公开UDDI 3注册中心
就像前面讨论的那样,可以免费使用公开访问的Web服务对刚刚开始研究Web服务的开发者来说真的是一件好事,同时,要记住,在有些情况下,为了实际业务使用的需要,你事实上可以购买服务,比如使用StrikeIron、Amazon、Google和eBay提供的服务。不仅如此,这些公司在Web服务领域都是先驱者。这两种因素加起来可以形成一些相当复杂、冗长的WSDL。如果你仍处于Hello World阶段,会因这些服务及其各种选项和认证机制的使用而崩溃。从XMethods开始或许会简单些,不过,其中提供的许多服务是由一些善意的人开发的,而这些人可能使用的是较老版本的服务平台,你可能会遇到不真正符合自己规范的服务或者服务没有实现互操作。因此,如果你发现自己相当长的时间没找着门,那么,所使用的服务可能是不合适的。
4.3 安装Metro
问题
希望使用最新的Java API来更轻松地开发可互操作的Web服务。
解决方案
安装Metro,https://metro.dev.java.net是将其作为一种免费的开源项目提供的。
讨论
Glassfish 9.1 update 1包含Metro 1.0.1。Java SE 6和Glassfish中包含的JAX-WS Reference Implementation提供了一组丰富的API,可用于生成和使用Web服务,包含WS-Addressing和MTOM功能。Metro扩展了这一基础平台的功能,但没有锁定供应商。
提示: WSIT,即Web Services Interoperability Technology,就是确保JAX-WS应用程序与来自符合Basic Profile 1.1的其他平台的Web服务和客户端之间进行互操作。WSIT和JAX-WS 2.1捆绑在一起包含在Glassfish中,WSIT技术不基于任何JSR,同样也不作为其他应用服务器的插件,不过,其他供应商(比如Oracle WebLogic 10.3)提供了大致等同的技术。实际上,Oracle的WebLogic实现使用了Glassfish和Metro开源项目的大量代码。
将Metro看作是两个基本层。核心层提供JAX-WS RI,并实现以下关键Web服务规范以提升互操作性:WS-I Basic Profile、WS-I Attachments Profile和WS-Addressing。WS-IT,即Web Services Interoperability Technology,在Windows上通过.NET 3.0和3.5 Web服务平台提供互操作性。如果你只下载Glassfish而不是单独的Metro 1.1,就会获得这些功能。
WSIT和WS-I
WSIT代表Web Services Interoperabiltiy Technology,它是Microsoft和Sun的合作产物,始于2005年,旨在确保.NET客户端可以使用以Java编写的Web服务,反之亦然。不要将它与WS-I混淆,后者是波士顿一种倡导互操作性标准的组织。它们作为互操作中心组织体为大约130个成员公司服务,最为重要的是,它们是WS-Basic Profile的发布者。你可以在http://www.ws-i.org中找到有关它们的更多信息。
Metro的第二层提供四类高级功能:传输、可靠性、安全性和事务,如表4-1所示。
表4-1:Metro功能
Metro功能 实现的关键功能
传输 带有FastInfoset的SOAP over TCP,JMS和SMTP传输,实现优化二进制编码的MTOM和XOP
可靠性 WS-ReliableMessaging实现
安全性 WS-Security和WS-Trust
事务 为Web服务提供事务支持以及实现WS-Coordination和WS-Atomic Transaction
JAX-WS依赖于JAXB(Java API for XML Binding)来提供XML和Java之间的转换或映射层。
提示: WS-Addressing将Web服务资源的物理位置抽象化以实现更松散的耦合。MTOM(Message Transmission Optimization Mechanism)优化SOAP消息中二进制数据的传输。
虽然Metro可以作为一种单独下载工具来和其他容器(比如WebLogic 10.3)联合工作,但它通常被看作是Sun Web服务栈,如图4-5所示。
图4-5:Metro:Java Web服务栈
除了实现这些重要的规范,Metro还提供一些便利。例如,它提供单个注解来允许你根据传入和传出的消息载荷进行Schema验证。Metro通过Jersey项目以及Spring和JSON(JavaScript Object Notation)集成提供REST支持。
可以通过以下方式获得Metro:
1. 从https://metro.dev.java.net下载最新的文档版本。
2. 在控制台中,定位到包含刚才所下载软件的目录。
3. 执行>java -jar metro-installer.jar,同意许可条款。安装程序将展开一个包含各种样例和文档的目录以及lib目录中的一些JAR。
4. 定位到安装程序创建的目录并运行与自己平台相匹配的Ant脚本。首先,获取Glassfish的根位置并在命令中设置变量。按照如下方式运行wsit-on-glassfish.xml:
>ant -Denv.AS_HOME=C:\\programs/glassfishv2ur1 -f wsit-on-glassfish.xml
你应该会看到一些文件被复制,而且安装成功完成。重新启动后,Glassfish现在应该具有最新版本的Metro。
提示: 一些工具附带的Metro版本比JDK附带的Metro版本要新,Glassfish中有些工具也是这样。例如,Java SE 1.6.0_05附带的wsimport工具的版本是2.1.l,但是,安装Metro后,从<glassfish-home>/bin目录调用>wsimport-version后却显示“JAX-WS RI 2.1.3-hudson-390-”。使用这些工具时,路径要保持正确,这一点非常重要!
这种解决方案假定你已经本地运行最新的Glassfish版本,它可以从https://glassfish.dev.java.net获得。Glassfish 9.1 update 1、Metro 1.3和JDK 1.6.0_05被用来测试本书编写的示例。如果你不希望使用Glassfish,Metro还可以与Tomcat、JBoss和WebLogic一起使用。
在Maven中使用Metro 1.3
如果你使用Maven 2来创建Web服务并希望使用Metro中的其他功能,需要向pom.xml中添加以下dependency:
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>Webservices-rt</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>Webservices-api</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
4.4 安装Oracle WebLogic
问题
希望使用最新的JAX-WS工具和WS-*实现,同时还希望支持和扩展某种供应商产品。
解决方案
获取Oracle WebLogic 10gR3(也称为10.3)。该安装包包含WebLogic Server以及Workshop for WebLogic,后者是与WebLogic服务器集成得很好的基于Eclipse的IDE。
在Linux上按照以下步骤执行:
1. 从http://www.oracle.com/technology/software/products/ias/bea_main.html下载适合于自己平台的WebLogic 10.3文件,你可以选择Net安装程序(下载程序较小,但需要Internet连接)或Package安装程序(下载程序较大,接近800MB,不过不需要Internet连接)。在Linux上,该文件是inet_server103_linux32.bin。
2. 改变该文件的权限,使得可以执行该文件。如果你的Linux中出现一个GUI,只要双击文件名就可以启动安装程序。如果没有,则只需定位到二进制文件下载到的目录并键入./net_server103_linux32.bin。
3. 按照向导中的指示进行操作。第一步是创建BEA根路径(以前一直是BEA生产WebLogic,直到2008年中期Oracle购买了该软件)。我的是/opt/oracle。接下来,将指定安装文件的临时目录。
4. 接着,指定自定义安装,这样就可以选择将要安装的各项内容(有很多)。如果你希望获得各项要安装条目的详细信息,可以单击相应的条目,或者查看http://edocs.bea.com/wls/docs103/getstart/overview.html#wp1062352提供的文档。
5. 最后,选择要安装的JDK。默认情况下,将是Sun的Java 1.6.0_05和JRockit JVM。JRockit是Oracle实现的Java虚拟机,它和WebLogic产品能够很好的集成。该步骤将获得完成安装所需要的所有其他文件。
6. 现在设置WebLogic 10gR3 Server和Workshop的安装目录。我将该服务器放在/opt/oracle/wlserver_10.3中。安装大小大约1.13 GB。
安装已完成,现在可以运行QuickStart应用程序,这使得你可以启动Server和Workshop组件。
启动服务器控制台
启动WebLogic Server后,可以通过访问http://localhost:7001/console来启动控制台。控制台登录屏幕如图4-6所示。
提示: 用户名和密码输入“WebLogic”。
登录后,就可以开始创建Web服务、包,接着可以将其部署到控制台。
图4-6:WebLogic 10g Release 3控制台
参见
4.5和4.6小节。
4.5 创建和部署最简单的Web服务
问题
作为Web服务新手,希望创建和部署最简单的具有一定可行功能的Web服务。
解决方案
编写带有@WebService注解的POJO(Plain Old Java Object),使用javax.xml.ws.Endpoint进行封装,接着在JVM中使用Java SE 6内置的HTTP服务器将其发布。
示例4-1显示了你可以编写的最简单Web服务。
示例 4-1:简单的服务接口,Hello.java
package com.soacookbook.ch03;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
@WebService
@SOAPBinding(style=SOAPBinding.Style.RPC)
public interface Hello {
String sayHello(String name);
}
在此例中,我们创建了一个public接口,客户端可以使用它来调用服务。使用@WebService注解说明该接口应该作为服务由发布程序提供,然后说明了RPC(Remote Procedure Call)的一种SOAP绑定样式。接下来,需要一个类来实现复杂的业务逻辑(参见示例4-2)。
示例 4-2:简单的Web服务实现,HelloWS.java
package com.soacookbook.ch03;
import javax.jws.WebService;
@WebService(endpointInterface="com.soacookbook.ch03.Hello")
public class HelloWS implements Hello {
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
该服务将接受一个参数并返回包含参数值的字符串。请注意,上面的接口和实现都必须包含@WebService注解来使用Endpoint进行发布和调用。实现使用@WebService注解的endpointInterface属性指向自己面向客户的接口,这是一个完全限定的字符串。
提示: 如果指定@WebService注解中endpointInterface属性的值,该接口将作为记录服务,它的其他注解将发挥作用,而实现类中的后续注解则会被忽略。这是为什么在接口中指定SOAP绑定的原因。
上面是创建一个最简单Web服务所需要做的所有事情。不过,在发布之前,它不是一个真正的Web服务,只有发布后,客户端程序才可以调用它。该示例不需要使用Web容器,我们使用javax.xml.ws.Endpoint类来将其发布到Java SE 6 VM内置的HTTP服务器,如示例4-3所示。
示例 4-3:发布Web服务端点
package com.soacookbook.ch03;
import javax.xml.ws.Endpoint;
public class HelloPublisher {
public static final String URI = "http://localhost:9999/hello";
public static void main(String[] args) {
//Create instance of service implementation
HelloWS impl = new HelloWS();
//Make available
Endpoint endpoint = Endpoint.publish(URI, impl);
//Test that it is available
boolean status = endpoint.isPublished();
System.out.println("Web service status = " + status);
}
}
Endpoint类包含一个静态的publish方法,该方法将注解的服务实现类绑定到一个位置并使服务在该位置可用。接着,可以使用isPublished方法查看服务的状态。这时,服务已被部署,在指定的位置可以使用该服务。
endpoint接口允许你访问整个Web服务,例如,可以通过调用endpoint实例的getMetadata方法来获取与该端点有关的元数据,这会返回XML文档的List<Source>。还可以通过调用endpoint实例的getExecutor方法来访问用于向该服务分发请求的执行程序。
参见
可以参阅4.8小节来了解更多有关所生成的内容及其工作方式方面的信息。
4.6 创建服务并将其部署到WebLogic
问题
希望将简单的Web服务部署到自己新安装的Oracle WebLogic 10gR3中。
解决方案
按照如下步骤进行。
首先,尽管可以使用默认的域来进行测试,但为自己创建一个新域或许是一个不错的主意。在本示例中,我创建了一个域并将其设定为监听端口7777。
提示: 要通过WebLogic Workshop设置域,可以右击“Servers”选项卡并选择“New Server”。在指定域目录的屏幕中,单击“Click Here to Launch Configuration Wizard to Create a New Domain”并按照步骤进行操作。创建域并添加服务器后,右击服务器并选择“Start”来启动服务器。接下来,就可以创建和部署Web服务项目。
创建Web服务项目
按照以下步骤创建使用可移植JAX-WS注解的WebLogic Web服务项目:
1. 在WebLogic Workshop中,选择“File→New→Project”。
2. 在“Web Services”项目类型下面,选择“Web Service Project”。单击“Next”。
3. 给项目指定名称,选择新的服务器作为“Project Runtime”。
4. 在“Configurations”区域,选择“Annotated Web Service Facets JAX-WS 10.3”。单击“Next”。
5. 不要选中“WebLogic Web Service Extensions”旁边的复选框,以便让代码更容易移植且仅依赖于JAX-WS标准。
6. 保持向导中其他选项不变,单击“Finish”创建项目。
项目存在后,让我们添加Web服务。
创建Web服务
1. 在“Java Resources”下,选择src目录,右击并创建一个新包,我自己将其命名为com.soacookbook。
2. 右击创建的包,选择“New WebLogic Web Service”。
3. 在“Filename”域中,输入“HelloWS”并结束向导。
现在,你拥有一个新的Web服务类,将该类的内容修改成如下所示:
@WebMethod
public String sayHello(String name) {
return "Hello, " + name;
}
接下来,需要为Web服务创建WSDL。
部署Web服务
拥有一个完整Web服务后,就可以将其部署到WebLogic服务器。下面是简单的部署步骤:
1. 右击Web服务实现类本身(此处是HelloWS.java)并选择“Run As→Run On Server”。
2. 选择前面安装的本地WebLogic服务器并单击“Finish”。
使用内置测试客户端来测试服务
像Glassfish一样,WebLogic Workshop附带有一种简单的方法来测试Web服务。当通过Workshop部署和运行服务时,它将自动创建一个简单的图形界面客户端来供你测试服务。
因此,部署完成后,Workshop应该启动一个与图4-7类似的屏幕,其中显示了Web服务操作并允许你输入值和获得响应。在这两种示例中,显示的是SOAP消息。
当你准备就绪,想看一看消息是何模样(不包含HTTP头)时,测试客户端就非常有用。此外,如果希望查看该服务的WSDL,可以单击测试客户端顶部的链接,在本例中,它是http://localhost:7777/TestWS/HelloWSService?WSDL。
图4-7:WebLogic Workshop测试客户端
4.7 设置Maven 2服务和客户端项目
问题
希望创建一个项目来在Maven 2中安置Web服务及其客户端。你已经了解Maven,但不确定需要什么插件,与此关联的目标是什么以及如何构建它。
解决方案
创建三个项目:一个用于服务,一个用于客户端,另一个是父级项目。对服务和客户端项目运行单元测试。此外,为了使用Java EE 5,希望设置一些常见依赖,而且需要将默认Java SDK更新成1.6。
讨论
对于本书中使用Java SE 6的那些示例,你需要将POM更新成包含使用SE 6的编译器插件,如下所示:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
...
这确保你所需要的JAX-WS和XML库将是可用的。
通常来说,创建Web服务的最简单方法是使用Maven 2,设置Web项目(指定<packaging> war</packaging>),然后创建其类具有@WebService注解的POJO。不过,如果就此打住,你的WAR将正确部署,但不会包含任何Web服务,这是因为默认情况下,Maven 2在Web.xml文件中使用较老的Servlet 2.3 DTD。为了解决这个问题,可以将下面的dependency添加到pom.xml中:
<dependency>
<groupId>javaee</groupId>
<artifactId>javaee-api</artifactId>
<version>5</version>
<scope>provided</scope>
</dependency>
接着,需要替换Web-app声明。在项目的src/main/Webapp/Web-INF文件夹中查找Web.xml文件,将如下代码:
<!DOCTYPE Web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/Web-app_2_3.dtd" >
<Web-app>
<display-name>My Service</display-name>
</Web-app>
用下面指定的Servlet 2.5版本替换:
<?xml version="1.0" encoding="UTF-8"?>
<Web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/Web-app_2_5.xsd">
<display-name>My Service</display-name>
</Web-app>
如果包含任何特定于Metro的代码,那么还需要设置有关Metro的dependency(像客户端的WS-Addressing或依赖于Metro导出机制的有状态的Web服务):
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>Webservices-rt</artifactId>
<version>1.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>Webservices-api</artifactId>
<version>1.3</version>
<scope>provided</scope>
</dependency>
净化服务客户端
如果创建客户端,需要在类路径中包含Metro JAR或将其unjar到客户端JAR。在Maven 2中,可以使用dependency的编译或默认范围来实现这一目标。
不过,如果创建的是服务应用程序,就会有所不同。如果要部署到Glassfish,则需要指定provided的依赖范围,因为Glassfish已经包含这些JAR,由于无法找到其他依赖,类装载器将会因这个骗局而窒息。在其他容器中,可以使用它们特定于供应商的扩展。
接下来,当部署项目时,它们知道解释注解并为你创建一个服务。
总之,观念就是:创建一个父级项目,使得你需要构建一种环境,在该环境中,单击就可以为你创建所有内容。首先创建服务,然后部署它,接着通过刚才部署的WSDL,创建客户端。
使用JUnit测试,确保对服务和客户端项目都进行测试。但愿这将很明显,不过,此时存在的混乱来源于我们倾向于认为必须将Web服务项目作为Web服务进行测试,因为创建的就是Web服务,但需要在部署后进行测试,这违反了Maven的工作方式。按照Maven惯例,可以非常容易地获得你想要的结果。
基本上来说,由于部署阶段是在测试阶段之后进行的,服务项目的创建过程应该直接测试代码,是作为常规Java代码,而不是作为使用WSDL部署的Web服务代码。为此,可以使用模拟对象。这时,Jakarta Commons项目非常有用。它允许你模拟JNDI调用并创建到作为容器中DataSource进行定义的数据库,这将确保Java代码被完全测试(你可以使用http://cobertura.sourceforge.net/中的Cobertura来体会完全的程度)。接着,客户端项目将根据刚才部署的当前服务来执行自己的测试。
创建服务项目
服务项目就是常规的EAR或WAR项目,这取决于是使用EJB还是Servlet来完成服务实现。要注意的唯一一件事是你希望创建时自动部署该服务项目,从而当创建客户端时,客户端可以读取新的WSDL并获得该服务的新实现。可以像对任何常规WAR或EAR那样来对服务项目进行部署。如果使用的是Glassfish,有一个不错的插件可以使用:
<plugin>
<groupId>org.n0pe.mojo</groupId>
<artifactId>asadmin-maven-plugin</artifactId>
<version>0.1</version>
<configuration>
<glassfishHome>${my.glassfish.home}</glassfishHome>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>redeploy</goal>
</goals>
</execution>
</executions>
</plugin>
该插件在安装阶段执行,从而在创建过程中自动部署WAR或EAR,这让人感觉非常方便。此处要注意的唯一一点是:使用给定开发者限定的特有对象(比如路径)的属性是非常重要的。此处,我引用的是其值在自己settings.xml文件中已指定的一项属性。
创建客户端项目
如果是创建公开使用的服务,创建完服务本身后就可以停止,让客户端自己决定如何使用该服务。然而,你或许还希望创建客户端JAR,作为一种方便在Java平台上提供给用户。这是Amazon、eBay、Google和其他公司在Web服务早期所做的事情,以使他们的服务更易于使用。他们分发同时包含Java和C#代码的客户端ZIP文件,使得开发者可以更快地熟悉和使用他们的服务。
即使你仅仅是开发供内部使用的Web服务,创建一个已包含通过WSDL生成的服务项目的客户端JAR仍是一个不错的想法,这样,不仅更容易将服务集成到业务应用程序中,而且能更好地控制这种集成使用。例如,如果创建客户端JAR,可以添加一些功能,比如WS-Addressing、Schema验证等,而无需要求购买该服务的各个开发者理解所有这些事情,甚至不要求他们知道幕后发生的这些事情。这赋予你更多的灵活性。当然,如果需要调用该服务的那些业务应用程序是采用COBOL或C#编写的,则这些应用程序无法使用对应的Java客户端。你可以为这些应用程序创建一个客户端,或者让它们自己创建。
提示: 如果既要创建服务实现,又要创建便利的客户端,则需要确保按照一种完全封装的方式来设计服务,而且不让客户端实现任何实际操作。这看起来非常明显,但当你在内部着手准备并创建服务时,却是一种容易掉入的陷阱——尤其是由于为了使学习曲线更短,Java Web服务API的设计者们使任何内容都基于熟悉的Web技术。
有一种简单的经验可使这有所不同。如果同时创建客户端和服务,当然没有问题,也是通常采取的方式,不过,一定不要在客户端JAR中放置任何代码,这是解决方案能够发挥作用所必需的。它只是一种便利,别无他意。如果去掉客户端中的代码就会使整个解决方案失效,则服务的设计存在问题,需要找到一种方法将对应的功能移回到服务中。
客户端和服务项目需要在版本上彼此独立。例如,可能向客户端添加了多线程,但这并不意味着服务中的内容发生了改变。需要能够将客户端单独地重新分发给Maven存储库,并且具有自己的版本。
对于客户端项目,可以使用wsimport插件,如下所示:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<version>1.9</version>
<executions>
<execution>
<goals>
<goal>wsimport</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>com.myProject</packageName>
<wsdlUrls>
<wsdlUrl>${com.myProject.wsdl.url}</wsdlUrl>
</wsdlUrls>
<verbose>true</verbose>
</configuration>
</plugin>
https://jax-ws-commons.dev.java.net/jaxws-maven-plugin/中提供了JAX-WS插件,通过使用该插件,你可以向Maven 2 build添加wsimport和wsgen功能。<packageName>是你所生成的包的名称。
创建父级项目
不是一定需要创建父级项目,但它可以允许你指定共享的依赖,比如Java SE 6、Log4J、JUnit或客户端和服务都需要的其他条目。父级项目可以声明这些依赖,还可以将其子条目作为服务或客户端项目来声明,如下所示:
<modules>
<module>ws</module>
<module>client</module>
</modules>
父级项目的目的是能够创建和部署服务,然后立即根据该服务来创建客户端,以确保WSDL中的任何改变可被客户端兼容,反之亦然。
另一个原因是使得你拥有一个伞形项目,形成进行集成构建时的便利目标。例如,如果使用Cruise Control(http://cruisecontrol.sourceforge.net/)或Hudson(http://hudson.dev.java.net/)之类的工具来执行连续集成,则更容易将该工具设置成指向需要创建的单个POM。
不过,如果Hudson之类的工具是在其自身的创建服务器上使用,可能需要设置创建服务器在指定WSDL位置时要使用的配置文件、应该部署到的应用服务器的路径等。下面是一个可在短时间内完成的示例:
<profiles>
<!-- For shared Continuous Integration Hudson Build.
Developers should have their own profile set in profiles.xml,
which should be ignored by source repository. -->
<profile>
<id>myProject-integration-profile</id>
<activation>
<!-- This property is passed to Maven from within
the Hudson build configuration on the build server. -->
<property>
<name>integrationBuild</name>
<value>true</value>
</property>
</activation>
<properties>
<glassfish.home>/domains/devtools/glassfish/...</glassfish.home>
<my.wsdl.url>http://example.com:7575/my/MyService?wsdl</my.wsdl.url>
</properties>
</profile>
</profiles>
接着,在连续集成(CI)工具所生成的服务的项目中,可以指定用来控制该项目从哪个机器build的系统属性。在这里,我使用的是integrationBuild属性,Hudson项目已将该属性配置成作为一个参数传递给build程序,因此,它要使用的属性(比如要部署到的应用服务器的位置和客户端应该读取的WSDL URL)被触发。
感谢Brian Mericle在这个解决方案上给予我的帮助。
4.8 理解WSDL
问题
刚部署完简单的Web服务后,希望查阅它的WSDL以理解已部署Web服务的有关代码。
解决方案
查看WSDL,方法是在服务名称后面加上?WSDL。这只是一种惯例,并没有任何规范规定这样做,但所有的主要供应商(IBM、Microsoft、BEA/Oracle、Sun、JBoss)都遵守该惯例。
为了更好理解Web服务合同中这一重要部分,可以参阅下面有关WSDL关键部分的讨论。
讨论
很难对WSDL各部分进行一般性的讨论,因为它们会随着你在一个特定服务中所做的不同选择而不同。因此,在本讨论中,我们介绍为4.5小节中创建的Web服务而生成的WSDL,如示例4-3所示。
可以查看针对Java 6内部的HTTP服务器而发布的WSDL,方法是将Web浏览器打开到http://localhost:9999/hello?wsdl,你应该会看到如示例4-4所示的内容。
示例 4-4:Hello WSDL
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://Schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://ch03.soacookbook.com/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://ch03.soacookbook.com/"
name="HelloWSService">
<types></types>
<message name="sayHello">
<part name="arg0" type="xsd:string"></part>
</message>
<message name="sayHelloResponse">
<part name="return" type="xsd:string"></part>
</message>
<portType name="Hello">
<operation name="sayHello" parameterOrder="arg0">
<input message="tns:sayHello"></input>
<output message="tns:sayHelloResponse"></output>
</operation>
</portType>
<binding name="HelloWSPortBinding" type="tns:Hello">
<soap:binding style="rpc" transport="http://Schemas.xmlsoap.org/soap/http"></soap:binding>
<operation name="sayHello">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" namespace="http://ch03.soacookbook.com/"></soap:body>
</input>
<output>
<soap:body use="literal" namespace="http://ch03.soacookbook.com/"></soap:body>
</output>
</operation>
</binding>
<service name="HelloWSService">
<port name="HelloWSPort" binding="tns:HelloWSPortBinding">
<soap:address location="http://localhost:9999/hello"></soap:address>
</port>
</service>
</definitions>
对于该端点发布的WSDL,有一些事情需要注意。让我们看看这些内容,从而更好地理解它们是如何工作的。
Types
该WSDL的Types部分是空的。Types是WSDL用来导入和局部定义Web服务交换消息要使用的XML Schema类型的地方。该部分为空的原因是这个Web服务定义的消息只使用字符串,这些字符串是作为XML Schema中的简单类型定义的——所有的Web服务将能够随时使用它们,因此,没有必要在此处放入任何内容。
通常来说,WSDL将Types定义成指向外部的Schema。在4.6小节创建的WebLogic服务中,WSDL指定服务器在部署时所生成的Schema的位置,定义了消息将使用的类型:
<types>
<xsd:Schema>
<xsd:import namespace=http://soacookbook.com/
SchemaLocation="http://localhost:7777/TestWS/HelloWSService?xsd=1"/>
</xsd:Schema>
</types>
该URI引用一个完整的XML Schema文档,它指定请求和响应消息中使用的复杂类型的值。
如果你自己编写WSDL,可以在一个外部Schema中按照这种方式定义类型,或者采用完全内嵌的方式定义它们,如下所示:
<types>
<xsd:Schema>
<xsd:simpleType name="ID">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[0-9]{5}"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:Schema>
</types>
通常来说,被认为是一种好的做法是不采用内嵌的方式定义类型,从而使接口尽可能地独立于实现,而是引用一个外部Schema。
Messages
WSDL的Messages部分包含请求和响应定义,与服务通信时,要使用的这些请求和响应。你将注意到,请求和响应各自都对应一条消息。请求消息与方法调用的名称相同,因为此处采用RPC样式。响应的名称是按照在操作后附加上“Response”的方式给出的,这是JAX-WS规范的默认方式。每个消息都包含一个part子元素,消息part与方法中的参数类似,具有名称和类型,第一个part的名称是“arg0”,这是默认名称,后续参数将被命名为“arg1”等。可以对这些名称进行定制,以使它们更易于阅读和理解,这对于一些更复杂的WSDL来说变得越来越重要。
对于响应,此处包含一个元素<partname="return" type="xsd:string">。由于默认参数样式值是“wrapped”,Web服务返回的值将封装在单个元素中,名称为“return”,你可以通过它提取剩余的载荷。7.4小节对此进行了更详细的讨论。
Binding
WSDL的Bindings部分指定与Web服务之间的消息传递方式。默认情况下是SOAP,其他高级选项包括HTTP、JMS、SMTP和TCP。由于通过对Hello服务接口实现类使用@SOAPBinding(style=SOAPBinding.Style.RPC)注解,你指定希望将服务绑定到SOAP,因此,将会获得SOAP绑定作为消息传输方式。
<soap:binding>元素提供了一个值为http://Schemas.xmlsoap.org/soap/http的transport属性,这意味着服务将使用SOAP 1.1作为消息的发送和接收协议。你也可以指定SOAP 1.2,不过1.1是默认协议,SOAP 1.2至今没有广为流行,因为绝大部分实现当前是针对1.1的。
你还可以看见<soap:binding>元素提供一个值为“rpc”的style属性,该WSDL编写它是因为Hello服务接口存在@SOAPBinding(style=SOAPBinding.Style.RPC)注解。此处还可以提供的另一个选项是“document”,7.4小节对此进行了详细的讨论。
该服务被赋予一个命名空间http://ch03.soacookbook.com,它与定义该端点的Java类的包名相对。与此类似,操作名与该类的方法名相对,因为没有自定义它。
Service
WSDL的Service部分指定服务的名称将是HelloWSService。该名称是在定义服务的类的名称后面加上“Service”,这是JAX-WS规范中没有自定义服务采用的命名方式。它还指定将使用SOAP绑定“HelloWSPort”端口,该部分中的元素带有soap前缀,说明它们来自http://Schemas.xmlsoap.org/wsdl/soap/命名空间。
此处重要的location属性指定可以在什么位置调用该服务,以及将使用哪些客户端来调用服务。下一节介绍如何调用该服务。
4.9 使用NetBeans中的引用来生成Web服务客户端
问题
已经厌倦了通过命令行基于WSDL手动生成客户端代码,希望它成为常规开发工作流的一部分。希望找到一种更快、更方便的方法来基于WSDL生成可移植JAX-WS对象,从而可以像任何其他依赖一样开始使用该服务。
解决方案
在常规Java项目中使用NetBeans 6 Web Service References。
讨论
许多现代IDE都附带有为客户端项目创建Web服务引用的功能,这使得你省去了从命令行执行许多手工操作时的部分易于出错的苦差事。首先,将客户端项目设置为常规Java项目,并通过指向WSDL位置添加服务引用。接着,一旦通过IDE对Ant目标运行clean和build,通常将插入wsimport步骤并编译所生成的服务客户端代码,使其包含在类路径中。这使得非常容易创建现有Web服务的客户端并将代码合并到你的项目中。
下面介绍如何使用NetBeans 6中的Web服务引用,它代表了各种IDE通常的处理方式:
1. 创建一个新的客户端项目。这可能是一个Web页,但对于这个示例,将只创建一个基于控制台的常规Java项目并创建包含一个要运行main方法的类。
2. 右击项目名称,选择“New→Web Service Client”。
3. 当向导出现时,如图4-8所示,输入本地或远程WSDL地址以及希望用来存放所生成代码的包的名称。
图4-8:NetBeans 6 Web服务客户端向导
4. 单击“Next”后,NetBeans将为WSDL创建一个本地目录并基于它生成可移植的JAX-WS对象,将这些对象放在build目录下名为generated/wsimport/client的文件夹中,你将在名为Web Service References的项目下看见一个新增的文件夹。它存储(NetBeans所知道的)你的项目使用的一组Web服务。
5. 服务存在后,你可以将希望直接调用的服务操作的名称拖放到main方法上,这将生成内嵌的所需代码框架,如图4-9所示。
这时,你可以向框架代码添加所需的值并可以通过右击来清除、构建和运行客户端项目。
图4-9:为调用服务引用而生成的框架代码
提示: 如果你的WSDL不稳定,将需要刷新Web服务引用以确保客户端仍然编译。WSDL获取IDE本地下载并引用的XML目录,如果Web服务操作参数发生改变,就需要刷新它。在这种情况下,通常需要一起删除Web服务引用并重新生成它。如果客户端代码以前依赖于某个给定WSDL进行工作,现在它无法编译,可以尝试删除该引用,然后再将其添加回来。这将重新导入WSDL并再次根据它来运行生成器。
4.10 通过Metro监控SOAP流量
问题
正在使用Glassfish/Metro,希望将Web服务客户端发送和接收的传输流量转储到控制台。
解决方案
将标记-Dcom.sun.xml.ws.transport.http.client.HttpTransportPipe.dump=true传递给JVM。
该转储技术显示的字节数,代表发送和接收的消息量。这意味着你还会获得传输所特有的信息,比如所有的HTTP头,这可能会非常有用。
也可以在Ant中通过jvmarg来实现这一目的:
<target name="run">
<java classname="com.soacookbook.ch03.MyClient" fork="true">
<arg value="someArg"/>
<classpath>
<path refid="jaxws.classpath"/>
<pathelement location="..."/>
</classpath>
<jvmarg value="-Dcom.sun.xml.ws.transport.http.client.HttpTransportPipe.dump= true"/>
</java>
</target>
注意,JUnit Ant任务也会接受jvmarg元素,因此,可以将它轻松地添加到客户端的那些测试调用中。请看示例4-5。
示例 4-5:启用SOAP消息转储的JUnit Ant任务
<target name="run-test" depends="compile-test">
<echo message="-----Running Tests-----" />
<junit fork="true" printsummary="true"
errorProperty="test.failed"
failureProperty="test.failed">
<classpath refid="cp.test" />
<jvmarg
value="-Dcom.sun.xml.ws.transport.http.client.HttpTransportPipe.dump=true"/>
<formatter type="brief" usefile="false"/>
<formatter type="xml"/>
<batchtest todir="${test.report.dir}">
<fileset dir="${test.classes.dir}"
includes="**/*Test.class" />
</batchtest>
</junit>
<echo message="-----Creating JUnit Report-----" />
<junitreport todir="${test.report.dir}">
<fileset dir="${test.report.dir}" includes="TEST-*.xml"/>
<report format="frames" todir="${test.report.dir}"/>
</junitreport>
<fail if="test.failed"
message="Tests failed. Check log and/or reports."/>
</target>
这是一种简单的方法来获取SOAP消息调用背后所执行过程的许多信息。例如,如果错误构建了一个消息,这可能会大大有助于发现该问题。
示例4-6显示了在对与某个信用认证Web服务进行通话的Web服务客户端执行JUnit测试调用过程中,HttpTransportPipe转储操作给出的输出结果(为了增加可读性,我添加了换行)。
示例 4-6:JUnit测试过程中HttpTransportPipe的输出结果
4/27/08-14:40 DEBUG com.soacookbook.ch03.test.SchemaValidateTest.testCreditAuth -
Invoking Credit Authorizer Service.
---[HTTP request]---
SOAPAction: ""
Accept: text/xml, multipart/related, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-Type: text/xml;charset="utf-8"
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body><creditCard xmlns="http://ns.soacookbook.com/credit">
<cardNumber>4011111111111111</cardNumber>
<name>
<firstName>Phineas</firstName>
<middleInitial>J</middleInitial>
<lastName>Fogg</lastName>
</name>
<expirationDate>2015-04-27-07:00</expirationDate>
</creditCard>
</S:Body></S:Envelope>--------------------
---[HTTP response 200]---
Transfer-encoding: chunked
null: HTTP/1.1 200 OK
Content-type: text/xml;charset="utf-8"
Server: Sun Java System Application Server 9.1_01
X-powered-by: Servlet/2.5
Date: Sun, 27 Apr 2008 21:40:44 GMT
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<authorization xmlns="http://ns.soacookbook.com/credit">
<amount>2500.0</amount>
</authorization>
</S:Body></S:Envelope>--------------------
服务器端也提供了一个相应的类来允许你将传入流量的消息转储到控制台。
提示: 如果是通过Web页调用Web服务,也可以很方便地使用名为Live HTTP Headers的Firefox插件,它在一个窗口中显示各个请求和响应的HTTP头。可以通过https://addons.mozilla.org/en-US/firefox/addon/3829获得该插件。
讨论
使用监控GUI工具的不足之处除了需要一种GUI环境来运行这些工具外,这些工具使用中间人策略,该策略要求重新路由消息的目的地。它们的工作方式是:监控器接收请求,转储载荷,然后将请求转发给中意的目的地。因此,迫使你将服务客户端改成指向监听器监听的端口。设置HttpTransportPipe.dump=true将使你在较少出错的情况下看见消息的内容。当准备开始时,设置一个这样的控制台转储器会是比较简单、快速和利落的。
提示: 控制台转储器的使用不适合于生产场合,但会节省你非常多的开发时间。
在Maven单元测试运行过程中转储
在执行单元测试过程中,查看要传递的SOAP消息是很有用的。如果是从命令行或在某种IDE中直接运行build,则很容易这样做,因为这两种方法都提供了一种清楚的方式来在执行过程中将参数发送给VM。
但是,如果是从Maven 2执行build,就不能立即明显地实现这一目的。需要做的所有操作是向Surefire插件添加一个系统属性,如下所示:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<systemProperties>
<property>
<name>wsdlLocation</name>
<value>${my.wsdl.url}</value>
</property>
<property>
<name>com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump</name>
<value>true</value>
</property>
</systemProperties>
</configuration>
</plugin>
这会产生与通过-D将参数直接传递给VM一样的效果。
独立于传输登录
当使用HTTP作为传输层时,前面的示例都会正常工作。如果是使用不同的协议,则需要一个传输不可知类。基本上使用刚才描述的同样机制,就可以替换该调用:
com.sun.xml.ws.util.pipe.StandaloneTubeAssembler.dump=true
参见
还有一些其他工具可以使用。可以尝试使用https://wsmonitor.dev.java.net提供的WS Monitor工具,http://ws.apache.org/axis/java/user-guide.html还提供了针对Axis的TCPMon工具。
4.11 通过TCPMon监控SOAP流量
问题
希望在不向类路径添加任何信息的情况下监控SOAP流量。
解决方案
使用https://tcpmon.dev.java.net提供的TCPMon,它是TCP流量的一种开源监控器。要使用该工具,你需要改变本地WSDL中的值。
讨论
可以采用不同的方法运行该工具——直接从Web使用Java Web Start运行,或者通过下载JAR。它将监听可用端口,为了能够转储流量数据,你必须通过该端口路由请求,TCPMon接着将请求转发到实际的目的地。
运行TCPMon
开始使用TCPMon的最简单方式是访问网站并找到名为“Click here to run directly from the Web”的链接。如果不希望这样做,可以将其下载,并通过双击在Windows上执行它,如果是在Linux上,则可以通过输入如下命令来运行它:
$ java -jar tcpmon.jar
这将启动Java Web Start程序。下面是使该程序正常启动并运行的一些步骤。我们将使用一个NetBeans样例项目,因为它的障碍最少。如果希望使用其他Web服务和客户端,有一些相关的常见指示:
1. 启动应用程序。在GUI中,将默认值修改成如下所示:
Local Port: 8090
Server Port: 8080
2. 单击“Add Monitor”,这将运行监听程序。
3. 希望发送SOAP请求到端口8080,因为服务监听该端口。因此,需要将WSDL改成指向8090,这样TCPMon就可以转储消息。接着,“Server Port”字段中提供的值就是TCPMon将各个请求转发到的目的地。因为此处你关心的全部事情是说明流量转储,而不是服务本身,你将在NetBeans中只创建一个新的Web服务样例项目。为此,单击“File→New Project→Samples→Web Service→Calculator”。当然,你可以使用自己喜欢的任何项目。这样做也将创建一个包含Web服务引用的“Calculator Client”项目。
客户端项目创建后,它从服务导入WSDL,需要将该WSDL改成指向TCPMon。要访问该WSDL,定位到“Configuration Files”下“xml-resources”中的“Web-service-references”,然后找到其下的WSDL。如果使用的是其他Web服务客户端项目,只需要找到将要调用的本地WSDL。找到该WSDL文件末尾的如下部分:
<soap:address location="http://localhost:8080/CalculatorApp/CalculatorWSService">
将位置改成http://localhost:8090/CalculatorApp/CalculatorWSService。
4. 接下来需要将应用程序改成使用已修改的WSDL。如果你使用该NetBeans样例,可以在org.me.calculator.client包中找到ClientServlet Java源文件。找到如下行:
@WebServiceRef(wsdlLocation =
"http://localhost:8080/CalculatorApp/CalculatorWSService?wsdl")
将它修改成使用TCPMon端口号8090。由于是在Servlet中,运行时容器将注入服务实例并使用已修改的WSDL位置。
如果没有使用Servlet,但从wsimport生成了可移植项目,可以使用如下所示的代码:
URL wsdlLocation = new URL("file:///C:/projects/etc/CalculatorWSService.wsdl");
QName serviceName = new QName("http://calculator.me.org/", "CalculatorWSService"); CalculatorWSService service =new CalculatorWSService(wsdlLocation, serviceName);
CalculatorWS port = service.getCalculatorWSPort();
int result = port.add(2, 3);
此处所做的事情是覆盖调用该服务时客户端将使用的WSDL,将指向已手工修改的文件。
5. 接下来将提出通过该监控器路由的请求。必要时,可以clean和build这个Web服务Calculator项目,确保它是使用Undeploy和Deploy目标部署的。
然后,右击Web服务Calculator Client项目,选择“Run”。
接着,转向监控应用程序并单击“Submit to Server”。监控器将以窗口的形式显示请求的时间戳,并给出请求和响应的内容,包括HTTP头。
图4-10显示了处于工作状态的TCPMon。
图4-10:显示SOAP请求和响应的TCPMon
TCPMon相当易于使用,给出为你所做的一切,而且非常有助于进行调试,只是需要记得将WSDL修改回来。
第5章
基于SAAJ的Web服务
5.1 概述
本章介绍如何使用SOAP with Attachments API for Java (SAAJ) API创建能够与基于SOAP的Web服务进行对话的Java客户端。
SOAP由W3C在2000年标准化,是一种在分布式系统中创建和发送消息的与平台无关的方法。
提示: 你可能熟悉SOAP是Simple Object Access Protocol的简称,但该规范2003年发布的1.2版以直接、清楚而又神秘的声明“不再是这样”对该简称进行了说明。或许他们决定它毕竟不是这么简单...
由于SOAP基于XML,人们阅读和理解它是相当容易的,而且SOAP消息结构本身是比较简单的。SOAP消息通常使用称为信封的容器进行封装,信封通常包含一个主体,该主体以一个或多个XML文档运载消息载荷。信封可以(不是必须)包含头,它们与HTTP头非常类似。如果处理过程中出现错误,SOAP故障将被添加为主体内容。图5-1给出了SOAP 1.1信封的结构,图中的*表示可以存在多个该类型的实例。
Web服务任何一方的开发者都可以使用XML处理工具来提取SOAP信封内容。
提示: SAAJ 1.3创建符合SOAP1.1和SOAP 1.2规范的消息,使用哪种版本的SOAP将取决于你的供应商。我已经尽力给出这两种版本在哪些要紧方面存在不同。SOAP 1.1(写这本书时它仍是大多数工具的默认规范)的命名空间是http://Schemas.xmlsoap.org/soap/envelope/。SOAP 1.2的命名空间是http://www.w3.org/2003/05/soap-envelope。
图5-1:SOAP信封的结构
SOAP消息通常基于HTTP进行传输(尽管有时可以使用JMS和其他机制)。合并防火墙通常允许交换已有HTTP流量,并使用适当的复杂机制来处理流量,包括防火墙规则、分支网络等。由于SOAP重用HTTP传输层,采用SOAP作为Web服务的标准是比较方便的,这促进了SOAP的早期流行。而且,开发者发现易于使用SOAP,因为他们已经熟悉XML,再加上许多平台包含内在的XML工具。很快,SOAP成为一种流行的创建Web服务的方法。
SOAP with Attachments API for Java(SAAJ)被创建用来特定满足基于SOAP的Web服务开发新手的需要。它允许你以编程的方式操纵SOAP信封,使用它提供的类和方法,你可以创建信封,向信封添加头,在头中放入数据,创建SOAP主体,向SOAP主体添加XML文档以及将主体添加到信封中。一旦消息是完整的,就可以基于HTTP传输完整的SOAP消息以使用调度程序来调用Web服务。本章将介绍SAAJ 1.3,它是Java中用来处理Web服务的基础API。各个Java EE供应商都提供了SAAJ实现。
提示: W3针对SOAP-JMS提出了一个规章,编写本书时,该规章还处于早期阶段,不过其思想是确保Web服务供应商针对SOAP-JMS创建的各个实现之间互操作性绑定机制的标准化。有关更多信息,可以参阅http://www.w3.org/2007/08/soap-jms-charter.html。
不过,应该注意的是,近些年,SAAJ被JAX-WS(Java API for XML Web Services)取代。SAAJ运行于管道级别,需要你手工创建SOAP信封的各个方面。代码可能会变得很长,令人有点厌烦,调用Web服务时需要对必须处理的请求和响应的内部结构有着深入的理解。JAX-WS重用SAAJ,并作为其上的一个抽象层。SAAJ可以看作是消息交换的XML视图,而JAX-WS是消息交换的对象视图。
下面是一个XML格式的包含HTTP头数据的SOAP信封示例:
POST /StockQuote HTTP/1.1
Host: www.soacookbook.com:8080
Content-Type: text/xml; charset="utf-8"
Content-Length: n
SOAPAction: ""
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://Schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:GetStockQuote xmlns:m="urn:com:soacookbook">
<ticker>JAVA</ticker>
</m:GetStockQuote>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
使用SAAJ创建SOAP消息结构后,就可以使用SOAPConnection对象来发送请求和接收响应。SAAJ连接基于java.net.URL类,该类可扩展成支持任何网络协议。
像电子邮件消息一样,SOAP信封还可以包含任意文档类型(XML、二进制图像或任何有效MIME类型)的附件,SAAJ API还允许你处理这类附件。
有关SAAJ的更多信息,可以参阅https://saaj.dev.java.net。
与JAX-M的关系
你或许听说过JAX-M API,Java API for XML Messaging在2001年12月作为JSR-67发布,到2002年6月,SAAJ API作为原始JAX-M 1.0规范的一个副产品项目诞生。JAX-M中的类都放在javax.xml.messaging包中,JAX-M不再受到支持。
SAAJ和JAX-WS
本章说明如何使用SAAJ API执行常见有用任务。由于它是一个基础层,我认为这有助于解释如何处理它,而且应该有助于你熟悉SOAP交换,这样,一旦你开始在本书后续内容中处理JAX-WS,就将知道内在会发生什么情况。如今,通常不需要在较低的层次上处理SOAP,在JAX-WS中,你希望通过SAAJ实现的大多数事情都作为域对象封装起来。
从实际的角度来说,使用SAAJ意味着不使用wsimport和wsdl2java之类的工具。这些工具适用于JAX-WS,意味着通过这些工具,客户端可以生成域对象并几乎可以像根本没有使用Web服务那样操作这些对象。使用SAAJ,你就不拥有服务的域视图,实际处理的是管道。使用JAX-WS进行开发会更快、更轻松,而且通常不会导致任何控制损失。但是,JAX-WS是一个便利层,而且令人鼓舞的是,如果SAAJ使用得当,就可以实现WSDL接口需要的任何内容。
SAAJ包和类
让我们首先快速概述一下该API结构本身。从版本5开始,Java SE就包含SAAJ API,因此,你不需要获得老的Web服务开发包或之类的东西来使用该API。你需要的类都放在包javax.xml.soap及其子包中,它们使用SOAP 1.1和当前的版本SOAP 1.2。
通常来说,SAAJ类名清楚地反映SOAP消息的结构,SOAPMessage类是所有SOAP类的根。该API使用了大量的工厂,因此可以使用MessageFactory.newInstance.createMessage来获得SOAPMessage的新实例。
一旦获得消息后,你就可以着手创建消息的信封。不过,消息直接包含“SOAP部件”,它们实现消息中特定于SOAP的部分的封装;这有别于任何消息附件,消息附件不算作是SOAP部件(它们被恰当地称为AttachmentPart)。因此,由于SOAPPart对象包含信封,信封转而成为头和主体的封装,为了创建它们,需要首先从消息获取SOAP部件对象。可以使用soapMessage.getSOAPPart方法来实现这一目标。
拥有SOAPEnvelope对象后,就可以着手创建SOAPBody和SOAPHeader类的实例。为了进行总结,下面给出了进行SAAJ处理时的一些常见调用:
MessageFactory mf = MessageFactory.newInstance();
SOAPMessage message = mf.getMessage();
SOAPPart soapPart = message.getSOAPPart();
SOAPEnvelope env = soapPart.getEnvelope();
SOAPBody body = env.getBody();
SOAPHeader header = env.getHeader();
值得注意的是,在SAAJ中,SOAP规范描述的对象(信封、主体、头、故障等)都实现SOAPElement接口。该接口提供了基本的方法来处理这些对象的内容,例如,可以使用该接口提供的方法来获取元信息,比如命名空间或编码样式。还可以使用它提供的方法来操纵XML树数据:例如,可以添加属性,添加子元素或获得内容的文本值。
此时,我认为我们已经准备就绪,可以开始实际工作了。
5.2 创建带有限定名称的SOAP元素
问题
需要将一个元素添加到正在创建的SOAP消息中,该元素需要具有命名空间URI、前缀和本地部件。
解决方案
使用javax.xml.namespace.QName,它代表一种限定名称,允许你定义命名空间URI、前缀和本地部件。
讨论
Java SE 5新增了QName,替换了原来的Name类。它们是不可变的,因此,必须使用构造方法专门创建它们。
让我们回顾一下XML限定名称的基本组件,使用下面的元素作为一种模式:
QName bodyName = new QName("http://example.com", "getQuote", "e");
该名称定义了一个名称空间URI、一个本地部件和一个前缀:
名称空间URI是“http://example.com”,其作用有点像Java包,它划分两组不同元素之间的逻辑界线。允许同名但不同属性的多个元素包含同一本地部件,它们在处理过程中不会冲突。
本地部件是“getQuote”,它定义元素名称。
前缀是“e”,它在文档中不代表一定的意义,只用作命名空间的一种快捷方式。
这将生成如下所示的XML元素:
<e:getQuote xmlns:e="http://example.com">
还可以创建只使用命名空间URI和本地部件的QName,就像公开Web服务有时要求的那样。例如,以.NET编写的WebServicesX使用如下格式:
QName portQName = new QName("http://www.WebserviceX.NET/", "StockQuoteSoap");
该Java代码创建如下所示的XML格式的QName:
<StockQuoteSoap xmlns="http://www.WebserviceX.NET/">
提示: 如果你曾经需要比较两个实例,有关QName的一件有趣的事是equals或hashcode方法不使用该前缀。为什么是这样的原因似乎很明显:前缀是用户定义的,对于当前文档来说不是必需的,它并不能真正区别两个名称。
第三种也是最后一种构造方法允许你创建仅包含本地部件的QName。
命名空间中的URI、URL和URN
QName文档将“URI引用”称为namespaceURI域允许的值,统一资源标识符(Uniform Resource Identifier)是其他两个的父集,URL显然是定位器:它们标识位置,或者暗示物理资源。命名空间迄今为止最常使用它们的原因有很多,尤其是由于Web的盛行导致了它们的流行。 此外,它们具有层次结构,能够被人们普遍理解,而且创建的内容需要使用命名空间时,公司通常会使用一个域名,通过该域名可以轻松地将其命名空间与他人的命名空间(像Java包)区别开来。
不过,随着学术倾斜越来越大,URL的使用存在一些不足。首先,它们指定一种协议,这在命名空间中是不合适的;此外,它们暗示有一种物理资源与命名空间标记的元素相对应(当然,这是它们的目的)。
因此,有些人转向使用URN,即Uniform Resource Names(统一资源名)。虽然它们的句法与URL类似,也是通常采用层次结构(这取决于作者),但它们还具有的优点是作为命名空间,看起来更纯粹,而且不暗含其他目的和对象。URN还允许维护我们所喜欢的有关URL的层次结构。
URN采用的基本格式是:"urn:"。例如:
<Namespace Identifier> ":" <Namespace Specific String>
它们通常按照以冒号分隔的列表形式指定,前面是“urn”,有时末尾带有版本信息,如下所示:
<e:getQuote xmlns:e="urn:example:quotes:2:0">
此处有一个指定书籍的有效正式URN定义:urn:isbn:9780596520724。该URN是正式的,因为它使用正式注册的命名空间isbn,这里的“正式注册”意味着“由IANA(Internet Assigned Numbers Authority,互联网编号分配机构)定义和认可”。你或许还会看见其他形式,比如urn:example.com/quotes,但它们都不是正式的。
提示: 该书在不同的场合使用URL和非正式的URN,以强调它们都是允许使用的。为了简短和有效,我给出的URN是非正式的,因为IANA不认可它们。你的SOAP消息仍将正常工作,无论你使用了哪种形式的URN。不过,重要的是在SOA环境中,及早建立一种清楚、具体的命名空间惯例并遵守它。
参见
要查找更多有关URI、URL和URN的信息,可以参阅http://www.ietf.org/rfc/rfc2396.txt给出的Tim Berners-Lee和其他人提供的原始文章。
http://www.iana.org/assignments/urn-namespaces/提供了一个完整的正式命名空间指定列表。
5.3 创建完整的SOAP消息
问题
希望以编程的方式创建一个完整的SOAP请求,而且物理文件中没有完整的SOAP信封可用来发送SAAJ请求。
解决方案
使用javax.xml.ws.soap.SOAPMessage类来构建信封、任何头、主体和需要的任何附件。
SOAPMessage是Java SE 6内置SAAJ 1.3 API的一部分,因此,如果没有基于WSDL生成对象,只需要手工创建类似的SOAP消息。
SAAJ由javax.xml.ws.soap包的内容定义,这使得你可以采用低级的编程方式完整控制SOAP消息的内容。SAAJ向你提供的是XML视图,而不是使用服务的对象视图。
使用SAAJ,你可以实现:
创建SOAP消息
从SOAP消息主体提取内容
为SOAP消息创建附件
创建和提取SOAP头的内容
处理SOAP故障(异常)
发送SOAP消息
下面的代码清单显示了如何创建一个简单的消息,其主体封装了一个基本的字符串。我们将使用这作为ISBN编号传递给Web服务的getBook方法。
正如你所知道的那样,SOAP消息包含一个信封,它是单外层包装类型。SOAP 1.1由http://Schemas.xmlsoap.org/soap/envelope/命名空间提供,定义了一组与HTTP头类似的可选头、一个可选故障子载荷、一组可选附件和一个必需元素:body。
你将使用Java创建的SOAP 1.1消息实质代表的是如下文本:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<isbn xmlns="http://ns.soacookbook.com/catalog">12345</isbn>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
这样,我们开始创建信封。为了清楚起见,我列出了整个方法:创建SOAP信封,使用相应的Dispatch<SOAPMessage>,将消息内容转储到控制台,然后调用服务器。该清单如示例5-1所示。
示例 5-1:手工创建完整的SOAP消息
public void buildSoapEnv() {
try {
URL wsdl =
new URL("http://localhost:8080/CatalogService/Catalog?wsdl");
String ns = "http://ns.soacookbook.com/ws/catalog";
//Create the Service name
String svcName = "CatalogService";
QName svcQName = new QName(ns, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
//Create the Port name
String portName = "CatalogPort";
QName portQName = new QName(ns, portName);
Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
//Create the message
SOAPMessage soapMsg =
MessageFactory.newInstance().createMessage();
//Get the body from the envelope
SOAPPart soapPart = soapMsg.getSOAPPart();
SOAPEnvelope env = soapPart.getEnvelope();
SOAPBody body = env.getBody();
//Create a qualified name for the namespace of the
//objects used by the service.
String iNs = "http://ns.soacookbook.com/catalog";
String elementName = "isbn";
QName isbnQName = new QName(iNs, elementName);
//Add the <isbn> element to the SOAP body
//as its only child
body.addBodyElement(isbnQName).setValue("12345");
//debug print what we're sending
soapMsg.writeTo(out);
out.println("\nInvoking...");
//send the message as request to service and get response
SOAPMessage response = dispatch.invoke(soapMsg);
//just show in the console for now
response.writeTo(System.out);
} catch (Exception ex) {
ex.printStackTrace();
}
}
本示例主要关注的是创建SOAPMessage本身。我列出了相关的Dispatch创建和调用,因为有太多不同的可能选项供使用,而且,如果没有提供足够的内容,只是通过一些简短的片段来重新创建该示例会是令人灰心的。不过,此处的重点是,如果你有一个字符串,希望将该字符串传递到Web服务中,则可以通过下面这些基本步骤相当快地完成:
1. 创建Dispatch<SOAPMessage>。
2. 从工厂创建SOAPMessage实例。
3. 从消息获取SOAPPart并使用它来获取信封和主体。
4. 为与给定Schema类型元素的目标命名空间相对应的那个元素创建QName。
5. 调用addBodyElement方法,将QName传递给该方法并设置元素值。
6. 调用SOAP消息的dispatch来发送请求。
该示例使用前面定义的目录Web服务,该服务让你传递一个ISBN号并返回匹配的book对象。在这个示例中,有两个命名空间:一个由Web服务本身使用,另一个与定义参数并返回类型的Schema相匹配。接着,为<isbn>元素创建限定名称对象时,必须使用参数命名空间。
5.4 将SOAP响应写出到输出流
问题
希望将SOAP信封的内容打印到输出流,比如控制台或某个文件。
解决方案
使用SOAPMessage.writeTo方法,并将某个输出流(比如System.out)传递给该方法。
讨论
这是一种简单的方法来进行一些调试。当使用SAAJ来创建SOAP信封时,很容易栽跟头,忘记声明头或命名空间,或者试图在错误的位置添加子载荷。非常方便的是SOAPMessage类包含了该方法,用来将内容转储到给定的输出流。请看示例5-2。
示例 5-2:将SOAP消息的内容写到控制台
package writesoapmessage;
import java.io.IOException;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
/**
* Creates a SOAP Message and writes it to the console.
*/
public class Main {
public static void main(String[] args) {
try {
SOAPMessage soapMsg =
MessageFactory.newInstance().createMessage();
//Get the body from the envelope
SOAPPart soapPart = soapMsg.getSOAPPart();
SOAPEnvelope env = soapPart.getEnvelope();
SOAPBody body = env.getBody();
//Create a qualified name for the namespace of the
//objects used by the service.
String iNs = "http://ns.soacookbook.com/catalog";
String elementName = "isbn";
QName isbnQName = new QName(iNs, elementName);
//Add the <isbn> element to the SOAP body
//as its only child
body.addBodyElement(isbnQName).setValue("12345");
//debug print what we're sending
soapMsg.writeTo(System.out);
} catch (SOAPException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
该代码没有调用任何Web服务,只是创建了一个消息,该消息适合于发送到某个需要这种格式消息的WSDL。下面是该程序的运行结果:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<isbn xmlns="http://ns.soacookbook.com/catalog">12345</isbn>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
如果消息没有附件,它就会作为XML写出。如果消息包含附件,就作为MIME编码的字节流写出。此外,如果无法写出到给定的输出流,该方法会抛出IOException。
5.5 基于现有的SOAP信封创建Web服务客户端
问题
希望手工创建一个客户端来调用Web服务。SOAP信封位于一个纯文本文件中,这可能是某种XSL转换的结果。
解决方案
使用SAAJ 1.3 API手工创建消息,然后使用Dispatch<T>调用消息的相关服务。下面是涉及到的几个步骤:
1. 基于WSDL位置创建URL。
2. 创建代表该服务的QName对象。
3. 创建代表要调用端口的QName对象。
4. 使用端口QName创建Dispatch对象,基于javax.xml.soap.SOAPMessage参数化Dispatch,以指定你将自己创建整个SOAP信封并将它提供给Dispatch。
5. 创建Dispatch时,指定模式是Message并使用SOAPMessage.class作为数据类型。
6. 按照Message模式读取包含完整SOAP信封的文本文件并使用它来创建SOAPMessage实例。
7. 调用的dispatch的invoke方法。由于Dispatch是用SOAPMessage进行参数化,这将是响应所获得的内容。
讨论
让我们开始手头的事情。刚开始时可能会非常棘手,因此,我将一直给出清楚的说明。
在服务器端,有一个EJB Web服务,其定义如示例5-3所示,客户端将调用该服务。
示例 5-3:Catalog EJB Web服务
@WebService(serviceName="CatalogService", name="Catalog",
targetNamespace="http://ns.soacookbook.com/ws/catalog")
@Stateless
@Local
public class CatalogEJB {
@WebMethod
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
use=SOAPBinding.Use.LITERAL,
parameterStyle=SOAPBinding.ParameterStyle.BARE)
public @WebResult(name="book",
targetNamespace="http://ns.soacookbook.com/catalog") Book
getBook(
@WebParam(name="isbn",
targetNamespace="http://ns.soacookbook.com/catalog") String isbn) {
LOG.info("Executing. ISBN=" + isbn);
Book book = new Book();
//you would go to a database here.
if ("12345".equals(isbn)) {
LOG.info("Search by ISBN: " + isbn);
book.setTitle("King Lear");
Author shakespeare = new Author();
shakespeare.setFirstName("William");
shakespeare.setLastName("Shakespeare");
book.setAuthor(shakespeare);
book.setCategory(Category.LITERATURE);
book.setIsbn("12345");
} else {
LOG.info("Search by ISBN: " + isbn + ". NO RESULTS.");
}
LOG.info("Returning book: " + book.getTitle());
return book;
}
//...
}
该Web服务使用http://ns.soacookbook.com/ws/catalog命名空间运行,基于Schema的人为结果是在http://ns.soacookbook.com/catalog命名空间中定义的。build过程在编译服务之前使用XJC来基于Schema生成Java对象。该示例中的强大业务逻辑仅说明如果将“12345”作为ISBN传入,将会返回一个书籍结果,否则返回一个空的book。
getBook方法接受一个字符串ISBN码并返回与之相匹配的book对象。在该示例中,希望在客户端不创建可移植封装类对象的情况下使用wsimport之类的工具调用该服务,因此,将编写一个包含完整SOAP信封的文件,然后将其读入SAAJ Dispatch以调用服务。下面是客户端中的SOAP消息,isbnMsg.txt:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://Schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<i:isbn xmlns:i="http://ns.soacookbook.com/catalog">12345</i:isbn>
</soap:Body>
</soap:Envelope>
注意,换行符不是必需的,无论是否包含它们,你都能成功调用服务。我在此处列出它们是为了增加可读性。
那么,接下来创建dispatch客户端,这涉及到几个步骤,示例5-4中的清单给出了一个完整示例。
示例 5-4:通过Dispatch以Message模式使用SOAPMessage手工调用Web服务
import static java.lang.System.out;
import java.io.FileInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;
public class CatalogTest {
public void dispatchMsgIsbnTest() {
try {
URL wsdl =
new URL("http://localhost:8080/CatalogService/Catalog?wsdl");
String ns = "http://ns.soacookbook.com/ws/catalog";
//Create the Service qualified name
String svcName = "CatalogService";
QName svcQName = new QName(ns, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
//Create the Port name
String portName = "CatalogPort";
QName portQName = new QName(ns, portName);
//Create the delegate to send the request:
Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
String dataFile = "/path/src/xml/ch03/isbnMsg.txt";
//read in the data to use in building the soap message from a file
FileInputStream fis = new FileInputStream(dataFile);
//create the message, using contents of file as envelope
SOAPMessage request =
MessageFactory.newInstance().createMessage(null, fis);
//debug print what we're sending
request.writeTo(out);
out.println("\nInvoking...");
//send the message as request to service and get response
SOAPMessage response = dispatch.invoke(request);
//just show in the console for now
response.writeTo(System.out);
} catch (MalformedURLException mue) {
mue.printStackTrace();
} catch (WebServiceException wsex) {
wsex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
在这个示例中,指定了几个相配的选项。在5.7小节中,我们将介绍如何使用不同的选项,比如PAYLOAD代替MESSAGE,Dispatch<Source>代替Dispatch<SOAPMessage>。
下面是该程序的输出结果(为了清楚起见,我添加了换行符):
<?xml version='1.0' encoding='utf-8'?>
<soap:Envelope xmlns:soap="http://Schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<i:isbn xmlns:i="http://ns.soacookbook.com/catalog">12345</i:isbn>
</soap:Body>
</soap:Envelope>
Invoking...
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/>
<S:Body>
<ns2:book xmlns:ns2="http://ns.soacookbook.com/catalog">
<isbn>12345</isbn>
<author>
<firstName>William</firstName><lastName>Shakespeare</lastName>
</author>
<title>King Lear</title>
<category>LITERATURE</category>
</ns2:book>
</S:Body>
</S:Envelope>
SOAP信封必须为服务注解说明的消息指定命名空间。
通常不从物理文件创建完整的SOAP信封。有许多选项可用于创建SOAP消息,但我选择说明文件版本,因为它关注的是调用机制,而且最清楚。这是一个好的起点,可供其他小节参考。
5.6 从SOAP消息提取内容
问题
已经使用Dispatch<SOAPMessage>调用Web服务,接下来希望处理消息响应的XML视图。
解决方案
使用SOAPMessage响应来获取SOAP主体,然后调用它的extractContentAsDocument方法,这会提供一个org.w3.dom.Document对象供你处理,接着对其执行XPath查询,或者像其他情形一样就使用DOM 3 API。
该代码与前一示例中的代码类似,除了不是使用便利方法writeTo将响应发送到控制台外,你将获得响应的一个DOM视图,接着给出作者的姓。示例5-5给出了这是如何实现的。
示例 5-5:获得SOAP响应的DOM视图
public void extractDOMFromSOAPResult() {
try {
URL wsdl =
new URL("http://localhost:8080/CatalogService/Catalog?wsdl");
String ns = "http://ns.soacookbook.com/ws/catalog";
//Create the Service name
String svcName = "CatalogService";
QName svcQName = new QName(ns, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
//Create the Port name
String portName = "CatalogPort";
QName portQName = new QName(ns, portName);
Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
//create the message
SOAPMessage soapMsg =
MessageFactory.newInstance().createMessage();
SOAPPart soapPart = soapMsg.getSOAPPart();
SOAPEnvelope env = soapPart.getEnvelope();
SOAPBody body = env.getBody();
//Create a qualified name for the namespace of the
//objects used by the service.
String iNs = "http://ns.soacookbook.com/catalog";
String elementName = "isbn";
QName isbnQName = new QName(iNs, elementName);
//Add the <isbn> element to the SOAP body
//as its only child
body.addBodyElement(isbnQName).setValue("12345");
//debug print what we're sending
soapMsg.writeTo(out);
out.println("\nInvoking...");
//send the message as request to service and get response
SOAPMessage response = dispatch.invoke(soapMsg);
//Extract response content as DOM view
Document doc =
response.getSOAPBody().extractContentAsDocument();
NodeList isbnNodes = (NodeList)
doc.getElementsByTagName("lastName");
//just get by index; we know there's only one
String value = isbnNodes.item(0).getTextContent();
out.println("\nAuthor LastName=" + value);
//just show in the console for now
//response.writeTo(System.out);
} catch (Exception ex) {
ex.printStackTrace();
}
}
下面是该程序的运行结果:
Author LastName=Shakespeare
如果你以前处理过DOM,这应该非常容易理解。你基本上告诉JAX-WS需要一个消息视图作为SOAPMessage类型,不过,当调用extractContentAsDocument方法时,JAX-WS将创建一个新的DOM节点,删除SOAPElement部件,将SOAP主体作为文档元素添加到新的DOM节点。
接着,你可以运行XPath查询,对NodeList使用迭代,或者执行你喜欢的任何操作。
5.7 使用原始XML源和DOM创建Web服务客户端
问题
希望在不首先生成任何人为结果的情况下调用Web服务,而且希望将拥有的字符串形式的某种简单数据用作SOAP消息载荷。
解决方案
使用Service.create工厂创建一个Service实例,根据javax.xml.transform.Source对Dispatch对象进行参数化,并使用StringReader构造消息。确保指定Service.Mode.PAYLOAD。
如果忘记将Dispatch对Source进行参数化,就会获得如下消息:
Can not create Dispatch<SOAPMessage> of PAYLOAD. Must be MESSAGE.
Dispatch的类型参数是告诉它指定请求内容时希望完成的工作量大小。在前一个示例中,我们将整个SOAP信封写入一个文件。在这里,希望将该工作最小化,让Dispatch使用相应的信封封装载荷(soap:body子载荷)。
本解决方案的思想与前面使用Dispatch<SOAPMessage>的小节的思想类似,不过也有一些很不一样的地方,不是从文件获取SOAP消息,而是从字符串;它不是完整的信封,而仅是载荷,这意味着也无法将请求和响应简单地写到System.out。下面是有关步骤:
1. 基于WSDL位置创建URL。
2. 创建代表该服务的QName对象。
3. 创建代表要调用端口的QName对象。
4. 使用端口QName创建Dispatch对象,基于javax.xml.transform.Source对Dispatch进行参数化,以指定你本身将不提供完整的SOAP信封以及将其提供给Dispatch,而是仅将SOAP主体的值作为消息的内核。
5. 创建Dispatch时,指定模式为Payload并使用Source.class作为数据类型。
6. 读取希望在请求中作为soap:body子载荷进行指定的数据。
7. 调用的Dispatch的invoke方法。由于Dispatch是用Source进行参数化,这将是响应所获得的内容。
8. 无法直接对Source进行太多操作,因此,将结果转换成可以处理的DOM树。
9. 使用XPath API提取你所感兴趣的数据。
将在服务器端使用相同的方法,示例5-6给出了完整的客户端清单。
示例 5-6:以Payload模式通过XML字符串使用Dispatch<Source>的Web服务客户端
public void dispatchPayloadIsbnTest() {
try {
URL wsdl =
new URL("http://localhost:8080/CatalogService/Catalog?wsdl");
String ns = "http://ns.soacookbook.com/ws/catalog";
String objNs = "http://ns.soacookbook.com/catalog";
//Create the Service name
String svcName = "CatalogService";
QName svcQName = new QName(ns, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
//Create the Port name
String portName = "CatalogPort";
QName portQName = new QName(ns, portName);
//Create the dispatcher on Source with Payload
Dispatch<Source> dispatch =
service.createDispatch(portQName,
Source.class, Service.Mode.PAYLOAD);
//Change to tick marks or escape double quotes
String payload =
"<i:isbn xmlns:i='http://ns.soacookbook.com/catalog'>12345</i:isbn>";
//Create a SOAP request based on our XML string
StreamSource request = new StreamSource(new StringReader(payload));
out.println("\nInvoking...");
//Send the request and get the response
Source bookResponse = dispatch.invoke(request);
//Now we have to transform our result source object
//into a DOM tree to work with it
DOMResult dom = new DOMResult();
Transformer trans = TransformerFactory.newInstance().newTransformer();
trans.transform(bookResponse, dom);
//Extract values with XPath
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
NodeList resultNodes = (NodeList) xp.evaluate("//title",
dom.getNode(), XPathConstants.NODESET);
//Show the result
String title = resultNodes.item(0).getTextContent();
out.println("TITLE=" + title);
} catch (MalformedURLException mue) {
mue.printStackTrace();
} catch (WebServiceException wsex) {
wsex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
}
}
Source中包含的响应SOAP信封的值如下所示:
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/>
<S:Body>
<ns2:book xmlns:ns2="http://ns.soacookbook.com/catalog">
<isbn>12345</isbn>
<author><firstName>William</firstName><lastName>Shakespeare</lastName>
</author>
<title>King Lear</title>
<category>LITERATURE</category>
</ns2:book>
</S:Body>
</S:Envelope>
该程序的运行结果如下:
Invoking...
TITLE=King Lear
将Source作为参数化指定给Dispatch<T>后,需要将结果转换成该应用程序可以使用的内容。我们将结果解析成DOM树,然后使用XPath提取响应所获得的book的标题。
通常来说,你可能不需要使用这类低级的API,而是依赖于通过某种工具(比如wsimport)完成这项工作所生成的结果。这两种方法都将使你获得好的灵活性。注意,使用SAAJ要求你对所发送和接收的XML结构有相当的了解,当使用SAAJ时,你不必亲历粗短的生成步骤,但仍需要知道WSDL位置以及服务名称和端口,并需要了解如何使用返回的XML。然而,饱受争议的是,如果使用JAXB来提供Java视图,无论采用何种实现语义,都将获得相同的实体结构,而且,如果不知道book对象是否具有标题,这两种方法都将失效。
不过,你或许已经拥有XML格式的数据,在这种情况下,使用SAAJ是极为合适的。
5.8 添加MIME头
问题
希望在消息的HTTP传输包装上添加友好的MIME头(不是SOAP头)。
解决方案
使用SOAPMessage.getMimeHeaders.addHeader方法。
讨论
尝试如下所示的操作:
MessageFactory factory = MessageFactory.newInstance();
SOAPMessage message = factory.createMessage();
message.getMimeHeaders().addHeader("X-Powered-By", "Duff");
5.9 添加命名空间声明
问题
希望向SOAP消息添加命名空间声明。
解决方案
使用SOAPEnvelope.addNamespaceDeclaration方法向信封添加命名空间,然后创建QName并借助SOAPBody.addBodyElement方法来使用命名空间。
讨论
下面的代码将创建一个命名空间并将其与SOAP信封关联,然后创建一个使用该命名空间的空主体元素:
//Declare the namespace
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
env.addNamespaceDeclaration("e", "http://example.com/myNs");
//Use the new namespace in the body
SOAPBody body = msg.getSOAPPart().getEnvelope().getBody();
QName bodyName = env.createQName("Quote", "e");
SOAPBodyElement q = body.addBodyElement(bodyName);
为了在SOAP主体中使用该命名空间,创建了一个限定名称并将其作为元素添加到主体。下面是主体中使用该命名空间的合成SOAP消息:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/"
xmlns:e="http://example.com/myNs">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<e:Quote/>...
注意,在试图使用命名空间之前必须先声明相应的命名空间,否则,就会获得javax.xml.soap.SOAPException,指出无法找到相应的命名空间。
5.10 指定SOAPAction
问题
需要指定SOAPAction MIME头来调用必须具有该头的Web服务。
解决方案
需要能够对Dispatch或Call对象发送soapAction,然后将其值设置成服务需要的URI,可以通过该操作的soapAction属性指定。
讨论
SOAPAction是通过SOAP请求指定的HTTP头,最开始它被用来以头的方式提供路由信息。由于头不需要检查SOAP载荷,访问起来非常快,而且可以提高性能,防火墙、过滤器或路由代理可以只检查SOAPAction的值来确定消息指定的端点和操作。
是否使用SOAPAction取决于Web服务的实现者,如果你选择使用,就会对在SOAP绑定中WSDL的具体方面进行指定:
<wsdl:operation name="GetQuote">
<soap:operation style="document"
soapAction="http://www.WebserviceX.NET/GetQuote" />
...
在SOAP请求中,SOAPAction值必须是带有引号的字符串,该字符串与WSDL中soapAction属性指定的值相匹配。下面的HTTP头需要设置成与刚才显示的WSDL中指定的soapAction相匹配:
SOAPAction: "http://www.WebserviceX.NET/GetQuote"
虽然性能仍然肯定是基于SOAP的Web服务的重要问题,但该方法具有一些不足。由于更倾向于学术,依赖传输协议(HTTP)来包含SOAP特有的信息是有问题的。尽管利用头提供一些临时数据或元数据是可以接受的,但使得所包含消息发送到传输层的内部细节没有封装,就像将一个JPanel传递到DAO中。建议应该将路由信息放在SOAP消息本身中,而不是外部指定路由信息。这种观点非常有意义,实际上,这是创建WS-Addressing规范的一部分原因。WS-Addressing仍以与SOAPAction类似的方式提供路由信息,不过是通过创建的SOAP头,而不是HTTP头。
Basic Profile(来自Web Services Interoperability组织的规范,给出了用来确保多个平台之间Web服务可以互操作的原则)要求存在SOAPAction且其值必须是带引号的字符串,不过该字符串可以是空的。Java实现,包括SAAJ和JAX-WS参考实现,将自动添加SOAPAction头,并将其值指定为“”(空字符串)。这符合Basic Profile,而且没有破坏封装。
要考虑的另一件事情是WSDL 1.2和2.0规范使得soapAction是可选的,因此,它们不久可能会被舍弃以支持WS-Addressing规范提供的元素。
有些Web服务,特别是.NET中实现的Web服务,要求你为SOAPAction头指定真实值。在Java中,将创建SOAPAction头,但其值会是一个空字符串。
SOAPAction通常看起来如下所示:
POST /stockquote.asmx HTTP/1.1
Host: www.Webservicex.net
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://www.WebserviceX.NET/GetQuote"
如果没有指定它,服务就会“抱怨”。用来发送消息的Dispatch和Call对象默认情况下不会添加SOAPAction,为了使用需要包含SOAPAction的服务,你必须做两件事:启用为SOAPAction提供值的消息,然后提供相应的值。
由于必须按照这种方式启用它,SOAPAction并不是可以像任何其他需要添加的头一样进行添加的另一种MIME头。因此,让我们来看一下http://Webservicesx.net中由好心人提供的一个Web服务,该服务已经存在多年了。
提示: 该示例使用了http://Webservices.net提供的一个可公共访问的免费Web服务。该站点上的服务是使用Microsoft.NET实现的,在这些服务的WSDL方,有些事情的处理稍微有点不同,它们对必须如何创建请求产生细微影响。例如,在Java中,你可能习惯使用前缀(比如“tns:”)来指定命名空间。但是,如果读取WebserviceX Schema,它的GetQuoteResponse不是通过前缀指定的,如下所示:
<GetQuoteResponse xmlns="http://www.WebserviceX.NET/">
创建QName时,需要将此铭记在心,而且按照如下方式进行操作:
QName q = new QName("http://www.WebserviceX.NET/","GetQuote");
如果的确指定了前缀,响应中就会获得一个错误。
如果希望读取相应的WSDL和Schema,将要调用的Web服务位于http://www.Webservicex.net/WCF/ServiceDetails.aspx?SID=19,它要求提供订单符号并在响应中返回有关库存数据。示例5-7中的代码显示了如何创建相应的完整客户端。在这里,就是创建一个SAAJ客户端,根据要求指定SOAPAction,并发送有关相应Java符号信息的请求。
示例 5-7:添加该.NET Web服务需要的SOAPAction头
package addSoapAction;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
/**
* Shows how to add a MIME header, of which SOAPAction is one.
*/
public class AddSoapAction {
private static final String WSDL =
"http://www.Webservicex.net/stockquote.asmx?wsdl";
private static final String NS =
"http://www.WebserviceX.NET/";
public static void main(String...arg) {
new AddSoapAction().invoke();
System.out.println("\nAll done.");
}
public AddSoapAction() { }
private void invoke(){
try {
//Prepare service to call
Service service = createService();
QName portQName = new QName(NS, "StockQuoteSoap");
Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
//Add SOAPAction
dispatch.getRequestContext().put(
Dispatch.SOAPACTION_USE_PROPERTY, "1");
dispatch.getRequestContext().put(
Dispatch.SOAPACTION_URI_PROPERTY,
"http://www.WebserviceX.NET/GetQuote");
//Prepare Request
SOAPMessage request = createMessage();
//send request and get response
SOAPMessage response = dispatch.invoke(request);
//Write response to console
System.out.println("\nGot Response:\n");
response.writeTo(System.out);
System.out.println("\n");
} catch (Exception ex) {
ex.printStackTrace();
}
}
private SOAPMessage createMessage() throws SOAPException {
//Create a SOAPMessage
MessageFactory messageFactory =
MessageFactory.newInstance();
SOAPMessage message = messageFactory.createMessage();
try {
SOAPEnvelope env = message.getSOAPPart().getEnvelope();
SOAPBody body = env.getBody();
//Create a SOAPBodyElement
QName bodyName = new QName("http://www.WebserviceX.NET/",
"GetQuote");
SOAPBodyElement bodyEl = body.addBodyElement(bodyName);
//Add our data
QName name = new QName("symbol");
SOAPElement symbol = bodyEl.addChildElement(name);
symbol.addTextNode("JAVA");
System.out.println("\nCreated Request:\n");
message.writeTo(System.out);
System.out.println("\n");
} catch (Exception e) {
System.out.println(e.getMessage());
}
return message;
}
private Service createService() throws MalformedURLException {
URL wsdl = new URL(WSDL);
//Create the Service name
String svcName = "StockQuote";
QName svcQName = new QName(NS, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
System.out.println("Created Service: " + service.getServiceName());
return service;
}
}
该代码的重要部分是从Dispatch对象获取RequestContext,使用Dispatch类中的常数来说明希望使用SOAPAction头,从而指定该服务控制的操作:
//Say we want to use SOAPAction
dispatch.getRequestContext().put(
Dispatch.SOAPACTION_USE_PROPERTY, "1");
//Specify our value
dispatch.getRequestContext().put(
Dispatch.SOAPACTION_URI_PROPERTY,
"http://www.WebserviceX.NET/GetQuote");
上面的代码指出你希望自己指定SOAPAction属性,而不是采用默认值;接着,指定要使用的实际值。这将在HTTP请求中生成如下所示的头:
SOAPAction: "http://www.WebserviceX.NET/GetQuote"
提示: 在这里,我们使用“1”表示布尔值true。不过,也可以直接用true,SOAP规范对这两者都认可。这也同样适用于false和0,而且不仅仅是针对SOAPAction——也可以对mustUnderstand等其他条目使用这些值。
结果包含有关市场和份额的数据:
Created Service: {http://www.WebserviceX.NET/}StockQuote
Created Request:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="...">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<GetQuote xmlns="http://www.WebserviceX.NET/">
<symbol>JAVA</symbol>
</GetQuote>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Got Response:
<soap:Envelope
xmlns:soap="http://Schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body>
<GetQuoteResponse xmlns="http://www.WebserviceX.NET/">
<GetQuoteResult>
<StockQuotes>
<Stock>
<Symbol>JAVA</Symbol>
<Last>10.10</Last>
<Date>7/24/2008</Date>
<Time>4:00pm</Time>
<Change>-0.42</Change>
<Open>10.47</Open><High>10.48</High>
<Low>10.06</Low><Volume>18796276</Volume>
<MktCap>7.896B</MktCap>
<PreviousClose>10.52</PreviousClose>
<PercentageChange>-3.99%</PercentageChange>
<Name>SUN MICROSYSTEMS </Name>
</Stock>
</StockQuotes>
</GetQuoteResult>
</GetQuoteResponse>
</soap:Body>
</soap:Envelope>
5.11 向元素添加属性
问题
希望向SOAP消息主体中的元素添加属性。
解决方案
为属性创建一个QName并使用SOAPElement.addAttribute方法将其添加到相应的元素。
讨论
当为属性创建QName时,不需要为其指定命名空间,因为它将从其完备的父级属性继承。使用本地部件创建它,作为QName指定属性的名称。然后,将它作为addAttribute方法的第二个参数为其指定值。这就是实际要做的所有事情。
示例5-8显示了向消息元素添加属性的完整清单。
示例 5-8:向元素添加属性
package addAttribute;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
public class SAAJCreateAttribute {
public static void main(String args[]) {
try {
//Create a SOAPMessage
MessageFactory messageFactory =
MessageFactory.newInstance();
SOAPMessage message = messageFactory.createMessage();
SOAPEnvelope env = message.getSOAPPart().getEnvelope();
SOAPBody body = env.getBody();
//Create a SOAPBodyElement
QName bodyName = new QName("http://example.com",
"getQuote", "e");
SOAPBodyElement bodyEl = body.addBodyElement(bodyName);
//Add our data
QName name = new QName("ticker");
SOAPElement ticker = bodyEl.addChildElement(name);
ticker.addTextNode("JAVA");
//to ticker element, add our countryCode attribute
//with a value of US
QName attributeName = new QName("countryCode");
ticker.addAttribute(attributeName, "US");
message.writeTo(System.out);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
正如你看到的那样,上面已经为属性创建了一个QName,并指定了你希望该属性具有的名称。然后,在将它添加到ticker元素时,指定了该属性的值。
下面是得到的输出结果:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<e:getQuote xmlns:e="http://example.com">
<ticker countryCode="US">JAVA</ticker>
</e:getQuote>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
属性已经被添加到ticker元素,如突出显示的代码所示。
5.12 从SOAP消息去掉头
问题
希望从SOAP消息去掉头,或许你已经对它进行了处理并希望将消息转发给下一个处理程序,或者,只是不希望它占用不必要的字节流空间,因为你没有使用它。
解决方案
使用SOAPHeader.detachNode方法。
讨论
如果你知道将不使用信封的头部分,可以去掉该部分以减轻线路上发送的载荷。下面是有关实现代码:
SOAPHeader header = env.getHeader();
header.detachNode();
获得的SOAP信封如下所示,仅包含信封和主体:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="...">
<SOAP-ENV:Body>...
这就是实际要做的所有事情。
5.13 向SOAP请求添加头
问题
希望向SOAP请求添加自定义头信息。
解决方案
使用SOAPEnvelop.addHeader方法。
讨论
SOAP 1.2规范详细介绍了头,它们是用来存储SOA中有关消息交换的所有数据样式的一种主流方法。当需要使用某些种类的操作时,还可以按照该规范定义许多SOAP头,该规范定义的头包括mustUnderstand、role和relay。因此,了解如何使用它们是非常重要的。
还可以通过服务客户端和提供商来定义任何自定义头。头可以用于给出供应商扩展(通常以“X-”开头)或者处理说明,比如可以高速缓存信息集的哪些部分,安全代码或其他元数据位。
SOAP头的使用有一些基本的方法。可以使用SAAJ API(它确保可移植性),并在调用时将头添加到代码中。这是我们将要在此节中介绍的内容。
在这个示例中,我们将创建一个Web服务,介绍定义它的Schema和WSDL,然后编写客户端程序来与该Web服务交互。我们将介绍每一步以确保清楚说明了所有的部分是如何组合在一起的。
创建Web服务
将要做的第一件事是创建Web服务。将指定希望客户端作为SOAP头提供的头值,接着就可以在服务器端读取并对它执行某种处理。将创建一个passwordHeader头(你实际不通过它完成任何安全任务,因为这不是此处的重点,不过可以这样做),让该服务本身尽可能简单,以便一直关注手头的任务,它只是将两个整数加起来。
在示例5-9中,你会看见已经定义的需要头的Web服务,将使用JAX-WS快速创建该服务,这使得你可以根据一些内容进行测试。
示例 5-9:一种定义头的Web服务操作
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@WebService(targetNamespace="urn:soacookbook.saaj")
public class CalculatorWS {
@WebMethod(operationName="add", action="add")
public int add(@WebParam(name = "i") int i,
@WebParam(name="j") int j,
@WebParam(header=true, name="passwordHeader",
mode=WebParam.Mode.IN) String passwordHeader) {
System.out.print("Header value was: " + passwordHeader);
System.out.print("i=" + i + ". j=" + j);
return i + j;
}
}
突出显示的代码给出了如何将头添加到Web服务定义。使用@WebParam注解,指定header=true,然后给参数指定名称和Java类型,生成WSDL时,该Java类型将转换成XML Schema类型。这种创建头的方法是非常快速和方便的,不过,对于服务和客户端来说,可能会具有一些隐含的东西,我们将在后面说明如何通过JAX-WS处理头时对此进行详细介绍。
眼下,只需要将该服务包装在WAR的Web-INF/classes目录中并进行部署,就会为你生成WSDL,接着,可以检查WSDL以确定需要如何创建符合该接口的SOAP消息。
检查WSDL
既然已经部署服务,wsgen就会在部署时(借助Glassfish)通过JAX-WS参考实现为你生成WSDL,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/
2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="urn:soacookbook.saaj"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
targetNamespace="urn:soacookbook.saaj"
name="CalculatorWSService">
<types>
<xsd:Schema>
<xsd:import namespace="urn:soacookbook.saaj"
SchemaLocation="http://localhost:8080/SecureCalculatorApp/
CalculatorWSService?xsd=1">
</xsd:import>
</xsd:Schema>
</types>
<message name="add">
<part name="parameters" element="tns:add"></part>
<part name="passwordHeader" element="tns:passwordHeader"></part>
</message>
<message name="addResponse">
<part name="result" element="tns:addResponse"></part>
</message>
<portType name="CalculatorWS">
<operation name="add" parameterOrder="parameters passwordHeader">
<input message="tns:add"></input>
<output message="tns:addResponse"></output>
</operation>
</portType>
<binding name="CalculatorWSPortBinding" type="tns:CalculatorWS">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"></soap:binding>
<operation name="add">
<soap:operation soapAction="add"></soap:operation>
<input>
<soap:body use="literal" parts="parameters"></soap:body>
<soap:header message="tns:add" part="passwordHeader"
use="literal"></soap:header>
</input>
<output>
<soap:body use="literal"></soap:body>
</output>
</operation>
</binding>
<service name="CalculatorWSService">
<port name="CalculatorWSPort" binding="tns:CalculatorWSPortBinding">
<soap:address location="http://localhost:8080/SecureCalculatorApp/
CalculatorWSService"></soap:address>
</port>
</service>
</definitions>
正如你看到的那样,由于头的存在,已经修改了WSDL的两个主要方面。第一个是作为抽象WSDL中add消息的部件为passwordHeader创建了元素。在具体的定义中,也是指定消息将使用的传输协议的地方,你会看见一个<soap:headerpart="passwordHeader"/>条目,这是因为必须将头的抽象思想(消息附带的某些东西,但它存在于包含你真正感兴趣的载荷的主体内容之外)对应成如何具体实现将头附着于消息,本实例中的答案是通过一个标准的SOAP头。
有一对WSDL部件值得特殊注意。首先,我们来看一下add操作,它指定add消息将提供add操作的输入数据(传入的SOAP消息),它实际上是将头和常规方法参数组合在一起。该操作说明两个参数应该按照什么顺序到达,中间由空格分开。
接下来,我们看一下WSDL中的<soap:header...>元素。我们指定的是将使用SOAP作为具体的传输机制,而且声明传入的消息必须包含一个头,这是在tns前缀对应的命名空间(即目标命名空间)的add消息定义包含的passwordHeader部件中定义的。注意,这个头是作为与该WSDL对应的Schema中的自身元素单独进行定义的,它不是add复杂类型的部件。
检查Schema
该Web服务导入一个定义了消息必须交换的类型的Schema,下面是基于服务中注解而生成的完整Schema,因此,需要在客户端使用SAAJ创建XML信息集:
<xs:Schema xmlns:tns="urn:soacookbook.saaj"
xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0"
targetNamespace="urn:soacookbook.saaj">
<xs:element name="add" type="tns:add"></xs:element>
<xs:element name="passwordHeader" nillable="true"
type="xs:string"></xs:element>
<xs:element name="addResponse"
type="tns:addResponse"></xs:element>
<xs:complexType name="add">
<xs:sequence>
<xs:element name="i" type="xs:int"></xs:element>
<xs:element name="j" type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
//... response omitted
</xs:Schema>
按照该Schema,你需要创建一个称为add的元素,在给定的命名空间中,它具有两个子元素,它们将依次具有整数值,如下所示:
<tns:add xmlns:tns="urn:soacookbook.saaj">
<i>5</i><j>4</j>
</tns:add>
当然,该头本身需要具有XML结构。
提示: SOAP 1.2中的头不再像SMTP或HTTP头那样仅仅是键/值对,它们必须是完整的XML信息集,包含命名空间。
下面是符合该Schema元素passwordHeader定义的XML片段:
<SOAP-ENV:Header>
<passwordHeader xmlns="urn:soacookbook.saaj">
s3cr3t
</passwordHeader>
</SOAP-ENV:Header>
既然对需要创建何种XML结构具有一定想法,就让我们来实现它吧。当然,就像我们在前面小节中看到的那样,可以使用SOAP消息模板并在运行时替换其中的值,或者从文件读取它。不过,以编程的方式创建所有内容是更常见和直接的,因此,此处我们将这样做。
构建客户端程序
现在,你终于接触到本节的要点了,它就是编写一个程序,该程序创建一个SOAP客户端消息(包含一个XML信息集头)并调用一个远程Web服务。在本实例中,假定Web服务具有一定的安全性,需要在头中提供密码,从而需要在消息创建中说明这些。示例5-10给出了满足这些要求的SAAJ客户端。
示例 5-10:传递头的SAAJ客户端
package createHeader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
/**
* Invokes the SAAJ header service.
*/
public class SAAJCreateHeader {
private static final String NS = "urn:soacookbook.saaj";
public static void main(String...arg) {
new SAAJCreateHeader().invoke();
System.out.println("\nAll done.");
}
public SAAJCreateHeader() { }
private void invoke(){
try {
//Prepare service to call
Service service = createService();
QName portQName = new QName(NS, "CalculatorWSPort");
Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
//Prepare Request
SOAPMessage request = createMessage();
//Send request and get response
SOAPMessage response = dispatch.invoke(request);
//Write response to console
response.writeTo(System.out);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private SOAPMessage createMessage() throws SOAPException {
SOAPMessage msg = MessageFactory.newInstance().createMessage();
try {
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
SOAPHeader header = env.getHeader();
//Create header
QName passwordQName =
new QName(NS, "passwordHeader");
SOAPHeaderElement headerElement =
header.addHeaderElement(passwordQName);
headerElement.addTextNode("s3cr3t");
//Create body
SOAPBody body = msg.getSOAPPart().getEnvelope().getBody();
QName addQName =
new QName("urn:soacookbook.saaj", "add", "tns");
SOAPBodyElement bodyEl = body.addBodyElement(addQName);
bodyEl.addChildElement("i").addTextNode("5");
bodyEl.addChildElement("j").setValue("4");
System.out.println("\nCreated Message:\n");
msg.writeTo(System.out);
System.out.println("\n");
} catch (SOAPException ex) {
ex.printStackTrace();
}catch (IOException ex) {
ex.printStackTrace();
}
return msg;
}
private Service createService() throws MalformedURLException {
URL wsdl =
new URL("http://localhost:8080/SecureCalculatorApp/" +
"CalculatorWSService?WSDL");
//Create the Service name
String svcName = "CalculatorWSService";
QName svcQName = new QName(NS, svcName);
//Get a delegate wrapper
Service service = Service.create(wsdl, svcQName);
System.out.println("Created Service: " + service.getServiceName());
return service;
}
}
让我们来看看该代码包含的内容。首先,这就是一个简单的基于控制台的Java程序,它包含main方法。我们没有在类路径中放置任何库,只采用Java SE 6提供的设置。当该程序运行时,main方法创建该类的一个新实例并调用自身的invoke方法,该方法依次完成整个过程包含的三件事。首先,它使用createService方法创建服务的一个引用;接着,使用SAAJ API创建一个可以有效发送到该服务的合适SOAP消息;最后,将这些内容组合在一起并使用消息调用服务,将请求和来自服务器的响应打印出来。
在该程序中,我们关心的主要元素是头。从信封直接获取头对象,就像获取主体那样,这会返回一个SOAPHeader类型的对象。在这里,我们将向头添加一个头元素,不过,如果需要,你可以添加多个。为希望添加的头元素创建QName,在本实例中是passwordHeader。然后对头调用addHeaderElement方法,以便将这个新元素作为头的子元素,这将创建并返回SOAPHeaderElement类型的元素,接着,可以向其添加XML数据。在本实例中,它只是一个包含密码的文本节点。
评估结果
下面是该程序的运行结果,为了提高可读性,稍微添加了一些格式:
Created Service: {urn:soacookbook.saaj}CalculatorWSService
Created Message:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<passwordHeader xmlns="urn:soacookbook.saaj">s3cr3t</passwordHeader>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<tns:add xmlns:tns="urn:soacookbook.saaj">
<i>5</i>
<j>4</j>
</tns:add>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/>
<S:Body>
<ns2:addResponse xmlns:ns2="urn:soacookbook.saaj">
<return>9</return>
</ns2:addResponse>
</S:Body>
</S:Envelope>
All done.
上面显示了发出的请求以及从服务返回的响应这两方面的SOAP信封。可以看见,头值是合适的,主体的add子元素符合该Schema。很明显,如果不是按照该Schema和其他有关该服务的要求创建SOAP消息,服务将无法正确读取传入的数据。这时的操作是不明确的,取决于你如何处理它。
下面是服务器端的输出结果:
Header value was: s3cr3t
i=5. j=4
这个来自服务器日志的片段说明向操作传递的是正确的参数,服务能够读取(从而可以处理)头的值。
参见
6.11小节给出了更详细的讨论和其他方法(还有各种其他方法来将头添加到发出的SOAP消息)。
5.14 访问所有SOAP头元素
问题
希望处理传入消息中所有的SOAP元素。
解决方案
使用SOAPHeader.examineAllHeaderElements方法获得迭代器,然后使用该迭代器一次检查一个SOAPHeaderElement对象。
讨论
示例5-11给出了这是如何实现的。
示例 5-11:显示消息中的所有头
package allHeaders;
import java.io.IOException;
import java.util.Iterator;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
/**
* Loops over headers in a message.
*/
public class AllHeaders {
private static final String NS = "urn:soacookbook.saaj";
public static void main(String args...) {
try {
AllHeaders me = new AllHeaders();
me.showHeaders(me.createMessage());
} catch (SOAPException ex) {
ex.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private static void showHeaders(SOAPMessage message)
throws SOAPException {
SOAPHeader header = message.getSOAPHeader();
Iterator<SOAPHeaderElement> allHeaders =
header.examineAllHeaderElements();
while (allHeaders.hasNext()) {
SOAPHeaderElement headerElement =
allHeaders.next();
QName headerName = headerElement.getElementQName();
System.out.println("\nHeader name=" +
headerName.getLocalPart());
System.out.println("Actor=" +
headerElement.getActor());
}
}
private SOAPMessage createMessage() throws SOAPException {
SOAPMessage msg = MessageFactory.newInstance().createMessage();
try {
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
SOAPHeader header = env.getHeader();
//Create header
QName passwordQName =
new QName(NS, "passwordHeader");
SOAPHeaderElement headerElement =
header.addHeaderElement(passwordQName);
headerElement.addTextNode("s3cr3t");
//Create body
SOAPBody body = msg.getSOAPPart().getEnvelope().getBody();
QName addQName = new QName("urn:soacookbook.saaj", "add", "tns");
SOAPBodyElement bodyEl = body.addBodyElement(addQName);
bodyEl.addChildElement("i").addTextNode("5");
bodyEl.addChildElement("j").setValue("4");
System.out.println("\nCreated Message:\n");
msg.writeTo(System.out);
System.out.println("\n");
} catch (SOAPException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
return msg;
}
}
需要铭记在心的唯一一件事是:要在迭代器中处理SOAPHeaderElement对象。
下面是运行该代码后的输出结果:
Header name=passwordHeader
Actor is null
5.15 向传出的SOAP消息添加附件
问题
希望向传出的SOAP消息添加附件。
解决方案
使用SOAPMessage.createAttachmentPart方法,然后通过setContent方法向AttachmentPart添加数据。
讨论
设置附件值时,可以使用4种内容类型:
String
Stream
javax.xml.transform.Source
javax.activation.DataHandler
下面是基本思路:
AttachmentPart attachment = message.createAttachmentPart();
String data = "Some attachment data";
attachment.setContent(data, "text/plain");
attachment.setContentID("my-data");
message.addAttachmentPart(attachment);
如果附件使用字符串形式的数据,上面是最简单的实现方法。如果使用二进制数据,比如图像或PDF,或者XML源,可以使用javax.activation.DataHandler类并借助该DataHandler将内容添加到附件:
URL url = new URL("http://java.sun.com/im/logo_sun_small_sdn.gif");
DataHandler dataHandler = new DataHandler(url);
AttachmentPart att = msg.createAttachmentPart(dataHandler);
msg.addAttachmentPart(att);
在JavaBeans Activation Framework中,javax.activation.DataHandler已经存在一段时间了,不过它新增加到Java SE 6中。该类提供一致的接口来处理各种格式、具有不同来源的数据。DataHandler可以自动检测传递给它的MIME类型对象,因此,它显然能够将MIME内容类型添加到附件。由于它是通过URL创建的,从而它秘密创建了一个URLDataSource对象,这也是Java SE 6中新增的,该类实现了DataSource接口,使你可以通过输入和输出流访问数据。在该环境中,你必须知道的有关DataHandler的两件主要事情是:它是将非字符串数据放入SAAJ附件的最方便的方法,而且,当需要在从附件获取数据后立即对数据进行处理时,可以使用标准的InputStream和OutputSteam来分别读取和写出数据。
我们在前面介绍了如何使用SAAJ消息源,因此,在此就不再讨论了。
参见
5.16小节。
5.16 访问传入的附件数据
问题
需要访问接收的SOAP消息的附件。
解决方案
使用AttachmentPart.getContent方法从消息获取所有附件,对各附件进行迭代,进行处理之前检查它们的内容类型。
讨论
这是非常简单的,只需要对调用SOAPMessage.getAttachments后返回的结果进行迭代,下面的Servlet代码从客户端接收一个请求,该请求传递了一个附件。从SOAP消息读取该附件,并自己创建一个附件,然后将该附件放到SOAP消息中以将响应返回给客户端。
下面的方法是在一个SAAJ提供商Servlet中定义的,这个private方法接受SOAPMessage并调用它的getAttachments方法,后者返回一个附件部件。接着,可以调用任何可用的方法来检查内容ID、内容类型等。在本实例中,我们打印附件ID的内容、内容类型和附件数据:
@SuppressWarnings("unchecked")
private void printReceivedAttachmentData(SOAPMessage msg)
throws SOAPException {
System.out.print("Getting attachment...");
Iterator<AttachmentPart> it = msg.getAttachments();
while (it.hasNext()) {
AttachmentPart attachment = it.next();
String id = attachment.getContentId();
//Check the ID, just to pretend some business logic
if ("clientVersion".equals(id)){
String type = attachment.getContentType();
System.out.print("Attachment ID=" + id +
". Content Type=" + type);
if ("text/plain".equals(type)) {
Object content = attachment.getContent();
System.out.println("Attachment data: " + content);
}
}
}
}
你可以在文件SAAJProviderAttachServlet.java中找到该方法的完整代码清单,它接收包含附件数据的请求,打印传入的附件数据,然后为响应创建附件。传入附件数据的打印结果如下:
Getting attachment...
Attachment ID=clientVersion. Content Type=text/plain
Attachment data: Client-Version=1.1
注意,因为Java SE 5语言功能没有存在相应的Servlet API,比如泛型和注解(它们将需要等到Java EE 6才得以实现),我们指定抑制有关迭代器类型错误的编译器警告。如果你愿意,可以忽略@SuppressWarnings注解,使用迭代器的原始类型,并在调用it.next时将Object强制转换成AttachmentPart。
参见
5.21小节。
5.17 在没有WSDL的情况下连接到SAAJ端点
问题
希望创建到SAAJ端点的连接,你知道消息所需的数据结果,但不存在WSDL,而且Service.create需要WSDL。
解决方案
创建一个SOAPConnectionFactory并通过它来获取SOAPConnection对象。
讨论
SOAP连接允许你将SOAP消息发送给URL末尾的资源。这适用于任何场合,不过,如果服务不具有某个定义的WSDL,就必须使用SOAP连接,这是因为调用Service.create时需要提供WSDL的位置。基于SOAP的服务不包含WSDL的情况是比较少的,但是,这确实存在,你将会有所准备。
//Prepare Request
SOAPMessage request =
MessageFactory.newInstance().createMessage();
//add data to request SOAP Message here...
//Create connection object
SOAPConnectionFactory scf = SOAPConnectionFactory.newInstance();
SOAPConnection connection = scf.createConnection();
//Create an endpoint to invoke
URL endpoint = new URL("http://localhost:8080/" +
"SAAJProvider/SAAJProviderServlet");
// Send request to endpoint, get response
SOAPMessage response = connection.call(request, endpoint);
要创建指向不包含WSDL的Web服务的连接,可以使用SOAPConnection类来与远程资源直接进行通信。首先,按照通常那样为请求创建SOAP消息并根据需要填充其主体数据,然后,使用SOAPConnectionFactory类获得SOAPConnection对象的实例,接着,创建代表希望调用的远程资源(Servlet)的URL对象。将SOAP请求消息和希望调用的端点传递给连接对象的call方法,然后等待它返回SOAP响应。
总的来说,这非常简单而且易于使用,下面是两点不太主要的使用说明:
传递给connection.call方法的端点URL可以是一个字符串,也可以是一个java.net.URL。
如同使用JDBC连接那样,完成后不要忘记关闭连接。调用后可以立即关闭连接,返回的响应还可以在后面接着使用。
提示: SOAPConnection类的实现是可选的,如果你的实现不支持它,调用SOAPConnectionFactory.newInstance时就会抛出UnsupportedOperationException。Sun的SE 6 VM支持它。
5.18 使用SOAP Actor
问题
希望指定用来处理消息某一方面的SOAP Actor。
解决方案
创建一个常规的SOAP头,向其添加头元素,接着对头元素调用setActor方法。
讨论
作为SOAPHeaderElements出现的属性提供了节点应该如何处理消息的有关说明。它们允许你指定消息是否必须由某个给定节点处理,应该如何管理事物,那些种类的策略适用于消息等。
提示: 在SOAP 1.2中的术语actor被role替换。该规范的5.2.2节给出了role的定义,它说明的是被指定了特殊头块的特殊节点,指定的节点应该在接收到消息后立即对消息进行处理。
大多数SOAP消息是由称为Ultimate Receiver的一个节点处理的,如果没有指定其他actor,这就是默认的actor。客户端发送消息到服务端点,服务端点处理消息的内容并可能返回一个响应。不过,通过一种管道和过滤器设计,可以创建一系列的节点来处理所接收到的SOAP消息的不同部分。在这种方案中,一组SOAP处理程序对应的各个节点检查每个头元素,以确定任何头是否必须由该节点支持的角色来处理。如果只有一个处理程序,按照定义是在Ultimate Receiver角色中进行处理,而且它执行所有的处理。
actor属性是可选的,如果不指定,消息将直接路由到Ultimate Receiver。SOAP Actor和role是通过URI标识。处理路径中的各个节点可以支持多个角色。
作为一个示例,我们假定传入的消息代表一个发货单。它的SOAPHeader定义各种SOAPHeaderElement,每个SOAPHeaderElement指定一种不同的actor属性来将消息路由到不同的应用程序,比如客户管理、库存、账目或一些关联方面(如登录和安全性)。一旦中间处理程序接收到消息,它会处理相应的头块,去掉已经处理的头,可能会添加新的头并转发消息。最终的接收程序只是处理所有头和所有主体信息并返回响应。
下面是通过头指定actor的有关基本说明:
//Create header
QName passwordQName = new QName(NS, "passwordHeader");
SOAPHeaderElement passwordHeader =
header.addHeaderElement(passwordQName);
passwordHeader.addTextNode("s3cr3t");
passwordHeader.setActor("http://example.com/authenticator");
//Set the Actor on it
passwordHeader.setActor("http://example.com/authenticator");
该代码使用头元素的setActor方法,创建了如下所示的SOAP信封:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<passwordHeader xmlns="http://soacookbook.com/saaj"
SOAP-ENV:actor="http://example.com/authenticator">
s3cr3t
</passwordHeader>
</SOAP-ENV:Header><SOAP-ENV:Body
只有消息路径中那些actor值被指定为http://example.com/authenticator的节点才会处理passwordHeader头元素。
这里有一个名为next的特殊actor,其URI值是http://Schema.xmlsoap.org/soap/actor/next,这表示该链条中的下一个接收者应该处理消息。使用next意味着:无论处理路径中下一个节点支持何种角色,它都必须处理消息。
节点处理头后,它必须从SOAP消息中去掉该头。有些开发者不喜欢这种从消息中去掉数据的方法,因为这可能会很难跟踪消息的历史而且可能会破坏完整性。因此,他们反而选择修改现有的头,这种修改事实上是去掉已经处理的实际头并添加他人都不将处理的一个头。
5.19 通过Dispatch进行异步调用
问题
将要使用SAAJ API的Dispatch类来调用Web服务,希望执行异步调用。
解决方案
像往常那样创建Dispatch对象,使用下面提供的两种方法中的一种来调用invokeAsync,而不是调用invoke方法:
Response<T>invokeAsync(T msg)
Future<?> invokeAsync(T msg, AsyncHandler<T> handler)
下面是一个示例:
Dispatch<SOAPMessage>dispatch =
service.createDispatch(portQName, SOAPMessage.class, Service.Mode.MESSAGE);
//Send the request and get the response
Response<Source> bookResponse = dispatch.invokeAsync(request);
讨论
除了获得Dispatch对象提供的方法外,轮询和回叫响应的行为与SEI是类似的。通过这种方法,没有生成SEI,但内在实现的任务是相同的,因为JAX-WS重用SAAJ。
提示: 请记住,在试图调用invokeAsync之前,需要使用JAX-WS:bindings元素来对WSDL启用异步映射。否则,调用看起来是成功的,但实际上不是异步的。也就是说,这些调用将阻塞,就像通常调用invoke一样。
参见
要想了解如何使用JAX-WS中的异步方法及其返回类型,请参阅6.19小节。
而要想在服务器端启用异步调用,可以参阅7.15小节。JAX-WS规范在第8章中介绍了异步绑定和其他自定义,你可以阅读http://jcp.org/aboutJava/communityprocess/pfd/jsr224/index.html提供的相应文档。
5.20 在客户端基于Schema验证载荷
问题
希望使用一个其Schema定义了约束(比如字符串的最小和最大长度,或者使用正则表达式给出模式限制)的Web服务,希望客户端生成的Java对象符合该Schema,然而,当使用wsimport时,生成的对象“忘记”了有关约束。希望以一种可移植的方式解决这个问题,不用使用供应商扩展。
解决方案
像往常一样在客户端应用程序中填充Java对象,然后创建JAXB Marshaller并对其设置Schema引用。接着,当需要调用服务时,将一个代表所生成参数类型的已填充Java对象传递给使用marshaler来填充载荷的Dispatch。JAXB将会为你完成验证。
讨论
本小节解决的基本问题是:希望调用一个其Schema定义了载荷必须遵守的约束的服务,而JAXB从该Schema生成的Java对象却没有采用这些约束。当需要调用服务时,就无法确保数据填充对象实际符合Schema定义的约束。你不希望在别的地方重复定义这些约束,但的确希望确保服务只让实际符合该Schema的有效对象通过。因此,需要一种方法验证这些对象。虽然希望由该服务执行验证(而且它应该这样),但不希望由于某人敲错电话号码而导致不必要的网络开销。
提示: 本小节说明如果不是使用最新版本的JAX-WS,如何手工实现这个任务。不过,如果使用的是最新版本的JAX-WS,实现起来就更简单,方法是使用com.sun.xml.ws.developer.SchemaValidation功能,为其指定一个处理程序,并将处理程序传递给代理。但是,这是Metro 1.1所特有的,而且不能移植。
需要几个步骤来完成这一任务,因此,我们将逐步实现。该解决方案将使用第7章中定义的Credit Authorization服务的Schema和WSDL。
提示: 本小节是Credit Authorization示例的客户端方,使用包含7.14小节JAXB所生成类型的自定义Schema。你还可以参考2.17小节有关在JAXB中进行Schema验证的讨论。
下面概述了所要进行的操作:
1. 将Schema副本导入到本地项目中。在NetBeans 6中,这是容易实现的,NetBeans 6将会获取从一个WSDL引用的所有Schema。另外,可以通过一个浏览器获取它们,接着基于WSDL中的Schema位置复制它们。
2. 生成和编译Schema中定义的与WSDL有关的Java类型,在你的应用程序中使用这些Java对象。
3. 在编组到XML过程中,验证Java对象,如2.17小节所示。基本上,你是通过在Schema中读取并附着到JAXB Marshal对象来实现的。
4. 将DOM对象中的原始XML作为通过SAAJ创建的SOAP消息的主体的子载荷来添加,使用Dispatch<T>通过XML表示直接调用Web服务。
5. 在调用服务之前,SAX分析器将会在编组过程中抛出任何验证异常,比如SAXParseException。接着,你可以处理这些异常,比如通过将这些异常报告给客户端。
让我们首先看看JUnit 4.4测试,你将使用它作为服务客户。为了清楚起见,保留了所需的导入列表。该测试定义了将用于验证的本地保存的Schema文件的物理路径。Schema定义请求和响应消息中使用的CreditCard、Name和Authorization类型,这些类是使用wsimport工具从WSDL生成到com.soacookbook.ns.credit包中的。
该测试将首先创建信用卡和名称对象并填充它们,就像你在客户端GUI中所做的那样。不过,你将调用自己的validateAndInvoke方法,而不是使用导入过程中同时生成的Service衍生物。它将借助其他方法来使用JAXB手工转换成XML,然后将载荷传入方法,因此,你可以自己创建SOAPMessage。此时,该过程变成一个调用程序黑匣子,SOAP调用返回一个Authorization对象,就像使用JAX-WS代理时那样。
提示: 通常不将这类辅助部分包含在测试用例中;理想情况下,这些应该封装在一个客户端类中;这样看起来更简单。
package com.soacookbook.ch03.test;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import static org.junit.Assert.*;
import com.soacookbook.ns.credit.*;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.soap.SOAPBinding;
import org.apache.log4j.Logger;
import org.junit.*;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Tests that JAXB-generated objects validate against a Schema
* at runtime.
*/
public class SchemaValidateTest {
private static final Logger LOGGER =
Logger.getLogger(SchemaValidateTest.class);
private static final String SchemaFile =
"C:/oreilly/soacookbook/code/" +
"chapters/client/config/ch03/Credit.xsd";
//Tests that our Schema constraints are not violated.
@Test
public void testCreditValidating() throws Exception {
LOGGER.debug("Executing.");
//JAXB-generated types from Schema
CreditCard card = new CreditCard();
Name cardholder = new Name();
cardholder.setFirstName("Eliza");
cardholder.setLastName("Doolittle");
card.setName(cardholder);
//4222222222222222
card.setCardNumber("4");
//see setup method which creates this
card.setExpirationDate(expiryDate);
LOGGER.debug("Invoking Credit Authorizer Service.");
//invoke service using SAAJ here:
Authorization auth = validateAndInvoke(card);
assertTrue(2500.0D == auth.getAmount());
}
//This is called by the test
private Authorization validateAndInvoke(CreditCard card){
Authorization auth = new Authorization();
try {
//Create DOM from CreditCard obj and validate against Schema
Document domCC = marshalCC(card);
LOGGER.debug("Got card. " + domCC);
//Create SOAP Message from DOM
SOAPMessage soapCC = createSoapMessage(domCC);
//Dispatch SOAP Message: invoke svc
SOAPMessage soapAuth = invoke(soapCC);
//unmarshall back into Authorization object
auth = unmarshal(soapAuth);
} catch (JAXBException iae){
LOGGER.warn("JAXB: Invalid data! ", iae);
} catch (SAXException se){
LOGGER.warn("SAX: Invalid data! " + se);
}
return auth;
}
/**
* Creates a SOAP Envelope with a SOAP Body containing this
* document as its child. */
public SOAPMessage createSoapMessage(Document document) {
LOGGER.debug("Executing.");
SOAPMessage message = null;
try {
message = MessageFactory.newInstance().createMessage();
final SOAPEnvelope env = message.getSOAPPart().getEnvelope();
final SOAPBody body = env.getBody();
body.addDocument(document);
message.saveChanges();
LOGGER.debug("Created SOAP Message.");
} catch(SOAPException se){
LOGGER.error("Could not create SOAP message. ", se);
}
return message;
}
/**
* Uses the previously created request message to call the
* Web service and return a response.
*/
public SOAPMessage invoke(SOAPMessage request) {
LOGGER.debug("Executing.");
String ns = "http://ns.soacookbook.com/credit";
QName svcQName = new QName(ns, "CreditService");
QName portQName = new QName(ns, "CreditAuthorizer");
String wsdlUrl = "http://localhost:8080/soaCookbookWS/CreditService?wsdl";
final Service service = Service.create(svcQName);
service.addPort(portQName,
SOAPBinding.SOAP11HTTP_BINDING, wsdlUrl);
LOGGER.debug("Invoking Service: " + service.getServiceName() +
". Port: " + portQName + ". WSDL Location: " + wsdlUrl);
final Dispatch<SOAPMessage> dispatch =
service.createDispatch(portQName,
SOAPMessage.class, Service.Mode.MESSAGE);
//Call the Service with our message
return dispatch.invoke(request);
}
@SuppressWarnings(value = "unchecked")
public static Authorization unmarshal(SOAPMessage soapMsg){
LOGGER.debug("Executing.");
String pkg = "com.soacookbook.ns.credit";
Authorization auth = null;
try {
JAXBContext ctx = JAXBContext.newInstance(pkg);
Unmarshaller unmarshaller = ctx.createUnmarshaller();
//Show returned SOAP Message
soapMsg.writeTo(System.out);
//Get the payload of the response
Document doc =
soapMsg.getSOAPBody().extractContentAsDocument();
//turn DOM docunment paydload into JAXBElement
JAXBElement<Authorization> el =
(JAXBElement<Authorization>) unmarshaller.unmarshal(doc);
//extract the payload as object
auth = el.getValue();
LOGGER.debug("DOM AUTH: " + auth);
} catch (IOException ioe) {
ioe.printStackTrace();
} catch (SOAPException se) {
se.printStackTrace();
} catch (JAXBException je) {
je.printStackTrace();
}
return auth;
}
private static Document marshalCC(final CreditCard card)
throws JAXBException, SAXException {
Document doc = null;
try {
Class[] clazz = {CreditCard.class};
JAXBContext ctx = JAXBContext.newInstance(clazz);
String ns = "http://ns.soacookbook.com/credit";
QName qName = new QName(ns, "creditCard", "");
JAXBElement<CreditCard> root =
new JAXBElement<CreditCard>(
qName, CreditCard.class, card);
Marshaller m = ctx.createMarshaller();
SchemaFactory sf = SchemaFactory.newInstance(
XMLConstants.W3C_XML_Schema_NS_URI);
Schema Schema = sf.newSchema(
new StreamSource(new File(SchemaFile)));
m.setSchema(Schema);
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.newDocument();
m.marshal(root, doc);
LOGGER.debug("DOM object has elements: " +
doc.getChildNodes().getLength());
} catch (ParserConfigurationException pce) {
pce.printStackTrace();
}
return doc;
}
createSoapMessage方法用于将CreditCard对象的内容接收为DOM文档并将它作为你所创建的SOAP消息的唯一子载荷来添加。一旦具有一个完整的SOAP消息,就将它传递给invoke方法,这将会建立实际调用服务机制。
为指定的WSDL中定义的端口和服务创建相应的QName,然后创建一个服务,向它添加已知端口。指定SOAP 1.1作为服务将遵守的协议。此外,dispatch是通过SOAP消息类型参数创建的,你将把创建的SOAP消息作为请求传递给它,它将返回类型SOAPMessage的值作为响应,其中将包含验证XML。
marshal和unmarshal方法互为镜像,前者处理Java到XML之间的转换,后者处理XML到Java之间的转换。因此,除了验证发出的请求,还可以使用相同的技术来在unmarshal方法中验证响应载荷。不过,这样做时要当心,因为如果响应实际上是SOAP故障而不是拙劣形式的验证,你将会获得JAXB错误,验证时将发现有个子载荷不是SOAP主体所期望的。
如果按照给定的Schema约束,调用的服务包含不合法的数据,你就会获得具有如下消息的SAXParseException:
org.xml.sax.SAXParseException: cvc-pattern-valid:
Value '4' is not facet-valid with respect to pattern ‘\d{16}' for type 'CardNumber'.
我们试图使用一个4位信用卡号码来调用服务,但约束告诉我们必须提供16位数字才是有效的。
这是一种可移植的灵活方法来处理基于Schema验证对象。
可以相当简单地将该解决方案修改成更一般的用例,以处理任意种类的请求和响应。也就是说,在这里,我们将Schema位置、包名、类名以及端口和服务的QName硬编码到方法中,这些可以轻松地传递给方法以创建一个适用于任何Schema、对象和服务的Schema验证处理程序。
5.21 提供基于SAAJ的Web服务
问题
希望在服务器端使用SAAJ来返回SOAP请求的响应。
解决方案
创建一个常规Servlet并使用SAAJ API创建SOAP消息响应,就像你在客户端所做的那样。实际上很少这样做,因为注解使问题得以更简单、更好地解决,不过,总是有些时候希望手工完成某些事情,或者可能想看看JAX-WS幕后在做什么。
讨论
下面是创建基于SAAJ的服务需要采用的基本步骤:
1. 创建一种常规Web应用程序结构,就像创建任何WAR那样。
2. 创建一个Servlet类,使其扩展javax.Servlet.http.HttpServlet。
3. 在一个静态的构造方法中,实例化一个静态的javax.xml.soap.MessageFactory字段。
4. 覆盖doPost方法,使用SAAJ为响应创建SOAP消息。
5. 使用SOAPMessage.writeTo方法将响应返回到Servlet的输出流中。
在本示例中,我们将逐步创建一个Hello World类型应用程序。为了让它尽可能的简单,将只要调用包含常规HTTP请求却返回SOAP响应的Servlet。
示例5-12中的代码清单显示了实现SAAJ SOAP provider的Servlet。实际上,此处根本没有涉及任何神秘的内容,就是使用本章介绍的标准SAAJ知识,创建一个常规Servlet,用来返回作为响应的SOAP消息。
示例 5-12:Servlet SOAP Provider
package saaj.provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import javax.Servlet.ServletException;
import javax.Servlet.http.HttpServlet;
import javax.Servlet.http.HttpServletRequest;
import javax.Servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
/**
* SAAJ Servlet to act as Web service provider.
*/
public class SAAJProviderServlet extends HttpServlet {
static MessageFactory mf;
static {
try {
mf = MessageFactory.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
System.out.println("\nGot Http Request: " +
request.getMethod());
try {
SOAPMessage reply = createReply(request);
if (reply != null) {
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("X-Powered-By", "SOA Cookbook");
response.setContentType("text/xml");
//Log for debug:
System.out.println("\nSending Response:\n");
reply.writeTo(System.out);
//Return response
OutputStream os = response.getOutputStream();
reply.writeTo(os);
os.flush();
os.close();
} else {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
} catch (Exception ex){
throw new ServletException("SAAJ could not " +
"understand request. " + ex.getMessage());
}
}
private SOAPMessage createReply(HttpServletRequest request)
throws SOAPException, IOException {
String ns = "urn:soacookbook:saaj";
//Get all HTTP Headers
MimeHeaders headers = getHeaders(request);
//Get incoming request as stream
InputStream is = request.getInputStream();
//use headers and request stream to create SOAP Msg
SOAPMessage reqMsg = mf.createMessage(headers, is);
//Create new message for response
SOAPMessage msg = mf.createMessage();
SOAPPart part = msg.getSOAPPart();
SOAPEnvelope env = part.getEnvelope();
SOAPBody body = env.getBody();
//Add Namespace
env.createName("message", "soa", ns);
//Put data in response
Name bn = env.createName("message", "soa", ns);
SOAPBodyElement be = body.addBodyElement(bn);
//Inspect request and get data from it as necessary...
String name = request.getParameter("name");
be.addTextNode("Hello, " + name + "!");
msg.saveChanges();
return msg;
}
//Here we inspect HTTP Request headers
@SuppressWarnings("unchecked")
private MimeHeaders getHeaders(HttpServletRequest request)
throws SOAPException {
MimeHeaders headers = new MimeHeaders();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()){
String key = names.nextElement();
String value = request.getHeader(key);
headers.addHeader(key, value);
System.out.println("Added MIME Header: " +
key + "=" + value);
}
return headers;
}
@Override
public void init() throws ServletException {
super.init();
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public SAAJProviderServlet() { }
}
由于只是单击一个链接,该HTTP方法将用作一个GET,你只是将这移交给doPost覆盖来处理。此处,我们处理了一些基本的常规事物,比如设置内容MIME类型并将HTTP状态代码设置成200。接着,我们做了一些实际事情,例如添加链条中非常有用的自定义头。
下面是该示例最基本的部分:可以使用请求来获取输入流,在为响应创建SOAP消息时可以使用该输入流。你将可以访问请求中的头和数据,并在必要的时候,基于它们来创建响应。
对于响应,我们将SOAP消息的内容类型设置成“text/xml”。如果保留默认的“text/html”不变,浏览器会试图将响应作为HTML进行解释。对于这个示例,这意味着只在浏览器中显示问候语,因为无法理解的标记会被禁用(就SOAP而言是这样)。通过将内容类型改成“text/xml”,整个消息就会显示在浏览器中。当然,在实际工作中,你或许不会遇到这样的使用情况,而是以另外一种方式调用该Servlet并准备处理SOAP响应。
此外,SOAPMessage.writeTo方法用于将创建的响应发送给HttpServletResponse获得的输出流,这会将包含SOAP消息和全部常规HTTP头信息的响应返回给客户端。
为了调用Web服务,我们将玩一点小花招,也就是说,不是向它发送SOAP请求,而是创建一个敏捷的JSP,该JSP通过一个链接调用Servlet,这个链接将人名作为一个请求参数传递给Servlet,该参数将用来返回包含典型问候语的SOAP响应。
因此,该JSP看起来如下所示:
<html>
<body>
<a href="SAAJProviderServlet?name=Eben">CLICK ME</a>
</body>
</html>
此处没有实际的SOAP客户端而且没有处理SOAP响应,只是一个调用该Servlet的链接。本章提供了许多关于获取SOAP响应的示例,因此,我们在这里将只关注服务器。
该Servlet是通过在Web.xml中使用一个映射来部署的,相对于JSP来说,该文件更容易调用(在本示例中,它们位于同一WAR):
<Web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/Web-app_2_5.xsd">
<Servlet>
<Servlet-name>SAAJProviderServlet</Servlet-name>
<Servlet-class>saaj.provider.SAAJProviderServlet</Servlet-class>
<load-on-startup>1</load-on-startup>
</Servlet>
<Servlet-mapping>
<Servlet-name>SAAJProviderServlet</Servlet-name>
<url-pattern>/SAAJProviderServlet</url-pattern>
</Servlet-mapping>
...
单击该链接将会在浏览器中给出如下响应:
HTTP/1.x 200 OK
X-powered-by: SOA Cookbook
Server: Sun Java System Application Server 9.1_02
Content-Type: text/xml
Transfer-Encoding: chunked
Date: Fri, 25 Jul 2008 18:21:21 GMT
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<soa:message xmlns:soa="urn:soacookbook:saaj">
Hello, Eben!
</soa:message>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
该SOAP消息包含愉快的问候语,而且,如果你转储消息的整个内容,将会看见用户定义的HTTP头。
5.22 发送和接收SOAP故障
问题
希望处理向SAAJ客户端返回了SOAP故障的Web服务。
解决方案
使用SAAJ故障类处理数据。
讨论
当Web服务提供程序抛出一个异常或错误时,就生成了一个SOAP故障。这与Java中的异常非常相似,比如有关异常的细节、可供人们阅读的说明和代码。
但是,要注意有很多不同之处。首先,不像Java异常那样,故障的使用不存在语言结构机制。也就是说,没有使用try-catch机制。故障替换SOAP响应的主体内容,因此,主体元素必须包含该故障,而且是作为唯一的子元素来包含。然后,客户端必须按照准备读取成功调用后所得数据那样来准备读取主体中的故障,这意味着故障就是SOAP中的数据。此外,无法像Java异常那样重新路由或停止处理。故障就是一个消息,和其他内容没什么区别。
如果消息交换采用请求-应答,故障消息将作为soap:Fault类型返回给调用程序。如果调用是单向的,故障就不会返回给调用程序。
下面是SOAP 1.1中soap:Fault只能包含的子元素:
faultstring
必需的元素,它对故障提供可供人们阅读的说明。
faultactor
它是指定actor时的一个必需元素,如果没有指定actor,Ultimate Receiver就被当作是节点,在这种情况下,该元素是可选的。它由标识导致故障的actor的URI指定。
detail
当错误是有关SOAP主体处理时,它是一个必须元素,在这种情况下,它提供错误内容的有关细节。
faultcode
是出现Fault型故障时的一个必需元素,它必须是完全限定名称(也就是说,它必须包含前缀和本地部件)。
SOAP 1.1规范定义了4类故障代码,如表5-1中所示。
表5-1:SOAP 1.1故障代码
本地名称 意义
Client(在SOAP 1.2中称为Sender) 处理SOAP消息的节点发现该消息格式不正确,或者没有包含足够的数据来完成处理
Server(在SOAP 1.2中称为Receiver) 消息本身没有任何错误,但抛出该故障的节点由于接收程序出现问题而无法处理消息,这通常意味着同一消息后来可能被再次发送,而接收程序那时可以对它进行处理
MustUnderstand 抛出该故障的节点没有理解消息的部件,而消息的头宣称它应该理解
VersionMismatch 抛出该故障的节点期望得到一个有效的信封,其中包含正确的命名空间和本地名称,去找到的是其他东西
下面是SOAP 1.2中soap:Fault只能包含的子元素:
Reason
对故障提供可供人们阅读的说明。
Node
给出故障发生时正在处理消息的那个节点。
Role
给出故障发生时处理程序充当的是何种角色。
Detail
提供特定于应用程序的错误信息,可用于对使用的故障代码提供额外说明。
Code
根据多个本地名称中的一个来对故障进行分类并提供需要遵守的明细条目背景。
在这些元素中,只有Code和Reason是强制性的。正如你在表5-2中看到的那样,SOAP 1.2比SOAP 1.1多定义了一个故障代码,你将注意到,SOAP 1.1的部分故障代码被SOAP 1.2继续采用,另外一些故障代码被重新命名。
表5-2:SOAP 1.2故障代码
本地名称 意义
DataEncodingUnknown 只存在于SOAP 1.2,抛出该故障的节点无法读取消息使用的数据编码
Sender 在SOAP 1.1中称为Client
Receiver 在SOAP 1.1中称为Server
MustUnderstand 与SOAP 1.1的相同
VersionMismatch 与SOAP 1.1的相同
发送故障
熟悉故障的基本结构后,让我们来看看如何在服务器端使用故障。
基本思路是获取消息主体,然后调用addFault方法。可以使用针对SOAPFault对象定义的其他方法来添加名称、代码、actor和故障字符串,如下所示:
//SOAP 1.1
Name codeName = soapFactory.createName("Server", "",
SOAPConstants.URI_NS_SOAP_ENVELOPE);
fault.setFaultCode(codeName);
fault.setFaultActor("http://soacookbook.com/books");
fault.setFaultString("The remote susbsytem host is unavailable.");
获取故障信息
由于缺少结构化的异常处理,而且故障内容替换主体内容,因此,需要一种方法来检查是否发生了故障。为此,可以使用便利方法SOAPBody.hasFault:
SOAPBody body = responseMessage.getSOAPBody();
if (body.hasFault()) {
SOAPFault fault = body.getFault();
Name code = fault.getFaultCodeAsName();
String string = fault.getFaultString();
String actor = fault.getFaultActor();
}
如果该检查确定接收到故障,就可以使用body.getFault方法获取故障对象的内容,接着,可以使用各种访问方法来获取故障的代码、字符串和actor值。
关于故障代码,有一些不同的方法。
获取故障细节
回顾一下,只有当节点无法处理主体内容时,SOAP故障将包含详细信息,这些信息采用的形式是一个javax.xml.soap.Detail对象以及一个或多个DetailEntry对象。Detail对象是DetailEntry对象的容器,Detail只提供了三个可用的实际方法,第一个方法获取它所包含的所有DetailEntry对象,而另外两个供你使用Name或QName来创建和添加DetailEntry对象到细节中:
Detail d = body.getFault().getDetail();
Iterator<DetailEntry> it = d.getDetailEntries();
while (it.hasNext()) {
DetailEntry e = it.next();
System.out.println("Detail Entry = " + e.getValue());
}
在此处,我们获取了故障的细节对象,从而可以迭代条目列表。回到服务器端,创建的条目如下所示:
QName name = new QName("http://example.com/quotes",
"getQuote", "e");
//Create the DetailEntry and add it
d.addDetailEntry(name);
在上面的清单中,我们创建了一个完全限定的名称来表示细节条目,该方法创建并添加该条目。
参见
如果你希望从服务器角度进行测试,并对其进行修改以在某些情况下返回故障,可以参阅5.20小节来检验该代码清单。
本章小结
在本章中,我们介绍了一些不同的方法来使用SAAJ API搭建SOAP消息,以及基于这些消息创建和调用Web服务。现在,你可以在XML层面上处理基于SOAP的服务,接下来,我们将讨论如何使用JAX-WS API来去掉一些样本代码以及更快地响应服务描述更改。
第6章
用JAX-WS创建Web服务应用程序
6.1 概述
第5章介绍的SAAJ API提供了一种有效的灵活方法来在较低的层次上处理Web服务,而Java API for XML Web Services(JAX-WS)是一种用于使用和提供Web服务的高级API,本章介绍JAX-WS并说明如何使用它实现各种实际任务。
JAX-WS与其他API相比
JAX-WS替代了老的JAX-RPC API,与SAAJ不同,JAX-WS不要求你非常了解XML或WSDL,整个XML层对开发者来说都是隐藏的,而开发者可以只处理由Java SE 6和EE 5附带的Web服务工具生成的对象,这些对象封装了与创建SOAP消息、调用服务和解析响应有关的所有工作,将非常多的复杂内容对开发者隐藏起来。这为开发者提供了便利,而且使客户端的维护更加容易。
JAX-WS实际上是建立在SAAJ之上的,内在是使用它来完成解析和通信工作。正如你所知道的那样,WSDL表示的Web服务将定义服务请求和响应中使用的各个消息部件的XML类型,为了创建这些类型的对象表示,需要使用一种绑定语言来实现从Java到XML的转换(编组)及其反过程。在老的API中,这是在JAX-RPC中直接完成的,JAX-RPC定义了自己的转换机制,但是,这后来被认为加深了规范的复杂性,从Java到XML的转换被看作是应该采用单独规范的内容,因此,与像JAX-RPC那样定义自己的转换机制不同,JAX-WS使用外部的JAXB 2.0(Java API for XML Binding)规范。JAX-WS支持面向消息和面向RPC的Web服务。
不同的Java EE供应商实现了JAX-WS。4.3小节介绍了参考实现,称为Metro,它提供了许多其他功能,特别是包括那些支持互操作性、安全性和可靠消息传递的重要WS-*规范的实现。JAX-WS还支持WS-Basic Profile 1.1,它是一种标准,服务实现可以遵守该标准来确保不同平台之间的互操作性。
三种方法
有三种不同的方法来使用JAX-WS开发应用程序:
WSDL到Java方法
指向一个WSDL,接着使用wsimport之类的工具生成可移植Web服务项目。
Java到WSDL方法
创建一个服务端点接口作为Java源文件,将它们用作输入来生成WSDL和所需的其他可移植项目。
从Java和WSDL同时开始方法
这是一种巧妙的处理方法,编写Java类并让wsgen为你创建WSDL和Schema,然后,本地保存所生成的结果,根据需要对其进行修改,并通过@WebService注解的wsdlLocation属性将服务实现指向它们。这意味着你需要将类与Schema和WSDL保持同步,不过,这样会为你带来最大的方便并使你拥有最大的控制权。
使用任何一种方法,JAX-WS都将生成大量代码,减少了处理机器可读代码方面的挑战。而且,这些生成的结果可在不同供应商之间移植。就像EJB或任何其他“可移植”Java结果一样,生成的代码可能稍微有些变化,没有完全符合规范;或者符合规范,但涉及规范某个有点模糊的方面,或该方面有待供应商做出实现决策。在这些情况下,可能会存在可移植性问题,因此,如果这对于你来说很重要,你会考虑额外检查一下生成的结果,看看那些可能出现冲突的地方。
注解
JAX-WS使用了大量的Java 5注解,因此,你编写的源代码将相当少,接着工具会处理注解,以创建所需的基础性结果来运行或使用服务。生成的结果本身,包括客户端、服务和JAXB 2.0值类,都使用大量的注解。
这为开发者提供了一些衍生物。例如,由于注解不允许使用运行时参数,如果你需要指定与所生成结果不同的值,则必须处理好注解属性。本章中有些小节说明了如何通过某些方法来解决这一问题,比如,改变客户端代理的WSDL位置。不过,常规框架也提供的一些方法来处理该限制。例如必要时,可以使用处理程序修改传出的消息。
服务端点
Web服务端点是一个服务器端组件,它接收SOAP消息,进行一些处理,并返回一个SOAP消息结果。由于是在应用服务器上执行,你可以创建两类服务端点组件:
Servlet
注意,尽管文档将这类Web服务实现称为“Servlet”实现,但这实际上是一个包含注解的POJO(Plain Old Java Object)类,而且该类没有扩展javax.Servlet.http.HttpServlet类。这种服务实现包装在一个WAR中,而且是作为Servlet进行部署的,但实际的Servlet实现将由JAX-WS提供,它处理HTTP请求和响应,转换SOAP消息,而且是多线程的。在一些Web容器中,你需要在Web.xml中提供常规的Servlet映射,就像对常规Servlet所做的那样。在Glassfish中,只需要将该类包装在WAR中并部署它,部署时程序将为你提供映射。
无状态会话bean
这是唯一一种可作为Web服务实现的EJB。就像上面的Servlet类型一样,你通过WSDL接口、HTTP和SOAP消息来使用EJB Web服务端点,而所有这些是由JAX-WS处理的。
因此,如果采用常规的“Servlet”路线,就不需要使用EJB容器来提供Web服务。
包和类
核心的JAX-WS API放在javax.xml.ws包及其子包中,这包括与HTTP、SOAP和处理程序使用有关的一些包。
JAX-WS位于两个包,主要的包是javax.jws,该包比较小,但很强大,包含6种注解和一个枚举,不包含类和接口。此处的注解是创建可移植Web服务的主要手段:
package com.soacookbook;
import javax.jws.WebService;
import javax.jws.WebMethod;
@WebService public class Hello {
public String sayHello(String name) {
return "Hello, " + name";
}
}
本章将对此进行详细讨论,不过眼下,上面的示例应该让你对基本的编程模型有个概念。
第二个JAX-WS包是javax.jws.soap,它不包含类和接口,但包含一种注解和4个枚举(实际上该包中存在四种注解,但有三种已禁用,不应该使用它们)。当创建Web服务时,对于绑定到SOAP(WSDL的具体方面),开发者使用该包将默认值替换为其他值。
下面是一个示例。如果没有为“Document/Literal Wrapped”的SOAP style、use和parameter style属性指定其他值,Hello Web服务将使用这三个属性的默认值。如果希望进行覆盖,就会使用javax.jws.soap包中的注解,示例如下:
package com.soacookbook;
import javax.jws.WebService;
import javax.jws.WebMethod;
@WebService public class Hello {
@SOAPBinding(parameterStyle=SOAPBinding.ParameterStyle.BARE)
public String sayHello(String name) {
return "Hello, " + name";
}
}
在这里,我们指定了非默认的参数样式(在该样式中,参数将包装在SOAP消息中),因此,说明这个唯一选项不是采用默认的“document”,而是“bare”,枚举将修改生成的WSDL以符合你的要求。同样,此处发生的具体细节或为什么希望选择“bare”参数样式眼下是不重要,这应该只是让你体会一下如何使用该包。
本章详细介绍JAX-WS以及如何使用它来创建Web服务,好好学吧!
6.2 从命令行调用Web服务
问题
希望使用一种快速、简单的方法来调用已经部署的Web服务。
解决方案
基本步骤如下:
1. 确保正在运行Java SE 6。
2. 使用wsimport工具,将服务的WSDL的位置传递给它,这将生成符合WSDL消息的类和用来调用服务的便利类。
3. 创建一个包含main方法的类,它将使用生成的类来调用服务。
4. 编译和运行程序。
讨论
下面的步骤提供了一种简单的方法来使用JAX-WS调用现有Web服务,接着你可以将这一基本思想转移到其他更复杂的Java项目。通过在任何IDE外部从头创建一切内容,你可以更好地理解各部分是如何一起工作的。让我们按照这些步骤来创建客户端和调用服务。
在这里,我们将使用http://WebServiceX.net免费提供的一个可公开访问的服务,如果你是根据其他服务进行测试,比如你所在组织使用的一个服务,则只需要替换WSDL位置字符串。当然,wsimport工具接着会生成与你提供的WSDL相对应的一组不同的类,你需要按照一种方式将对象放在一起,而这种方式会为服务创建有意义的消息。不过,基本思想通常都是一样的。
我们将调用的Web服务位于http://www.Webservicex.net/WCF/ServiceDetails.aspx?SID=44,它被称为“美国天气预报”:传入一个邮政编码,它就会返回相应的天气预报。我选择该服务是因为尽管它只接受简单的字符串,却返回包含各种类的一个组合对象,这使得wsimport可以实现更多任务。
接下来,让我们逐个看看这些步骤并将它们组合在一起。
验证wsimport
为了验证运行的是Java SE 6(这样就可以使用wsimport工具),请打开命令终端并运行以下命令(应该会打印JAX-WS的版本号):
$ wsimport -version
JAX-WS RI 2.1.1 in JDK 6
只要版本号是2.0以上,就应该能开始工作。如果路径中不存在wsimport,就不会显示版本号,而是一个说明OS无法找到相应程序的错误。在这种情况下,需要将其添加到环境变量中或者使用完整的路径来调用程序。
生成客户端代码
接下来,指向WSDL并生成可用于构建服务所期望的SOAP消息的可移植客户端代码。通过使用JAX-WS而不是SAAJ来完成这一任务,避免了需要直接处理XML。
在文件系统上创建一个新目录用来包含程序,我创建的目录是~/weatherTest。
然后,在终端中定位到该目录,在其下创建一个名为gen的目录,这就是你将告诉wsimport用来放置基于WSDL生成的类的地方。
提示: 该Web服务是用.NET编写的,这提供了一种好机会来测试Web服务的功能,但是,它的WSDL是采用一种非标准的方式编写的,使用了HTTP POST和GET操作,没有指定SOAP绑定,因此,当运行wsimport时,你可能会看见一些警告。
对于这个Web服务,WSDL位于http://www.Webservicex.net/WeatherForecast.asmx?wsdl。下面是用来生成客户端对象的命令:
wsimport -verbose -d gen extension
-keep http://www.Webservicex.net/WeatherForecast.asmx?wsdl
使用-keep选项会保留Java源文件,这样就可以查看对象是如何组合在一起来形成请求。-d选项允许你指定JAX-WS应该将生成的文件放在什么位置。运行该命令将会创建符合Schema中所定义类型的多个Java文件。
创建客户端类
使用文本编辑器,创建一个Java源文件,把生成一个符合WSDL的消息所需的对象组合在一起,如示例6-1所示。
示例 6-1:调用WebServiceX.net中天气预报服务的客户端类
import net.Webservicex.*;
import java.math.*;
/*
Calls the forecast service at WebServiceX.net.
*/
public class WeatherClient {
public static void main(String...arg) {
System.out.println("Invoking...");
WeatherForecast service = new WeatherForecast();
WeatherForecastSoap port = service.getWeatherForecastSoap();
//Invoke Service and Get Result
WeatherForecasts forecasts = port.getWeatherByZipCode("85255");
//Use the generated objects in the result
String placeName = forecasts.getPlaceName();
ArrayOfWeatherData arr = forecasts.getDetails();
WeatherData data = arr.getWeatherData().get(0);
System.out.println("Place=" + placeName);
System.out.println("Day=" + data.getDay());
System.out.println("High Temp (F)=" + data.getMaxTemperatureF());
System.out.println("All done.");
}
}
该客户端类创建了服务代理的一个实例,获取了具有我们感兴趣的业务方法的端口。当调用getWeatherByZipCode时,该服务就会被调用,而且会在一切正常时返回相应的结果。接下来,需要做的所有工作是编译和运行客户端。
开始组合JAX-WS客户端的最好方法是从服务开始,反向规划各个数据点。首先,找到扩展了javax.xml.ws.Service的类,使用它来获取相应的端口。这将告诉你该端口返回什么类型,该类型将是一个域对象,它会提供一些访问(获取)方法,你可以检查这些方法来获得想要的信息。在你的实例中,该服务返回的数据比本示例显示的要多很多。
编译客户端
要编译客户端,可以使用如下命令:
$ javac -cp gen WeatherClient.java
运行客户端
在Linux上,可以使用如下命令运行客户端:
$ java -cp gen:. WeatherClient
在Windows系统上,需要使用;字符分隔路径,因此,可以使用如下内容进行相应替换:
gen;
这会产生如下所示的结果:
Invoking...
Place=SCOTTSDALE
Day=Tuesday, July 29, 2008
High Temp (F)=94
All done.
你可以按照类似的基本步骤通过JAX-WS调用任何Web服务。
6.3 使用JAX-WS注解名称属性
问题
你希望了解如果改变了引用某类名称的一些注解属性,将会对整个设计产生什么样的影响。
解决方案
阅读下面的“讨论”部分。
讨论
通常来说,在JAX-WS中,命名是比较棘手的。Web服务名称变体的许多属性没有直接说明设定它们的值将会如何影响服务和客户端,甚至规范中没有明确给出填充某些JAX-WS注解属性将会带来哪些实际衍生物。下面列出的内容应该可以阐明部分最常见的情况。
WebService.targetNamespace
@WebService(targetNamespace="http://ns.soacookbook.com",name="CatalogService")
@WebService注解说明用一个Java类实现Web服务。如果用于某个接口,它用作Web服务接口定义,服务端点接口映射到wsdl:portType元素。在@WebService注解中,targetNamespace属性改变将要在其中定义服务的命名空间,带有如上所示注解的类的WSDL将如下所示:
<definitions targetNamespace="http://ns.soacookbook.com"
name="CatalogService">
如果没有指定WebService.targetNamespace属性,相应位置使用的默认值将是倒退的包名,例如:
<definitions targetNamespace="http://ch03.soacookbook.com/"
WebService.name
@WebService(targetNamespace="http://ns.soacookbook.com",
name="CatalogService")
在@WebService注解中,name属性改变所生成的@WebServiceClient类的端口名称,结果,服务客户端就可以使用下面的方法:
service.getCatalogServicePort
元素@javax.jws.WebService.name无法与元素@javax.jws.WebService.endpointInterface一起使用。
如果没有指定@WebService.name属性,它将默认采用类名,例如:
@WebService(serviceName="CatalogService", targetNamespace="http://ns.soacookbook.com")
public class CatalogWS implements Catalog { ... }
要求客户端代码如下所示:
private CatalogService service;
CatalogWS port = service.getCatalogWSPort();
就像使用Servlet那样,如果你是在同一JVM中使用Web服务客户端和提供商进行操作,则区分所生成的存根与实际类实现是很重要的,前者是带有@WebService注解的接口(而且它从实际实现该Web服务业务逻辑的类的@WebService注解中的name属性值获取相应的名称)。
WebService.serviceName
该属性定义已部署服务的URL,例如,某个类上面的如下@WebService注解:
package com.soacookbook.ch03;
//...
@WebService(serviceName="CatalogServiceSN",
targetNamespace="http://ns.soacookbook.com")
将生成以下已部署的URL:
http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl
这还会修改所生成的服务类的名称。当执行wsimport时,同一@WebService注解还生成如下内容:
@WebServiceClient(name = "CatalogServiceSN",
targetNamespace = "http://ns.soacookbook.com",
wsdlLocation = "http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl")
public class CatalogServiceSN
extends Service { ... }
此外,注意它用于填充@WebServiceClient.name注解属性的值。而且,虽然它没有对客户端编码器带来太多的不同,但也可以用于定义所生成服务类中使用的常量名称:
private final static URL CATALOGSERVICESN_WSDL_LOCATION;
提示: 不允许对接口使用注解属性@WebService.serviceName。
WebService.portName
该属性改变从所生成服务类返回服务实现存根的方法的名称,在接口中,它没有任何效果。下面的代码:
@WebService(serviceName="CatalogServiceSN",
portName="CatalogPort",
targetNamespace="http://ns.soacookbook.com")
将创建:
public class CatalogServiceSN extends Service {
@WebEndpoint(name = "CatalogPort")
public CatalogWS getCatalogPort() { ...}
该WSDL中的QName(限定名称)不再是CatalogWSPort,它是通过将服务实现类名和“Port”连接在一起指定的默认名称。WSDL现在看起来如下所示:
<service name="CatalogServiceSN">
<port name="CatalogPort" binding="tns:CatalogPortBinding">
提示: 当对接口指定WebService.portName时,它没有任何效果。
通过依赖注射实现服务引用
通过使用@WebServiceRef注解,你可以轻松地从容器管理的组件(例如Servlet)中引用Web服务。就像通常那样使用依赖注射:
@WebServiceRef
private CatalogServiceSN catalogServiceIT;
如果名为“InjectionServlet”的Servlet包含这个注解域,部署时,如下所示的内容会自动生成到Web.xml中:
<service-ref>
<display-name>com.soacookbook.ch03.InjectionServlet/catalogServiceIT
</display-name>
<service-ref-name>com.soacookbook.ch03.InjectionServlet/catalogServiceIT
</service-ref-name>
<service-interface>com.soacookbook.ch03.CatalogServiceSN
</service-interface>
<wsdl-file>http://localhost:8080/soaCookbookWS/CatalogServiceSN?wsdl
</wsdl-file>
<service-qname xmlns:service-qname_ns__="http://ns.soacookbook.com">
service-qname_ns__:CatalogServiceSN</service-qname>
<injection-target>
<injection-target-class>com.soacookbook.ch03.InjectionServlet
</injection-target-class>
<injection-target-name>catalogServiceIT</injection-target-name>
</injection-target>
</service-ref>
此处的injection-target-name元素基于你指定给服务的变量名称。在调用代码中,我们在名为com.soacookbook.ch03.InjectionServlet的类中定义了catalogServiceIT变量,因此,在生成的Web.xml中注射目标会得到相应的填充。除了<service-ref>元素,这对于Web服务来说没什么特殊的,几乎就是Java EE 5注射如何处理Servlet。虽然你可能使用Web服务相当长的时间了而且不需要知道有关内容是按这种方式生成的,但这类知识将有助于调试工作。
6.4 调用最简单的Web服务
问题
在4.5小节中我们使用端点部署了Hello Web服务,现在希望调用它。
解决方案
该解决方案假定你已经按照4.5小节部署了Web服务,回顾一下,该服务是在JDK 6附带的HTTP服务器中部署的,没有部署到容器。
要调用该Web服务,可以创建一个指向WSDL位置的URL,然后创建一个代表服务名称的QName对象,就像WSDL的<service>元素的name属性指定的那样。然后,基于这两个位置符创建一个服务实例,该服务实例给出的是与接口相对应的端口的一种表示。可以将该过程看作是有点类似从EJB Home获取业务接口,使用wsimport工具时就会经历这样的过程。
拥有端口类型(它将是接口类型)后,就可以开始调用业务方法。示例6-2给出了一个完整的清单。
示例 6-2:Hello Web服务客户端,HelloClient.java
package com.soacookbook.ch03;
import java.net.URL;
import java.util.Iterator;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
public class HelloClient {
public static void main(String[] args) throws Exception {
//Specify the WSDL
URL wsdlLocation = new URL("http://localhost:9999/hello?wsdl");
//Create a Qualified Name that represents the
//namespace and local part of the service
QName serviceName = new QName("http://ch03.soacookbook.com/",
"HelloWSService");
//Create a proxy to get a port stub from
Service service = Service.create(wsdlLocation, serviceName);
// Return a list of QNames of ports
System.out.println("QNames of service endpoints:");
Iterator<QName> it = service.getPorts();
QName lastEndpoint = null;
while (it.hasNext()) {
lastEndpoint = it.next();
System.out.println("Name: " + lastEndpoint);
//prints: Name: {http://ch03.soacookbook.com/}HelloWSPort
}
// Get the Hello stub
Hello hello = service.getPort(lastEndpoint, Hello.class);
//Invoke the business method
String result = hello.sayHello("Eben");
System.out.println("\nResponse: " + result);
}
}
该文件中的注释应该说明发生的情况,我希望显示最简单的实际服务调用。该客户端唯一不是绝对必要的部分是针对可用端口的迭代器,你可以从服务获取端口后就调用该业务方法。
下面是调用该程序后的输出结果:
QNames of service endpoints:
Name: {http://ch03.soacookbook.com/}HelloWSPort
Response: Hello, Duke!
通过读取基于指定WSDL的<service>元素的<port>子元素,该客户端程序列出了可用的QName,在这里,存在一个QName:{http://ch03.soacookbook.com/}HelloWSPort。QName是命名空间和本地部件的组合,在QName的toString方法中,命名空间是放在一对花括号中,本地部件附加在后面。
在调用过程中,该QName的binding属性被读取,调用程序被指向WSDL的<binding>元素,它列出可以执行的操作以及在调用这些操作过程中消息必须使用的类型。会基于指定的接口生成一个存根,客户端程序使用该存根来调用方法,仿佛这些方法是同一JVM的本地方法。
参见
6.5小节。
6.5 创建客户端代理
问题
对于创建基于SAAJ的SOAP调用,如果要你通过手动方式完成所有的工作,你会感到厌烦,从而希望让Java为自己生成调用只基于WSDL的服务时所必需的一切内容。
解决方案
使用wsimport工具生成必需的客户端存根并像一个常规Java对象那样调用它们。
讨论
如果你是Web服务新手,则整个计划可能是非常令人心悸的。本讨论将从客户端开发人员的角度介绍wsimport工具的使用,将涉及Schema、JAXB以及部件内在是如何组合在一起的,这样你将具有足够的基础来了解自己需要关注哪些条目和当心哪些内容。
wsimport工具将读取已部署Web服务的WSDL并生成调用该服务时所必需的Java对象,包括一个扩展了javax.xml.ws.Service的类,该类提供Web服务的客户端视图。这可能是一个令人迷惑的概念,因为我们试图将服务看作是位于服务器上,不过,服务实例是作为创建代理的一个工厂,使得你可以像调用本地服务那样来调用Web服务,这些代理有时称为SEI(Service Endpoint Interface,服务端点接口)对象。
该工具生成只使用标准Java方法的可移植结果,它会自动调用JAXB来创建将Java映射到XML的值类型,而且结果可用于执行Web服务操作。
动态代理
在JAX-WS中创建动态代理有时称为“静态”客户端编程模型,客户端使用所生成的SEI来指定类型,对象树是使用服务调用时所需的工厂方法创建的,实际请求处理是在内部通过委托来执行的。
该委托是通过一个动态代理对象来动态实现的,动态代理是在Java SE 1.3中引入的,它们由java.lang.reflect.Proxy支持,用于实现运行时指定的一组接口,因此,JAX-WS创建的项目是完全可移植的,你可以将自己的客户端代码移到另一个平台上,而且不必使用该平台的有关工具来重新生成代码。这类似于存根处理那些被JAX-WS替代的老JAX-RPC模型,因为JAX-RPC也使用SEI。除了易于使用之外,主要的区别是对于JAX-RPC,工具生成特定于平台的存根,使得你需要为新平台重新创建客户端。
动态代理与SAAJ模型不同,在SAAJ模型中,不会预先生成SEI,而且我们需要自己组合SOAP消息,就像我们在第5章中看到的那样。这是有关仅执行像SEI之类的简单调用方面的大量工作,而且不进行任何修补。不过,如果你希望客户端程序基于运行时属性来动态处理SOAP请求,则SAAJ无能为力。
你可以从命令行使用wsimport,也可以在Ant或Maven中使用wsimport。让我们来看一个简单的计算器Web服务示例,该服务的WSDL位于http://localhost:4933/CalculatorApp/CalculatorWSService?wsdl,它定义了一个端口类型,如下面的代码所示:
<portType name="CalculatorWS">
<operation name="add">
<input message="tns:add"></input>
<output message="tns:addResponse"></output>
</operation>
</portType>
//The service name element is:
<service name="CalculatorWSService">
提示: 为了使用本小节,你需要在某处部署一个真实的WSDL。如果使用的是NetBeans,只需片刻就可以设置和部署该Web服务。也可以尝试按照4.5小节部署端点,或者使用某处(如StrikeIron.com)提供的可公开访问的服务。这些方法都会使所生成的结果带有不同程度的复杂性,就像基于消息类型那样。
接下来,运行wsimport工具创建一个可以调用该服务的代理,下面是命令及其输出结果:
>wsimport -d /home/ehewitt/soacookbook/code/imported -target 2.1 \
-verbose http://localhost:4933/CalculatorApp/CalculatorWSService?wsdl
parsing WSDL...
generating code...
org\me\calculator\Add.java
org\me\calculator\AddResponse.java
org\me\calculator\CalculatorWS.java
org\me\calculator\CalculatorWSService.java
org\me\calculator\ObjectFactory.java
org\me\calculator\package-info.java
compiling code...
javac -d /home/ehewitt/soacookbook/code/imported -classpath //...
>
wsimport工具具有各种选项,许多选项是与定制有关的。不过,在基本的调用中,是将所需的选项传递给该工具,最后的参数是WSDL的位置。第一个选项-d指定你希望将导入的源代码写入哪个目录,-target选项用于指定希望兼容的JAX-WS版本(默认是2.0),而-verbose选项告诉该工具在执行过程中给出正在执行的内容。让我们来看一下该工具生成的内容。
首先,该工具生成一组与服务命名空间相对应的包,在本示例中,它是org.me. calculator,该包内部是一些Java类,下面我们将讨论该工具生成的一些关键Java类。
提示: 如果希望除类文件外,wsimport还保留所生成的Java源文件,就可以使用-keep选项。
生成的Service类
在本示例中,Service类名为CalculatorWSService.java,它与WSDL <service>元素的name属性的值相对应。生成的Service类允许你:
获取可用端口(服务端点接口)
获取服务关联的WSDL文档的位置
获取服务相关的Executor实例,它提供服务调用的线程功能
创建Dispatch
创建Service实例
调用Service实例的getPort方法以调用Web服务操作
该类扩展了javax.xml.ws.Service,并带有一个@WebServiceClient注解,该注解指定代表要调用的服务的WSDL的位置。它包含返回Java对象的工厂方法,该Java对象代表可以用来调用操作的WSDL端口。生成的Service类如下所示:
@WebServiceClient(name = "CalculatorWSService",
targetNamespace = "http://calculator.me.org/",
wsdlLocation = "http://localhost:4933/CalculatorApp/CalculatorWSService?wsdl")
public class CalculatorWSService extends Service { //...
@WebEndpoint(name = "CalculatorWSPort")
public CalculatorWS getCalculatorWSPort() {
return super.getPort(new QName("http://calculator.me.org/",
"CalculatorWSPort"), CalculatorWS.class);
}
提示: wsdlLocation属性必须允许使用OASIS指定的XML Catalog功能(如果存在)。3.13小节对此进行了讨论。
在这里,getCalculatorWSPort方法返回一个实现了CalculatorWS接口的对象,下面将对它进行讨论。通常可以使用不带参数的getPort方法,第二个getPort方法接受一组长度可变的javax.xml.ws.WebServiceFeature对象,客户端可以使用这些对象来设定调用的某些方面,比如,是否启用MTOM或WS-Addressing。
生成的Port类
由于前面清单中显示的WSDL端口的name属性的值为CalculatorWS,因此,这就是所生成的代表该端口的Java接口的名称。该接口带有@WebService注解(令人有点迷惑),以说明它是一个将用作代理的服务端点接口。JAX-WS没有为该类生成相应的实现,运行时将在幕后完成该项工作,方法是将调用委托给一个javax.xml.ws.spi.ServiceDelegate实现,而Service类修饰该实现。
提示: 服务提供商接口(SPI,Service Provider Interface)是在Java SE 6中公开引入的,但自从版本1.4开始,它就在Java API内部得以使用。通过在JAR的META-INF目录中指定实现了给定接口的类的名称,它允许使用可插式结构。虽然开发者不必担心这方面,但这是运行时提供ServiceDelegate实现的方式。
在这个计算器示例中,端口具有一个方法,以匹配WSDL中定义的单个add操作。让我们回退一步并对此进行一下解释,因为此处包含许多内容:
@WebMethod
@WebResult(targetNamespace = "")
@RequestWrapper(localName = "add",
targetNamespace = "http://calculator.me.org/",
className = "org.me.calculator.Add")
@ResponseWrapper(localName = "addResponse",
targetNamespace = "http://calculator.me.org/",
className = "org.me.calculator.AddResponse")
public int add(
@WebParam(name = "i", targetNamespace = "")
int i,
@WebParam(name = "j", targetNamespace = "")
int j);
正如你看到的那样,表面上简单、平淡无奇的add方法突然装饰有许多注解。我们将逐一解释它们。
首先,WSDL在消息部分指定如下内容:
<message name="add"> <part name="parameters" element="tns:add"></part> </message>
因此,SEI需要解决该消息,创建一个注解来说明运行时将使用一个QName创建消息,在指定的命名空间中,QName包含add的本地部件。该消息来自生成的Java类org.me.calculator.Add,该类如下所示:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "add", propOrder = {
"i",
"j"
})
public class Add {
protected int i;
protected int j;
//getters and setters omitted
该类用作请求中将要发送的各个整数的封装类,该类的@Xml注解来自JAXB,它们说明JAXB应该如何将该类的实例编组和解组到XML。@XmlType注解用于指定这个Add类与XML Schema中的顶级复杂类型(或枚举)相对应,其中的“name”属性是作为“add”指定的,以对应Schema中该条目的名称。如果查看WSDL引用的Schema,你会看到如下复杂类型,它与Add类相对应:
<xs:complexType name="add">
<xs:sequence>
<xs:element name="i" type="xs:int"></xs:element>
<xs:element name="j" type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
不过,为什么要创建该类型?整数是作为XML Schema提供的基本类型进行定义的,它们不是你所编写的需要特殊处理的自定义类型,创建封装了这两个整数的复杂类型是为了与WSDL相匹配,后者使用document/literal样式。下面是WSDL中有关这方面的部分:
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"></soap:binding>
<operation name="add">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal"></soap:body>
</input>
如果使用RPC而不是文档,值就会像方法参数一样被独立传递给操作调用。
提示: 有关文档与RPC以及文字与编码方面的更多信息,请参见7.4小节。
@RequestWrapper和@ResponseWrapper注解捕获执行编组和解组操作时JAXB需要的信息。如果服务是使用document/literal模式定义的,就像本示例一样,该注解还用于解决超载冲突。
接下来,让我们编写一个快捷程序来调用所生成的客户端代码并获得来自服务的结果。下面是一些形式最简单的步骤,其中去掉了任何不必要的条目,为的是让你可以看得更清楚。
首先,编写一个名为CalculatorInvoker.java的调用程序(示例6-3)。定位到前面传递给wsimport工具的顶级目录,对于我来说,它是“/home/ehewitt/soacookbook/code/imported”。你将在其中编写客户端。
示例 6-3:将调用所生成的服务端点代码的CalculatorInvoker.java
import org.me.calculator.*;
public class CalculatorInvoker {
public static void main(String... arg) {
CalculatorWSService service = new CalculatorWSService();
CalculatorWS port = service.getCalculatorWSPort();
int result = port.add(2, 3);
System.out.println("Result: " + result);
}
}
示例6-3中的类就是创建了一个服务实例,使用该实例获取端口,接着使用该端口调用业务方法add。让我们编译它,确保类路径包含当前目录中的所生成类:
>javac -cp . CalculatorInvoker.java
然后运行它:
>java -cp . CalculatorInvoker
Result: 5
对于基本的客户端来说,这就是实际要做的所有事情。Web服务在JAX-WS方面取得很大进展。
有关使用wsimport的更多专业信息,可以在不带参数的情况下调用它,这样会获得有关帮助信息。
最后,在使用所生成的客户端时,有一些注意事项需要谨记在心:
正如你可能会设想的那样,客户端无法创建或销毁Web服务实现,而且无法查看自己的生命周期,这完全是在服务器上处理的。
端口对象没有身份,无法将它与其他端口对象进行有意义的比较,你无法获得一个具体的端口实例。
将服务调用看作是无状态进行处理,Service中没有提供相应的机制来跨请求处理状态。
所有的数据绑定是由JAXB执行的,因此不需要JAX-RPC映射文件。
6.6 从Servlet或EJB使用Web服务
问题
你拥有EJB、Servlet或其他容器管理的资源,希望将它用作Web服务的客户端。
解决方案
使用@WebServiceRef注解加入希望调用服务的引用。注意,这不必只能是Servlet或EJB,你或许还希望从其他容器管理的资源调用Web服务,比如Filter、SessionContextListener、ServletContextListener或TagHandler,这取决于你的使用情况。
通过使用@WebServiceRef,你可以获得对Web服务的引用及其注入目标。带有@WebServiceRef注解的条目遵守Java EE 5中有关资源注入的标准规则,它定义了5种属性,如表6-1中所述。
表6-1:@WebServiceRef属性
属性 描述
name 标识使用指定资源的组件的引用名称,就像JNDI名称。如果是对某个域进行注解,默认情况下就是该域的名称。如果是对某个方法进行注解,默认情况下是该方法定义的Java Bean属性的名称。如果是对类进行注解,则没有默认值
wsdlLocation 该URL指向引用的Web服务的WSDL。如果你在物理文件中定义自己的WSDL并将其包含在WAR或EAR中,就需要使用该属性
type 资源类类型。如果是对域进行注解,默认值是该域的类型。如果是对方法进行注解,默认值与Java Bean属性相同。如果是对域进行注解,则没有默认值,此时必须指定一个相应的值
value 服务类类型,它必须扩展javax.xml.ws.Service。如果引用类型是SEI,则必须指定该值
mappedName 用于将“name”属性的值映射到服务器某个已知资源的名称,比如JNDI名称。任何mappedName值都是特定于应用服务器平台的,而且不可移植。没有强制要求引用服务器支持它们
在这里,我们将使用@WebServiceRef注解通过注入的方式编写一个调用Web服务的Servlet。下面是一些有关步骤,它们与从EJB客户端使用引用时采用的步骤是完全一样的:
1. 创建Web服务将实现的接口,客户端(比如你的Servlet)将引用该接口。
2. 实现Web服务
3. 向Servlet添加引用注解,将自动注入所生成SEI的一个实例。
让我们开始操作吧。
示例6-4中的Catalog.java是客户端将引用的接口,它定义了一个简单操作,返回给定标识符所对应的书籍标题。
示例 6-4:用来定义服务接口的Catalog.java
package com.soacookbook.ch03;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
/**
* Public interface for CatalogWS impl.
*/
@WebService(targetNamespace="http://ns.soacookbook.com")
public interface Catalog {
@WebMethod
@WebResult(name="title")
String getTitle(
@WebParam(name="id") String id);
}
注意,该接口需要包含@WebService注解,否则,客户端将抱怨。此外,@WebService注解必须包含targetNamespace,因为也希望在服务实现中对它进行自定义。
CatalogWS.java实现了该服务接口,如示例6-5所示。
示例 6-5:用来实现该服务接口的CatalogWS.java
package com.soacookbook.ch03;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import org.apache.log4j.Logger;
/**
* This annotation will produce a WSDL URL of:
* http://localhost:8080/soaCookbookWS/CatalogService?wsdl
* That's because it is in the Web context of "soaCookbookWS",
* which is the WAR name, appended with the value of the
* serviceName property.
*
* Use that value in the properties file to generate client
* artifacts.
*/
@WebService(serviceName="CatalogService", name="Catalog",
targetNamespace="http://ns.soacookbook.com")
public class CatalogWS implements Catalog {
private static final Logger LOGGER =
Logger.getLogger(CatalogWS.class);
@WebMethod
public @WebResult(name="title") String
getTitle(
@WebParam(name="id") String id) {
if ("12345".equals(id)) return "Hamlet";
if ("98765".equals(id)) return "King Lear";
if ("55555".equals(id)) return "Macbeth";
return "--Item not in catalog--";
}
public CatalogWS() { }
}
示例6-6中的代码是Servlet,它将获取@WebServiceRef注解并在运行时从容器接收服务注入。
示例 6-6:使用@WebServiceRef引用Web服务的InjectionServlet.java
public class InjectionServlet extends HttpServlet {
@WebServiceRef(type=Catalog.class)
private CatalogService service;
protected void processRequest(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
//service instance injected...
Catalog port = service.getCatalogPort();
String title = port.getTitle("12345");
try {
out.println("<html>");
out.println("<head>");
out.println("<title>WebServiceRef Test</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Title= " + title + "</h1>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
}
该Servlet本身包含所生成SEI代理实例的引用,该容器就像对任何其他可注入资源一样执行注入过程。
下面是对应的客户端,它使用通过调用wsimport而生成的SEI来调用该Web服务:
CatalogService svc = new CatalogService();
Catalog port = svc.getCatalogPort();
return port.getTitle("12345");
图6-1以一个浏览器窗口显示了输出结果。
图6-1:WebServiceRef InjectionServlet.java的输出结果
这就是实际要做的所有事情。
讨论
有两种方法可以使用@WebServiceRef注解:
让注解引用已生成的服务类。如果无法从注解修饰的域或方法推断默认值,则至少必须为type属性指定值,该属性引用已生成的服务类类型。
让注解引用SEI。使用该选项,你必须为value属性指定服务类型对象,该对象必须是已生成的服务类类型(javax.xml.ws.Service的子类型)。
wsdlLocation属性的值将覆盖已生成的服务类中建立的URL(该类带有@WebService注解)。
WebServiceRefs
Java中的注解阻止你为同一类型定义多个相同注解,因此,如果客户端类需要引用多个Web服务,你可以使用@WebServiceRefs注解来封装多个@WebServiceRef实例。这降低了JAX-WS推断name和type属性相应值的能力,因为定义了多个@WebServiceRef,而且它们是在类级别上定义的。为了解决这个问题,@WebServiceRefs注解中封装的各个@WebServiceRef注解必须定义name和type的值。
@WebServiceRefs只包含一个隐含的值属性,它是一组@WebServiceRef注解。
6.7 从JSP使用Web服务
问题
希望使用现有的JSP作为Web服务的客户端。
解决方案
基于WSDL生成一个客户端并将这些资源包含在WAR部署中,在JSP中使用脚本小程序实例化客户端。
讨论
在JSP应用程序中,通过在浏览器中测试WSDL URL确保部署了相应的Web服务。在build过程中或从命令行通过wsimport使用WSDL位置生成一个客户端,编写一个调用该客户端的脚本小程序,就像Servlet中那样。部署时,将这些客户端类包含在WAR中。
提示: 就像Web应用程序使用的任何其他基于Java类的资源一样,你可以将这些类包装在一起形成一个库并将它们包含在Web-INF/lib中,也可以将这些类直接放在Web-INF/classes目录中。当然,常规类加载器方面像往常一样适用。
示例6-7是采用这些步骤后执行的JSP代码。
示例 6-7:从JSP调用Web服务
<%@page import="com.soacookbook.client.CatalogServiceSN,
com.soacookbook.client.CatalogService"
contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head><title>JSP Client</title></head>
<body>
<h2>Catalog Service Client</h2>
<%
try {
CatalogServiceSN service = new CatalogServiceSN();
CatalogService port = service.getCatalogPort();
String id = "98765";
String result = port.getTitle(id);
out.println("Result for ID 98765= "+result);
} catch (Exception ex) {
//...
}
%>
</body>
</html>
注意,使用页指令导入调用该操作时将使用的服务存根和端口,图6-2以浏览器方式显示了相应的结果。
不需要使用JNDI查找或向部署描述器添加任何内容。
图6-2:从JSP调用的Catalog Web服务
6.8 在SOAP消息中使用JAXB注解实例
问题
希望以对象视图的方式处理SOAP请求内容,而不是使用低级XML管道来创建消息。现有一个从JAXB注解类创建的Java对象,希望将它自动编组到XML并将它用作SOAP请求主体的子元素。
解决方案
使用一个工具(比如wsimport)生成客户端方对象(如果使用的是Apache Axis,则可以使用WSDL2Java工具),然后按照常规方式使用所生成对象模型。所有必要的XML内容都被隐藏起来。
讨论
本小节的重要方面是它向你显示了借助JAX-WS,几乎可以像使用原始类型那样使用复杂类型来作为服务操作参数。
在本示例中,将使用一个代表书籍数据库的目录服务,如下面的代码所示:
@WebService(serviceName="CatalogService", name="Catalog",
targetNamespace="http://ns.soacookbook.com/ws/catalog")
@Stateless
@Local
public class CatalogEJB {
@WebMethod
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
use=SOAPBinding.Use.LITERAL,
parameterStyle=SOAPBinding.ParameterStyle.BARE)
public @WebResult(name="searchResults",
targetNamespace="http://ns.soacookbook.com/catalog") SearchResults
authorSearch(
@WebParam(name="author",
targetNamespace="http://ns.soacookbook.com/catalog") Author author)
//...
}
为了进行比较,下面的JUnit测试创建了该服务应该返回的一个Book示例,该测试使用了从相应WSDL生成的JAXB注解对象:
public class CatalogTest {
private Book hamlet;
@org.junit.Before
public void init(){
hamlet = new Book();
Author shakespeare = new Author();
shakespeare.setFirstName("William");
shakespeare.setLastName("Shakespeare");
hamlet.setAuthor(shakespeare);
hamlet.setIsbn("1234");
hamlet.setCategory(Category.LITERATURE);
hamlet.setTitle("Hamlet");
}
public CatalogTest() { }
@Test
public void searchByAuthorTest() {
Author shakespeare = new Author();
shakespeare.setFirstName("William");
shakespeare.setLastName("Shakespeare");
CatalogService svc = new CatalogService();
Catalog catalog = svc.getCatalogPort();
SearchResults results = catalog.authorSearch(shakespeare);
Book book = results.getBookList().get(0);
assertTrue(book.getTitle().equals(hamlet.getTitle()));
}
该单元测试说明在针对Web服务进行编程时,如何采用一种熟悉的面向对象的方式处理从JAXB生成的域对象。此处没有涉及任何XML或低级管道细节。
从authorSearch方法返回的SearchResults对象包含List<Book>,因此,可以在List接口中使用常规方法(比如get)来浏览结果。通过这种方式,JAX-WS提供了一个非常不错的编程模型。
6.9 在Maven项目中使用wsimport
问题
希望使用Maven 2创建Web客户端项目,而需要运行wsimport来完成部分创建。
解决方案
使用Java.net的jaxws-maven-plugin。
讨论
如果你熟悉Maven,就可以从https://jax-ws-commons.dev.java.net/jaxws-maven-pluging/下载该插件并开始工作。
该插件开始时是作为Codehaus的一个项目,但是,在2007年,它的所有相关工作被转移到Java.net网站,该网站也是Glassfish、Metro、OpenESB以及相关项目的主站。
这个插件易于使用,而且很好地实现了即插即用。你需要做的所有工作就是在pom.xml中指定该插件,如示例6-8所示。
示例 6-8:使用了JAX-WS插件的Maven 2 POM
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<version>1.9</version>
<executions>
<execution>
<goals>
<goal>wsimport</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>com.example</packageName>
<wsdlUrls>
<wsdlUrl>${my.wsdl.url}</wsdlUrl>
</wsdlUrls>
<verbose>true</verbose>
</configuration>
</plugin>
//...
当然,为了使用该插件,需要能够访问存储该插件的Maven 2存储库,它位于http://download.java.net/maven/2/。如果将它添加到你所在组织的内部Maven存储库(比如Archiva),将会使得该存储库可以用作代理,而且会简化开发者或团队之间的插件管理过程。
要使用内部存储库,必须在文件settings.xml中指定它,该文件通常位于你的用户主目录中。如果所在组织没有内部存储库,没有关系——可以直接跳过该部分:
<mirrors>
<mirror>
<id>my-internal</id>
<mirrorOf>*</mirrorOf>
<url>http://repo.example.com/archiva/repository/internal</url>
<name>My - Archiva</name>
</mirror>
</mirrors>
使用变量代表WSDL位置
在本示例中,我们将一个变量用作WSDL URL来对插件运行wsimport。该变量的存在是为了解决你所处环境可能需要的各种WSDL位置,例如,如果当前使用的是开发工作站并正在运行某个本地服务器,WSDL可以位于本地主机,一旦将工作提升到某个集成构建服务器,相应的WSDL就可能需要使用不同的地址。这个问题是通过在Maven配置文件中使用变量来解决的。
为了遵循这个示例,需要具有一个开发者配置文件和一个集成服务器配置文件,它们各自依赖于环境中的不同变量而被激活。
为了进行此种设置,在项目的根文件夹中创建一个profiles.xml文件。在此处,定义一个与POM中使用的变量名相对应的变量来代表wsdlUrl的值。变量名元素中的文本在build时将用作值,因此,http://localhost:8080/myProject/SomeService?wsdl值将作为wsimport插件要使用的URL而插入:
<profilesXml xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:SchemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/profiles-1.0.0.xsd">
<profiles>
<profile>
<id>development</id>
<properties>
<my.sigcap.wsdl.url>
http://localhost:8080/myProject/SomeService?wsdl
</my.sigcap.wsdl.url>
</properties>
</profile>
//...other profiles
</profiles>
6.10 在wsgen和wsimport中处理版本错误
问题
主要Web服务工具(wsimport和wsgen)有两种不同的版本。Java SE 6包含一种版本,Glassfish/Metro包含另一种版本。你希望使用新库,而Java SE包含的是旧库,换句话说,遇到以下错误:
You are running on JDK6 which comes with JAX-WS 2.0 API, but this tool
requires JAX-WS 2.1 API. Use the endorsed standards override mechanism
(http://java.sun.com/javase/6/docs/technotes/guides/standards/),
or use -Xendorsed option.
解决方案
采用Java SE 1.6.0_4或更高版本,因为它附带有JAX-WS 2.1;或者使用endorsed目录机制覆盖旧版本。
讨论
如果使用的是Java 5,需要将jaxws-api.jar和jaxb-api.jar包含在类路径中,因为Java SE 6之前的版本本身不支持JAX-WS,Java SE 6包含JAX-WS 2.0。
Java SE 6 update 4是第一个将JAX-WS 2.1 API包含在rt.jar中的版本,解决该问题的最简单方法是安装该版本或更新的Java版本。如果由于某种原因不能这样做,那么你将需要蒙蔽类加载器。
下面对此进行解密。如果你打开一个命名提示符并发出命令wsimport -version,假定路径中包含Java SE 1.6.0_04以上版本的bin目录,你应该会看到如下所示的输出结果:
JAX-WS RI 2.1.1 in JDK 6
如果使用的是较老版本,就会显示JAX-WS 2.0 in JDK 6,这样你就可以升级到附带有JAX-WS 2.1的较新JDK版本。
如果你拥有Glassfish/Metro,可以定位到Glassfish安装目录下的bin目录并运行同一命令wsimport -version。从Glassfish bin目录执行该操作后,我得到了如下输出结果:
JAX-WS RI 2.1.3-hudson-390-
如果你需要JAX-WS运行时JAR(jaxws-rt.jar)来执行某些扩展了基本功能的任务,版本的差别是非常重要的。例如,WSBindingProvider类位于com.sun.xml.ws.developer包中,你可以通过该类轻松访问设定出站头。
提示: 不要被这些类位于com.sun包中这一事实误导,Sun本身提倡不使用任何此类的包,因为它们不是公开接口的一部分,通常不带有文档,而且会随时发生改变。不能保证它们可以在未来版本的平台中使用,这些包被认为是危险的而且带有试验性。不过,JAX-WS类不会发生这种情况。Sun提醒大家小心使用com.sun包含的类仅针对的是Java SE本身附带的类。JAX-WS类是在JAX-WS单独的库中定义的,它们应该作为应用程序的一部分。包名是一样的,但该规则不适用。这说明,编写应用程序时,你应该仔细考虑希望进行何种程度的属性扩展。
因此,如果一个非常新的JAX-WS版本(比如2.1.3)包含某种你希望使用的功能,你可能仍需要做一些工作,在这种情况下,不能仅是获得一种较新的Java版本需要覆盖这些JAR,除非你可以从控制台完成所有工作。
如果已经下载Metro 1.1,接着可以将JAVA_HOME环境变量指向Java SE 6 update 4的安装位置,然后从Metro运行Web服务工具。Metro 1.1包含一个名为jax-ws-latest-wsit/bin的目录,该目录包含已更新的wsgen和wsimport工具。但是,如果希望确保环境总是提供同样的功能而且不希望指向Metro,可以使用endorsed目录机制。
使用endorsed目录
如果你使用的是更新版本比4低的Java SE 6(例如1.6.0._03或更低)而且正在使用Metro或WSIT(Web Services Interoperability Technology,Web服务互操作技术),那么就会熟悉下面的错误消息:
You are running on JDK6 which comes with JAX-WS 2.0 API, but this tool requires
JAX-WS 2.1 API. Use the endorsed standards override mechanism
(http://java.sun.com/javase/6/docs/technotes/guides/standards/),
or use -Xendorsed option.
基本问题是类加载器混乱,因为Java SE附带的JAX-WS版本比当前操作所需要的版本稍微低一些。需要在类路径中放置两个API JAR的较新版本:JAX-WS 2.1.x和JAXB 2.1.x,它们位于jaxws-api.jar和jaxb-api.jar。不过,因为Java已经包含这些类,将它们放在类路径中不像第三方JAR那样简单,需要让Java读取新的JAR,之后才能让它读取自身的内容。可以使用endorsed目录完成这项任务。
提示: Web Start(JNLP)技术不支持endorsed目录机制,这可能会在Java SE 7中解决。
由于Java在运行时使用的类是放在rt.jar中的,因此需要将这些类放入自举类路径,该路径是在rt.jar之前加载的。为此,可以在<java-home>/lib下面创建一个名为“endorsed”的新目录,将新的JAR放在其中。
另外,也可以使用java.endorsed.dirs属性指向其他目录,Java运行时将在列出的目录中搜索要加载的类。如果使用多个目录,则这些目录必须使用操作系统目录分隔符分隔开,这相当于File.pathSeparatorChar的值。
使用Ant
你实际只希望将jaxws-api.jar和jaxb-api.jar放在endorsed目录中,不要将每个与JAX-WS有关的JAR都复制到该目录中,因为这样做会导致Ant任务失败,而且更可能的是你使用了Ant而不是命令行来完成wsimport工作。将会看见如下所示的错误:
taskdef A class needed by class com.sun.tools.ws.ant.WsImport cannot
be found: org/apache/tools/ant/taskdefs/MatchingTask
必要时,可以使用Ant中的类路径任务来指定两个JAR。
6.11 向SOAP请求添加头
问题
希望使用JAX-WS向SOAP请求添加自定义头信息。
解决方案
有一些基本方法可以解决这一问题。可以使用SAAJ API(它确保可移植性),并在调用时将头添加到代码中。另外,根据你需要调用的WSDL结构,可以创建一个Holder<T>来将头作为消息参数进行添加。
还可以使用特定于供应商的便利代码完成这一任务,例如,对于Metro,可以使用Headers.create。
讨论
下面我们将介绍这些方法。
在JAX-WS RI 2.1.3中,一个新的选项,-XadditionalHeaders,被添加到wsimport任务,该选项自动将头映射为方法参数。你或许具有较早的Java版本,比如2.1.1,它不支持这项功能。
提示: 如果使用的是CXF,则可以指定-exsh true来代替。
让我们假定已经定义下面的简单WSDL:
<message name="usernameHeader">
<part name="usernameHeader" element="types:usernameHeader"/>
</message>
<wsdl:portType name="SecureCatalogPortType">
<wsdl:operation name="execute">
<wsdl:input message="tns:aRequest"/>
<wsdl:output message="tns:aResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="SecureCatalogBinding" type="tns:SecureCatalogPortType">
<soap:binding style="document"
transport="http://Schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="execute">
<soap:operation/>
<wsdl:input>
<soap:body message="tns:aRequest"/>
<soap:header message="tns:usernameHeader" part="usernameHeader"/>
</wsdl:input>
<wsdl:output>
<soap:body message="tns:aResponse"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
在这个WSDL中,有一个名为execute的操作,它接受单个参数aRequest并返回aResponse。该绑定还指定了随着SOAP消息传递的单个头usernameHeader,该头不是在portType中指定的,而是出现在操作定义中,这意味着它不是服务抽象合同的一部分。正因为如此,当使用wsimport创建SEI时,不会生成任何内容来说明该头。只有代表抽象合同的wsdl:part信息被映射到SEI中的方法参数,该方法映射成如下签名:
public String execute(String arg0)
此处没有说明头,因此,需要另一种机制来允许客户端传递相应的值,如果你使用的是RI,该机制是将XadditionalHeaders传递给wsimport工具。
警告: 尽管使用wsimport之类的工具可以很方便地生成SEI,但要注意XadditionalHeaders选项的使用会限制可移植性。
通过使用该选项,wsimport工具将生成如下签名:
public String execute(String arg0, String additionalHeader);
使用Holder<T>
如果具体的WSDL定义了头参数,那么你就可以像往常一样使用wsimport,然后使用Holder来表示发出的头。Holder中的类型参数代表头的任何类型,例如,考虑如下WSDL片段:
<message name="verify">
<part name="parameters" element="tns:verify"></part>
<part name="username" element="tns:username"></part>
<part name="password" element="tns:password"></part>
</message>
<portType name="EmailCheck">
<operation name="verify" parameterOrder="parameters username password">
<input message="tns:verify"></input>
<output message="tns:verifyResponse"></output>
</operation>
</portType>
<binding name="EmailCheckPortBinding" type="tns:EmailCheck">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"></soap:binding>
<operation name="verify">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" parts="parameters"></soap:body>
<soap:header message="tns:verify" part="username"
use="literal"></soap:header>
<soap:header message="tns:verify" part="password"
use="literal"></soap:header>
</input>
<output>
<soap:body use="literal" parts="result"></soap:body>
<soap:header message="tns:verifyResponse" part="username"
use="literal"></soap:header>
<soap:header message="tns:verifyResponse" part="password"
use="literal"></soap:header>
</output>
</operation>
</binding>
<service name="EmailCheckService">
<port name="EmailCheckPort" binding="tns:EmailCheckPortBinding">
<soap:address location="http://localhost:8080/TestHeaders/EmailCheckService">
</soap:address>
</port>
</service>
</definitions>
通过使用SOAP头元素,该WSDL允许头作为方法参数传递。就像使用Servlet时可以在常规HTTP请求中添加HTTP头一样,该WSDL指定将用户名和密码头作为信封中SOAP <header>元素的子元素进行添加。
注意EmailCheck portType的verify操作,它指定一个名为parameterOrder的属性,该属性给出按照方法的任何常规参数,Holder将哪种顺序指定给数组中的参数。在本示例中,parameters消息最先出现,接着是用户名头,然后是密码头。
该WSDL是使用下面的Web服务实现类生成的:
public String verify(
@WebParam(mode=WebParam.Mode.IN,
name="email")String email,
@WebParam(mode=WebParam.Mode.INOUT, header=true,
name="username") Holder<String> username,
@WebParam(mode=WebParam.Mode.INOUT, header=true,
name="password") Holder<String> password){
对于定义需要头的Web服务来说,这或许是最简单的方法。需要执行一些操作来完成该项任务。首先,使用@WebParam像常规参数那样定义方法参数,设置header=true,然后指定头作为类型Holder<T>的方法参数,其中的T是希望客户端定义的类型。最后,使用Mode枚举说明头是入站的。这对于客户端来说非常易于使用,因为标准的wsimport将完成所有的相应工作,使得非常容易包含合适的头值。
这可以正常工作是因为在JAX-WS 2.1中,WSDL抽象部分的wsdl:part(也就是portType中定义的那些内容)被映射成Java方法参数。但是,并不是所有的WSDL都按照这种方式定义,因此,需要额外做一点工作来处理不同种类的WSDL。接下来,我们将说明这些不同方法。
提示: 记住,在Glassfish中,通过使用Webservices-rt.jar附带的HttpTransportPipe类可以查看传出的SOAP消息流量。即使是在另外的平台上开发,这也是非常容易而且十分有用。只需要将下面的代码行添加到你的命令行调用:
-Dcom.sun.xml.ws.transport.http.client.HttpTransportPipe.dump=true
当然,如果是在一种IDE中运行,可以将这个VM参数包含在运行配置中。在NetBeans 6中,只需要右击项目名称,单击“Run”,并将它添加到“VM options”字段。在Eclipse中,单击“Run→Open Run Dialog”,然后选择“Arguments”选项卡,在“VM Arguments”字段中输入值并单击“Apply”。
让客户端调用将下面的SOAP消息发送到服务器:
---[HTTP request]---
SOAPAction: ""
Accept: text/xml, multipart/related, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-Type: text/xml;charset="utf-8"
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header>
<ns2:username xmlns:ns2="http://soacookbook.com/">eben</ns2:username>
<ns2:password xmlns:ns2="http://soacookbook.com/">secret</ns2:password>
</S:Header>
<S:Body>
<ns2:verify xmlns:ns2="http://soacookbook.com/">
<email>me@example.com</email></ns2:verify>
</S:Body>
</S:Envelope>
因此,尽管Java客户端视图通过常规方法调用来发送以标志符封装的头,却像SOAP头期望的那样将用户名和密码值真正发送到服务。
使用Headers.create和JAXB
可以将Headers.create作为一种方便方法来使用,不过,这只适用于参考实现,虽然其他平台可能提供类似功能,比如WebLogic(毕竟,WebLogic 10g R3内在是经过修改的Glassfish)。此外,如果创建相应的代理,则无法对WSDL使用这种方法,例如前一示例中的WSDL。也就是说,操作将头当作常规方法参数来接收,因此,代理方法将期望收到标志符。
在本示例中,我们将使用第三方定义的一个WSDL,它代表的是一个实际Web服务,它有点长和复杂(至少是比我们某些简单的示例要复杂),而且是用ASP.NET而不是Java编写的。该WSDL定义SOAP头中使用的复杂对象,因此,使用起来具有一定挑战,不会让你借助任何做作的玩偶式代码侥幸成功。
除了头对象是复合对象而不仅仅是字符串外,该WSDL还以不同方式定义头的使用。在前面的用户名和密码示例中,WSDL是通过JAX-WS生成的,它对于使用者来说使用起来也是很方便的。让我们看一下这是如何适用于来自StrikeIron.com的一个WSDL,该WSDL将头定义为复合Java对象:
<wsdl:definitions xmlns:s1="http://ws.strikeiron.com"
xmlns:http="http://Schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:si="http://www.strikeiron.com"
xmlns:soapenc="http://Schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://Schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="http://www.strikeiron.com"
xmlns:wsdl="http://Schemas.xmlsoap.org/wsdl/">
<wsdl:types>
//...all types omitted for brevity
</wsdl:types>
<wsdl:message name="AddressToAddressDistanceSoapIn">
<wsdl:part name="parameters" element="si:AddressToAddressDistance" />
</wsdl:message>
<wsdl:message name="AddressToAddressDistanceSoapOut">
<wsdl:part name="parameters" element="si:AddressToAddressDistanceResponse" />
</wsdl:message>
<wsdl:message name="AddressToAddressDistanceResponseInfo">
<wsdl:part name="ResponseInfo" element="si:ResponseInfo" />
</wsdl:message>
<wsdl:message name="GetRemainingHitsSoapIn">
<wsdl:part name="parameters" element="s1:GetRemainingHits" />
</wsdl:message>
<wsdl:message name="GetRemainingHitsSoapOut">
<wsdl:part name="parameters" element="s1:GetRemainingHitsResponse" />
</wsdl:message>
<wsdl:message name="LicenseInfoMessage">
<wsdl:part name="LicenseInfo" element="s1:LicenseInfo" />
</wsdl:message>
<wsdl:message name="SubscriptionInfoMessage">
<wsdl:part name="SubscriptionInfo" element="s1:SubscriptionInfo" />
</wsdl:message>
<wsdl:portType name="AddressDistanceCalculatorSoap">
<wsdl:operation name="AddressToAddressDistance">
<wsdl:input message="si:AddressToAddressDistanceSoapIn" />
<wsdl:output message="si:AddressToAddressDistanceSoapOut" />
</wsdl:operation>
<wsdl:operation name="GetRemainingHits">
<wsdl:input message="si:GetRemainingHitsSoapIn" />
<wsdl:output message="si:GetRemainingHitsSoapOut" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="AddressDistanceCalculatorSoap"
type="si:AddressDistanceCalculatorSoap">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http" style="document" />
<wsdl:operation name="AddressToAddressDistance">
<soap:operation soapAction="http://www.strikeiron.com/AddressToAddressDistance"
style="document" />
<wsdl:input>
<soap:body use="literal" />
<soap:header message="si:LicenseInfoMessage" part="LicenseInfo"
use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
<soap:header message="si:AddressToAddressDistanceResponseInfo"
part="ResponseInfo" use="literal" />
<soap:header message="si:SubscriptionInfoMessage"
part="SubscriptionInfo" use="literal" />
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="GetRemainingHits">
<soap:operation soapAction="http://ws.strikeiron.com/StrikeIron/
AddressDistanceCalculator/GetRemainingHits" />
<wsdl:input>
<soap:body use="literal" />
<soap:header message="si:LicenseInfoMessage"
part="LicenseInfo" use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
<soap:header message="si:SubscriptionInfoMessage"
part="SubscriptionInfo" use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="AddressDistanceCalculator">
<wsdl:port name="AddressDistanceCalculatorSoap"
binding="si:AddressDistanceCalculatorSoap">
<soap:address
location="http://ws.strikeiron.com/StrikeIron/AddressDistanceCalculator" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
在这里,该WSDL没有将头指定为方法参数,因此你无法使用生成的Holder对象来将头随着请求一起传递。更准确地说,WSDL的端口类型抽象部分中没有声明使用头。不过,我们将要调用其AddressToAddressDistance操作的绑定元素声明了头。因为这些头不属于抽象部分,因此,JAX-WS将不会生成它们,你需要通过其他方法将它们添加到消息。
需要添加的对象是LicenseInfo,它还包含RegisteredUser,后者具有字符串类型的用户ID和密码。为了节省空间,我省去了类型,不过它们都相当简单,可以清楚、直接地转换成在Java客户端中调用操作时使用的由JAX-WS生成的JAXB对象。
提示: 如果为了查看类型,你希望浏览一下该服务使用的完整WSDL,可以访问http://ws.strikeiron.com/AddressDistanceCalculator?WSDL。首先查看基于Web的客户端,使得你可以在试图将所有对象组合起来之前对客户端进行测试,它位于http://www.strikeiron.com/sample/AddressDistanceCalculator_v2_0/AddressDistanceCalculator.aspx。
不过,你不必完全放弃JAX-WS提供的便利而重新采取冗长的SAAJ代码。在了解涉及到的内容后,只需要进行一些相当简单又优美的细小改动就可以了。
示例6-9中的类显示了如何构建一个客户端,该客户端使用JAX-WS从WSDL生成的对象以及Headers.create参考实现便利方法来将复杂对象设置到头中。
示例 6-9:使用JAXB和Headers.create将复杂对象设置到SOAP头中
package headersclientsiaddress;
import com.sun.xml.ws.api.message.Headers;
import com.sun.xml.ws.developer.WSBindingProvider;
import javax.xml.namespace.QName;
import com.strikeiron.*;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* Creates a compound object (License) generated by JAX-WS as
* a JAXB object so we can marshall it into XML to attach as
* a SOAP header before invoking service.
*/
public class Main {
public static void main(String... args) {
try {
//instantiate JAX-WS service object and its port
AddressDistanceCalculator service =
new AddressDistanceCalculator();
AddressDistanceCalculatorSoap port =
service.getAddressDistanceCalculatorSoap();
//After registering, use your values here.
RegisteredUser registeredUser = new RegisteredUser();
registeredUser.setUserID("eben@example.com");
registeredUser.setPassword("secret");
LicenseInfo licenseInfo = new LicenseInfo();
licenseInfo.setRegisteredUser(registeredUser);
//setup a context to marshall our license header info
JAXBContext jaxbContext = JAXBContext.newInstance(
LicenseInfo.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
//must do this because LicenseInfo is not a root element
QName q = new QName("http://ws.strikeiron.com",
"LicenseInfo");
//set up the license as XML so we can attach as header
JAXBElement<LicenseInfo> jaxbLicense =
new JAXBElement<LicenseInfo>(
q, LicenseInfo.class,licenseInfo);
//set up a parser to hold the XML result of marshalling
javax.xml.parsers.DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
//this will store the XML result after marshalling
org.w3c.dom.Document doc =
dbf.newDocumentBuilder().newDocument();
//turn our JAX-WS object into XML
marshaller.marshal(jaxbLicense, doc);
//JAX-WS RI Only--downcast
WSBindingProvider bp = (WSBindingProvider)port;
//use RI convenience method to create header
//using the Document object
bp.setOutboundHeaders(
Headers.create(doc.getDocumentElement()));
//Starting address bean
AddressInput location1 = new AddressInput();
location1.setAddress1("10 Columbus Circle");
location1.setCityStateZip("New York,NY,10019");
location1.setCountry(CountryCode.US);
//End address bean
AddressInput location2 = new AddressInput();
location2.setAddress1("301 Park Avenue");
location2.setCityStateZip("New York,NY,10022");
location2.setCountry(CountryCode.US);
UnitOfMeasure unitOfMeasure = UnitOfMeasure.MILES;
//make the call--no explicit headers
double result = port.addressToAddressDistance(
location1, location2, unitOfMeasure);
//show result
System.out.println("Distance: " + result);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
该代码包含的内容很多,因此,让我们对它稍微进行分解。在这里,我们对LicenseInfo类采用JAXB环境,这意味着JAXB运行时将“识别”LicenseInfo实例以及它引用的任何类。接着,创建一个基于LicenseInfo类进行参数化的JAXBElement,它允许你将该类的实例编组成XML,即使它不带有@XmlRoot注解也没关系。然后,创建用来存储许可信息对象XML树的DOM文档实例,调用编组方法时,DOM树中XML内容会被填充。
准备好头后,将所用代理端口指向WSBindingProvider实例,这允许你调用便利方法setOutboundHeaders。由于到目前为止只有DOM文档的根元素包含许可信息XML内容,因此在发送到端口之前,需要将它们封装为SOAP头。可以使用Headers.create方法完成这一任务,该方法将XML许可信息内容封装到SOAP头中。
提示: 可以参阅https://jaxb.dev.java.net/nonav/jaxb20-pfd/api/index.html提供的针对JAXB的JavaDoc。
这样,此时你极为轻松。要做的所有事情就是像往常一样继续填充所生成的对象,像往常一样对端口调用addressToAddressDistance方法。WSBindingProvider已经为该端口的任何出站调用设置头,因此,你不必再做任何有关事情。这与前面随请求一起发送头的方法形成对比,此处不需要标志符,而且操作调用也不会令人迷惑。不过,不足之处是客户端调用需要参考实现中的类,这使得客户端的可移植性降低。
提示: 为了使用com.sun.xml.ws包中的工具,你需要将Webservices-rt.jar添加到项目的类路径。
下面是将作为请求发送的SOAP消息:
---[HTTP request]---
SOAPAction: “http://www.strikeiron.com/AddressToAddressDistance"
Accept: text/xml, multipart/related, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-Type: text/xml;charset="utf-8"
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header>
<LicenseInfo xmlns="http://ws.strikeiron.com">
<RegisteredUser>
<UserID>eben@example.com</UserID>
<Password>secret</Password>
</RegisteredUser>
</LicenseInfo>
</S:Header>
<S:Body>
<ns2:AddressToAddressDistance xmlns="http://ws.strikeiron.com"
xmlns:ns2="http://www.strikeiron.com">
<ns2:Location1>
<ns2:address1>10 Columbus Circle</ns2:address1>
<ns2:city_state_zip>New York,NY,10019</ns2:city_state_zip>
<ns2:country>US</ns2:country></ns2:Location1>
<ns2:Location2><ns2:address1>301 Park Avenue</ns2:address1>
<ns2:city_state_zip>New York,NY,10022</ns2:city_state_zip>
<ns2:country>US</ns2:country>
</ns2:Location2>
<ns2:UnitOfMeasure>Miles</ns2:UnitOfMeasure>
</ns2:AddressToAddressDistance>
</S:Body>
</S:Envelope>
这些头将像你希望的那样出现在信封中,它们使用的是服务定义的复合对象,而不仅仅是某种内置的Schema类型。
下面是成功调用该服务来计算Park Avenue上Waldorf Hotel和Columbus Circle上Thomas Keller's Per Se餐厅(两者都位于纽约市)之间的距离后得到的输出结果:
---[HTTP response 200]---
null: HTTP/1.1 200 OK
Cache-control: private
Content-type: text/xml; charset=utf-8
Content-length: 1009
X-powered-by: ASP.NET
Server: Microsoft-IIS/6.0
Date: Mon, 07 Jul 2008 00:33:18 GMT
X-aspnet-version: 1.1.4322
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://Schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<ResponseInfo xmlns="http://www.strikeiron.com">
<ResponseCode>0</ResponseCode>
<Response>Success</Response>
</ResponseInfo>
<SubscriptionInfo xmlns="http://ws.strikeiron.com">
<LicenseStatusCode>0</LicenseStatusCode>
<LicenseStatus>Valid license key</LicenseStatus>
<LicenseActionCode>0</LicenseActionCode>
<LicenseAction>Decremented hit count</LicenseAction>
<RemainingHits>24</RemainingHits>
<Amount>0</Amount>
</SubscriptionInfo>
</soap:Header>
<soap:Body>
<AddressToAddressDistanceResponse xmlns="http://www.strikeiron.com">
<AddressToAddressDistanceResult>
1.0286261980640472
</AddressToAddressDistanceResult>
</AddressToAddressDistanceResponse>
</soap:Body>
</soap:Envelope>--------------------
Distance: 1.0286261980640472
响应消息说明一些事情。首先,它给出服务是在HTTP头中用ASP.NET实现的,而且你还可以在扩展头中看见它的版本。SOAP头中有大量的订阅信息,最后,你可以看见SOAP主体包含你所期望的元素,特别是获得了指定的餐厅和指定的旅馆之间的实际距离,单位为英里。
如果你不希望在所编写的客户端代码中直接实现这一工作,也可以将它放在一个注册的处理程序中,从而可以在发送消息之前添加头。
其他解决方法
如果提供商坚持定义隐含的头,而不是将它们放在生成的代码中,而且你无法或者不希望使用上面指定的某种方法,那么,或许可以使用下面的某种解决方法:
通过向方法添加参数来修改所生成的SEI。不过,通常来说希望避免修改所生成的代码,因为这样将需要通过版本控制来维护代码。而且,如果需要在build过程中重新生成相应的代码,这种解决方法是不可行的。因此,这可能不是你真正希望做的事情。
采用老套路并使用SAAJ API。
使用供应商的IDE来添加头。
将头添加到自己的消息,也就是说,在发送实际内容消息之前发送头。这可用于多个供应商实现(包括Apache CXF),但它在JAX-WS规范中是可选的,因此不推荐使用该方法。
参见
5.13小节。
6.12 截取请求以执行特定于协议的工作
问题
希望在调用前立即截取服务请求以对其进行装饰,从而可以修改请求中某些特定于协议的部分,比如SOAP头。不希望将这类代码放入消息结构中,或者当前没有使用SAAJ。
解决方案
实现javax.xml.ws.handler.soap.SOAPHandler<T extends SOAPMessageContext>接口以使用ProtocolHandler。编写一个实现HandlerResolver接口的类并将处理程序放在一个列表中,在获取接口之前,将一个解析器实例添加到服务代理中。运行时发送每个请求和接收各个响应时将调用该处理程序。
讨论
JAX-WS中定义了两类消息处理程序:SOAPHandler和LogicalHandler。如果希望对消息载荷本身或消息环境属性进行操作,可以使用逻辑处理程序(6.13小节对此进行了讨论)。如果希望对消息中特定于所用绑定协议的那部分内容进行操作,则可以使用协议处理程序。SOAPHandler是此类的处理程序,它由JAX-WS独特定义,允许你对SOAP信封(比如信封的头、附件或故障)进行操作。本节将介绍该类。
处理程序与EJB 3拦截器或Servlet过滤器类似。在客户端,处理程序用于在即将向服务发送SOAP消息之际修改或简单访问SOAP消息。
提示: 消息处理程序也可以用于服务器端,但这是下一章中讨论的内容。
采用JAX-RPC后,为了在运行时对处理程序提供支持,需要在Webservices.xml中进行大量配置工作。Java EE 5通过使用注解去掉了这一重担的大部分工作。
在SOA中,处理程序非常有用,因为它们提供了一种简单标准的方法来应用一些SOAP规范提供的许多装饰。例如,可以使用处理程序向出站消息添加安全块或利用WS-Reliable Messaging。你或许希望记录出站消息或者甚至将它们保存到磁盘,以致于在出现网络故障的情况下可以重发这些消息。向消息头添加独特ID是比较有用的。
提示: 事实上,向消息头添加独特ID或许是一个好主意,这实现了两件事情:确保拥有有用的引用键来对网格或其他分布式工作流扩展消息操作,而且它还有助于防止消息重放攻击。如果恶意用户记录某个经过的消息并重新发送该消息,就会发生消息重放攻击。避免重放的唯一方法是使用ID或时间戳来确保消息的唯一性,然后你就可以登记已经处理的具有给定ID的消息,该消息应该不再被处理。
正如JAX-WS规范中给出的那样,处理程序的另一种用途是确保消息符合一个或多个WS-I配置文件。
javax.xml.ws.handler.Handler<C extends MessageContext>接口定义了如下三种方法,每种方法使用一种环境,该环境用作处理程序类型参数,使得开发者可以在处理同一消息的多个处理程序之间传递属性。
boolean handleMessage(C context)
它是开发者感兴趣的主要方法处理程序,因为常规消息处理都会调用它,无论是入站消息还是出站消息。在希望继续处理时返回true,否则返回false来停止后续处理。
boolean handleFault(C context)
该方法确定消息处理过程中抛出SOAP故障时应该进行何种处理。在希望继续处理时返回true,否则返回false来停止后续处理。
boolean close(MessageContext context)
该方法用于在处理结束时执行任何必要的清理工作,它是由运行时在即将分发之际调用的。
为了以编程的方式指定处理程序应该与某个代理实例相连,可以使用javax.xml.ws. handler.HandlerResolver,该接口使得开发者可以控制服务代理或分发对象上发送的处理程序链。
两种不同的时刻会以典型的SOAP请求/响应消息交换模式调用handleMessage方法:发送消息时和接收请求时。更明确地说,出站消息是由处理程序在绑定提供商处理后立即处理,而入站消息是由处理程序在即将绑定提供商处理之际处理。
实现简单的处理程序
让我们实现一个简单的处理程序来看这些方法是如何工作的。这个协议处理程序类只是将所有的SOAP消息写入一个文件输出流。
快速查看消息环境以确定当前消息是请求(出站)还是响应(入站),然后根据情况编写一个新文件。注意,在生产系统中,你不会希望对所有的入站和出站消息都使用静态名称,因为每个消息会被新消息重写。要做的事情是在出站消息的头中设置一个独特的消息ID(也可以使用处理程序SAAJ来这样做),然后用它作为文件名来帮助自己以后找到相应的消息。或者,更简单的是,每次调用时添加一个文件而不是创建一个新文件。不过这不是此处的要点(请看示例6-10)。
示例 6-10:SaveMessageHandler.java
package com.soacookbook.ch03.handler;
import java.io.File;
import java.io.FileOutputStream;
import static java.lang.System.out;
import java.io.IOException;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPException;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import org.apache.log4j.Logger;
public class SaveMessageHandler implements
SOAPHandler<SOAPMessageContext> {
private static final Logger LOGGER =
Logger.getLogger(SaveMessageHandler.class);
public boolean handleMessage(SOAPMessageContext ctx) {
LOGGER.debug("Handling SOAP MESSAGE.");
//determine if the message is coming or going
Boolean outbound = (Boolean)
ctx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
//different file name for request and response
String msgFileName = "";
//auto-unbox
if (outbound) {
LOGGER.debug("Message is OUTBOUND");
msgFileName = "outMsg.xml";
} else {
LOGGER.debug("Message is INBOUND");
msgFileName = "inMsg.xml";
}
try {
//do business logic here. We'll just log msg to file
LOGGER.debug("Logging....\n");
String dirName = "/tmp";
File logFile = new File(dirName, msgFileName);
logFile.createNewFile();
FileOutputStream fos = new FileOutputStream(logFile);
ctx.getMessage().writeTo(fos);
fos.close();
LOGGER.debug("Log complete.");
} catch (SOAPException ex) {
LOGGER.error("SOAP Exception--", ex);
} catch (IOException ex) {
LOGGER.error("IO Exception--", ex);
}
LOGGER.debug("Exiting handler normally.");
//indicate that we want to continue processing
return true;
}
public boolean handleFault(SOAPMessageContext ctx) {
LOGGER.error("SOAP FAULT! Quitting.");
return false;
}
public void close(MessageContext ctx) {
LOGGER.debug("Closing handler.");
}
public Set<QName> getHeaders() {
return null;
}
}
实现SOAPHandler接口使得你可以访问要处理的消息中的SOAP特有属性,比如SOAP头。需要实现一些方法,但主要方法是handleMessage。
拥有处理程序后,需要让服务代理可以访问它。实现方法是将处理程序与一个解析器相连,处理程序解析器指定所设定服务实例的处理程序链。示例6-11显示了解析器的实现。
示例 6-11:HelloHandlerResolver.java
package com.soacookbook.ch03.handler;
import java.util.ArrayList;
import java.util.List;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;
import org.apache.log4j.Logger;
public class HelloHandlerResolver implements HandlerResolver {
private static final Logger LOGGER =
Logger.getLogger(HelloHandlerResolver.class);
private final List<Handler> chain;
//constructor. we'll set up chain here.
public HelloHandlerResolver() {
chain = new ArrayList<Handler>();
chain.add(new SaveMessageHandler());
}
public List<Handler> getHandlerChain(PortInfo portInfo) {
LOGGER.debug("Returning handler chain...");
return chain;
}
}
各个处理程序实现是在HandlerChain环境中起作用,HandlerChain在概念上类似于过滤器链,它允许程序员为某个给定的请求指定多个处理程序以及这些处理程序将以何种顺序调用。解析器类必须实现HandlerResolver接口,该接口定义了单个方法getHandlerChain,该链本身就是一个List<Handler>。注意,你不必自己从客户端或测试类调用该方法,JAX-WS将为你这样做。你需要做的所有事情是实现该方法以指定哪些处理程序包含在链中,然后将链设定到服务中。
提示: 出站消息(请求)是按照处理程序链列表中指定的顺序处理的,而入站消息(响应)是按照相反的顺序处理的。
设置了所有内容后,将使用示例6-12中的JUnit测试用例来调用服务。你需要做的所有工作是使用setHandlerResolver方法将处理程序的实例与服务代理相连。只要指定的实例实现了Handler接口或该接口的某个子接口(比如SOAPHandler),应该就可以了。
示例 6-12:TestHandler.java
package com.soacookbook.ch03.test;
import static org.junit.Assert.*;
import com.soacookbook.ch03.*;
import com.soacookbook.ch03.handler.HelloHandlerResolver;
import com.soacookbook.ns.bin.*;
import org.apache.log4j.Logger;
import org.junit.Test;
import java.io.*;
/**
* Tests that the handler is called on an invocation to
* a service operation.
*/
public class HandlerTest {
private static final Logger LOGGER =
Logger.getLogger(HandlerTest.class);
private String name;
public HandlerTest() {
name = "Eben";
}
@Test
public void testHandler(){
HelloWSService service = new HelloWSService();
//Set handler resolver into service.
service.setHandlerResolver(new HelloHandlerResolver());
// Get the Hello stub
Hello hello = service.getHelloWSPort();
//Invoke the business method
String result = hello.sayHello(name);
assertEquals("Hello, " + name + "!", result);
//Now go check your log file to see if handler worked.
}
}
此处的客户端就是一种标准的代理调用,在Ant脚本中调用wsimport,这会基于WSDL生成服务和端口类。你在调用getXXXPort方法之前,需要确保对服务设置了处理程序解析器实例。
该测试执行通常的声明检查,以确保响应返回的结果与你希望的结果相匹配。不过,实际测试在于通过查验来确保文件系统包含处理程序所编写的文件。如果打开响应中处理程序所编写的文件的内容,它包含整个SOAP信封,如下所示:
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:sayHello xmlns:ns2="http://ch03.soacookbook.com/">
<arg0>Eben</arg0>
</ns2:sayHello>
</S:Body></S:Envelope>
这是写入到/tmp目录中outMsg.xml文件的整个请求SOAP信封,响应消息将写入到文件inMsg.xml,它具有如下内容:
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/><S:Body>
<ns2:sayHelloResponse xmlns:ns2="http://ch03.soacookbook.com/">
<return>Hello, Eben!</return>
</ns2:sayHelloResponse>
</S:Body></S:Envelope>
这里介绍的处理程序只是执行了一个简单的过程,为的是关注处理程序接口的骨架。但是,像过滤器和EJB拦截器一样,处理程序为客户端开发者提供了一个重要的机会来访问或修改消息内容,特别是当开发者需要提供安全凭据、添加头、动态改变消息内容(例如通过加密)或确保出站消息符合某种策略时。
关于处理程序的更多信息
处理程序的使用包含很多内容,因此值得花点时间说明一些“黑暗的角落”。通常来说,使用处理程序时,要考虑如下一些事情。
JAX-WS处理程序的生命周期由容器管理,该生命周期定义了两个方法,它们与EJB 3生命周期方法类似。第一个方法带有@PostConstruct注解,它用作初始化器。容器通常在调用任何其他方法之前调用该方法。第二个方法带有@PreDestroy注解,该方法用于在删除处理程序实例之前执行清理工作。如果希望从容器接收有关通知这些事情发生的回调,可以在处理程序中定义这些生命周期方法。这些方法必须具有空的返回类型而且不带参数。
如果在处理程序处理过程中抛出了RuntimeException,将会调用带有@PreDestroy注解的方法,这样你就有机会在删除处理程序实例之前执行清理工作。
只要愿意,容器可以以池的形式汇聚处理程序实例,但并不要求容器这样做。如果处理程序实例以池的形式汇聚,它们将只与一种端口类型相关联。
处理程序是无状态的。当调用handleMessage或handleFault方法时,容器不必使用同一处理程序实例,因为可能会以池的形式汇聚实例。处理程序不维护请求中任何特定于客户端的状态。
处理程序运行的事务环境是通过它们的关联组件指定的,处理程序是不允许使用javax.transaction.UserTransaction来指定自身的事务的。
处理程序运行的安全环境由容器指定,处理程序不能使用基于角色的验证来执行安全检查,也不能访问与请求关联的内容。
可使用不同的线程来调用各个处理程序,容器不必使用同一线程来调用处理程序和服务实现。
你可以声明处理程序将使用getHeaders方法处理WSDL中的某些头,它会返回一个Set<QName>。如果处理程序没有处理任何头或者WSDL没有定义任何头,返回null也是可以的。
逻辑处理程序通常是在所有协议处理程序之前执行的,即使在处理程序链中将它们以交替的方式进行定义,它们的排序还是逻辑处理程序排在前面。
如果处理程序是在某个受控环境(例如容器)中运行的,允许它们指定@Resource注解来定义要使用的可注射资源。
参见
要想了解如何编写只处理载荷而不处理协议封装(在本示例中为SOAP信封)的处理程序,可以参阅6.13小节。
6.13 拦截请求以对载荷执行操作
问题
希望通过拦截来只访问或修改请求或响应的载荷,并且不希望将相应的代码放入消息结构,而对协议封装(比如SOAP信封)不感兴趣。
解决方案
通过实现javax.xml.ws.handler.LogicalHandler接口来使用LogicalHandler。
讨论
如果需要只对消息的载荷进行操作,无论你使用的是SOAP还是REST,这时逻辑处理程序是比较有用的。如果希望使用JAXB来处理消息载荷,逻辑处理程序也是合适的选择。
逻辑处理程序的框架与协议处理程序的框架类似,因此,有关更多背景知识,可以参阅6.12小节,特别是其中给出的补充说明“关于处理程序的更多信息”。
printMetaData方法说明即使在逻辑处理程序中,你也可以访问传输的某些方面,了解所处理消息的很多内容。在客户端得到的响应(入站消息)中,元数据打印方法代码的运行结果如下所示:
********** HEADERS:
List of headers for Transfer-encoding: [chunked]
List of headers for null: [HTTP/1.1 200 OK]
List of headers for Content-type: [text/xml;charset="utf-8"]
List of headers for Server: [Sun Java System Application Server 9.1_01]
List of headers for X-powered-by: [Servlet/2.5]
List of headers for Date: [Thu, 15 May 2008 17:02:12 GMT]
********** RESPONSE CODE: 200
********** ATTACH: {}
6.14 多个处理程序调用之间共享数据
问题
在处理过程中,希望对链中多个处理程序进行调用时,这些调用可以共享数据,但处理程序实例无法依赖线程局部状态。
解决方案
从处理程序的参数获得消息环境并向其添加数据。你可以随后设置和访问预定义的属性,或者添加自己的属性,这些属性表现为一种对应关系。
下面的代码说明了如何使用自定义属性和消息环境的内置属性来对同一服务定义的两个处理程序共享数据。
在获得端口之前,测试客户端向服务添加了处理程序解析器:
@Test
public void testMessageContext(){
HelloWSService service = new HelloWSService();
//Set handler resolver into service.
service.setHandlerResolver(new MultipleHandlerResolver());
// Get the Hello stub
Hello hello = service.getHelloWSPort();
//Invoke the business method
String result = hello.sayHello(name);
assertEquals("Hello, " + name + "!", result);
.
}
下面是定义了多个处理程序的一个新处理程序解析器:
public class MultipleHandlerResolver implements HandlerResolver {
private static final Logger LOGGER =
Logger.getLogger(MultipleHandlerResolver.class);
private final List<Handler> chain;
//constructor. we'll set up chain here.
public MultipleHandlerResolver() {
chain = new ArrayList<Handler>();
chain.add(new SetVersionHandler());
chain.add(new VersionInstructionsHandler());
}
//...
这个解析器定义了一个处理程序将设置消息环境中客户端使用的版本,定义的另一个处理程序将其他说明添加到已确定客户端版本的外出SOAP消息中。示例6-13显示了修改消息环境的处理程序。
示例6-13: SetVersionHandler.java
package com.soacookbook.ch03.handler;
import static java.lang.System.out;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import org.apache.log4j.Logger;
public class SetVersionHandler implements
SOAPHandler<SOAPMessageContext> {
private static final Logger LOGGER =
Logger.getLogger(SetVersionHandler.class);
public boolean handleMessage(SOAPMessageContext ctx) {
LOGGER.debug("Handling SOAP MESSAGE.");
//determine if the message is coming or going
final Boolean outbound = (Boolean)
ctx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (outbound){
ctx.put("SVC-VERSION", "1.0");
}
return true;
}
public boolean handleFault(SOAPMessageContext ctx) {
LOGGER.error("SOAP FAULT! Handle here...");
return false;
}
public void close(MessageContext ctx) {
LOGGER.debug("Closing handler.");
}
public Set<QName> getHeaders() {
return null;
}
}
上面的处理程序就是将字符串类型的键/值对添加到消息环境,链中的其他处理程序可以使用该键来获取相应的值。注意,如果消息是出站消息,则只设置版本元数据。
提示: 请回想一下,出站时,处理程序是按照解析器指定的顺序调用的,而入站时,处理程序是按照相反的顺序调用的。逻辑处理程序通常是在协议处理程序之前调用的。
接下来,在分发消息之前,将调用链中的下一个处理程序,它指定使用某一给定版本时的服务说明,并将这些说明添加到出站消息的附件中,如示例6-14所示。
示例 6-14:从Web服务返回二进制数据
public class VersionInstructionsHandler implements
SOAPHandler<SOAPMessageContext> {
public boolean handleMessage(SOAPMessageContext ctx) {
LOGGER.debug("Handling SOAP MESSAGE.");
//determine if the message is coming or going
final Boolean outbound = (Boolean)
ctx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
//is message incoming?
if (outbound) {
//get the language from context
final String version = (String)ctx.get("SVC-VERSION");
LOGGER.debug("Service Version: " + version);
//if the version is 1.0, give user instructions
//for upgrading to coming version.
if ("1.0".equals(version)) {
try {
SOAPMessage msg = ctx.getMessage();
SOAPBody body = msg.getSOAPBody();
SOAPElement content =
(SOAPElement)body.getFirstChild();
String value = content.getTextContent();
LOGGER.debug("Print value: " + value);
//because version is old, attach instructions
//for migrating to new version
//Create SOAP attachment
AttachmentPart ap = msg.createAttachmentPart();
String s = "Client will support JMS in " +
"version 1.5.";
ap.setContent(s, "text/plain");
ap.setContentId("Version-1.5-Notice");
msg.addAttachmentPart(ap);
LOGGER.debug("Attachment added.");
} catch (Exception ex) {
LOGGER.error("Problem.", ex);
}
}
}
return true;
}
//...
该附件机制本身不是此处的重点,此处希望关注的内容是这样一种想法:可以使用环境来在处理程序之间传递数据,然后基于它采取某种操作。或许我们通常不让客户端采用这种方式向服务发送某种通知,但这种示范对于我们此处的目的是比较有用的。
处理程序链是完整的,消息被分发。不过,由于在出站时对消息进行了修改,从而生成了一个多部分的SOAP请求。附件和SOAP主体中的数据一起被发送,最终的SOAP消息如示例6-15所示。
示例6-15:向Web服务传递二进制数据
---[HTTP request]---
SOAPAction: ""
Accept: text/xml, multipart/related, text/html,
image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-Type: multipart/related; type="text/xml";
boundary="uuid:002fa642-b700-47c4-a298-ed1e1fb03795"
--uuid:002fa642-b700-47c4-a298-ed1e1fb03795
Content-Type: text/xml
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:sayHello xmlns:ns2="http://ch03.soacookbook.com/">
<arg0 xmlns="">Eben</arg0>
</ns2:sayHello></S:Body>
</S:Envelope>
--uuid:002fa642-b700-47c4-a298-ed1e1fb03795
Content-Id:<Version-1.5-Notice>
Content-Type: text/plain
Content-Transfer-Encoding: binary
Client will support JMS in version 1.5.
--uuid:002fa642-b700-47c4-a298-ed1e1fb03795
该附件被添加到给定UUID的关联消息,而该消息将被分发到服务。
讨论
消息环境可以用于共享消息处理的有关状态。javax.xml.ws.handler.MessageContext是JAX-WS中消息环境的父接口。我们可以回顾一下6.12小节,Handler的handleMessage和handleFault方法接受<C extends MessageContext>的一般类型参数。LogicalMessageContext和SOAPMessageContext都扩展MessageContext来为各自的处理程序类型提供有用的属性,从而履行一般类型参数合同。我们稍后将介绍这些子接口的功能。不过,MessageContext提供了许多灵活的键,使你可以获取处理程序处理的当前消息的有用信息。
MessageContext属性
MessageContext接口提供了许多属性,包括:
与HTTP有关的属性,使你可以查看头、请求方法、响应代码和响应头。
与Serlvet和路径有关的属性,使你可以获得路径信息对象、查询字符串以及Servlet请求和响应对象。
处理程序特有的属性,例如,MESSAGE_OUTBOUND_PROPERTY用于确定是在处理入站还是出站消息(当消息是出站消息时为true,否则为false)。检查该属性可以简化老式JAX-RPC接口,在JAX-RPC中,需要在处理程序接口中定义两个方法。使用这些属性还可以访问MIME附件。
WSDL特有的属性,通过这些属性可以访问处理程序的有关服务、端口和操作方面的信息。
可以参阅http://java.sun.com/javaee/5/docs/api/javax/xml/ws/handler/MessageContext.html提供的JavaDoc以获取完整列表。
SOAPMessageContext
SOAPMessageContext接口定义了用于如下方面的方法:从环境获取SOAP消息本身,将消息设置回环境中,根据QName从环境中的消息获取头以及获取与链处理有关的SOAP actor角色。
LogicalMessageContext
LogicalMessageContext接口扩展了MessageContext以提供对消息的访问,消息是以一种与协议无关的格式表示的。为了这个目的,该接口只向MessageContext接口添加了一个方法:getMessage,该方法返回一个LogicalMessage。
6.15 在请求中传递二进制数据
问题
需要在SOAP消息的主体中发送二进制数据,比如图像文件或PDF文件。
解决方案
使用字节数组表示二进制数据,WSDL将这表示为xs:base64或xs:hexBinary。
示例6-16说明了这种方法。在这里,该用例的核心是一个名为getImage的Web服务操作,它接受字符串作为输入,该字符串是数据库中以前存储为BLOB(Binary Large Object,二进制大对象)的图像的标识符。可以使用该标识符来执行SQL查询和获取BLOB,以字节数组的形式返回的。不过,为了简单和聚焦重点,只创建了一个常规字节数组并将其返回。
示例 6-16:二进制数据方法
@WebMethod
public @WebResult(name="imageResponse",
targetNamespace="http://ns.soacookbook.com/ch03")
byte[]
getImage(
@WebParam(name="imageRequest",
targetNamespace="http://ns.soacookbook.com/ch03")
String imageId) {
//Use the passed ID to find this instance in the database
//This is our fake image data...
byte[] imageBytes = {1,0};
//If you want to save your image data to a database,
//create a PreparedStatement and use:
...
ps.setBinaryStream(1,
new ByteArrayInputStream(sigImageData), imageBytes.length);
return imageBytes;
}
注意,这个示例说明如何将BLOB转换成服务操作的可返回字节数组,因为这是一个比较实际的用例。
这些JAX-WS注解生成的WSDL将为返回的字节数组生成基本的XML Schema xs:base64类型。此处需要考虑的重要事项是:处理二进制数据可能需要与带外客户端进行会话,从而客户端知道期望得到什么结果。就像在本示例中,如果你基于一个标识符请求图像数据,那么,客户端如何知道期望得到何种图像编码器?是GIF、JPG还是PNG?如果它是一个位图或TIFF,就像许多信用卡读取器产生的那样,那么,该文件格式本身不被Java SE支持,客户端必须在类路径中加入Advanced Imaging API。
提示: 将二进制数据包装在SOAP消息中时要当心,否则客户端可能不知道期望得到什么结果。你可以在注释中给出二进制数据的类型或编码器,或者用特定的方法名称给出二进制数据的类型或编码器。例如,获取mugshot的方法可以被相应地命名成getMugshotTiff()或getMugshotJpg()。
参见
7.23小节。
6.16 在SOAP消息中使用二进制数据
问题
需要将现有的二进制数据(比如图像或PDF)传递给某个Web服务。
解决方案
使用字节数组作为参数类型。
讨论
使用字节数组作为参数类型使得你可以在客户端读取字节流并将它直接传递给方法,SOAP机制会将它作为xs:base64Binary内容进行编码并将其传递给服务。下面是Web服务定义的一个此类方法:
@WebMethod
@SOAPBinding(parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public @WebResult(name="putResponse",
targetNamespace="http://ns.soacookbook.com/bin")
String
put(
@WebParam(name="putData",
targetNamespace="http://ns.soacookbook.com/bin")
byte[] binaryData) {
相应生成的Schema如下所示:
<xs:element name="put" type="tns:put"></xs:element>
<xs:complexType name="put">
<xs:sequence>
<xs:element name="putData" type="xs:base64Binary"
form="qualified" nillable="true" minOccurs="0" />
</xs:sequence>
</xs:complexType>
让我们以单元测试的形式生成一个客户端,它将读取一些图像数据并将其传递给服务。在这里,我们将传入一个JPG图像作为服务中put请求的参数值,服务将返回一个ID,出于单元测试的目的,它将是“007”。请看示例6-17。
示例 6-17:BinaryDataTest.java
package com.soacookbook.ch03.test;
import static org.junit.Assert.*;
import com.soacookbook.ns.bin.*;
import org.apache.log4j.Logger;
import org.junit.Test;
import java.io.*;
/**
* Tests the BinaryData service.
*/
public class BinaryDataTest {
private static final Logger LOGGER =
Logger.getLogger(BinaryDataTest.class);
private static final String FILE_PATH =
"/home/ehewitt/soacookbook/repository/code/" +
"chapters/client/winchesterHouse.jpg";
public BinaryDataTest() { }
/**
* Client has a binary file such as image or PDF it would
* like to send to service for storage or processing.
* This client reads it in and passes to service, which
* generates an ID for it and returns the ID so client
* can get back data later if necessary.
*/
@Test
public void testPutBinaryData(){
LOGGER.debug("");
try {
BinaryDataService svc = new BinaryDataService();
BinaryData port = svc.getBinaryDataPort();
File f = new File(FILE_PATH);
byte[] imageData = getFileAsBytes(f);
LOGGER.debug("*** Read in file of bytes: " +
imageData.length);
String id = port.put(imageData);
LOGGER.debug("Got id returned from service: " + id);
assertEquals("007", id);
} catch (IOException ex) {
fail();
ex.printStackTrace();
}
}
// Returns file contents in a byte array.
private static byte[] getFileAsBytes(File file) throws IOException {
InputStream is = new FileInputStream(file);
// Get file size
long length = file.length();
if (length > Integer.MAX_VALUE) {
throw new IOException("File is too big to read: " +
file.getName());
}
// Create the byte array to hold the data
byte[] bytes = new byte[(int)length];
// Read the bytes in
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead=is.read(bytes, offset,
bytes.length-offset)) >= 0) {
offset += numRead;
}
// Make sure we read all bytes
if (offset < bytes.length) {
throw new IOException("Could not read file " +
file.getName());
}
is.close();
return bytes;
}
}
正如你看到的那样,困难的部分是由JAX-WS运行时处理的。你需要做的所有工作是从本地文件系统读入图像文件,从而可以将它用作当前操作的参数。
传递的SOAP消息如下所示:
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<put xmlns="http://ns.soacookbook.com/bin">
<putData>/9j/4AAQSkZJRgABAgEBLAEsAAD/4RFdRXhpZgAATU0AKgAAAAgABwE
SAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAE
xAAIAAAAUAAAAcgEyAAIAAAAUAAAAhodpAAQAAAABAAAAnAAAAMgAAAEsAAAAAQA
AASwAAAABQWRvYmUgUGhvdG9zaG9wIDcuD2qDO1Kt9Vv+Cr7f0fyvp11DwJ//9k=
//....
</putData>
</put>
</S:Body>
</S:Envelope
参见
要了解如何在客户端优化二进制数据的传输,可以参阅6.17小节。
6.17 在客户端启用二进制优化
问题
客户端需要将二进制数据发送给某个基于SOAP的Web服务,为了提高性能,希望对数据进行优化。
解决方案
将二进制数据作为xs:base64Binary内容进行编码,然后在客户端通过将javax.xml.ws. MTOMFeature传递给代理构造函数来启用MTOM。
MTOM代表Message Transmission Optimization Mechanism(消息传输优化机制),它是由W3C创建的一种规范,用于支持对消息的各部分进行有选择性的编码。在SOAP消息中仍然可以使用XML信息集,因此,该过程没有使其混乱。
提示: 采用JAX-WS的供应商需要支持MTOM,应该该代码是可移植的。
那么,如何设置它?如果查看JAX-WS生成的WebServiceClient类,你将会注意到每个类包含两个get<X>Port方法:一个不带参数,另一个接受长度可变的WebServiceFeature对象。示例6-18显示了在wsimport过程中由JAX-WS生成的目录WebServiceClient。
示例 6-18:生成的Web服务客户端
@WebEndpoint(name = "CatalogPort")
public Catalog getCatalogPort() {
return super.getPort(new QName("http://ns.soacookbook.com",
"CatalogPort"), Catalog.class);
}
@WebEndpoint(name = "CatalogPort")
public Catalog getCatalogPort(WebServiceFeature... features) {
return super.getPort(new QName("http://ns.soacookbook.com",
"CatalogPort"), Catalog.class, features);
}
WebServiceFeature是在运行时启用和禁用各种有用机制的一种标准方式。JAX-WS内置了一些功能,供应商可以提供一些附加功能,你还可以自己编写相应的功能。
javax.xml.ws.soap.MTOMFeature类扩展了WebServiceFeature,生成的客户端接口使得可以轻松地通过内置或自定义功能调用服务操作。要想在客户端使用MTOM,只需要通过接受可变长度WebServiceFeature对象的工厂来获取端口,传入一个MTOMFeature实例。下面给出了如何使用getCatalogPort方法来启用MTOM:
Catalog catalogPort = service.getCatalogPort(new MTOMFeature());
运行时将为你处理实现。
MTOMFeature构造函数还接受一个可选的整数参数,它表示作为附件发送之前二进制数据的阈值或字节数。默认的阈值是0。
参见
7.23小节。
6.18 使用Metro根据Schema验证SOAP载荷
问题
希望使用一种比5.20小节中借助SAAJ的方式更为轻松的方法来根据XML Schema验证消息。
解决方案
通过Metro对服务类使用注解com.sun.xml.ws.developer.SchemaValidation,还可以对该注解使用处理程序属性来指向实现了Handler接口的特殊类,而该接口对应于相应的验证事件。
在客户端,创建一个新的SchemaValidationFeature并将其传递给端口。
讨论
如果希望在客户端根据Schema验证载荷,可以使用该Schema验证功能,如下所示:
SignatureCapture port = null;
try {
final SigCapService service =
new SigCapService(wsdlLocation, QNAME);
port = service.getSignatureCapturePort(
new AddressingFeature(),
new SchemaValidationFeature(
SigCapClientValidationHandler.class));
//invoke service...
在这里,我们使用了wsimport来生成调用服务时要使用的结果。所生成的端口代理接受的可变长度参数列表允许你传递JAX-WS运行时管理的0个或多个功能,在此处,我使用两个功能:WS-Addressing和Schema验证,这将在Java对象编组到XML的过程中检查出站SOAP消息的载荷以及创建SigCapClientValidationHandler类的实例。在这个处理程序类内部,我们实现了所需的接口并当SAX解析事件发生时进行相应的处理。示例6-19显示了一个简单的处理程序实现。
示例 6-19:用于验证的处理程序实现
import com.sun.xml.ws.developer.ValidationErrorHandler;
import org.xml.sax.SAXParseException;
import org.xml.sax.SAXException;
import org.apache.log4j.Logger;
/**
* The error handler that catches validation problems against
* the Schema.
*
* @author ehewitt
* @author bmericle
*/
public final class SigCapClientValidationHandler
extends ValidationErrorHandler {
private static final Logger LOGGER =
Logger.getLogger(SigCapClientValidationHandler.class);
public SigCapClientValidationHandler() {
LOGGER.debug("Schema Validation Handler created.");
}
@Override
public void warning(final SAXParseException e) throws SAXException {
LOGGER.warn("Schema Validation Warning: " +
e.getLocalizedMessage());
// Store warnings in the packet so that they can be retrieved
//from the endpoint
packet.invocationProperties.put("Schema Validation Warning.", e);
throw e;
}
@Override
public void error(final SAXParseException e) throws SAXException {
LOGGER.error("Schema Validation Error: " + e.getLocalizedMessage());
throw e;
}
@Override
public void fatalError(final SAXParseException e) throws SAXException {
LOGGER.warn("Schema Validation Fatal Error: " +
e.getLocalizedMessage());
throw e;
}
}
此处所发生的情况是相当简单的。当解析器对XML实例中与Schema不匹配的内容迷惑时,就会触发相应的事件。在这里,我只是记录问题并重新抛出异常,不过你可以根据需要做一些更有意义的事情。
在Maven中将Metro添加到客户端
该解决方案的明显不足是需要在客户端包含Metro JAR,如果你是在Maven中构建Java客户端应用程序,下面是需要添加到POM的<dependencies>中的一些内容:
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>Webservices-rt</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>Webservices-api</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
6.19 对JAX-WS客户端实现异步调用
问题
希望异步调用服务端点接口的操作。
解决方案
在WSDL中,向服务器端添加<enableAsyncMapping>自定义绑定,然后使用SEI提供的某种invokeAsync方法。
异步Web服务客户端可以通过两种方式构建:回调或轮询。使用单个<enableAsyncMapping>定制后,这两种方法都可用。
提示: 在试图调用invokeAsync之前,需要使用jaxws:bindings元素来对WSDL启用异步映射。否则,调用看起来是成功的,但实际上不是异步的。也就是说,这些调用将阻塞,就像通常调用invoke一样。
如果使用轮询方法,客户端会阻塞,重复检查响应。对GUI应用程序来说,要求其主线程必须自由地对用户交互做出响应是不合适的。如果使用回调方法,调用时客户端必须将一个Handler<T>传递给Web服务操作,获得响应时将填充该处理程序对象。这使得应用程序线程可以同时继续其他业务。
为了允许客户端异步调用操作,必须首先在一个文件中指定绑定定制,在使用wsimport进行客户端生成过程中,会指向该绑定定制。
提示: 赋予绑定定制文件什么名称或者使用何种扩展名都没有关系,不过,JAX-WS自定义绑定文件的惯例后缀为.xml,这与.xjb相对,.xjb是JAXB自定义绑定文件的惯例后缀。
示例6-20显示了启用异步映射的自定义绑定文件的一个例子。
示例 6-20:自定义JAX-WS绑定以启用异步操作
<bindings
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wsdl="http://Schemas.xmlsoap.org/wsdl/"
wsdlLocaption="http://localhost:8080/soaCookbookWS/SoaCookbookService?wsdl"
xmlns="http://java.sun.com/xml/ns/jaxws">
<bindings node="wsdl:definitions">
<package name="com.soacookbook.ch03"/>
<enableAsyncMapping>true</enableAsyncMapping>
</bindings>
</bindings>
在这个文件中,我们使用http://java.sun.com/xml/ns/jaxws命名空间提供的绑定自定义来指定希望改变哪些节点。
当在wsimport中包含绑定定制时,必须创建wsimport任务的一个子元素来指向绑定文件,如下所示:
<binding dir="..." includes="..." />
因此,需要更新build.xml文件来包含该代码。新的wsimport任务如示例6-21所示。
示例 6-21:包含绑定定制的wsimport Ant任务
<wsimport
wsdl="${wsdl.url}"
destdir="${gen.classes.dir}"
sourcedestdir="${src.gen.dir}"
keep="true"
extension="false"
verbose="true" >
<binding dir="${binding.dir}" includes="${binding.file}" />
</wsimport>
如果给定的目录中存在多个绑定定制文件,可以使用通配符指定带有给定扩展名的所有文件。如果希望指向多个目录,可以使用多个<binding>元素。
提示: 如果是从命令行执行,可以使用-b选项指定绑定文件的位置。不必将所有的定制放在一个文件中;如果希望将它们分开以获得更大的灵活性,只需要对每个文件使用一个新的-b选项。同样,可以在Ant任务中指定多个binding元素。
接下来,运行构建的脚本来修改生成的代码。除了生成的常规同步方法外,它还在SEI(此处是SoaCookbook.java)中生成两个独立的方法。每个异步方法采用不同的方式处理异步行为:轮询和回调。我们将在下面的各节中介绍这些内容。
使用异步轮询
本小节假定你已执行本节中前面给出的步骤来生成有关的异步SEI,其代码如示例6-22中所示。
示例 6-22:生成的轮询方法
@WebMethod(operationName = "doLongJob")
@RequestWrapper(localName = "doLongJob",
targetNamespace = "http://ns.soacookbook.com",
className = "com.soacookbook.ns.DoLongJob")
@ResponseWrapper(localName = "doLongJobResponse",
targetNamespace = "http://ns.soacookbook.com",
className = "com.soacookbook.ns.DoLongJobResponse")
public Response<DoLongJobResponse> doLongJobAsync(
@WebParam(name = "jobName", targetNamespace = "http://ns.soacookbook.com")
String jobName);
轮询操作签名不像常规阻塞方法那样返回DoLongJobResponse,而是返回Response<DoLongJobResponse>。javax.xml.ws.Response<T>是封装了常规返回类型的标准JAX-WS类,它扩展了Future<T>,使你可以访问操作响应的环境映射。对于异步操作,它提供isDone方法来查看操作是否完成,并提供cancel操作允许客户端中断调用。
使用轮询的客户端将需要手动执行该检查,让我们向客户端类添加一个方法来处理与SEI的所有交互,示例6-23中显示了该类,它将用作较大应用程序中的一个业务代表以封装Web服务执行任务这一事实。
示例 6-23:使用轮询来异步调用操作
package com.soacookbook.ch03;
import java.util.*;
import javax.xml.bind.*;
import javax.xml.soap.*;
import org.apache.log4j.Logger;
import com.soacookbook.ns.*;
import javax.xml.ws.Response;
public class AsynchClient {
private static final Logger LOGGER = Logger.getLogger(AsynchClient.class);
public String doLongJobPolling(String jobName) throws Exception {
LOGGER.debug("Executing.");
SoaCookbookService svc = new SoaCookbookService();
SoaCookbook port = svc.getSoaCookbookPort();
Response<DoLongJobResponse> response =
port.doLongJobAsync(jobName);
LOGGER.debug("Invoked service.");
while(!response.isDone()){
LOGGER.debug("Waiting...");
Thread.sleep(1000); //do something
}
DoLongJobResponse res = response.get();
String status = res.getJobDone();
LOGGER.debug("Status: " + status);
return status;
}
}
一旦isDone方法返回true,就会获得响应对象。
通过单元测试使用“My Batch”虚构任务名运行该客户端后,得到如下输出结果:
Executing.
Invoked service.
Waiting...
Waiting...
Waiting...
Waiting...
Status: Job is done running: My Batch
使用异步回调
本小节假定你已执行本节中前面给出的步骤来生成有关的异步SEI。
生成的第二种方法使用客户端定义的一个回调,完成时它将从服务操作接收响应。相应的方法签名如下所示:
@WebMethod(operationName = "doLongJob")
@RequestWrapper(localName = "doLongJob",
targetNamespace = "http://ns.soacookbook.com",
className = "com.soacookbook.ns.DoLongJob")
@ResponseWrapper(localName = "doLongJobResponse",
targetNamespace = "http://ns.soacookbook.com",
className = "com.soacookbook.ns.DoLongJobResponse")
public Future<?> doLongJobAsync(
@WebParam(name="jobName", targetNamespace="http://ns.soacookbook.com")
String jobName,
@WebParam(name = "asyncHandler", targetNamespace = "")
AsyncHandler<DoLongJobResponse> asyncHandler);
注意,该常规方法签名发生了两处变动。该方法没有返回WSDL定义的DoLongJobResponse,也没有像异步轮询方法那样返回Response<DoLongJobResponse>,而是返回一个Future<?>。第二个变动是不像没有修改原始方法参数列表的轮询方法,该方法添加了基于DoLongJobResponse进行参数化的一个参数,它是一个AsyncHandler。但是,这个AsyncHandler实现来自何处?你需要编写它并将它的一个实例传递给服务调用。该接口只包含一个名为handleResponse的方法,它接受Response<T>。
有关Future<?>
虽然GUI程序员如今可能熟悉Future<?>,但它或许不是Web程序员经常遇到的事物。Future最初是在Java 5中作为并发库的一部分引入的。一个Future代表一个抽象任务,它给出某些结果,提供一些方法来管理此类任务的生命周期,这些方法使得你可以查看任务是否已经取消、任务是否完成,并且可以收集任务的结果。需要时,你还可以取消任务,而且可以指定在抛出TimeoutException之前希望等待多长时间来让任务完成。任务本身通常是通过实现Runnable或Callable接口来描述的,而Future通常用于封装它们。
用于收集Future所代表任务的结果的方法是get,必要时它会等待。
在屏幕的背后,Future将结果放入处理程序,一旦获得结果,你可以使用该处理程序执行其他工作。
提示: 如果Future<?>按照这种方式工作,你不希望从Future对象本身直接获取结果。“?”代表一个通配符,没有上限和标准类型,这意味着有效类型是Object。因此,虽然可以进行如下操作:
DoLongJobResponse r = (DoLongJobResponse)task.get();
但客户端不会以这种方式获得结果,因为这会导致不可移植的行为。相反,却是使用处理程序实现来在必要时获取结果。
在试图使用结果之前,确保调用了isDone。如果还没有准备就绪,future的get将会返回null。
编写回调处理程序
可以为分发客户端或SEI客户端实现回调处理程序,它就是一个实现了AsyncHandler接口的类,该接口定义了单个方法handleResponse。
在回调处理程序中,开发者可以访问如下内容:
java:comp/env提供的JNDI环境
资源管理器
企业级Java Bean
提示: 在回调处理程序中不能使用依赖注射。供应商可能允许这样做,但规范没有相应的要求,因此,如果存在此类行为,它将是不可移植的。可以使用JNDI来查看自己的资源是否受到这类约束。
有一些规则是有关编写该处理程序的。首先,它本身不能是EJB或Servlet。此外,没有为回调处理程序指定事务环境,这意味着虽然你可以在handleResponse方法中创建UserTransaction,但无法将它传播给其他资源;在方法退出之前,必须提交或回滚事务。另外,回调处理程序实现相当简单,如示例6-24所示。
示例 6-24:AsyncHandler<T>实现
class MyHandler implements AsyncHandler<DoLongJobResponse> {
private static final Logger LOGGER =
Logger.getLogger(MyHandler.class);
private DoLongJobResponse response;
public void handleResponse(Response<DoLongJobResponse> in) {
LOGGER.debug("Executing callback handler.");
try {
response = in.get();
LOGGER.debug("Got response! " +
response.getJobDone());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public DoLongJobResponse get() {
return response;
}
}
该实现只需要一个handleResponse方法。由于是将响应保存到一个实例域,需要一个其他方法来获取操作结果。通常来说,不必返回结果,而是在处理程序中直接使用它。
示例6-25显示了使用该处理程序来产生回调的客户端代码。
示例 6-25:异步回调客户端
public void doLongJobCallback(String jobName) throws Exception {
LOGGER.debug("Executing.");
SoaCookbookService svc = new SoaCookbookService();
SoaCookbook port = svc.getSoaCookbookPort();
MyHandler handler = new MyHandler();
Future<?> task = port.doLongJobAsync(jobName, handler);
LOGGER.debug("Invoked service.");
while(!task.isDone()){
LOGGER.debug("Waiting...");
Thread.sleep(1000); //do something
}
}
该代码获得SEI,然后调用它重载的doLongJobAsync方法。我们创建自定义处理程序的一个实例,并记住WSDL中没有实际定义这类方法,它是由于客户端方的绑定定制生成的。该方法返回一个Future,它最后将接收和封装操作的结果,从而可以在处理程序中对其进行操作。
运行该代码后,得到的输出结果如下所示:
AsynchClient.doLongJobCallback - Executing.
AsynchClient.doLongJobCallback - Invoked service.
AsynchClient.doLongJobCallback - Waiting...
AsynchClient.doLongJobCallback - Waiting...
AsynchClient.doLongJobCallback - Waiting...
AsynchClient.doLongJobCallback - Waiting...
MyHandler.handleResponse - Executing callback handler.
MyHandler.handleResponse - Got response! Job is done running: Some Batch
当使用某种异步调用方法调用Web操作出现失败时,规范要求从Response.get方法抛出java.util.concurrent.ExecutionException。缘由是被抛出的原始异常,它通常是WebServiceException或某个子类。
6.20 覆盖SEI中的端点地址
问题
希望创建一个JAX-WS客户端并在运行时向其传递不同端点地址。
解决方案
从端口获取请求环境并使用其环境映射来将BindingProvider.ENDPOINT_ADDRESS_ PROPERTY常数作为一个键进行设置,并使用新的端点位置作为其值。
讨论
当从WSDL生成服务代理时,端点地址被硬编码到类中,不过,可以在javax.xml.ws.BindingProvider接口中使用常数来覆盖客户端将接触的端点位置,该接口不仅定义了端点位置的常数,而且还定义了用于实现以下方面的常数:指定SOAP操作、维护请求间的会话状态等。
它的使用非常简单,如示例6-26所示。
示例 6-26:设置端点地址属性
public class ClientServlet extends HttpServlet {
@WebServiceRef(wsdlLocation=
"http://localhost:8080/CalculatorApp/CalculatorWSService?wsdl")
public CalculatorWSService service;
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
org.me.calculator.client.CalculatorWS port = service.getCalculatorWSPort();
((BindingProvider)port).getRequestContext().put(
BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:4933/CalculatorApp/CalculatorWSService?wsdl");
int i = Integer.parseInt(request.getParameter("value1"));
int j = Integer.parseInt(request.getParameter("value2"));
int result = port.add(i, j);
在该代码中,当从服务实例获取代理时,就会设置要调用的地址,而不论SEI中指定了什么值。该请求环境不是一个封装器,而是一个简单的映射,用于指定键/值对。在这个示例中,客户端是一个Servlet,它的@WebServiceRef.wsdLocation属性指定端口8080上的一个WSDL;可以使用BindingProvider.ENDPOINT_ADDRESS_PROPERTY指定其他端口上的WSDL。在运行时,该SEI将使用4933上的WSDL。
这类代码有许多用途。由于WSDL位置值是由一个字符串指定的,因此可以动态地覆盖@WebServiceRef注解中设置的位置。这比较有用,例如,当希望将某些客户端指向指定了不同策略的不同版本的WSDL时。
当然,只有当目标端点位置所代表的服务和端口与原来创建客户端时所用的WSDL上的服务和端口相匹配时,这才会有效。否则,你将会得到一个错误。
本章小结
在本章中,我们介绍了如何利用新的Web服务API来实现各种操作,其中使用了大量的Java 5注解。在有些情况下,这些示例替代了我们必须使用SAAJ完成的较复杂的低级工作。一般来说,本章中的代码可以在不同的JAX-WS供应商实现之间进行移植,包括Glassfish、Oracle WebLogic以及Apache产品,比如CXF和Axis。
第7章
提供基于SOAP的Web服务
7.1 概述
在Java中,多年前就可以使用Web服务了,但是,作为一个开发者,使用Web服务是一个复杂的过程,涉及到许多需要小心处理的步骤。随着Java EE 5的出现,复杂性被大大降低。部署描述符让位于注解,为你生成了大量样本成果。
尽管存在这一好消息,但在实际工作中Web服务的实现过程仍是非常复杂的。Web服务的实际复杂程度仍旧非常高;向我们屏蔽了一些繁重工作这一事实并没有改变管道工作方式。对任何项目来说,编写尽可能避免锁定供应商的可维护的灵活代码是一项挑战。Java EE 5中灵活性得到提高,从而赋予我们更多选择;这非常不错,但也向我们提供了一系列新的配置和实现选项,而它们对于新手来说是令人畏缩的。因此,要想获得好的设计,我们需要做大量的工作。
如果你在以前版本的Java中处理过Web服务,可以基于自己的基础知识进行构建,不过,对于Web服务开发者如何进行设计,新的API发生了根本性变化。下面是Java EE 5中发生变化的一些方面:
不需要编写Webservices.xml部署描述符。该文件过去是必需的,现在,注解代替了与Web服务有关的单个描述符文件,处理所有的相关工作。当然,现在需要学习编写所有这些注解。
部署中不需要包装有用来将Java映射到WSDL的JAX-RPC文件。
不需要接口。JAX-RPC要求开发者在自己的服务实现中实现一个扩展了Remote的接口,现在,只需要提供一个带有@WebService注解的类。
不需要JNDI查找。客户端(比如Servlet或EJB)不再需要在自己的部署描述符(Web.xml或ejb-jar.xml)中使用<service-ref>元素来为了调用服务而查找服务,使用JAX-WS生成的带有@WebServiceRef或@WebServiceClient注解的新客户端就可以为你实现这一任务。
在常规Java SE 6 JVM中,对于着手将Web服务用于测试目的来说,使用端点是一种非常简单的方式。这降低了门槛,而且现在甚至得到了进一步的简化——过去需要对这类端点单独运行APT工具,而如今没有这个必要。
本章从提供商的角度介绍Web服务,说明如何借助不同的选项和使用不同的API来创建和发布Web服务。
7.2 组装用于部署的服务
问题
作为Web服务新手,希望组装所需的组件来创建部署。
解决方案
有三种方法,你可以:
发布javax.xml.ws.Endpoint,这是最简单的一种方法,因为你不需要创建可部署文件(比如WAR或EAR)。Endpoint API为你进行了相应考虑,并使服务在Java SE 6内置的HTTP服务器中可用。它适合于测试,而且适合于你希望允许在运行时创建和发布服务时。
编写一个装饰有JAX-WS注解@WebService的Servlet并将其发布在WAR中。包含WSDL和Schema,或者让Glassfish在部署时根据注解中的值生成它们。构造WAR,就像部署常规Web应用程序时那样。必须包含所有的常规WAR描述符,比如Web.xml。
编写一个装饰有JAX-WS注解@WebService的无状态会话EJB并将其发布在EAR中。构造EAR,就像部署常规企业应用程序时那样。
讨论
这三种方法还包含一些选项,随后的一些小节对它们进行了讨论。
Web服务或提供类
你可以对服务使用SAAJ @Provider注解来替代@WebService注解,这使你能够在原始XML层次上检查传入和发出的消息,这样,在客户端,Provider与Dispatch相对应。
接口
为无状态会话EJB编写接口是一种好的形式,它使得你的EJB成为其他企业资源可以访问的一种资源。例如,如果你希望在Web层中通过环境监听器调用EJB,则该EJB必须实现一种接口。不过,严格来说,如果只是作为服务进行调用,不是将它用作EJB组件,则没有必要为了正确部署和执行Web服务而实现接口。
同样,如果使用WAR来包装服务,严格来说,包含@WebService注解的类没有必要实际实现javax.Servlet.http.HttpServlet接口。它可以就是Web-INF下包装的一个POJO(Plain Old Java Object,简单Java对象)类。
忽略这些接口或许是或许不是你想要的。不像预期的那样定义它们似乎是一种不好的做法,可能会给其他开发人员带来混淆。另一方面,这会将它们的用途限制为纯粹的Web服务对象,不允许作为直接的Servlet或EJB进行调用。我还是推荐像预期的那样定义Servlet和组件并使用接口。
包装WSDL
你还需要决定是否希望在部署中包装WSDL。准备开始时,可能更容易让容器在部署时基于注解为你生成WSDL。如果选择自己编写WSDL,将需要单独维护它,不过这样也会得到全部的控制权。
提示: 起初,你可以试着在没有WSDL的情况下部署存档,然后让容器生成一个绝对正确的WSDL。你可以去掉不需要的条目,使它更抽象等。需要记住的是修改@WebService注解,使它包含wsdlLocation属性,该值应该类似wsdlLocation="META-INF/wsdl/My.wsdl"。
如果是在WAR中部署并希望包含自己的WSDL文件,而不是让容器在部署时为你生成WSDL,则可以将WSDL位置设置成类似wsdlLocation="Web-INF/wsdl/Some.wsdl",这应该适用于一般容器,包括Glassfish和Oracle WebLogic。
如果选择自己包含WSDL,则可以使用wsdlLocation属性修改@WebService来指定WSDL的位置,它应该指向META-INF/wsdl目录或其某个子目录。如果是在WAR中部署,必须将它放在Web-INF/wsdl中。如果已经编写Schema,还应该将这些Schema包含在其中并确保WSDL本地指向它们。容器会传送Schema位置值以使它们可以公开访问,因为在EAR中META-INF目录的内容不是可以公开访问的。
通常来说,当开始基于Java进行开发时,你不希望指定WSDL——只是允许JAX-WS为你创建WSDL。对于这一问题,Glassfish没有给出确定支持,规范指定基于Java进行开发和WSDL是可选的。不过,如果你希望指定自己的Schema(比如,因为你希望添加限制),则它是唯一方法。
部署描述符
你或许看见对部署描述符的引用,比如sun-JAX-WS.xml,这是针对Metro的部署描述符,用于描述服务端点。各个端点代表WSDL中的不同端口,包含有关绑定、实现类和可用于调用该端点的URL模式的有关信息。通常来说,可以忽略该文件,它定义的元素都与注解是一样的。在许多情况下,你可以就使用相应的注解,而不是必须创建、维护和包装一个单独的文件来包含服务部署有关的描述。例如,为了指定希望启用MTOM(Message Transmission Optimization Mechanism,消息传输优化机制),就可以对服务端点接口使用@MTOM(enable=true)注解。对于处理程序链来说也可以这样,下面是一个基本文件示例:
<?xml version="1.0" encoding="UTF-8"?>
<endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0">
<endpoint name="HelloService"
implementation="com.soacookbook.ch04.HelloServer" url-pattern="/hello" />
</endpoints>
如果使用默认的端口,该描述符将Servlet配置成作为http://localhost:8080/HelloService/hello发布。
本章的一些小节说明了如何使用其中的部分选项。
7.3 确定服务开发模型
问题
需要开始为SOA解决方案开发Web服务,但有太多的不同组件需要实现,以致于不知道从何开始。需要确定哪些内容是必须通过手工编写的,而哪些不用。
解决方案
使用规范中说明的三种基本开发模型中的一种:“从Java开始”、“从WSDL开始”或“从WSDL和Java开始”。还有一种引申开发模型,我将其称为“从Schema开始”。所选择的起点不同,你需要编写的内容不同。
讨论
在本节中,我们将讨论刚才列出的四种开发模型。
从代码(Java)开始
通过使用这种开发模型,Java开发者编写一个代表Web服务实现的Java类。实现时使用特殊的Web服务注解,你可以让Java为你生成许多其他与Web服务相关的内容,比如Schema、WSDL和部署描述符。
下面是使用代码优先方法的基本步骤:
1. 编写一个简单Java类、Servlet或EJB,使用@WebService对其进行注解。
2. 将它部署到采用JAX-WS的容器。
3. JAX-WS运行时将为你生成一个WSDL,处理Java和SOAP/XML之间的转换。
对于Java开发新手来说,这或许是最简单的方法,因为大多数手工工作是通过使用我们已经知道的工具和API来完成的。
不过,对所有Web服务内容处理得越多,尤其是当需要将自己的服务推销给他人时,你就会发现自己对所生成的默认值不是完全满意的。例如,在默认情况下,方法参数的名称是根据它们在参数列表中的索引采用arg0、arg1等这类方式生成到WSDL中的,这不能特别清晰地传达你的意图,结果是令客户混淆。
因此,第二步是当预处理程序创建WSDL和其他内容时,自定义将由Java类生成的值。例如,可以使用@WebService.serviceName属性来指定WSDL中wsdl:service元素的值。
提示: 用于创建可移植客户端方程序的代码生成工具的名称和实现不是标准的。例如,如果运行的是Glassfish,可以使用wsimport工具创建客户端;如果使用的是Axis,该工具称为WSDL2Java(或另外一种工具Java2WSDL)。
可以从命令行手工使用这些工具,或者通过编写自定义内容影响这些生成结果的创建方式,从而将代码生成自动化为构建过程的一部分。
使用标准方法自定义生成结果中的值后,可以选择接着进行外部定制,这包括修改JAXB代码如何生成客户端将使用的Java类。
这时,你可能关心Web服务的范围比常规Java类大,甚至比EJB组件还大;你希望真正开始将服务看作是服务,而不是经过装饰的Java类。或者,认识到不希望将实现细节泄露到WSDL中,有些工具会这样做。希望包含手动编写的Schema。如果打算创建一个规范数据模型来代表组织中的常见类型,这最后一点是特别重要的。
不是给定编程语言中的所有数据类型都映射到XML中的可互操作类型。仔细观察所用的工具,确保它们没有引入特定于实现的映射或依赖。例如,考察Apache Axis 1.2附带的Java2WSDL工具,它从Java服务实现创建WSDL,但引入Apache SOAP命名空间中的自定义类型来表示图像数据、八位字节流和一些其他信息。生成的WSDL如下所示:
<wsdl:definitions targetNamespace="http://example.org/ns";
xmlns:apachesoap="http://xml.apache.org/xml-soap";
//...
<element name="anImage">
<complexType>
<sequence>
<element name="image" type="apachesoap:Image">
<sequence>
<complexType>
</element>
你不希望这类实现细节出现在WSDL中,尤其是不需要时。
从代码开始的另一个问题是WSDL中的类型比你所希望的强很多或弱很多。虽然强类型被看作是Java类语言的一项功能,但你必须考虑在SOA中要实现的目的。有许多流行的动态类型语言(Ruby、Python等),不能盲目地假定采用最强的可接受类型永远是最好的。服务不是“Java Program++”——它是服务,Java平台只是提供一种方式来创建它。但另一方面,不希望放弃有利控制。如果服务实际需要长度为12的数组,则确保WSDL中给出了该约束。生成的WSDL通常掩盖这类细节,结果是得到的WSDL接受任何类型的数组。
此外,你可以决定Web服务的核心是基于一个独立于实现的合同与外部客户端进行互操作。在Web服务中,该合同就是WSDL。作为开发者/架构师,与其事后生成WSDL,或许你更应该将它作为焦点。另外,该方法受到众多批评。在Java Web服务早期,它被认为是值得质疑的。
对“从Java开始”方法的大众看法
在“从Java开始”模型的发展过程中,由于一些不同的原因,它遭受了众多批评。
早期的Web服务主要是使用RPC/encoded方式实现的,因为SOAP编码模式会从服务提供商数据结构直接生成XML模型并复制它们以供客户端应用程序使用。这是有利的,使得RPC/encoded成为一种流行方法。但是,它也意味着客户端与SOAP编码本身紧密耦合,使得当数据结构发生改变时,客户端需要重新生成有关代码。而且,SOAP编码是一种序列化机制;因为它是由SOAP本身驱动的,在这类协议的外部很少或不使用它。验证不可能使用该模式,得到的数据结构是不可转换的。
当今的Web服务应用程序采用数据绑定,数据绑定是作为某种数据结构(比如Java Bean)的程序化表示和XML表示之间的一种间接层。数据绑定层由Castor、BEA的XMLBeans或JAXB(它们都是Java-XML绑定技术)实现,用于将这些表示隔离开,使得开发者可以独立控制这些表示,从而最大限度地使用各种表示。
总地来说,新的Java Web服务规范,包括JAXB (JSR 222)和JAX-WS 2.1 (JSR 224),轻易地解决了这些要求。因此,过去应该谨慎使用该方法的一些原因已不复存在。
这样一些异议导致开发者改为从另一个方向开始:手动编写WSDL并生成服务,我们将在下一节中对此进行讨论。
从合同(WSDL)开始
对于基于SOAP的服务来说,WSDL表示合同。该模型具有一个至关重要的样式:WSDL定义服务合同的主要功能,独立于实现的合同是SOA的一种不可或缺的必需内容。合同不是一种事后添加的内容,而是一种核心内容。通过这种模型,你可以自己手动编写WSDL,为服务实现生成Java代码外形,然后为服务实现填充业务逻辑。这种方法有时也称为“合同优先”。
开始时可以采用一个先前存在的抽象WSDL并将其指向一种工具(比如wsimport),来生成服务端点接口以及代表WSDL中指定的Schema定义和消息部分的Java类。生成端点接口后,编写一个实现该接口的Java类。基本步骤如下所示:
1. 编写一个WSDL来表示希望部署的服务。
2. 使用wsimport生成客户端代码。其中,将为各个portType生成服务接口。
3. 通过编写Web服务实现类来实现各个接口。这意味着实现如下所示内容:
@WebService
(endpointInterface="com.soacookbook.MyGeneratedInterface",
wsdlLocation="/META-INF/wsdl/MyWsdl.wsdl")
public class MyService implements MyGeneratedInterface { ... }
4. 将服务端点实现部署到符合JAX-WS规范的容器。
在这个示例中,端点接口是作为wsimport生成的接口的完全限定名称指定的,写作一个字符串。WSDL位置属性允许你指定相对或绝对URI。如果是相对URI,它必须位于META-INF/wsdl。
该方法的一个好处是如果你所创建的服务实现实际不符合WSDL,供应商实现必须通知你。
为了完善实现,可以使用JSR-181(Web服务元数据)注解来指定WSDL的具体方面,比如协议绑定。
由于Web服务的目的是开发与实现依赖无关的强有力合同,这似乎像是一个好方法。尽管在理论上它非常吸引人,但对于许多Java开发者来说,却是一种难以开始的方法。如果你对WSDL没什么经验,考虑以手动方式编写WSDL是令人畏缩的。不是要创建简单的文档,尤其是需要具有大量有关Web服务内在工作方式的知识来理解所做选择的含义。这部分是由于Java IDE都非常强大,单击某个按钮就可以轻松地执行复杂代码重构。另一方面,以任何方式都很难获得可以执行更加基本XML Schema或WSDL重构的工具。
因此,虽然“从合同开始”方法可能非常非常强大,但Java开发者通常会避开使用该方法,因为其核心竞争力在于Java,而不是WSDL。不过,尽管WSDL复杂,或许极度复杂,但它绝对是可以学习的。一个给定的WSDL实例文档本身就是复杂的,因为选项(比如参数样式和使用)改变了文档的结构功能。不过,在概念上,WSDL将只影响一些基本的构造,这使得它相对容易阅读和编写。
另一方面,XML Schema几乎像许多编程语言一样丰富和具有表现力。在作为Web服务可以轻松进行互操作的接口定义中,各种高级功能没有得到充分发挥。Schema流畅是该方法的一个重要先决条件。
警告: 处理当今的Web服务时,需要依赖大量的代码生成,而无论你是决定生成Schema和WSDL还是Java。不过,这类生成的底线是应该永远不要生成自己不理解的内容。进行代码生成是非常不错的,可以节省大量的时间和精力,但是,如果生成不能理解的内容,可能会导致许多问题。
通常听说术语“合同”用于单独指WSDL。我和其他一些人认为“合同”不仅包含WSDL中的所有要素,比如数据类型、操作、绑定和策略,而且还包含WSDL外部的条目。对于服务来说,特别是SOA,服务合同不仅是WSDL,而且还可以包含一些条目,比如策略、服务水平协议、用户验证以及企业注册/存储库中出现的其他条目。
实际方面,企业会从项目中早期发布的WSDL受益,使得客户端开发团队和服务器端开发团队可以独立工作。
从WSDL开始的另一个重要好处是XML Schema提供的数据类型比许多编程语言要丰富很多。如果从WSDL开始,你可以使用一组类型选项,从而最大化互操作性。
从Java和WSDL开始
该方法采用现实的中间线路。如果你拥有的成熟组织包含SOA打算互连的各种系统,就会发现自己需要在那些提供自身API和Schema集的传统系统之上实现Web服务。服务实现Java类可以使用注解(比如@WebMethod.operationName)来将WSDL中定义的操作与该类中的某个特定方法相关联。
警告: “从WSDL和Java开始”模型(JSR 181对其进行了概述)是可选的。由于没有要求供应商支持该开发模型,使用它会限制便携性。
可以证明该方法非常难维护,因为一旦使用有关工具生成了类,还需要手动调整各种资源。与此不同的是,“从Java开始”方法在开发过程中赋予你很大的灵活性,因为你可以几乎忘掉所使用的WSDL和Schema,而关注业务逻辑。当使用“从WSDL开始”方法时,可以只关注合同。但如果是从Java和WSDL开始,就需要你关注多个活动部分。
从Schema开始
这是一种用得较少的解决方案,不过我曾经成功使用过它。借助该模型,你不是从WSDL或Java开始,而是通过在XML Schema中定义业务文档来开始。从WSDL或Java开始将向你提供一个更面向过程的视图:首先编写操作或方法,然后从动词开始。当从Schema开始时,没有动词,只有名词,因此你更关注服务必须交换的业务文档。下面是涉及到的几个步骤:
1. 编写一组XML Schema来代表业务模型需要的数据类型。
2. 使用JAXB基于这些Schema生成Java类。
3. 以Java语言创建带有注解的服务端点实现,该实现以方法签名的方式引用JAXB生成的类型。如果“bare”样式采用document/literal,各种类型在服务中将只使用一次。如果使用某种封装样式,将需要创建封装元素。
4. 包装和部署应用程序,让工具生成WSDL。
5. 在根据Schema进行编组过程中执行运行时验证。
借助“从Schema开始”视图,你的SOA会变得更多关注文档交换,较少关注执行远程过程调用,从长远来说,这对于构建服务编排是有利的。该方法鼓励你主要关注业务文档,比如Invoice、Purchase Order、Customer和Credit Application。接着,服务成为这类文档的一个入口,用作可能有点复杂的企业工作流的外观。
“从Schema开始”方法能够与style和use的组合形式document/literal一起很好工作。像从Java开始一样,RPC以微妙的方式引领你从参数列表角度考虑服务。对于企业SOA环境来说,这通常是不合适的,因为它不是面向业务文档的,结果是得到的服务不太可能重用。
当然,该方法也有不足。其中之一是增加了复杂性。你无法依赖某些不仅已经习惯而且在Java代码中绝对会使用的结构,比如toString、equals和hashcode实现(没有使用第三方工具进行定制)。实际上,如果使用该方法,为了确保不受工具默认值支配,使用数据绑定定制将变得很重要。
该方法的另一处危险是容易出现紧密耦合。通过将Schema放在最前方,在组合的应用程序中,如果服务共享根Schema,则它们可能会出现紧密耦合。这类紧密耦合是服务首先想要抗击的部分问题。规范数据模型在SOA中非常重要,但是必须要正确处理它们。有关规范数据模型的更多讨论,请参阅2.3小节。
提示: 如果你没有确保在WSDL中引用源Schema并在服务部署中包装它们,服务仍将部署和工作,这取决于容器。但是,你可能会遭受某种手法,结果实际是基于JAXB从你的Schema生成的Java Bean来引用部署时生成的Schema,这意味着会失去已经写入原始Schema的任何基于Schema的验证限制。如果希望保留XSD限制,必须通过服务部署包装它们。此处另一个有用窍门是使用XML目录(请参阅3.13小节),这样可以在WSDL中放置占位符Schema位置,然后在部署中将其替换。
从“Schema开始”的一个优点是你可以保持对XML的无障碍关注。可以验证原始XML格式的数据,不会忽略自己互操作性方面的目标。在SOA中,从长远来说,让代码尽可能常规化会是非常有益的。“从Java开始”的结果不错,只要你不是只因为该方法更舒服而使用它。也需要小心使用“从Java开始”方法,因为它会导致无意识地依赖编程语言功能,使得你陷入使用高级Java结构,比如泛型。
在这里,你还可以利用一些工具,比如WebLogic Workshop,这样就不需要每次构建时完成整个重新生成过程。不过,你不必依赖任何IED工具;编写完Schema后,就可以立即在一个build中通过单元测试来执行所有其他步骤。
提示: 如果使用的是WebLogic 9.2或10gR3,此处可以用XMLBeans替换JAXB。
注意,使用上面任何一种“从...开始”方法最后都可以创建同样的服务,不同之处主要是一些细微差别和样式,而且你所处的开发环境、SOA成熟度以及组织的最终SOA目标对它们都会产生影响。
7.4 选择编码、使用和参数样式
问题
需要为Web服务确定最佳编码样式、参数样式和使用值。
解决方案
参阅下面的讨论,了解不同组合的优缺点。
讨论
为了指定用于与服务进行通信的传输机制,Web服务定义被映射到WSDL绑定。考虑下面的WSDL绑定:
<binding name="CartEJBPortBinding" type="tns:CartEJB">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http" style="document"></soap:binding>
<operation name="getVersion">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal"></soap:body>
</input>
<output>
<soap:body use="literal"></soap:body>
</output>
</operation>
</binding>
此处有三个条目在工作,两个是显式的,一个是隐式的:
SOAP绑定样式(“document”)
SOAP绑定使用(“literal”)
SOAP绑定参数样式(“wrapped”)
两个显式条目是样式和使用,它们分别是在soap:binding style和soap:body use属性中指定。我们稍后再讨论参数样式。
样式
soap:binding元素的style属性的值可以是“rpc”或“document”。“rpc”值表明希望使用包含参数和返回值的SOAP消息。按照这种方式,Web服务将模拟RMI调用。“document”值表明希望SOAP消息使用XML文档实例,“Document”是默认值。
JAX-WS替代了JAX-RPC技术,JAX-RPC这一较老技术名称给出了其样式。许多Oracle的WebLogic Web服务实现和文档就假定你使用的是RPC样式,这样是部分由于供应商更容易实现RPC,而且RPC的使用对于开发者来说也是较为熟悉和简单的。据我所交谈的一位Oracle Web服务团队管理人员透露,这还在于客户的许多JAX-RPC Web服务在该领域已经处于落后,需要实现众多有关支持。不过,近些年来,相关实现已经变得较为复杂,开发者也较为适应,该行业已经远远超越RPC而寻求新的目标,改为选择文档。文档比RPC的互操作性更强,而且更以消息为中心。
RPC样式。在基于RPC的服务中,客户端与服务进行交互时,服务被看作是单个集成应用程序,该应用程序包含一些你可以向其传递参数的方法。消息直接映射到输入和输出。
RPC样式的SOAP请求将消息参数封装在当前操作后命名的一个元素中。默认情况下,RPC响应封装在操作后命名的带有“Response”的一个元素中。
Document样式。在基于文档的服务中,客户端使用XML文档实例基于代表完整业务实体(比如Author、Order、Invoice或Product)的Schema来与服务进行交互。XML文档实例是自描述性的,为你提供了最大限度的互操作性。
Document样式本质上意味着各个消息部分将使用其元素属性来引用具体的Schema类型,该Schema完全描述SOAP请求和响应,不需要引入外部编码规则。
与RPC样式的服务相比,Document样式的服务更难创建,这是因为你通常需要创建Schema,需要相应地转换思维,将自己的服务看作是服务,而不单是作为对碰巧使用SOAP的一组现有方法的封装。
由于文档消息代表完整的自描述实体,而不是方法参数,它们是作为SOAP消息载荷呈现给你的,因此,在处理方面,文档样式提供了更大的灵活性。可以将传入载荷创建为完整的XML文档并对其运行XSL转换,根据某个Schema对其进行验证,或者按照你喜欢的任何方式对它进行手动处理。
对于文档样式来说,维护状态较为轻松,因为只有一个实体而不是一组实体需要管理。
使用
soap:body元素的use属性指定SOAP消息的主体内容按照何种方式被序列化到XML,可能的值是“literal”或“encoded”。“literal”表示消息将作为XML文档进行序列化,而“encoded”表示将对消息进行默认的SOAP编码。
样式和使用组合
在下面的几个小节中,让我们来看看各种隐含的样式和使用组合。
RPC/encoded
RPC/encoded的唯一长处是:用这种格式编写的WSDL是最简单的和最易阅读的。
其他都是不好的消息。WS-I建议soap:body只有一个子元素,RPC/encoded违反了这一建议,允许SOAP主体元素有多个子元素。使用该样式意味着将通过“href”元素对复杂类型进行SOAP编码和引用,而这些“href”元素指向操作封装元素外部的multiref元素,这使得消息难于阅读和理解。
RPC/encoded绑定必须为各个参数维护类型编码信息,从而对每次调用增加了不必要的开销。在所有可能的样式中,这种形式的样式的性能最差。
提示: RPC/encoded不符合WS-I Basic Profile 1.1,因此,对于创建希望与其他平台实现的客户端或服务进行互操作的Web服务来说,它不是一种好的候选方法。
借助RPC/encoded,SOAP编码通常用于序列化复杂类型,致使你失去XML Schema的较为具体和复杂的功能。
RPC/literal
这是一个不可思议的选项。采用RPC/literal组合会阻止你执行基于Schema的验证,它是比较复杂的,因为不仅需要处理RPC元素命名惯例,而且还需要处理WSDL命名空间。
RPC/literal不具备document/literal所具有的优点,但与RPC/encoded相比,它的长处是符合WS-I。尽管如此,Microsoft只对该样式提供了有限支持,因此它很少使用。
document/literal
对于确保当前服务与其他平台编写的服务和客户端之间的互操作性来说,这是最佳选项。可以使用任何XML验证器来验证消息的内容。采用document/literal后,操作类型是作为Schema中声明的复杂类型进行编写的。
注意,有意思的是.NET最初只支持document/literal。在SOAP消息内部,document/literal废除了操作名称。RPC关注方法调用,而文档关注所发送的文档实例。
document/encoded
document/encoded组合已经废弃了,没有人使用它,即使在JAX-RPC早期,该模式也不被支持。假装它不存在就好。
使用参数样式
在JAX-WS中,指定参数样式时有两种选择:wrapped或bare,该参数样式说明参数的封装方式。WSDL本身没有给出有关区别,该选项只与document/literal样式的服务有关。如下小节讨论了有关区别。
wrapped。wrapped参数样式用于指定<soap:body>的子元素将作为操作的封装元素。作为一种封装,它不是载荷的一部分。此处的优点是在解组过程中,SOAP引擎将具有清晰、简单的命名惯例。
通过使用在外部定义了Schema的document/literal wrapped样式的服务,你就最有可能来确保互操作性以及利用Schema限制的内在优势。document/literal wrapped样式的主要不足是通常导致WSDL非常复杂。
不过,还有另外一个问题:如果你计划使用document/literal wrapped,那么,在XML Schema中,封装元素的内容类型必须指定为sequence,用于表示其组成元素是有顺序的,这对于在Java中为方法签名建模来说是非常关键的,因为这时参数顺序很重要。
如果希望元素是可选的,需要在Schema中将其声明为nillable。
使用document/literal wrapped后,WSDL如下所示:
<message name="authorSearch">
<part name="parameters" element="tns:authorSearch"></part>
</message>
<message name="authorSearchResponse">
<part name="parameters" element="tns:authorSearchResponse"></part>
</message>
<portType name="Catalog">...</portType>
bare。bare(非封装)参数样式用于指定服务操作将接收完整的XML文档实例。使用bare样式的明显不足是由于传递的是整个XML文档实例而且先前使用了任意操作名称,因此你只有“一发子弹”,也就是说,只能为每个服务定义一个方法来接受特殊文档类型,操作也限于定义单个参数。
假定你要使用document/literal bare定义一个名为“authorSearch”的服务操作,操作将接受符合Schema中Author类型的一个Author对象,然后搜索该作者编写的书籍。
但是,如果希望在同一服务中定义一个操作来向数据库添加作者,会怎样?就会陷入困境,因为你没有指定操作名称。运行时必须根据将所传递的参数与操作签名相匹配来选择要执行的操作。如果具有一个add(Author a)操作和一个search(Author a)操作,则运行时不知道你想要执行哪一个。在调用wsimport过程中,会接收到如下错误:
[ERROR] Non unique body parts! In a port, as per BP 1.1 R2710 operations must have
unique operation signaure on the wire for successful dispatch. In port CatalogPort,
Operations "authorSearch" and "add" have the same request body block
{http://ns.soacookbook.com/catalog}author. Try running wsimport with
-extension switch, runtime will try to dispatch using SOAPAction
line 70 of http://localhost:8080/CatalogService/Catalog?wsdl
遵循错误消息中给出的建议,看看它是否有帮助。将wsimport任务属性扩展设置为true,然后重新编译。这使得你执行导入,从而获得进一步的信息:
Testcase: searchByAuthorTest(com.soacookbook.catalog.test.CatalogTest):
Caused an ERROR
Cannot find dispatch method for Request=[SOAPAction="",
Payload={http://ns.soacookbook.com/catalog}author]
javax.xml.ws.soap.SOAPFaultException: Cannot find dispatch method
for Request=[SOAPAction="",Payload={http://ns.soacookbook.com/catalog}author]
因此,要使它工作需要设置SOAPAction头,在JAX-WS实现中,该头是可选的,而且只适用于HTTP是传输层时,这与首先指定bare这一目的相悖。虽然它赋予你一种非常松散耦合的感觉(这当然是一种目标),但如果计划执行CRUD操作或清晰地给出自己的意图,bare是一个需要慎重对待的选项。
当然,这也有一个巧妙的解决办法。如果要拥有两个接受单个Author参数的操作的唯一条件是每个元素必须用Schema中的不同元素来表示,就可以定义另外一个元素,它具有不同的名称,但碰巧具有完全相同的类型:
<xs:Schema xmlns:tns="http://ns.soacookbook.com/catalog" ...>
<xs:element name="searchAuthor" nillable="true" type="tns:Author"></xs:element>
<xs:element name="addAuthor" nillable="true" type="tns:Author"></xs:element>
<xs:complexType name="Author">
<xs:sequence>
<xs:element name="firstName" type="xs:string"></xs:element>
...
</xs:sequence>
</xs:complexType>
在我看来,这是一种苦工,不建议这样做,不过它不失是一种方法。
我有几分喜欢bare样式提供的纯粹理论,没有必要完全将其排除。当从长远来看,wrapped可能更具优势。
使用document/literal bare后,WSDL如下所示:
<message name="authorSearch">
<part xmlns:ns4="http://ns.soacookbook.com/catalog" name="author"
element="ns4:author"></part></message>
<message name="authorSearchResponse">
<part xmlns:ns5="http://ns.soacookbook.com/catalog" name="searchResults"
element="ns5:searchResults"></part></message>
<portType name="Catalog">...</portType>
对于这两种样式来说,wsdl:portType的内容是相同的。
小结
值得注意的是,指定样式和使用组合是一个配置问题。它将改变服务实现的语义,你将能够确定WSDL中的差异,但整体可用性应该对开发者透明;它没有改变服务的工作方式。
主要经验是:根据你所指定的组合,互操作性可能会增强,也可能会减弱,主要是因为指定的组合会影响Schema。为了互操作性和验证,必要时,我建议你不要依赖工具来生成Schema,而是基于Schema来设计服务。这有助于你将SOA看作是通过文档交换来实现企业集成的一种结构,从而鼓励你将各个服务实现的重点放在其核心业务逻辑上,而将装饰性业务逻辑移到编排中。
7.5 基于Java服务端点实现生成WSDL和可移植结果
问题
希望从服务实现类开始生成WSDL和可移植Web服务结果(一些基于该WSDL的类)。
解决方案
使用JAX-WS包含的wsgen命令行工具。
讨论
创建一个带有@WebService注解的Java类,然后执行Java附带的wsgen工具。它将为你生成如下可移植结果:
一个WSDL文件,如果你指定使用wsdl选项。如果明确指定该选项,wsgen工具将只生成一个WSDL。
编组和解组服务定义中所交换的消息时所需的JAXB类。
wsgen只需要一个参数:实现Web服务的Java类的名称。还可以指定各种选项,该工具的使用形式是:
$ wsgen [options] service-implementation-class
表7-1给出了wsgen的可用选项。
表7-1:wsgen工具的选项
选项 作用
-classpath <path>或 指定在什么位置查找输入类文件
-cp <path>
-d <directory> 指定何处放置所生成的输出文件。运行wsgen之前,该目录必须存在
-extension 指定是否允许供应商扩展
-help 显示有关使用信息
-keep 指定是否保留所生成的文件
-r <directory> 指定源文件(比如WSDL)的目标目录,该选项只有在使用了-wsdl选项时才能使用
-s <directory> 指定何处放置所生成的Java源文件
-verbose 提供有关编译器当前所做操作的输出信息
-version 打印版本信息,例如JAX-WS RI 2.1.3-hudson-390-
-wsdl[:protocol] 指出希望wsgen生成WSDL文件。协议是可选的,有效协议是soap1.1(默认协议)和Xsoap1.2(它不是标准的)。该选项只有在使用了extension选项时才能使用
-servicename <name> 指定所生成的WSDL中使用的服务名称。该选项只有在使用了-wsdl选项时才能使用
-servicename <name> 指定所生成的WSDL中使用的端口名称。该选项只有在使用了-wsdl选项时才能使用
提示: 使用扩展(功能不是由规范明确给定)可能会导致应用程序不可移植或者不能与其他实现进行互操作。
下面是一个示例。在下面的类中,有一个Web服务定义,我们将为其创建一个WSDL:
package com.soacookbook;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
/**
* Used to manually run the wsgen tool against.
*/
@WebService(targetNamespace="http://ns.soacookbook.com",
name="GenCatalog", serviceName="GenCatalog")
public class CatalogToGen {
@WebMethod
@WebResult(name="title")
public String getTitle(
@WebParam(name="id") String id) {
return "King Lear";
}
}
假设这是源文件,我们将从存储顶级包的目录按照如下形式编译它:
javac com/soacookbook/CatalogToGen.java
接下来,可以对该类运行wsgen工具来生成WSDL、Schema和相应的JAX-WS结果:
$ wsgen -verbose -cp . -wsdl -servicename {http://ns.com/}Catalog \
-keep -r gen -s gen com.soacookbook.CatalogToGen
Note: ap round: 1
[ProcessedMethods Class: com.soacookbook.CatalogToGen]
[should process method: getTitle hasWebMethods: true ]
[endpointReferencesInterface: false]
[declaring class has WebSevice: true]
[returning: true]
[WrapperGen - method: getTitle(java.lang.String)]
[method.getDeclaringType(): com.soacookbook.CatalogToGen]
[requestWrapper: com.soacookbook.jaxws.GetTitle]
[ProcessedMethods Class: java.lang.Object]
com\soacookbook\jaxws\GetTitle.java
com\soacookbook\jaxws\GetTitleResponse.java
Note: ap round: 2
你看到的输出是运行带有verbose选项的wsgen命令的结果。由于使用了keep选项,编译后,所生成的Java源文件不会删除。该工具生成两个类,GetTitle代表请求消息,GetTitleResponse代表响应消息,并将它们放在所用的包名中,前面添加有jaxws。这些类放在名为gen的目录中,这是通过s选项指定的。
该工具还创建了两个WSDL。第一个是抽象WSDL,只包含<messages>、<types>元素(它从所生成的Schema导入所有类型)和<portType>元素。根据命令中-r选项的值,这些WSDL被放在gen目录中。在此处,名为GenCatalog.wsdl的抽象WSDL如下所示:
<definitions targetNamespace="http://ns.soacookbook.com"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://ns.soacookbook.com"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<types>
<xsd:Schema>
<xsd:import namespace="http://ns.soacookbook.com"
SchemaLocation="GenCatalog_Schema1.xsd"/>
</xsd:Schema>
</types>
<message name="getTitle">
<part name="parameters" element="tns:getTitle"/>
</message>
<message name="getTitleResponse">
<part name="parameters" element="tns:getTitleResponse"/>
</message>
<portType name="GenCatalog">
<operation name="getTitle">
<input message="tns:getTitle"/>
<output message="tns:getTitleResponse"/>
</operation>
</portType>
</definitions>
当然,该抽象WSDL本身不足以运行服务。还需要一个具体WSDL,该工具也生成了这一WSDL,如下所示:
<definitions targetNamespace="http://ns.com/" name="Catalog"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://ns.com/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/">
<import namespace="http://ns.soacookbook.com" location="GenCatalog.wsdl"/>
<binding name="GenCatalogPortBinding"
type="ns1:GenCatalog" xmlns:ns1="http://ns.soacookbook.com">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"/>
<operation name="getTitle">
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<service name="Catalog">
<port name="GenCatalogPort" binding="tns:GenCatalogPortBinding">
<soap:address location="REPLACE_WITH_ACTUAL_URL"/>
</port>
</service>
</definitions>
在上面的这个WSDL中,有一些有意思的内容。首先,它接受servicename选项花括号之间指定的目标命名空间,保存该名称以供<definitions>元素使用。接下来,导入抽象WSDL,该技术类似于从实现分离Java接口。最后,没有指定SOAP地址位置,需要使用一个URI值来代替。
注意,必须是对已经编译的Java类(而不是源程序)运行wsgen,而且如果指定该工具应该将所生成的文件(比如WSDL)放在一个目录中,那么,在运行程序之前,该目录必须存在。另外,必须是对一个实现类而不是接口运行该工具。
使用wsgen Ant任务
如果希望将wsgen作为Ant构建的一部分来运行,可在常规位置对该工具使用Ant任务封装器,它具有如下结构:
<wsgen
sei="..."
destdir="directory for generated class files"
classpath="classpath" | cp="classpath"
resourcedestdir="directory for generated resource files such as WSDLs"
sourcedestdir="directory for generated source files"
keep="true|false"
verbose="true|false"
genwsdl="true|false"
protocol="soap1.1|Xsoap1.2"
servicename="..."
portname="...">
extension="true|false"
<classpath refid="..."/>
</wsgen>
这一任务反映的是该命令行工具,因此,除了提及嵌套的类路径元素是一个常规Ant路径外,我将不对其做出进一步的解释。为了使用<wsgen>任务,需要在Ant构建脚本中指向实现类:
<taskdef name="wsgen" classname="com.sun.tools.ws.ant.WsGen">
<classpath path="jaxws.classpath"/>
</taskdef>
在Maven 2中使用wsgen
如果希望将wsgen作为Maven 2构建的一部分来使用,可以从http://mojo.codehaus.org/jaxws-maven-plugin/wsgen-mojo.html获取相应的插件。在generate-sources阶段,它将自动执行。
7.6 创建基本的Web服务
问题
希望在Java EE 5中创建一个基本但实际的Web服务。
解决方案
创建一个EJB或Servlet并用@WebService对其进行注解,然后像往常那样对其进行包装和部署,部署时,JAX-WS将负责创建WSDL和必要的映射。
下面是通过EJB解决该问题最简单的可行方法:
package com.soacookbook.catalog.ejb;
import com.soacookbook.ns.catalog.*;
import javax.ejb.*;
import javax.jws.*;
/**
* Basic Web Service does shopping cart operations.
*/
@WebService
@Stateless
@Local
public class CartEJB {
public Double getVersion() {
return 5.0;
}
}
上面足以创建一个接受默认映射的Web服务。检查一下这些映射的内容是很重要的。在后面的小节中,我们将为生成的WSDL定制映射,使得元素的名称更加友好。
注意,该bean没有实现业务接口。在实际设计中,这或许是你希望实现的内容,因为它会让你的bean可用于其他企业流程(比如基于Web的环境监听器),但从EJB 3起,这不再是必须的。
有关如何在Java中创建和调用最简单的Web服务,请参阅4.5小节。
讨论
让我们来看看WSDL的各个重要方面,了解一下它是如何映射的。在当前阶段,这似乎不重要,因为可以使用wsdimport工具生成调用服务时所需的有关内容,不过,以后在处理编排时能够这样做是非常重要的,而且,了解一些内在情况是比较好的,以防事情突变。
提示: 在本讨论中,我将使用限定名称wsdl:<elementName>来引用名称空间xmlns="http://Schemas.xmlsoap.org/wsdl/"中的WSDL元素。默认情况下,JAX-WS生成的WSDL没有限定这些元素,从而使得WSDL是默认的命名空间。为了清楚起见,我使用了前缀,尽管你的WSDL文档没有按照这种方式进行限定。例如,在这里,wsdl:service引用将作为服务出现在所生成的WSDL中。
包和类型
注意,定义所需服务的EJB包含com.soacookbook.catalog.ejb包。如果没有进行定制,JAX-WS将为导入的类型生成一个与包名相匹配的命名空间:
<types>
<xsd:Schema>
<xsd:import namespace="http://ejb.catalog.soacookbook.com/"
SchemaLocation="http://localhost:8080/CartEJBService/CartEJB?xsd=1">
</xsd:import>
</xsd:Schema>
</types>
它还生成一个Schema文档并将其放在SchemaLocation属性指定的位置。与IBM的WebSphere中的实现(该实现在WSDL中生成内嵌类型)不同,Glassfish按照最佳惯例将Schema编写到一个外部文件中并将其导入WSDL,从而让该WSDL保持为抽象WSDL。生成的Schema如下所示:
<xs:Schema xmlns:tns="http://ejb.catalog.soacookbook.com/"
xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0"
targetNamespace="http://ejb.catalog.soacookbook.com/">
<xs:element name="getVersion" type="tns:getVersion">
</xs:element>
<xs:element name="getVersionResponse" type="tns:getVersionResponse">
</xs:element>
<xs:complexType name="getVersion">
<xs:sequence></xs:sequence>
</xs:complexType>
<xs:complexType name="getVersionResponse">
<xs:sequence>
<xs:element name="return" type="xs:double" minOccurs="0"></xs:element>
</xs:sequence>
</xs:complexType>
</xs:Schema>
这个Schema定义了一个与定义服务时所用包名相匹配的目标命名空间,它包含两个元素:getVersion和getVersionResponse。getVersion元素是空的,因为该操作不带有参数。响应是在该Java方法后命名的,带有后缀“Response”。
类和服务
实现服务端点接口(SEI)的Java类在本例中是CartEJB,它映射到wsdl:service元素(如果回头看一下WSDL,wsdl:service元素位于底部)。
<service name="CartEJBService">
<port name="CartEJBPort" binding="tns:CartEJBPortBinding">
<soap:address location="http://localhost:8080/CartEJBService/CartEJB"></soap:address>
</port>
</service>
默认情况下,服务的名称是在SEI的名称(“CartEJB”)后加上“Service”。
提示: 在这个例子中,你暴露了EJB的内在实现,因为采用了命名惯例。一种好的做法是隐藏这些内容,通过使用@WebService注解的serviceName元素,将这些内容对客户端隐藏起来。
在WSDL 1.1中,服务是端口元素的集合。端口元素定义用来连接到Web服务的URI。
类和PortType
将wsdl:portType看作是类,而将其wsdl:operation子元素看作是方法:
<portType name="CartEJB">
<operation name="getVersion">
<input message="tns:getVersion"/>
<output message="tns:getVersionResponse"/>
</operation>
</portType>
Java服务中的各个方法是在portType中定义的,wsdl:operation指定服务可以执行的抽象操作,不过,每个操作还重复作为wsdl:binding的子元素(我们将在下一小节讨论)。可以看出,默认情况下,getVersion方法名称被用作WSDL操作名称。有关soapAction属性的更多细节,请参阅7.22小节。
该操作的输入用于指定方法参数,输出元素指定返回类型。
方法和绑定
Java服务中的方法是作为WSDL中的操作进行定义的。WSDL必须描述当前服务的实际调用方式,它是在wsdl:binding元素中给出有关描述的。wsdl:portType中定义的operation告诉你可以实现的操作,而wsdl:binding元素的operation子元素告诉你必须如何实现操作。
由于生成这个WSDL时,没有添加绑定定制,因此,JAX-WS选择了SOAP over HTTP默认绑定。其他绑定,比如REST over HTTP或SOAP over JMS,也是可以的。
所生成的WSDL的有关部分如下所示:
<binding name="CartEJBPortBinding" type="tns:CartEJB">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http" style="document">
</soap:binding>
<operation name="getVersion">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal"></soap:body>
</input>
<output>
<soap:body use="literal"></soap:body>
</output>
</operation>
</binding>
该绑定元素还给出了服务采用何种SOAP编码样式和使用。有关选择相应编码样式、参数样式和使用的更多讨论,请参阅7.4小节。
可以从如下位置获得没有使用定制的WSDL:http://localhost:8080/CartEJBService/CartEJB?wsdl。
提示: 在一个服务URL的后面使用?wsdl(或?WSDL——不区分大小写)只是一种惯例;大多数应用服务器——包括Glassfish、ebLogic 10和WebSphere 6.1——都按这种方式提供WSDL,不过不是必须要这样。如果就像我们此处所做的那样,让JAX-WS为你生成WSDL,那么,通过wsimport创建的客户端将指向这个远程WSDL。
7.7 指定命名空间
问题
你是从Java开始,希望在所生成的WSDL中指定服务应该定义的命名空间,因为你不愿意使用默认命名空间。
解决方案
使用@WebService注解的targetNamespace属性来指定Web服务自身的命名空间。
讨论
targetNamespace属性是可选的。如果没有使用该属性,服务将被指定一个命名空间,该命名空间是将实现类所处包名进行倒置。例如,如果类位于包com.soacookbook.ch04,则服务的默认命名空间将是http://ch04.soacookbook.com/。该属性使得你可以用自己喜欢的内容覆盖默认命名空间。
还可以对服务相关的其他条目使用targetNamespace属性。@WebResult和@WebParam注解都带有targetNamespace参数。它们的参数既可以来自原始Schema,也可以让Schema表示来生成,因此,这两个注解的命名空间没有必要相匹配,也没有必要与服务命名空间相匹配。
如果没有为Web参数或Web结果指定命名空间,它们将继承服务的目标命名空间。
如果希望指定空的命名空间,可以使用@WebParam(targetNamespace="")。
提示: WSDL和Schema使用不同的命名空间是一种好的做法。
7.8 创建Web服务操作
问题
希望定义Web服务操作。
解决方案
只需要向定义@WebService的服务实现类添加一个public方法,它将自动被添加到所生成的WSDL。
添加到带有注解的服务类的任何public方法都将被自动添加到WSDL操作;如果希望接受默认映射,就不必再做任何其他事情。
讨论
可以明确地将一个Java方法指定为服务操作,方式是使用@WebMethod注解对其进行注解。不过,该注解还允许你指定一些属性,来说明该操作在WSDL中的表示方式及其行为方式。表7-2列出了这些属性。
表7-2:@WebMethod属性
属性 作用
operationName 定制从Java开始或映射到现有WSDL时wsdl:operation的值
action 在SOAP中,设定操作的值
exclude 指出你明确希望该方法不表示为WSDL中的操作。对端点接口这样 指定是非法的。如果指定了该属性,则指定@WebMethod的任何其 他属性都是非法的
提示: 如果对Java类中的一个方法使用@WebMethod注解,则必须对希望在服务定义中包含的所有其他方法进行明确设置。否则,JAX-WS认为你打算排除它们,不会将这些方法添加到WSDL。
在“从Java开始”方法中,@WebMethod注解用于指定在WSDL中应该生成什么样的操作名;如果没有使用该注解,操作名将与方法名相匹配。
如果使用的是“从WSDL开始”方法,注解的operationName属性使你有机会按照自己喜欢的方式调用Java方法,不过不能将方法映射到WSDL操作。
7.9 指定Web服务消息部分
问题
希望指定从方法参数名到wsdl:part名的映射,而不是使用默认值。
解决方案
使用@WebParam注解。它用于指定定义消息部分时所用的目标命名空间,该参数是否应该从消息头抽取,该命名空间在WSDL中的名称会是什么,参数流是何种方向(in、out或in/out)。
讨论
可以做出的最简单、最直观的更改在于WebParam.name属性。如果对于你的服务来说,符合下面三方面的内容,就将该参数设置为required:
操作样式是document。
参数样式是bare。
模式是OUT或INOUT。
也可以指定WebParam.partName属性的值,如下所示:
@WebMethod(operationName = "add")
public int add(@WebParam(name = "i", partName="iPart")
提示: 只有当操作样式为RPC时或当操作样式为document且参数样式为bare时,设置@WebParam.partName才会有效果。
7.10 指定操作返回值
问题
希望定义Java方法的返回值,从而映射到WSDL输出。
解决方案
使用@WebResult注解来映射到现有wsdl:output或定制其生成方式。
讨论
可以明确地将一个Java方法指定为服务操作,方式是使用@WebResult注解对其进行注解。不过,该注解还允许你指定一些属性,来说明该操作在WSDL中的表示方式。表7-3给出了这些属性。
表7-3:@WebResult属性
属性 作用
name 返回值的名称,这将与Schema中从WSDL的响应消息映射的有关元素 名称相匹配。如果样式是RPC而且没有指定@WebResult.partName,该 值将被用作表示返回值的wsdl:part。如果操作是文档样式或返回类型 映射到头,该值将是表示返回值的Schema元素的名称
partName 表示返回值的wsdl:part的名称。它只适用于当操作是RPC样式时或当 操作是document样式且参数样式为bare时
targetNamespace 用于指定当样式是RPC时或当样式是document且参数样式为bare时, 代表返回值的元素的XML目标命名空间
header 如果设置为true,有关结果将从消息头而不是消息主体获取
下面是一个示例,给出了name属性的指定如何影响WSDL。假定一个服务实现中存在如下Java方法,它带有空的@WebService注解(这给出的是默认样式document/literal):
@WebMethod
public @WebResult(name="doItOne") int
doIt(int x) {
return 1;
}
JAX-WS将从这个方法生成如下WSDL片段:
<message name="doItResponse">
<part name="parameters" element="tns:doItResponse"></part>
</message>
<portType name="Worker">
<operation name="doIt">
<input message="tns:doIt"></input>
<output message="tns:doItResponse"></output>
</operation>
</portType>
该片段指定一个操作,其响应类型是在相关联的Schema的doItResponse元素类型中定义的。由于你的定制,这将为如下所示:
<xs:complexType name="doItResponse">
<xs:sequence>
<xs:element name="doItOne" type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
Schema元素名称采用@WebResult.name的值。
7.11 定义无参数操作
问题
你正使用“从Schema开始”方法开发服务,需要知道如何表示不带任何参数的Java方法。
解决方案
假定你具有如下不带任何参数的Java方法:
@WebMethod
public boolean startBatch(){
return true;
}
如果希望让JAX-WS为你生成WSDL,就已经完事了。但是,如果是从Schema开始,那么,应该键入什么内容来表示什么也没有?可以使用如下Schema定义:
<xs:Schema xmlns:tns="urn:myNs"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
version="1.0" targetNamespace="urn:myNs">
<xs:element name="startBatch" type="tns:startBatch" />
<xs:element name="startBatchResponse" type="tns:startBatchResponse" />
<xs:complexType name="startBatch">
<xs:sequence />
</xs:complexType>
<xs:complexType name="startBatchResponse">
<xs:sequence>
<xs:element name="return" type="xs:boolean"/>
</xs:sequence>
</xs:complexType>
</xs:Schema>
此处的关键是定义一个Schema元素并让它引用一个复杂类型,而让该复杂类型定义一个空的序列。
7.12 定义带有Void返回类型的操作
问题
希望创建不返回任何值的Web服务操作。
解决方案
定义一个带有void返回类型的Java方法,然后向其添加@OneWay注解。
讨论
这或许不常见,不过,有时你希望用一个新值更新服务,同时不需要得到响应。无法简单地定义一个带有void返回类型的Java方法。假定我们希望创建如下Web服务操作:
@WebMethod(operationName="update")
public void update(int status) {
System.out.println("The new status is: " + status);
}
从该方法生成的WSDL操作将不具有期望的效果,如下所示:
<operation name="update">
<input message="tns:update" />
<output message="tns:updateResponse" />
</operation>
当然,我们可以接着往下进行,这时调用它,如下所示:
//Remember WebServiceRef only works in managed environment
@WebServiceRef(wsdlLocation=
"http://localhost:8080/CalculatorApp/CalculatorWSService?wsdl")
public CalculatorWSService service;
service.getCalculatorWSPort();
port.update(200);
该调用会正常使用JAX-WS执行,不过,它不是我们真正想要的,因为我们会得到一个结果,只是将其忽略了。而且,SAAJ客户端对此头疼,因为运行时抱怨没有得到期望的响应(至少根据WSDL应该得到)。
实际上,在XML Schema中编写的与该操作的响应类型有关的内容是一个空序列:
<xs:element name="updateResponse" type="tns:updateResponse" />
<xs:complexType name="updateResponse">
<xs:sequence />
</xs:complexType>
实现这一目的的一种更清晰、互操作性更好的方法就是向具有void返回类型的任一服务方法添加javax.jws.Oneway注解。让我们修改该方法并看看新的WSDL内容:
@Oneway
@WebMethod(operationName="update")
public void update(int status) {
System.out.println("The new status is: " + status);
}
现在,XML Schema中没有任何内容表示空响应(就像它应该的那样)。WSDL不再包含空响应消息:
<message name="update">
<part name="parameters" element="tns:update" />
</message>
SOAP操作被修正成只包含一个输入:
<operation name="update">
<soap:operation soapAction="" />
<input>
<soap:body use="literal" />
</input>
</operation>
7.13 创建使用基于自定义WSDL和自定义Schema的复杂类型的Web服务
问题
需要进行实际编程,而不是Hello World,因此,希望编写发送和接收复杂类型的Web服务,而这些复杂类型基于已经编写的Schema。该Schema是通过已经编写的WSDL来引用的,希望确保维护Schema中指定的约束。没有使用Provider,而是一个带有@WebService注解的类,因此,需要在编译Web服务之前,从Schema生成可移植类型。
解决方案
基本上来说,将会使用@WebService.wsdlLocation注解,在使用XJC Ant任务进行构建的适当时候生成相应的类。
讨论
将创建一个定义如下单个操作的Credit Web服务:authorize。它将接受CreditCard复杂类型并执行某种业务逻辑来确定应该向该卡授予多大金额,然后返回一个自定义的Authorization Java类型。
你将按照实际顺序编写各项内容:Schema、WSDL、Java Web服务以及将所有内容组合起来并进行部署的构建脚本。
Schema
示例7-1中的Schema包含一些元素,用来指定信用卡有关信息以及授权金额。该解决方案的思想是创建一个复杂Schema,就像你在实际工作中使用的那样,而且该Schema还要不太长。
该Schema适用于参数样式为bare时,因为定义的两个元素(CreditCard和authorization)表示调用时希望交换的实际文档。如果希望使用wrapped参数样式,可以指定元素作为实际元素的封装器。
示例 7-1:Credit.xsd信用卡Schema
<?xml version="1.0" encoding="UTF-8"?>
<xs:Schema
version="1.0"
targetNamespace="http://ns.soacookbook.com/credit"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://ns.soacookbook.com/credit"
elementFormDefault="qualified">
<xs:element name="creditCard" type="tns:CreditCard" />
<xs:element name="authorization" type="tns:Authorization" />
<xs:annotation>
<xs:documentation xml:lang="en">
A Credit Card contains a number, a cardholder name,
and an expiry date. The date is just an XSD date,
and the others are custom types with constraints.
</xs:documentation>
</xs:annotation>
<xs:complexType name="CreditCard">
<xs:sequence>
<xs:element id="cardNumber" name="cardNumber"
type="tns:CardNumber"
minOccurs="1" maxOccurs="1"/>
<xs:element id="name" name="name" type="tns:Name" />
<xs:element id="expirationDate" name="expirationDate"
type="xs:date" nillable="true" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Name">
<xs:sequence>
<xs:element name="firstName" type="tns:NameString"/>
<xs:element name="middleInitial" type="tns:InitialString"
nillable="true"/>
<xs:element name="lastName" type="tns:NameString"/>
</xs:sequence>
</xs:complexType>
<!-- Names must be at least 2 characters, no more than 35
characters, and consist of alphabetic characters, hyphens,
single quotes, periods and spaces -->
<xs:simpleType name="NameString">
<xs:restriction base="xs:string">
<xs:minLength value="2" />
<xs:maxLength value="35" />
<xs:pattern value="[A-Za-z\-. ]{2,35}" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="InitialString">
<xs:restriction base="xs:string">
<xs:minLength value="0" />
<xs:maxLength value="1" />
<xs:pattern value="[A-Za-z]?" />
</xs:restriction>
</xs:simpleType>
<!--Just simple constraint to keep it this short.
Not good enough for real world.-->
<xs:simpleType name="CardNumber">
<xs:restriction base="xs:string">
<xs:pattern value="\^(\d{4}[- ]){3}\d{4}|\d{16}$" />
<xs:minLength value="10" />
<xs:maxLength value="16" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Authorization">
<xs:sequence>
<xs:element name="amount" type="xs:double"/>
</xs:sequence>
</xs:complexType>
</xs:Schema>
该Schema定义了两个全局元素:creditCard和authorization。信用卡是一种复合类型,包含三个元素:cardNumber、name和expirationDate。这些类型中的每一个依次由其他类型指定。
cardNumber是作为simpleType进行定义的,给出了允许的数字结构以及最小和最大长度方面的限制。持卡人姓名实际指向另一个复杂类型Name,它定义了名、姓和中间名首字母。接着,为它们指定了自身的约束,包括可接受数据长度和可接受字符。
第二个元素authorization定义了可以为给定卡授予的金额。它比较简单,因为我同时希望显示返回的自定义类型,但希望它不是多余的。
提示: 虽然在Schema中,包含一些有关Schema元素与Schema的WSDL部分之间联系的提示是比较吸引人的,但不要这样做。除了对WSDL提供支持外,Schema还有其他任务。如果你确定Schema不再做任何其他操作,将它们内嵌在WSDL中会更简单,而且会减少维护者的迷惑。不过,这确实损伤了WSDL的模块性。
Web服务实现本身不指定Schema的任何内容,但WSDL必须指向它。
提示: 部署服务后,如果在Internet Explorer中打开服务来查看,则只有Schema的注解可以看见。可以使用查看源文件选项来查看整个Schema内容。这在Firefox中是不必要的,这时就会显示整个Schema。
WSDL
示例7-2中给出的WSDL非常简单。它定义了一个目标命名空间,Schema和服务实现共享该命名空间。在wsdl:types部分中,导入前面清单中定义的Credit.xsd Schema。在这个示例中,它和WSDL位于同一目录,不过,也可以将它指定为URL或者与WSDL位于不同目录。
提示: 你或许注意到Glassfish改写了WSDL,如果没有向xsd:import的SchemaLocation属性传递相对路径,则将该属性指向一个绝对的URL(采用形式http://localhost:8080/soaCookbookWS/CreditService?xsd=1)。
该WSDL定义了单个wsdl:portType,CreditAuthorizer,这与定义单个操作authorize的Java接口类似。soap:address元素指定提供该服务的URL。使用soap前缀是因为使用SOAP协议绑定该服务。
提示: 请回顾一下,如果希望查看服务关联的WSDL,可以在浏览器中通过soap:address指定服务位置,并在末尾处加上?wsdl。这在规范中没有定义,但所有供应商(包括Sun、IBM、JBoss和Oracle)都支持这样做。
示例 7-2:Credit.wsdl
<?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://ns.soacookbook.com/credit"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
targetNamespace="http://ns.soacookbook.com/credit"
name="CreditService">
<types>
<xsd:Schema>
<xsd:import namespace="http://ns.soacookbook.com/credit"
SchemaLocation="Credit.xsd"/>
</xsd:Schema>
</types>
<message name="authorizeRequest">
<part name="creditCard" element="tns:creditCard" />
</message>
<message name="authorizeResponse">
<part name="authorization" element="tns:authorization" />
</message>
<portType name="CreditAuthorizer">
<operation name="authorize">
<input message="tns:authorizeRequest" />
<output message="tns:authorizeResponse" />
</operation>
</portType>
<binding name="CreditAuthorizerPortBinding"
type="tns:CreditAuthorizer">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="authorize">
<soap:operation soapAction="" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<service name="CreditService">
<port name="CreditAuthorizerPort"
binding="tns:CreditAuthorizerPortBinding">
<soap:address
location="http://localhost:8080/soaCookbookWS/CreditService" />
</port>
</service>
</definitions>
服务实现类将直接引用这个WSDL文件,创建时,它将和服务实现一起包装在Web-INF/wsdl文件夹中。同时还可以注意到,Schema被定义为Credit.xsd,因为在相应的WAR中,它被部署到与WSDL相同的目录。
该WSDL指定document/literal样式和使用,而且参数样式为bare,这样,你可以理解服务将被调用的方式。你的Web服务将使这种关系清晰化。
提示: 记住,如果在Glassfish中使用该机制自动为你生成WSDL,将会重新定义Schema并失去约束。这就是为什么使用应用程序包装Schema而不是让Glassfish为你编写的原因,这样做会更有利一些。
服务实现
Web服务的实现是一个简单类。尽管HttpServlet接口可以用作容器中的一个Servlet,但你没有必要实际实现该接口。可以只定义一个常规类并将其包装在一个WAR中。也没有必要将任何内容编写到Web.xml中。
提示: 对于WAR形式的部署来说,该文件是一个标准的服务实现。它可以是一个没有进行诸多变换的EJB,不过,接着你需要在构建脚本中完成较多的包装工作并将其放入一个EAR中。
让我们更详细地介绍该文件。
注意,为了使用参数和返回类型,将要导入com.soacookbook.ns.credit包。JAXB将类型生成到该包中,但这些类型不会存在,直到我们运行构建脚本。
当首先创建该类时,Web服务操作接受基于Schema的类型,这些类型不会存在,直到使用JAXB生成它们。接着,需要确保服务编译时它们位于类路径中,然后将它们包含在WAR中。构建程序将为你处理该问题,就像我们稍后看到的那样。
该服务实现使用@WebService注解并指定了几个属性:
@WebService.name值与wsdl:portType名称的值相匹配,在这里它是CreditAuthorizer。
@WebService.serviceName值与WSDL的服务名中指定的值(如<servicename="CreditService">)相匹配。
@WebService.targetNamespace属性用于指定命名空间。虽然在此处Schema类型的命名空间是一样的,但这是为了方便,并不存在有关限制。通常来说,将类型命名空间和WSDL命名空间分开是比较好的。
最后,指定wsdlLocation,在服务实现WAR中,它必须是实际存在的。由于是手动指定WSDL并将它与编写的Schema包含在一起,因此,Glassfish部署工具将支持该Schema,因而它仍包含类型约束。
请看一下示例7-3中的代码清单,我们将解释业务方法注解。
示例 7-3:CreditService.java
package com.soacookbook.ch03.validate;
import com.soacookbook.ns.credit.*;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import org.apache.log4j.Logger;
/**
* Demonstrates a service that uses JAXB-generated types as
* parameters for a start-from-Schema-and-java method.
* Uses Credit.wsdl and Credit.xsd.
*/
@WebService(
name="CreditAuthorizer",
serviceName="CreditService",
targetNamespace="http://ns.soacookbook.com/credit",
wsdlLocation="Web-INF/wsdl/ch03/Credit.wsdl")
public class CreditService {
private static final Logger LOGGER =
Logger.getLogger(CreditService.class);
/** Creates an instance of CreditService.
*/
public CreditService() {
LOGGER.debug("Created provider instance.");
}
//business method
@WebMethod(operationName="authorize")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
use=SOAPBinding.Use.LITERAL,
parameterStyle=SOAPBinding.ParameterStyle.BARE)
public @WebResult(name="authorization",
targetNamespace="http://ns.soacookbook.com/credit")
Authorization
authorize(
@WebParam(name="creditCard",
mode=WebParam.Mode.IN,
targetNamespace="http://ns.soacookbook.com/credit")
CreditCard creditCard) {
LOGGER.debug("Authorizing.");
LOGGER.debug("Card Number: " + creditCard.getCardNumber());
//get data from compound type
String cardNumber = creditCard.getCardNumber();
//create custom type for return
Authorization auth = new Authorization();
//business logic here
if (cardNumber.startsWith("4")) {
auth.setAmount(2500.0);
} else {
auth.setAmount(0.0);
}
LOGGER.debug("Returning auth for amt: " + auth.getAmount());
return auth;
}
}
让我们首先研究一下authorize业务方法。它接受CreditCard类型并返回Authorization类型,这两种类型都不是现有的Java类;它们是在编译服务之前由XJC在构建脚本中生成的。你需要在注解中做一些调整,使得服务与已经定义的WSDL相匹配。
使用@WebMethod对方法进行注解。通常来说,类中所有带有@WebService注解的public方法将作为操作出现在所生成的WSDL中。不过,因为是硬编码的WSDL,因此希望方法名称与WSDL中指定的操作名称(如<operation name="authorize">)相匹配。我使用@WebMethod.operationName属性来这样做。
接下来要包含绑定信息,指定希望使用的style为document,use为literal,而parameterStyle为bare(可以参阅7.14小节中有关这些信息的更多内容)。此处的主要事情是你没有任何包装类型,因此需要使用bare参数样式。注意,是对类而不是某个特定方法指定有关信息,因为无法在同一服务中混合这些样式。
返回类型和方法参数都指定了自己的命名空间,而且它们的名称属性与WSDL中的有关部分名称相匹配。
至于方法的主体,业务逻辑只返回了一个authorization,如果卡号以4开头,金额为$2500,否则就返回0。它将返回值设置成所生成的Authorization类型,该类型将返回给客户端。
示例7-4显示了这些类型所对应的消息是什么样的。
示例 7-4:SOAP消息信用卡请求
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<creditCard xmlns="http://ns.soacookbook.com/credit">
<cardNumber>4011111111111111</cardNumber>
<name>
<firstName>Phineas</firstName>
<middleInitial>J</middleInitial>
<lastName>Fogg</lastName>
</name>
<expirationDate>2015-04-27-07:00</expirationDate>
</creditCard></S:Body></S:Envelope>
如果你打算在客户端手动创建一个信用卡SOAP消息来调用该服务,这就是你需要使用SAAJ创建的内容。如果在客户端你使用了wsimport并让JAX-WS为你生成CreditCard Java类型,可以像任何其他对象那样填充该对象并将其传递给代理,以编组到相同的XML。
示例7-5显示了Authorization返回类型的SOAP消息是什么样的。
示例 7-5:Authorization返回类型的SOAP消息XML
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<authorization xmlns="http://ns.soacookbook.com/credit">
<amount>2500.0</amount>
</authorization>
</S:Body>
</S:Envelope>
这些XML文档是服务调用过程中实际交换的文档,而且,对于每个文档来说,SOAP主体的唯一子元素是完全与相应Schema元素匹配的类型。
JAXB生成的类型
正如前面提到的那样,参数和返回类型是基于XJC所生成的Java类型的Schema元素。示例7-6显示了CreditCard.java类型。这是你导入WSDL时,可以在客户端编译和使用的内容。
示例 7-6:CreditCard.java
package com.soacookbook.ns.credit;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlType;
import javax.xml.datatype.XMLGregorianCalendar;
/**
* Java class for CreditCard complex type. Comments ommitted.
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "CreditCard", propOrder = {
"cardNumber",
"name",
"expirationDate"
})
public class CreditCard {
@XmlElement(required = true)
protected String cardNumber;
@XmlElement(required = true)
protected Name name;
@XmlElement(required = true, nillable = true)
@XmlSchemaType(name = "date")
protected XMLGregorianCalendar expirationDate;
//getters and setters for each field ommitted.
为Authorization生成的类型看上去与这是类似的。如果希望使用SAAJ XML视图,XML注解允许你在客户端使用JAXB将该类型的实例编组回XML。不过,如果是使用代理,可以在服务器端和客户端方将它当作常规Java类来处理。
注意,以Java方式表示的该类型已经“忘记了”你在Schema中对相应元素施加的所有约束。Java代理将让你填充这些域,而不管所施加的约束。
为了完整起见,接下来让我们看看构建文件以了解所有这些条目是如何包装的。
构建文件
下面是构建文件,它将上面的所有内容组合在一起并将其部署到Glassfish:
<project name="soacookbookServer" default="all">
<property file="soacookbookServer.properties"/>
<!-- PATHS -->
<path id="cp">
<pathelement location="${classes.dir}"/>
<pathelement location="${gen.classes.dir}"/>
<pathelement location="${javaee.jar}"/>
<pathelement location="${common.jar}"/>
<pathelement location="${config.dir}"/>
<pathelement location="${commons.lang.jar}"/>
<pathelement location="${service.Schemas.classes.dir}"/>
<pathelement location="${sun.ws.rt.jar}"/>
<pathelement location="${log4j.jar}"/>
<pathelement location="${client.jar}"/>
</path>
<path id="cp.test">
<!-- Added src.test.dir to allow a specific log4j.xml
file just for the tests -->
<pathelement location="${src.test.dir}"/>
<pathelement location="${test.classes.dir}"/>
<pathelement location="${junit.jar}"/>
<path refid="cp" />
</path>
<path id="srcs.path">
<pathelement path="${src.dir}" />
<pathelement path="${src.gen.dir}" />
</path>
<!-- BUILD TARGETS -->
<target name="all" depends="init,clean,prepare,
Schema-class-gen,compile,build-war,deploy" />
<!-- INIT & TASK DEFS -->
<target name="init">
<echo message="Testing for glassfish root directory"/>
<fail>
<condition>
<not>
<available file="${javaee.jar}"/>
</not>
</condition>
Please open the user.properties file and set the
gf.root property to point
to your glassfish install directory.
(default /opt/glassfish on Linux and
c:/glassfish-v2 on windows)
</fail>
<echo message="Testing for gf.password.file setting in user.properties"/>
<fail>
<condition>
<not>
<available file="${gf.password.file}"/>
</not>
</condition>
Please open the user.properties file and set the gf.password.file property to point to the absolute path on your machine of this file. This is required for the glassfish deploy task (which does not work on relative paths).
</fail>
<echo message="Using Path Separator: ${path.separator}"/>
<taskdef name="junit" classname="org.apache.tools.ant.taskdefs.optional.junit.JUnitTask">
<classpath path="${junit.task.path}" />
<classpath path="${junit.jar}" />
</taskdef>
<taskdef name="xjc" classname="com.sun.tools.xjc.XJCTask">
<classpath path="${xjc.task.path}"/>
</taskdef>
<taskdef name="gf-deploy"
classname="org.apache.tools.ant.taskdefs.optional.sun.appserv.DeployTask"
classpath="${deploy.cp}" />
</target>
<!-- END INIT -->
<!-- CLEAN -->
<target name="clean">
<echo message="-----Cleaning-----" />
<delete dir="${dist.dir}"/>
<delete dir="${build.dir}"/>
<delete dir="${test.dir}"/>
<delete dir="${src.gen.dir}" />
</target>
<!-- PREPARE -->
<target name="prepare">
<mkdir dir="${dist.dir}"/>
<mkdir dir="${build.dir}"/>
<mkdir dir="${ear.lib.dir}"/>
<mkdir dir="${src.gen.dir}"/>
<mkdir dir="${classes.dir}"/>
<mkdir dir="${gen.classes.dir}"/>
<mkdir dir="${ejb.dependencies.classes.dir}"/>
<mkdir dir="${test.dir}"/>
<mkdir dir="${test.classes.dir}"/>
<mkdir dir="${test.report.dir}"/>
</target>
<!-- RUN JAXB TO GENERATE JAVA FROM Schema-->
<target name="Schema-class-gen">
<echo message="---Generating Java src from Schema---" />
<xjc destdir="${src.gen.dir}"
extension="false">
<Schema dir="${Schemas.dir}"
includes="**/*.Schemalet,
${Schemas.includes.pattern}"/>
</xjc>
</target>
<!-- COMPILE CLASSES-->
<target name="compile">
<echo message="-----Compiling-----" />
<javac compiler="modern" debug="${debug}" fork="true"
source="${source.java.version}"
target="${target.java.version}"
excludes="**/client/**"
destdir="${classes.dir}">
<src path="${src.gen.dir}"/>
<src path="${src.dir}"/>
<classpath refid="cp"/>
</javac>
</target>
<!-- BUILD WAR -->
<target name="build-war">
<echo message="-----Building WAR-----" />
<war destfile="${build.dir}/${war.name}"
Webxml="${Webinf.dir}/Web.xml">
<manifest>
<attribute name="Manifest-Version" value="1.0"/>
<attribute name="Built-By" value="${user.name}"/>
</manifest>
<Webinf dir="${config.meta.inf.dir}"
includes="**/*.wsdl,**/*.xsd"/>
<lib file="${client.jar}" />
<classes dir="${build.dir}/classes"
excludes="**/ejb/**"/>
<fileset dir="${root.dir}/jsp" includes="*.jsp" />
</war>
</target>
<!-- DEPLOY TO GLASSFISH-->
<!-- Undeploy happens automatically on a new deploy so it
does not need to be called explicitly -->
<target name="deploy">
<echo message="Deploying ${war.name}"/>
<gf-deploy user="${gf.username}"
passwordfile="${gf.password.file}"
host="${gf.server.address}"
port="${gf.adminport}"
file="${build.dir}/${war.name}"
asinstalldir="${gf.root}"/>
<property name="artifact.deployed" value="true" />
</target>
</project>
这是一个相当标准的构建文件,但可用于阐明所涉及的任务的类名并给出了一种合适的方式来构造Ant任务。还值得注意的是,它给出了你可以在Ant中获得的基本内容。它清除了临时目录,从Schema生成Java源代码,编译Java类,并将所有内容以及WSDL和Schema包装在一个WAR中,然后将所得到的WAR部署到Glassfish。
提示: 如果使用的是Maven 2,你或许希望将此处介绍的构建过程分解成多个单独的模块。例如,如果这样做,一种想法是为服务创建一个模块,为支持所需的类创建一个模块,为生成客户端内容创建一个模块,为客户端JAR创建一个模块。接着,客户端JAR可以依赖所生成内容的JAR。
为了完整起见,我包含了属性文件(如示例7-7所示)。你可以看见涉及哪些类和JAR,而且轻松地映射到你所处环境的源目录和其他条目。
示例 7-7:soacookbookServer.properties
project.name=soaCookbookWS
#USER PROPERTIES: LINUX
#gf.password.file=/home/ehewitt/soacookbook/repository/code/chapters/ws/soacookbookServer.properties
#gf.root=/opt/glassfish91u1
#USER PROPERTIES: WINDOWS
gf.password.file=C:\\oreilly/soacookbook/code/chapters/ws/soacookbookServer.properties
gf.root=C:\\programs/glassfishv2ur1
#GENERAL JAVA
source.java.version=1.6
target.java.version=1.6
debug=on
#DIRS
root.dir=.
build.dir=./build
classes.dir=${build.dir}/classes
gen.classes.dir=${build.dir}/gen-classes
dist.dir=dist
docs.dir=./docs
src.dir=./src/java
src.gen.dir=./src/gen
src.xml.dir=./src/xml
src.test.dir=./src/test
test.dir=./test
test.classes.dir=${test.dir}/classes
test.report.dir=${test.dir}/report
tmp.dir=${build.dir}/tmp
config.dir=./config
Webinf.dir=${config.dir}/Web-INF
config.meta.inf.dir=${config.dir}/META-INF
ear.lib.dir=${build.dir}/lib
Schemas.dir=${config.meta.inf.dir}/wsdl/ch03
ejb.dependencies.classes.dir=${build.dir}/ejb-dependency-classes
#location of the .class files for classes generated from Schemas used by the service and the client
service.Schemas.classes.dir=${root.dir}/../ws/build/classes
#generated classes from wsimport go here
gen.client.package.name=com.dte.soa.soacookbook.wsclient
#PATTERNS
ejb.dir.pattern=**/ejb/**
spi.includes.pattern=**/*.spi.*
Schemas.dir=config/META-INF/wsdl/ch03
Schemas.includes.pattern=**/*.xsd
#DEPENDENCIES: LIB DIR
lib.dir=../../lib
commons.lang.jar=${lib.dir}/commons-lang-2.3.jar
javaee.jar=${lib.dir}/javaee.jar
junit.jar=${lib.dir}/junit-4.4.jar
log4j.jar=${lib.dir}/log4j-1.2.9.jar
Webservices-rt.jar=${lib.dir}/Webservices-rt.jar
#From the Client project, to be included in WAR
client.jar=../client/dist/soaCookbookClient.jar
#ARTIFACT NAMES
client.classes.jar=${project.name}-client.jar
ejb-jar.name=${project.name}-ejb.jar
ear.name=${project.name}.ear
log4j.xml=${config.dir}/log4j.xml
log4j.props.jar=log4jProps.jar
war.name=${project.name}.war
war.name=${project.name}.war
svc.common.jar.name=svc-common.jar
#GLASSFISH DEPLOYMENT
deploy.target=glassfish
gf.server.address=localhost
gf.domain.name=soacookbookdomain
#You may need to modify these for your system
# GlassFish requires the AS_ADMIN_PASSWORD to be in a file.
# The task requires an absolute path. We are pointing directly
# to this file.
gf.username=admin
gf.adminport=5050
gf.port=8080
gf.esb.port=18181
AS_ADMIN_PASSWORD=adminadmin
deploy.cp=${sun.app.ant.jar}${path.separator}${gf.root}/lib/admin-cli.jar${path.separator}${gf.root}/lib/appserv-rt.jar${path.separator}${gf.root}/lib/appserv-admin.jar
server.location=http://${gf.server.address}:${gf.port}
#2.1
sun.app.ant.jar=${lib.dir}/sun-appserv-ant.jar
sun.ws.tools.jar=${lib.dir}/Webservices-tools.jar
sun.ws.rt.jar=${lib.dir}/Webservices-rt.jar
#TASKS
junit.task.path=${lib.dir}/ant-junit.jar
xjc.task.path=${sun.app.ant.jar}${path.separator}${sun.ws.tools.jar}${path.separator}${sun.ws.rt.jar}
在Ant build上,运行“all”目标,就应该开始运行。
不要忘了你希望客户端使用原始Schema;从其生成的JAXB类型将丢失所有类型限制信息。有多种方法可以根据服务Schema来验证Java对象,我们在其他小节对此进行了讨论。如果你只具有Java客户端,可以将所生成的类和原始Schema包装在一个客户端JAR中,从而来调用它们。
参见
有关Schema验证选项的详细讨论,可以参阅2.17小节,该小节还以一个示例给出了如何使用Schema、WSDL和服务来验证客户端数据。
7.14 指定SOAP绑定样式、使用和参数样式
问题
希望为Web服务操作定制SOAP绑定样式、SOAP主体使用和参数样式。
解决方案
对Web方法使用@SOAPBinding注解,并在javax.jws.soap.SOAPBinding类中使用枚举指定其属性,如下所示:
@WebMethod
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
use=SOAPBinding.Use.LITERAL,
parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public @WebResult(name="searchResults",
targetNamespace="http://ns.soacookbook.com/catalog") SearchResults
authorSearch(
@WebParam(name="author", mode=WebParam.Mode.IN,
targetNamespace="http://ns.soacookbook.com/catalog") Author author) {
//...
DOCUMENT和RPC是SOAPBinding.Style的可能值,LITERAL和ENCODED是SOAPBinding.Use的可能值,而WRAPPED和BARE是SOAPBinding.ParameterStyle的可能值。如果没有指定相应的值,也就是说,如果没有对Web方法使用@SOAPBinding注解,默认值将是document/literal wrapped。
有关如何选择这些组合的讨论,请参阅7.4小节。
7.15 配置标准自定义绑定
问题
希望定制Java和WSDL之间的绑定,从而启用异步操作,规定客户端Java包名,指定MIME内容或启用封装样式。
解决方案
在WSDL中以内嵌的方式或在外部绑定文件中编写自定义绑定。所生成的SEI将基于定义的方法提供异步方法。如果使用的是SAAJ,Dispatch<T>已经为你定义了异步方法。它们是:
Response<T>invokeAsync(T msg)
Future<?> invokeAsync(T msg, AsyncHandler<T> handler)
讨论
JAX-WS规范定义了4种现成的标准定制:
package
用于规定客户端中wsdl:definitions元素指定的目标命名空间的Java包的名称,以及为客户端package-info.java文件插入包级别的JavaDoc文本。
enableWrapperStyle
为所有具有资格的操作打开封装样式
enableAsyncMapping
允许客户端异步调用指定的操作
enableMIMEContent
默认情况下对所有指定操作启用MIME内容
由于客户端的唯一服务视图是WSDL,因此,你需要在其中指出服务是否支持上面这些功能中的任何功能。JAX-WS为这些条目中的每一项定义了单独的绑定元素,而且使用如下两种方法将它们绑定到WSDL:WSDL导入的外部绑定文档或WSDL中直接嵌入的元素。
这两种方法都是通过为元素指定简单值来启用这些选项(这些元素是在http://java.sun.com/xml/ns/jaxws命名空间中定义的),并通常使用一个JAX-WS前缀。不管哪种方法(内嵌或是文件),元素JAX-WS:bindings都用于包含所有其他绑定声明,你可以按照任意顺序指定这些声明。
不过,每种标准自定义绑定只允许在某些位置使用。如果是使用外部文件,必须是对绑定@node属性使用XPath来指定正确的节点。如果是在WSDL中指定内嵌绑定,需要注意只对希望应用绑定(而且允许应用绑定)的元素施加绑定,否则,你就会遇到一个部署错误,向你报告服务不可用。
提示: 如果是在WSDL文档中使用绑定,则无法使用@node或@wsdlLocation元素。
下面的一些小节讨论了这两种方法的使用。
使用外部绑定文件
借助这种方法,可以将WSDL的所有绑定声明组合到单个文档中,该文档是一个单独文件。你可以编写jaxws:bindings元素,指定wsdlLocation属性的值来说明该绑定适用于哪个WSDL。还可以将一个XPath表达式提供给node属性,以指出该绑定适用于WSDL的哪一方面。示例7-8给出了一个可以启用异步操作的单独绑定文件。
示例 7-8:JAX-WS绑定定制单独文件
<jaxws:bindings wsdlLocation="http://ns.soacookbook.com/Calculator.wsdl">
<jaxws:packageName="com.soacookbook.calc">
<jaxws:javadoc>The ‘add' operations in this package will be
available asynchronously.
</jaxws:javadoc>
</jaxws:packageName>
<jaxws:bindings node="wsdl:operation[@name='add']">
<jaxws:enableAsyncMapping>true</jaxws:enableAsyncMapping>
</jaxws:bindings>
<jaxws:bindings>
该文件适用于给定位置的单个WSDL,它指定了两个绑定定制。第一个是指定的包名将包含给定字符串JavaDoc,第二个是SEI将包含其他方法来异步调用add操作。
bindings元素允许使用另外两个属性。一个是version,默认情况下它是2.0,用于指代JAX-WS版本;另一个是node,该属性允许你只对目标WSDL中XPath表达式指定的元素应用绑定定制。
使用内嵌元素
使用这种方法时,你一定不要在jaxws:bindings元素中指定WSDL位置,因为任何定制只适用于当前WSDL,也不允许使用XPath表达式指定定制声明适用的节点。
示例7-9显示了如何对enableAsyncMapping使用内嵌绑定声明。
示例 7-9:内嵌jaxws:bindings元素
<wsdl:definitions targetNamespace="..."
xmlns:jaxws="http:java.sun.com/xml/ns/jaxws">
<wsdl:portType name="CalculatorWS">
<wsdl:operation name="add">
<wsdl:input message="tns:add"></input>
<wsdl:output message="tns:addResponse"></output>
</wsdl:operation>
<jaxws:bindings>
<jaxws:enableAsyncMapping>true</jaxws:enableAsyncMapping>
<jaxws:bindings>
</wsdl:portType>
</wsdl:definitions>
从运行时应用定制的方式角度来说,指定外部绑定和内嵌绑定是没有语义区别的。不过,对于维护程序来说,是有区别的。你会发现,下面这种做法更灵活:将无关定制放在单独文件中并根据需要将它们都应用到同一WSDL。这使得内容独立开来,同时也更难确定何种绑定被正确地应用到WSDL,因为绑定文件引用WSDL,而不是相反。
此外,还应确定不希望将某些功能直接暴露在WSDL中,以便使其避免使用特定于平台的代码。
提示: 如果对包含jaxws:provider的端口进行注解,将不会为该端口生成SEI。在相应的位置,应用程序代码将使用javax.xml.ws.Provider接口。这意味着所生成的服务类型中不会创建getXXXPort方法(否则,客户端会调用一个方法来获取实际不存在的端口)。
要注意你允许将哪些实现细节放入public接口。
参见
可以参阅JAX-WS规范的第8章来了解不同元素允许使用哪些自定义绑定。
7.16 从服务排除Public方法
问题
有一个使用@WebService注解的服务实现,但这自动使得所有的public方法在所生成的WSDL中都可用。
解决方案
有三种方法可以解决这个问题:
自己编写WSDL。如果没有生成WSDL,希望省去的方法不会出现在WSDL中。
如果打算生成WSDL,可以用@WebMethod对WSDL中明确希望出现的每个方法进行注解。不要对希望省去的方法添加注解。
对希望省去的方法明确指定@WebMethod (exclude=true)。
讨论
你会因为一些原因遇到这种情况。你或许拥有一个依赖于第三方应用程序来运行的Web服务,比如信用处理器软件。你希望确保依赖的生命周期与定义服务的EJB的生命周期相匹配,该EJB是一个无状态会话bean,还实现了TimedObject,这个EJB被设置为一个定时器,这样,当应用程序开始并调用该EJB的一个方法来启动依赖时,你就拥有一个基于Web的环境监听器来获得回叫信号。这种设置没有问题,但你一定不希望暴露生命周期方法Web服务。尽管如此,它需要是public的,这样环境监听器就可以找到它。
提示: 可以使用许多方法来处理依赖的生命周期。在这里,一种显而易见的设计方法是让环境监听器调用与该EJB位于同一个包的另一个类中的某个public方法,并让它调用该EJB中某个只适用于当前包的方法。这仅是一种假设。不过,按照这种预备解决方案的目的,当希望重新考虑看起来需要使用@WebMethod(exclude=true)的设计时,它或许是非常不错的。
7.17 创建带有XML视图的服务提供类
问题
希望创建在请求和响应中直接使用SOAP XML而不是对象视图的服务实现。
解决方案
让服务实现javax.xml.ws.Provider<T>接口。
讨论
基于JAX-WS的Web服务通常是使用注解实现的,这使得开发者可以快速创建和运行。这类端点实现非常易于使用,使开发者免去了处理众多管道的麻烦。它们提供了一个对象级别的视图,给出了WSDL中描述的消息和端口类型,使得开发者可以在服务调用中来回传递的XML消息内容之上轻松地处理抽象信息。
但是,有时你希望拥有一个原始的XML视图来呈现服务中的请求和响应,这就是需要使用Provider<T>的地方。
Provider<T>是JAX-WS的一部分,但它允许你使用SAAJ API,并提供另外一种方法来处理带有注解的SEI。它是服务器端类似于Dispatch<T>的接口,提供一个XML消息级别的视图来呈现请求。该接口定义了单个需要实现的方法:
T invoke(T request)
每当接收到一个新消息,就会调用这个invoke方法。
对于该提供类的类型参数来说,要求JAX-WS的供应商实现至少支持三个值:
javax.xml.transform.Source
javax.activation.DataSource
javax.xml.soap.SOAPMessage
Source用作XML源的容器,它可以包含实现,比如DOMSource和SAXSource。SOAPMessage表示包含头、主体、任何附件等信息的SOAP消息。DataSource使用HTTP绑定来提取数据的任意集合,以InputStream和OutputStream形式提供对数据的访问。
Provider的使用需要详细了解载荷或消息的结构。
创建Provider<T>
要创建一个提供类,必须完成三件事情:
1. 提供一个public的无参数构造函数。
2. 实现Provider<T>接口,为类型参数指定三个可接受值中的一个。
3. 使用@WebServiceProvider注解为类添加注解。
除了@WebServiceProvider接口,开发者还可以提供@javax.xml.ws.Service.Mode注解,它具有两个可能值:Mode.PAYLOAD或Mode.MESSAGE,该值指定提供类实例将访问的数据视图。
如果使用Mode.PAYLOAD,则提供类将只访问SOAP主体元素的子元素。如果使用Mode.MESSAGE,提供类就可以访问整个SOAP信封,包括头。注意,在这里,我的示例使用SOAP,不过MESSAGE模式实际是为更一般的使用而准备的,意味着它适用于协议消息本身,而不管是哪种协议。默认值是PAYLOAD。
提示: 通过提供类,你还可以使用处理程序。处理程序是在7.18小节中详细讨论的。
示例7-10显示了你将手动编写的基本WSDL。Provider将使用wsdlLocation属性指定它符合该WSDL。由于该提供类不是一个EJB,因此,只将它包装在一个WAR中,并让Ant脚本将该WSDL放在Web-INF/wsdl目录中。服务接口规定请求应该包含用户名,而且,如果服务验证该用户,将会在头和SOAP主体的一个子元素中返回一个SSO令牌,用以说明用户的角色。
示例 7-10:一个验证服务的WSDL
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="GatewayService"
xmlns="http://Schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://Schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://ns.soacookbook.com/gateway"
xmlns:tns="http://ns.soacookbook.com/gateway">
<types>
<xs:Schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ns.soacookbook.com/gateway">
<xs:element name="gatewayRequest" type="xs:string" />
<xs:element name="gatewayResponse" type="xs:string" />
</xs:Schema>
</types>
<message name="gatewayRequest">
<part name="parameters" element="tns:gatewayRequest"></part>
</message>
<message name="gatewayResponse">
<part name="parameters" element="tns:gatewayResponse"></part>
</message>
<portType name="Gateway">
<operation name="authorize">
<input message="tns:gatewayRequest"></input>
<output message="tns:gatewayResponse"></output>
</operation>
</portType>
<binding name="GatewayPortBinding" type="tns:Gateway">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"></soap:binding>
<operation name="authorize">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal"></soap:body>
</input>
<output>
<soap:body use="literal"></soap:body>
</output>
</operation>
</binding>
<service name="GatewayService">
<port name="GatewayPort" binding="tns:GatewayPortBinding">
<soap:address
location="http://localhost:8080/GatewayService/Gateway">
</soap:address>
</port>
</service>
</definitions>
为了简单起见,该示例将类型直接放入Schema本身中。这对于这样简单的示例来说是可以接受的,但或许不是你实际希望面对的事情。我们定义了一个名为authorize的操作,它接受和返回的都是一个字符串。
接下来,我们将定义提供类。由于希望添加头,我们将使用SOAPMessage作为类型参数,这意味着还必须指定@ServiceMode(Mode.MESSAGE)。
提示: 有一些服务模式组合对提供类的类型参数来说没有什么意义。例如,这种做法是错误的:试图将提供类在javax.xml.soap.SOAPMessage上进行参数化,然后指定Mode.PAYLOAD(这也是默认值)。为了纠正这一错误,如果你实际只需要访问载荷,就使用Source类型;如果你实际希望访问整个SOAP消息,包括头和附件,就使用@ServiceMode(Mode.MESSAGE)。
示例7-11显示了一个简单的Provider实现。
示例 7-11:Provider<SOAPMessage>实现
package com.soacookbook.ch04;
import java.io.IOException;
import java.util.UUID;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFactory;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.Provider;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.WebServiceProvider;
import org.apache.log4j.Logger;
/**
* Demonstrates simple Provider.
*/
@WebServiceProvider(
serviceName="GatewayService",
portName="GatewayPort",
targetNamespace="http://ns.soacookbook.com/gateway",
wsdlLocation="Web-INF/wsdl/Gateway.wsdl")
@ServiceMode(Mode.MESSAGE)
public class MyProvider implements Provider<SOAPMessage> {
private static final Logger LOGGER =
Logger.getLogger(MyProvider.class);
public MyProvider() {
LOGGER.debug("Created provider instance.");
}
public SOAPMessage invoke(SOAPMessage request) {
SOAPMessage response = null;
try {
LOGGER.debug("Received request:\n");
//Dump request to console
request.writeTo(System.out);
LOGGER.debug("Building SOAP Response.");
String user = request.getSOAPBody().getTextContent();
response = createResponse(user);
} catch (SOAPException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
return response;
}
public SOAPMessage createResponse(String user)
throws SOAPException, IOException {
LOGGER.debug("Creating SOAP Response for user: " + user);
//Create a response message
MessageFactory mf = MessageFactory.newInstance();
SOAPFactory sf = SOAPFactory.newInstance();
SOAPMessage response = mf.createMessage();
//Could go to database here to check creds
if ("jgosling".equals(user)) {
//Create Header
QName q = new QName("urn:myNS", "sso");
SOAPHeader h = response.getSOAPHeader();
SOAPHeaderElement headerEl = h.addHeaderElement(q);
headerEl.addAttribute(new QName("gateway"),
"192.168.1.102");
headerEl.addAttribute(new QName("authToken"),
UUID.randomUUID().toString());
//Create Body
SOAPBody body = response.getSOAPBody();
SOAPElement respContent =
body.addChildElement("gatewayResponse");
respContent.setValue("ADMIN");
} else {
//Create body for unauthorized user, and no header
SOAPBody body = response.getSOAPBody();
SOAPElement respContent =
body.addChildElement("gatewayResponse");
respContent.setValue("N/A");
}
response.saveChanges();
LOGGER.debug("Returning response.");
response.writeTo(System.out);
return response;
}
}
该提供类实现了所需的invoke方法;它读取请求并返回一个响应,将发送验证令牌的工作委托给有效用户,拒绝无效用户。该示例说明了如下一些事情:
如何实现Provider
如何将Provider关联到给定的WSDL
如何为响应创建完整的SOAP消息
如何向SOAP头添加完整元素
在这里,我们使用了一种消息模式来确保可以访问整个SOAP信封。WSDL包装在WAR中,其他注解属性指定该提供类将响应哪个服务和端口的请求。
因此,现在你可以构建和部署该服务,并开始监听请求。如果一切正常,部署消息将指出提供类在指定地址是可用的:
DPL5306:Servlet Web Service Endpoint [com.soacookbook.ch04.MyProvider]
listening at address [http://localhost:8080/soaCookbookWS/GatewayService]
Glassfish提供一个Web页来代表相应的服务,允许你查看服务名称、端口名称和实现类,如图7-1所示。
图7-1:Glassfish服务清单页
这时,提供类实例已经创建,可以发送一个SOAP请求来测试它。
测试提供类
在本节中,我们将创建一个JUnit测试来向提供类发送请求。可以创建如示例7-12中所示的JUnit 4.4类来执行两个测试,该类将测试单个方法authorize,该方法接受一个用户名字符串并返回一个说明该用户所处角色的字符串。
当使用用户名jgosling后,你期望该用户被授予Admin角色,并发送一个可以用于路由的<sso>头元素。如果使用数据库中不存在的一个用户名调用该服务,就会得到值“N/A”。
示例 7-12:提供类单元测试
package com.soacookbook.ch04.test;
import static org.junit.Assert.*;
import com.soacookbook.ns.gateway.*;
import java.util.*;
import org.apache.log4j.Logger;
import org.junit.*;
/**
* JUnit test for the authorization provider.
*/
public class ProviderTest {
private static final Logger LOGGER =
Logger.getLogger(ProviderTest.class);
private GatewayService authService;
private Gateway gateway;
@Before
public void setup() {
authService = new GatewayService();
gateway = authService.getGatewayPort();
}
//This user should get authorized in Admin role
@Test
public void testProviderAuth() throws Exception {
LOGGER.debug("Executing.");
String response = gateway.authorize("jgosling");
LOGGER.debug("Response: " + response);
assertEquals("ADMIN", response);
}
//This user should fail authorization
@Test
public void testProviderNotAuth() throws Exception {
LOGGER.debug("Executing.");
String response = gateway.authorize("bgates");
assertEquals("N/A", response);
}
}
通过使用JUnit 4.4,可以一次设置多个代理,而且JUnit的@Before注解将设置每个测试方法要使用的服务和端口的新实例。
下面显示了该JUnit测试的运行结果:
Received request:
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/>
<S:Body>
<gatewayRequest
xmlns="http://ns.soacookbook.com/gateway">jgosling</gatewayRequest>
</S:Body>
</S:Envelope>
Building SOAP Response.
Creating SOAP Response for user: jgosling
4/15/08-15:05 DEBUG com.soacookbook.ch04.MyProvider.createResponse - Response ready:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://Schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<sso xmlns="urn:myNS" authToken="2878f38f-5515..."
gateway="192.168.1.102"/>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<gatewayResponse>ADMIN</gatewayResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
该单元测试发送两个消息。一个消息包含一个应该被授权的用户名,并获得一个临时授权Single Sign On令牌。另一个消息包含一个不应该被授权的用户名,没有获得包含有关令牌的头元素,得到的响应用“N/A”代表“Not Authorized”。
7.18 实现服务器端处理程序链
问题
需要指定希望服务针对传入和传出消息而调用的处理程序,希望在一个外部配置文件中定义处理程序链,而不是将它嵌入在代码中。
解决方案
编写一个XML文件来按照Java EE规则定义处理程序链,然后向Web服务添加javax.jws.HandlerChain注解来指定处理程序链文件的名称。该文件将是一个放入Web-INF/classes目录或应用程序类路径上其他位置的XML文件。
让我们看看如何进行实现。
如果是从Java开始,可以按照你喜欢的任何方式创建一个WAR项目并向其添加一个Web服务POJO。然后向类添加@HandlerChain注解,使用file属性指定包含有关处理程序的XML文件的名称。如示例7-13所示。
示例 7-13:HandlerService.java
package com.soacookbook;
import javax.jws.HandlerChain;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
/**
* Uses a handler chain on the server.
*/
@WebService
@HandlerChain(file="myHandlers.xml")
public class HandlerWebService {
@WebMethod(operationName = "doWork")
public String doWork(
@WebParam(name = "msg") String msg) {
System.out.println("doing work");
return "Processed work for: " + msg;
}
}
除了新的注解,该Web服务没有任何内容值得探讨。接着,需要编写实际处理程序实现类。可以根据自己的意愿只编写一个实现类,我在这里就将这样做。它将是一个简单的类,用于将消息发送到系统输出流。我有意让它保持简单,这样就不会有麻烦。例如,你希望实现Servlet过滤器中通常想要实现的任何内容。
最后一步是创建myHandlers.xml文件(如示例7-14所示)。将它放在WAR的Web-INF/classes目录中。
示例 7-14:myHandlers.xml
<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
<handler-chain>
<handler>
<handler-class>com.soacookbook.LogHandler</handler-class>
<handler-class>com.soacookbook.LateNotifyHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
这些处理程序将根据指定的实现而被动态加载,对于入站和出站消息来说,都可以执行它们。处理程序是按照给定的顺序执行的。
测试该服务会生成如下输出结果,它说明了运行时调用处理程序接口方法的顺序:
LH: handleMessage
LH: logToOut
Inbound message:
<S:Envelope xmlns:S="http://Schemas.xmlsoap.org/soap/envelope/">
<S:Header/>
<S:Body><ns2:doWork xmlns:ns2="http://soacookbook.com/">
<msg>SOME IMPORTANT JOB</msg></ns2:doWork></S:Body>
</S:Envelope>
doing work
LH: handleMessage
LH: logToOut
Outbound message:
LH: close
就像Servlet过滤器一样,处理程序在服务之前收到请求,使得你有时间对数据进行某种其他处理或调整。
示例7-15显示了处理程序类的实现。
示例 7-15:LogHandler.java
package com.soacookbook;
import java.io.IOException;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPException;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.util.Set;
/**
* Logs inbound SOAP messages to the console.
*/
public class LogHandler
implements SOAPHandler<SOAPMessageContext> {
public Set<QName> getHeaders() {
System.out.println("LH: getHeaders");
return null;
}
public boolean handleMessage(SOAPMessageContext ctx) {
System.out.println("LH: handleMessage");
logToSystemOut(ctx);
return true;
}
public boolean handleFault(SOAPMessageContext ctx) {
System.out.println("LH: handleFault");
logToSystemOut(ctx);
return true;
}
// nothing to clean up
public void close(MessageContext messageContext) {
System.out.println("LH: close");
}
private void logToSystemOut(SOAPMessageContext ctx) {
System.out.println("LH: logToOut");
Boolean outboundProperty = (Boolean)
ctx.get (MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (outboundProperty.booleanValue()) {
System.out.println("\nOutbound message:");
} else {
try {
System.out.println("\nInbound message:");
ctx.getMessage().writeTo(System.out);
} catch (SOAPException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
回顾一下,有两种处理程序:协议处理程序和逻辑处理程序。由于使用的是协议处理程序,因此可以通过SAAJ API访问SOAP信封。协议处理程序关注的是WSDL中绑定信息的相关处理,因此,对于这类处理程序来说,消息是作为SOAP消息出现的。另一方面,逻辑处理程序将消息载荷看作javax.xml.transform.Source或JAXB注解的实例。
提示: 可以在同一链中混合和匹配处理程序类型。
7.19 提供有状态的服务
问题
希望Web服务跟踪客户端会话,就像Servlet那样。
解决方案
这是一个棘手的问题——至少在编写本书时是这样。有几种不同的解决方案,但都不太理想。下面是有关方法:
如果是使用HTTP作为传输层,可以将一个WebServiceContext对象声明为Resource,用它获取HttpSession对象。像往常在Servlet中那样使用HttpSession。唯一不足的是它不适用于HTTP之外的传输,但它比较简单和轻便。
如果使用的是JAX-WS参考实现,可以使用一个简单的注解@HttpSessionScope。这是一种好的解决方案,它封装了上面提到的第一种解决方案,因此,编码比较少,处理将所需的状态存储在常规实例变量中以外,不需要做任何事情,运行时将会一个HTTP会话分发一个服务实例。不过,它是特定于RI的,而且现在还没有发挥效用。(参阅https://jax-ws.dev.java.net/issues/show_bug.cgi?id=545。)
此外,可以使用WS-Addressing,它可以工作于HTTP之外,不过这非常复杂。第12章介绍了WS-Addressing。
讨论
HTTP是一种无状态协议,这意味着每个新连接与任何先前连接没有关系;请求之间不保持状态。虽然这是HTTP的一项基本功能,但许多时候需要保持状态(每个请求的特殊消息,因为它与来自同一客户端的其他请求有关)。例如,有一种机制,允许你将货物放在购物车中。
使用WebServiceContext
让我们看看如何使用WebServiceContext对象保持状态,它与Web容器中的ServletCon-text对象类似。可以将该上下文环境声明为容器注射的一种Resource,然后用它来存储来自同一客户端的所有请求的有关数据。让我们来看一个示例。首先需要创建服务,如示例7-16所示。
示例 7-16:使用WebServiceContext的有状态的Web服务
package com.soacookbook;
import javax.annotation.Resource;
import javax.jws.WebService;
import javax.Servlet.http.HttpSession;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
@WebService
public class CounterContextWS {
@Resource
private WebServiceContext wsContext;
public int getCounter(){
MessageContext mc = wsContext.getMessageContext();
HttpSession session = ((javax.Servlet.http.HttpServletRequest)mc.get(
MessageContext.Servlet_REQUEST)).getSession();
Integer count = (Integer)session.getAttribute("count");
if (count == null) {
count = 0;
System.out.println("New Session ID=" + session.getId());
}
count++;
session.setAttribute("count", count);
return count;
}
}
在这里,我们利用了计数器基元的自动装箱和拆箱。该服务类只是将会话存储在WebServiceContext中。与给定会话有关的计数器存储为属性,就像购物车货物或Servlet会话中的任何其他对象。不过,现在还没有完成。注意,在该服务中,count变量是方法局部变量,不能将它存储为实例变量,同样也无法对常规Servlet这样做。虽然它看起来工作于一个简单的测试,但多个客户端会获取这个共享的变量。
这时,如果一个客户端以常规的方式调用了该服务,该计数器不会增加,这是因为会话ID没有和各个请求一起发送,因此,需要配置客户端来维护请求会话。示例7-17显示了将调用该服务而且计数器会增加的客户端。
示例 7-17:有状态Web服务客户端JSP
<%@page contentType="text/html" pageEncoding="UTF-8"
import="com.soacookbook.*, javax.xml.ws.*"%>
<html>
<head><title>Counter Service</title></head>
<body>
<h1>Counter Context Service</h1>
<hr/>
<%
try {
CounterContextWS port = null;
if (session.isNew()){
CounterContextWSService service = new CounterContextWSService();
port = service.getCounterContextWSPort();
session.setAttribute("port", port);
((BindingProvider)port).getRequestContext().put(
BindingProvider.SESSION_MAINTAIN_PROPERTY, true);
}
port = (CounterContextWS)session.getAttribute("port");
int result = port.getCounter();
out.print(result);
result = port.getCounter();
out.print("<br/>");
out.print(result);
} catch (Exception ex) {
ex.printStackTrace();
}
%>
</body>
</html>
让我们对该客户端代码稍作解读。BindingProvider类位于javax.xml.ws包中,因此,在顶部导入了该包以及为服务生成的JAX-WS内容。然后,查看会话是否为新的会话,如果是,创建一个新存根并通过将其设置为一个属性,使其与会话关联。接下来,获取请求上下文环境,使用BindingProvider类指定希望维护该服务各请求间的会话状态。无论会话是否为新的会话,都将使用该会话前面关联的端口以调用其业务方法(getCounter)。
为了进行测试,打开浏览器并指向部署WAR的位置,访问JSP。可以刷新该页面,计数器将会增加。为了验证没有让计数器混乱,可以打开另一个浏览器,就会看到一个新的会话启动了,每个浏览器的页面刷新是被单独维护的。在打开第二个浏览器实例的控制台中,输出结果显示:
New Session ID=2aece0c189a202a05699f7e44893
New Session ID=2af2bfb1860fdd0f7debc1286ea9
不过,每次页面刷新,JSP将显示增加的和独立的计数器值,因为它们是和两个不同的会话有关。使用会话状态维护属性创建服务代理后,将端口保存在Servlet应用程序上下文环境中。否则,每次将获得一个新的端口,实例就不会增加。
7.20 添加带有方法参数的头
问题
希望指定服务操作接受SOAP头。
解决方案
使用@WebParam注解属性header=true和mode=WebParam.Mode.IN,下面是一个示例:
@WebMethod
public @WebResult(name="title") String
secureGetTitle(
@WebParam(name="id") String id,
@WebParam(name="usernameToken",
header=true, mode=WebParam.Mode.IN)
String usernameToken) {...}
讨论
就像上面示例所示的那样,使用@WebParam注解的header=true属性会导致生成一个WSDL,用抽象WSDL说明头。该参数将从消息头而不是消息主体获得。@WebParam.Mode.IN枚举常量指定该参数只可用于传入请求,而不用于响应。对于此处的示例来说,这是正确的,不过可以将头参数映射到OUT模式。
示例7-18给出了当指定上面所示的头值后,所生成的WSDL的部分清单。
示例 7-18:指定头值后得到的部分WSDL
<message name="secureGetTitle">
<part name="parameters" element="tns:secureGetTitle"></part>
<part name="usernameToken" element="tns:usernameToken"></part>
</message>
<portType name="CatalogService">
<operation name="secureGetTitle"
parameterOrder="parameters usernameToken">
<input message="tns:secureGetTitle"></input>
<output message="tns:secureGetTitleResponse"></output>
</operation>
</portType>
<binding name="CatalogPortBinding" type="tns:CatalogService">
<soap:binding transport="http://Schemas.xmlsoap.org/soap/http"
style="document"></soap:binding>
<operation name="secureGetTitle">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" parts="parameters"></soap:body>
<soap:header message="tns:secureGetTitle" part="usernameToken"
use="literal">
</soap:header>
</input>
<output>
<soap:body use="literal"></soap:body>
</output>
</operation>
</binding>
由于part元素(它是抽象合同的一部分)指定了头信息,客户端应用能够轻松地处理头。
但是,并不是所有的WSDL都将像本示例那样指定头——这取决于WSDL是否是通过手动方式编写的,是否正常运行但非顺从,是否使用不同工具等等。JAX-WS规范规定生成SEI时,不要求工具考虑绑定部分——它们只需要检查portType,因为它是抽象WSDL的一部分。
提示: 关于正确的头使用,有一些灰色区域。许多公共服务(美国邮政局发出的一些服务)使用有时称为“隐含头”的头。这些头不是像此处一样作为wsdl:portType的一部分进行定义的。乍一看,没有将合同所需的信息包含在操作中似乎是一种不好的做法。争论的焦点是,如果头信息实际是有关请求的元数据,那么它不应该是该信号的一部分,因为它不是业务操作的一部分。实际结果是,无论选择哪种路线,都需要考虑客户端将如何与你的代码进行交互,当调用服务操作时,如何将需要传递的内容传达给客户端。
7.21 访问服务中的传入头参数
问题
希望访问传入请求的头中的所有参数集。
解决方案
如果使用的是参考实现,可以使用消息上下文环境中的JAXWSProperties.INBOUND_HEADER_LIST_PROPERTY。还可以借助SAAJ来这样做。
讨论
下面的代码只适用于参考实现,它获取WS-Addressing头:
@WebService
public class MyService {
private static final QName MY_HEADER =
new QName("http://ns.soacookbook.com","headerName");
@Resource
WebServiceContext context;
@WebMethod
public void sayHello(String name) {
//RI only
HeaderList headers = context.getMessageContext().get(
JAXWSProperties.INBOUND_HEADER_LIST_PROPERTY);
Header header = headers.get(MY_HEADER);
//do something with header
}
}
在这里,你是位于一个Web服务,这是一种受控的代码情形。这意味着你可以只将WebServiceContext声明为Resource,并让容器为你注射它。从这个上下文环境中,你可以获得消息上下文环境,并使用指定的属性通过QName来获得你所感兴趣的头的名称。
5.14小节显示了如何使用SAAJ API来读取SOAP头的值。但是,如果你处于一个JAX-WS提供类,则希望使用代码来代替该API。
7.22 为SOAP操作或WS-Addressing操作提供值
问题
希望指定WSDL中soapAction的值,或者,如果使用的是WS-Addressing,希望指定操作值。
解决方案
使用@WebMethod注解并指定action属性的值。
讨论
SOAP请求带有名为SOAPAction的HTTP头,该头的值由soap:operation的soapAction属性值决定,其值必须是一个指明当前操作的预期处理资源的URI。按照Basic Profile 1.1,如果没有指定相应的值,JAX-WS运行时将使用一对空的双引号("")作为soap:operation元素的soapAction属性的值。
下面的代码显示了一个示例,该示例在一个Java服务端点实现中指定SOAPAction的值:
@WebMethod(operationName="greet",
action="http://soacookbook.com/Hello/sayHello")
public String greet(
@WebParam(name="name") String name) {
return "Hi " + name;
}
通过将该属性包含在服务操作的注解中,你就会有两方面的收获。其一是在所生成的WSDL中,操作的soapAction属性被指定值,如下所示:
<operation name="greet">
<soap:operation soapAction="http://soacookbook.com/Hello/sayHello"/>
此外,通过该操作生成的每个请求都带有一个HTTP头,它包含指定的值。
提示: 当使用除HTTP以外的其他传输层时,WSDL 1.1规范(3.5小节)明确禁止使用SOAPAction属性。另外,值得注意的是在WSDL 1.2和2.0中,SOAPAction是可选的属性。
SOAPAction的目的是允许防火墙和消息路径上的其他处理节点过滤或帮助发送消息。但是,WS-Addressing标准包含该头提供的功能。与SOAPAction相比,WS-Addressing以一种更有力的方式处理路由信息,它处理起来更贴近SOAP消息本身(而不是依赖于其下的传输层)。
提示: 如果计划使用WS-Addressing对服务添加任何.NET WCF(Windows Communication Foundation),那么,指定soapAction的值是一个不错的主意。如果其值不是默认的空字符串,WCF客户端将基于WSDL生成相应的值。
使用BindingProvider
还有一些其他方法可以指定SOAP操作。也可以使用javax.xml.ws.BindingProvider,如下所示:
//indicate to use soap action
((javax.xml.ws.BindingProvider)port).getRequestContext().put(
javax.xml.ws.BindingProvider.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
//Specify the soap action uri
((javax.xml.ws.BindingProvider)port).getRequestContext().put(
javax.xml.ws.BindingProvider.SOAPACTION_URI_PROPERTY, "http://soacookbook/myService/myOp");
7.23 优化服务器上二进制内容的传输
问题
拥有的Web服务必须发送和接收二进制数据,比如图像或PDF,希望能够优化性能。
解决方案
对Web服务端点实现使用javax.xml.ws.soap.MTOM注解。
讨论
当定义一个Web服务来包含发送和接收二进制数据的一些操作时,编码成xs:base64Binary会非常冗长,因为所有二进制内容必须进行编码,而且需要将消息设置为文本。base64Binary的字符编码会增加消息大小,通常是使用UTF-8时的1.33倍。需要压缩该内容,就像在Web服务器中使用HTTP压缩一样。所幸的是,这非常容易实现。
默认情况下,JAX-WS禁用MTOM编码。为了对Web服务启用它,只需使用MTOM注解:
@WebService
@MTOM
public class MyService { ... }
作为二进制优化的一种方法,MTOM意在替换MIME附件(Microsoft的DIME附件几乎废除),它依赖于XOP(XML Binary Optimized Packaging)。尽管优化的形式压缩了消息中二进制内容的字符序列,但该内容仍然可以在接收方重建。
提示: 可以访问http://www.w3.org/TR/soap12-mtom/来阅读MTOM规范。
MTOM被广泛支持,而且非常轻便,能够被多个对象共同操作。除Glassfish和WebLogic10gR3以外,Axis 2中也实现了MTOM。有关如何在Axis 2中使用MTOM的信息,可以参阅http://ws.apache.org/axis2/1_0/mtom-guide.html。
提示: WS-I Basic Profile 1.2合并了SOAP 1.1 MTOM绑定,确保实现更大的支持和互操作性。
参见
参阅6.16小节来了解如何从客户端调用启用了MTOM的服务。
7.24 获得和共享有关用户和请求的数据
问题
希望获得有关服务调用的数据,比如调用该服务的用户,有关HTTP状况的信息以及特定于Web服务的信息(诸如附件和WSDL)。你希望获得类似于ServletContext的东西,但是针对Web服务。或者,找到一种方法来在处理程序链中的处理程序之间共享与处理有关的状态。
解决方案
在Web服务实现中引用javax.xml.ws.WebServiceContext,使用它来获取MessageContext、用户特征或验证该用户是否具有给定的角色。
讨论
WebServiceContext接口是由SOAP容器在屏幕背后实现的,它具有三个方法:
MessageContext getMessageContext
Principal getUserPrincipal
boolean isUserInRole(String role)
当WebServiceContext类型的域被声明为Resource时,端点实现类将获得一个注射的WebServiceContext实例,如下所示:
@WebService
public class Hello {
@Resource
private WebServiceContext wsContext;
public void doWork(){
MessageContext mContext = wsContext.getMessageContext();
...}
}
和Java EE 5中的任何注射资源一样,该上下文环境将在运行时由容器填充。从Web服务上下文环境获取消息上下文环境对象后,就可以使用标准的Map方法来放置和获取数据(MessageContext扩展了Map),从而能够在处理程序链中的处理程序之间共享数据。
如果希望获得有关当前请求的某一特定方面的数据,可以将WebServiceContext类提供的许多域常量中的一个作为参数传递给get调用。
7.25 通过Holder<T>使用头引用
问题
希望定义一个需要SOAP头的操作并从客户端调用该操作。
解决方案
在服务中,对操作参数使用@WebParam注解。指定header=true,并将参数封装在Holder<T>中,其中,T是所需参数的实际类型。在客户端,使用Holder<T>设置参数元素的值并通过引用获取返回值。
讨论
下面的一些示例说明了如何使用Holder来处理使用Web服务时所需的SOAP头。这是Web服务的命令行客户端,它是完全便携式的;它不依赖Glassfish或其他容器中的专有扩展。同时也显示了Web服务和WSDL来帮助你理解该过程。
首先,我们将使用“从Java开始”方法创建一个Web服务。这是比较容易实现的,使你通过wsgen生成一个指定所需头值的WSDL。部署时,大多数容器将自动为你生成一个WSDL,因此,你应该要做的所有操作就是,在自己喜欢的IDE中,将该Java类作为WAR项目的一部分来创建,并像对任何其他常规Java类一样,将该类包装在Web-INF/classes中。示例7-19显示了该Web服务。
示例 7-19:需要SOAP头中凭据的电子邮件验证Web服务
package com.soacookbook;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.ws.Holder;
/*
* Very simple Web service that shows how to define a service
* that accepts SOAP headers. This is frequently useful for user
* credentials, an authorization token, or some other
* identifier.
*
* Making param mode INOUT means your type must be a Holder.
*/
@WebService
public class EmailCheck {
private static String NO_CREDENITALS_MSG =
"You must be registered and supply a " +
"valid username and password to use this service.";
/**
* The single op we'll expose on the service. Checks the
* value of the email address passed in.
* @param email The address clients want to verify.
* @param username The username of a hypoethetically
* pre-registered user so that only authorized users
* can access the service.
* @param password the passsword for this ‘registered' user.
* @return a string indicating if the email address is
* valid or not.
*/
public String verify(
@WebParam(mode=WebParam.Mode.IN,
name="email")String email,
@WebParam(mode=WebParam.Mode.INOUT, header=true,
name="username") Holder<String> username,
@WebParam(mode=WebParam.Mode.INOUT, header=true,
name="password") Holder<String> password){
if (!isValidUser(username, password))
return NO_CREDENITALS_MSG;
//Silly check. Robust business logic here...
if (email != null && email.endsWith(".com")) {
return "VALID";
} else {
return "INVALID";
}
}
/*Checks that the supplied username/password combination
is valid. Replace this with a run to a database or LDAP.
*/
private boolean isValidUser(Holder<String> username,
Holder<String> password) {
boolean suppliedUsername = username != null &&
!username.value.isEmpty();
boolean suppliedPassword = password != null &&
!password.value.isEmpty();
if (suppliedUsername && suppliedPassword) {
//log
System.out.println("Username: " + username.value);
System.out.println("Password: " + password.value);
//check registered user credentials in LDAP
//or database or something
boolean validUser = username.value.equals("eben") &&
password.value.equals("secret");
if (validUser) {
return true;
}
}
return false;
}
}
突出显示的代码说明的是该服务中用来处理SOAP头及其值的相关部分。
为了使用这个Web服务,首先需要使用wsimport工具导入WSDL并生成与之匹配的客户端类,就像以前所做的那样。请看示例7-20。
示例 7-20:传递SOAP头的电子邮件验证Web服务客户端
package headerclient;
import com.soacookbook.headerClient.EmailCheckService;
import com.soacookbook.headerClient.EmailCheck;
import com.soacookbook.headerClient.Verify;
import com.soacookbook.headerClient.VerifyResponse;
import javax.xml.ws.Holder;
/**
* Invokes a Web service that checks the validity of an
* email address. The Web service requires a registered
* user's credentials as SOAP headers, so we use a
* {@code javax.xml.ws.Holder<T>} to pass the username
* and password.
*/
public class EmailHeaderClient {
/**
* Command line client invokes the Web service,
* passing header data along with the operation parameter.
*/
public static void main(String... args) {
String emailToCheck = "me@example.com";
try {
EmailCheckService service = new EmailCheckService();
EmailCheck port = service.getEmailCheckPort();
//This is the email address we want to check
Verify params = new Verify();
params.setEmail(emailToCheck);
//Used for header data, because username is a String
Holder<String> username = new Holder<String>();
//Holder's value is of type T
username.value = "eben";
//Same deal for password header
Holder<String> password = new Holder<String>();
password.value = "secret1";
//Note that we pass the SOAP header into the op
VerifyResponse result =
port.verify(params, username, password);
System.out.println("Email check result: " +
result.getReturn());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
如果传递一个无效的用户名和密码组合,就会得到如下结果:
C:\oreilly\soacookbook\code\chapters\HeaderClient>java -jar dist/HeaderClient.jar
Email check result: You must be registered and supply a valid username and password
to use this service.
如果使用有效的用户名和密码组合,就应该获得一个较为令人高兴的消息:
C:\oreilly\soacookbook\code\chapters\HeaderClient>java -jar dist/HeaderClient.jar
Email check result: VALID
本章小结
在本章中,我们介绍了许多不同的方法来处理你所面临的各种挑战,尤其是当提供Web服务而不是使用它们时。我们说明了如何使用Holder、头和MTOM来接收二进制数据以及如何使用SOAPAction之类的结构,并讨论了提供服务时的基本条件。