14.5 使用CXF开发Web Service

前面详细介绍了Web Service平台的两个重要内容:SOAP和WSDL,从前面的介绍可以看出,不管是SOAP,还是WSDL,它们都比较复杂,如果开发者希望自己手动编写WSDL来开发Web Service,难度是相当大的。

为了降低Web Service的开发难度,Java领域提供了两个成熟的Web Service开发框架:Axis和CXF,这两个框架都由Apache软件基金组织提供支持,而且都具有简单易用的特性。由于CXF具有更广泛的占有率,因此本书将介绍使用CXF来开发Web Service。

14.5.1 CXF概述

Apache CXF是一个开源的Service框架,可用于简化用户的Service开发,基于CXF开发的应用可提供SOAP、XML/HTTP、RESTful HTTP或CORBA等服务。CXF底层也可以使用不同的传输协议,包括HTTP、JMS或JBI等。

根据CXF官方站点的说法,CXF包含以下特性:

alt 支持大量的Web Service标准,包括SOAP、WS-I Basic Profile、WSDL、WS-Addressing、WS-Policy、WS-ReliableMessaging和WS-Security。

alt CXF支持大量前端(frontend)编程模型。CXF实现了标准的JAX-WS API,它也包括一种被称为简单前端(simple frontend)的模型,这种模型无须Annotation支持。CXF支持Web Service的两种开发模式:① 规则(contract)优先的开发模式,即通过编写WSDL来开发Web Service;② 代码优先的开发模式,即通过编写Java代码来开发Web Service。


alt提示

WSDL的作用就是描述Web Service,定义服务所提供的操作,以及操作支持消息数据的信息等。因此,Web Service的WSDL文档可称为Web Service的规则(contracts)。对于大部分Java程序员而言,他们更熟悉如何编写Java代码,因此往往更愿意采用代码优先的开发模式,本书也将具体介绍这种开发模式。


CXF起源于早期的两个框架:Celtix和XFire,或者说,Celtix和XFire合并产生了今天的CXF框架。Celtix是ESB(Enterprise Service Bus)架构实现框架,它提供了一个面向服务的体系,用来简化商业服务的构建和集成。

XFire本身就是一个相当成熟的Web Service框架,在合并进CXF框架之前,就已经是一个可以和Axis鼎足而立的Web Service框架了。它同样支持大量的Web Service标准:SOAP、WSDL、WS-I Basic Profile、WS-Addressing和WS-Security。


alt提示

SOA的两个基石就是Web Service和ESB。XFire是一个非常成熟的Web Service框架,而Celtix则是一个成熟的ESB框架。由此可见,与其说CXF是一个Web Service框架,不如说CXF是一个SOA的开源解决方案。


14.5.2 下载和安装CXF

下载并安装CXF的步骤如下:

(1)登录CXF的官方站点:http://cxf.apache.org/,下载CXF最新稳定版。笔者成书之时,该项目的最新版本是CXF 2.4.0,建议读者也下载该版本。

(2)下载完成后将得到apache-cxf-2.4.0.zip文件,解压缩后可看到如下所示目录结构:

alt bin:该目录下保存了CXF提供的一些小工具,这些工具的主要作用是完成根据WSDL代码生成Java代码,以及根据WSDL代码生成JavaScript代码等代码生成任务。

alt docs:该目录下有一个api子目录,其中保存了CXF的API文档。

alt etc:该目录主要存放了CXF框架的一些杂项文档。

alt lib:该目录下存放了CXF的核心类库(cxf-2.4.0.jar)及其编译和运行所依赖的第三方类库。

alt licenses:该目录下存放了CXF以及第三方框架的授权文件。

alt modules:该目录下存放了CXF按模块打包的JAR包。

alt samples:该目录下存放了CXF的大量示例应用,这些示例应用是学习CXF极好的资料。

alt LICENSE和README等文档。

(3)为了在项目中使用CXF框架,除了需要将cxf-2.4.0.jar文件添加到系统类加载路径中之外,还需要将CXF框架所依赖的第三方类库添加到系统类加载路径下。下面是CXF所依赖的核心JAR包:

alt asm-3.3.jar。

alt commons-logging-1.1.1.jar。

alt geronimo-servlet_3.0_spec-1.0.jar。

alt jetty-continuation-7.3.1.v20110307.jar。

alt jetty-http-7.3.1.v20110307.jar。

alt jetty-io-7.3.1.v20110307.jar。

alt jetty-server-7.3.1.v20110307.jar。

alt jetty-util-7.3.1.v20110307.jar。

alt neethi-3.0.0.jar

alt wsdl4j-1.6.2.jar

alt xmlschema-core-2.0.jar。

在应用中添加这些JAR包之后就可以使用CXF来开发Web Service了。

(4)将CXF解压缩路径下的bin目录添加到系统的PATH环境变量,以便操作系统能找到bin目录下的命令,方便我们以后使用CXF提供的小工具。

14.5.3 使用CXF开发Web Service

本书所介绍的开发方式是代码优先的开发模式,也就是说,程序员只需按CXF要求开发符合要求的Java文件即可,CXF将会自动生成所需的WSDL文档,并将其导出成Web Service。

下面先为Web Service服务提供类定义的一个接口:

程序清单:codes\14\14.5\firstWs\src\org\crazyit\cxfapp\service\FirstWs.java

alt

接下来再为该接口提供对应的实现类:

程序清单:codes\14\14.5\firstWs\src\org\crazyit\cxfapp\service\impl\FirstWsImpl.java

alt

上面的FirstWs和FirstWsImpl都使用了@WebService注释来标注,它是位于javax.jws包下的一个接口,属于JAX-WS规范的注释。


alt提示

如果读者需要了解@WebService Annotation的功能和用法,可以参考Java EE规范的API文档,或参考疯狂Java体系的《经典Java EE企业应用实战》。


上面的Web Service服务的接口和实现类中都用到了一个User类,它是一个简单的DTO(数据传输对象),对于熟悉Java EE多层架构的读者来说,DTO的概念是非常容易理解的:业务逻辑层通常不会将底层数据直接传到表现层,而是选择将其包装成DTO或DTO集合再传到视图页面,这样才能保证各层之间的良好分离。

User类的代码如下:

程序清单:codes\14\14.5\firstWs\src\org\crazyit\cxfapp\domain\User.java

alt

alt 经过上面的步骤,使用CXF开发Web Service所需的服务提供类就已经开发完成了,下面使用一个简单的主类来发布该Web Service。CXF支持两种常用的发布Web Service的方式:

alt 使用JAX-WS所提供的Endpoint发布Web Service。

alt 使用CXF自身提供的JaxWsServerFactoryBean工厂类发布Web Service。

如果使用JAX-WS提供的Endpoint来发布Web Service,使用以下代码即可:

alt

如果使用CXF自身提供的JaxWsServerFactoryBean来发布Web Service,则可增加更多的控制,例如,增加额外的CXF拦截器,如以下代码片段所示:

alt

需要指出的是,CXF不再推荐使用JaxWsServerFactoryBean来发布Web Service,而是推荐使用兼容JAX-WS规范的Endpoint来发布Web Service,因此本书后面的内容都将采用JAX-WS的Endpoint来发布Web Service。


alt提示

拦截器是CXF的一项重要机制,通过使用拦截器可以极好地扩展CXF的功能。关于CXF的拦截器机制,读者可以类比Struts 2。本章后面还会介绍如何通过CXF拦截器来为Web Service增加权限控制。


下面是本应用的服务器类代码:

程序清单:codes\14\14.5\firstWs\src\lee\Server.java

alt

上面的服务器类包含了两种发布Web Service的代码,其中前面的代码使用了JAX-WS的Endpoint发布Web Service的代码,后面的则是使用CXF提供的JaxWsServerFactoryBean发布Web Service的代码。运行上面的Server类可以看到如图14.10所示的结果。

alt

图14.10 启动Web Service的控制台

实际上,使用CXF运行Web Service同样需要Web服务器的支持,但本应用程序并没有部署到Tomcat之下,这是因为CXF默认使用了Jetty 7.3.1作为Web服务器。


alt提示

Jetty是Java领域另一个非常优秀的Web服务器,它还可以作为内置有Web服务器使用,如上所示。如果读者需要获取有关Jetty的更多详细信息,可以参考疯狂Java体系的《轻量级Java EE企业应用实战》第1章。


在Web Service发布成功之后,我们可以使用浏览器来访问该Web Service所对应的WSDL,在浏览器地址栏里输入http://localhost:9999/crazyit?wsdl即可看到该Web Service所对应的WSDL,如图14.11所示。

alt

图14.11 Web Service对应的WSDL文档

通过查看该Web Service对应的WSDL文档,客户端即可远程调用该Web Service。


alt提示

前面已经提到,Web Service是跨平台、跨语言的,如果读者有其他语言编程经验,比如C#、Delphi等,那么由于这些语言都提供了调用Web Service的方法,因此完全可以使用它们调用此处暴露的Web Service服务。


本示例依然使用Java程序来调用Web Service,CXF提供的JaxWsProxyFactoryBean可用于获取Web Service代理,客户端将该代理对象当成Web Service服务,提供者调用其方法即可。下面是本示例程序的客户端代码:

程序清单:codes\14\14.5\firstWs\src\lee\Client.java

alt


alt学生提问:是否必须将Web Service服务接口的.class文件复制到客户端呢?


答:从上面的代码中可以看出,调用Web Service操作时至少需要FirstWs和User两个类,看上去似乎应该将Web Service服务接口的.class文件复制到客户端才行。但如果这样做,那就违背了Web Service的初衷——如果客户端与服务器端不在同一个地方,如果客户端与服务器没有物理接触,我们就将无法直接将服务器端的任何类复制到客户端。在这种情况下,我们完全可以借助于CXF提供的wsdl2java工具根据WSDL文档来生成Java类。wsdl2java工具位于CXF解压缩路径的bin目录下,该命令的运行格式如下所示:

alt

关于该命令的用法,读者可运行wsdl2java -?获取。使用wsdl2java来生成Java文件时,不仅可以生成Web Service接口等Java文件,还可以生成一个简单的Web Service客户端。

alt


下面将介绍一种称为动态客户端的访问方式,在这种访问方式下,我们将模拟客户端无法接触Web Service服务提供者的情形。

14.5.4 动态客户端

使用CXF开发动态客户端需要借助于wsdl2java工具,因此首先运行以下命令:

alt


alt注意

运行上面的命令时一定要保证Web Service已经启动,而且在http://localhost:9999/ crazyit处对外提供服务。添加-frontend jaxws21选项的原因是CXF 2.4默认使用JAX-WS 2.2规范,但JDK 1.6却只支持JAX-WS 2.1规范。因此该选项指定生成兼容JAX-WS 2.1规范的客户端代码。


运行上面的命令后,系统将会生成一个org\crazyit\cxfapp\service文件结构,该文件结构下包含了大量的Java源文件,这些源文件是开发动态客户端的基础。

有了wsdl2java根据WSDL所生成的Java源文件之后,接下来只需提供以下简单的Java文件即可调用远程的Web Service:

程序清单:codes\14\14.5\Dyna_Client\src\lee\DynaClient.java

alt

上面的客户端代码中用到的FirstWsService、FirstWs、User都是wsdl2java工具根据WSDL文档生成的Java类,不再由Web Service服务端提供。

wsdl2java工具所生成的XxxService(本例中是FirstWsService)是一个用于简化Web Service客户端开发的类,上面的动态客户端代码直接使用了FirstWsService对象来获取Web Service代理,这样将更加简洁。

14.5.5 复杂类型的处理

正如前面示例中所看到的,如果Web Service所暴露的操作的参数、返回值类型不是基本类型、String等简单类型,而是User、List等类型时,Web Service依然可以处理得很好,这得益于XML Schema的功能。前面介绍XML Schema时已经指出,XML Schema最大的优势在于自定义类型,而且这种自定义类型是与语言、平台无关的,Web Service正是通过自定义类型来实现跨平台、跨语言的数据交换。

打开前面Web Service对应的WSDL文档,该文档中第一行导入了另一份WSDL文档,如下代码所示:

alt

使用XML Spy打开http://localhost:9999/crazyit?wsdl=FirstWs.wsdl文档,将可以看到如图14.12所示的结果。

alt

图14.12 FirstWs的服务接口

从图14.12可以清楚地看到该Web Service包含3个request-response操作,而且可以清晰地看出每个操作所需的Input、Output数据。

以sayHiToUser为例,从图14.12可以看出该操作所需的Input数据为syaHiToUser参数,该参数是一个sayHiToUser元素,该元素的类型定义如下:

alt

从上面定义可以看到,sayHiToUser的Input参数类型为一个序列,该序列里最多可出现一个user类型的<arg0…/>元素,该元素的类型定义如下:

alt

从上面的类型定义发现,XML Schema中的user类型正好对应于Java程序中定义的User类。通过XML Schema提供的自定义类型支持,这才实现了不同平台、不同语言之间的数据交换。

结合前面介绍的XML Schema的知识,通过该WSDL文档可以看出,调用sayHiToUser所需传入的XML文档片段的结构应为:

alt

这份XML片段正好封装一个User实例,当客户端需要调用该Web Service操作时,客户端所发送的SOAP消息包应该包含这份XML文档片段。

sayHiToUser操作的Output数据为sayHiToUserResponse参数,该参数是一个sayHiToUserResponse元素,该元素的类型定义如下:

alt

从上面的类型定义可以看出,sayHiToUser操作响应的XML文档片段应该满足以下格式:

alt

由此可见:对于大部分Java Bean式的复合类型、List集合、数组等,通过CXF开发的Web Service可以自动处理这些类型,但如果Web Service操作的参数、返回值是接口类型,CXF就无法自动处理了。

对于CXF无法自动处理的数据类型,程序员需要自行处理,处理步骤分3步:

(1)提供自定义类,该类通常应该是CXF能处理的类,而且它可以与CXF无法处理的类之间进行相互转换。

(2)实现一个扩展XmlAdapter的子类,它会负责完成CXF无法处理的类与能处理的类之间的转换。

(3)在Web Service接口定义中使用@XmlJavaTypeAdapter修饰CXF无法自动处理的类型(返回值类型,形参类型)。

假如有一个Web Service操作的返回值是一个Map<String , User>类型的值,而CXF显然无法自动处理该Map集合,因此我们需要使用@XmlJavaTypeAdapter来修饰该返回值类型,如下代码所示:

程序清单:codes\14\14.5\ComplexType\src\org\crazyit\cxfapp\service\UserService.java

alt

上面程序中粗体字代码使用了@XmlJavaTypeAdapter修饰Map<String , User>类型,这就是告诉CXF:开发者将会提供MapXmlAdapter来处理Map<String , User>类型。

MapXmlAdapter就是一个自定义的类型转换器,它需要扩展XmlAdapter父类。扩展XmlAdapter需要实现两个抽象方法:

alt abstract ValueType marshal(BoundType v):该方法负责把CXF不能处理的类型转换为CXF能处理的类型。

alt abstract BoundType unmarshal(ValueType v):该方法负责把CXF能处理的类型转换为CXF不能处理的类型。


alt提示

XmlAdapter提供的这两个方法实际上就是进行序列化、反序列化的两个方法,当Web Service操作中参数、返回值中包含CXF无法自动处理的类型时,解决问题的思路就是把这种不能自动处理的类型转换为CXF能自动处理的类型。为此开发者需要做两件事情:①开发一个CXF能处理的自定义类型(这种类型可以与CXF不能自动处理的类型相互转换);②提供一个转换器,该转换器负责完成自定义类型与CXF不能处理的类型之间的相互转换。


为了封装Map<String , User>类型的对象,本程序提供了一个StringMap类,该StringMap封装了一个List集合,List集合的每个元素就是key-value对象。StringMap类的代码如下:

程序清单:codes\14\14.5\ComplexType\src\org\crazyit\cxfapp\util\StringMap.java

alt

上面StringMap封装的List集合元素是Entry类型,每个Entry对象封装Map<String , User>的一个key-value对,Entry类的代码如下:

程序清单:codes\14\14.5\ComplexType\src\org\crazyit\cxfapp\util\Entry.java

alt

该程序提供的StringMap可用于封装Map<String, User>对象,接下来只要提供一个扩展XmlAdapter的MapXmlAdapter子类即可,该子类用于完成StringMap与Map<String, User>之间的转换。下面是MapXmlAdapter转换器的代码:

程序清单:codes\14\14.5\ComplexType\src\org\crazyit\cxfapp\util\MapXmlAdapter.java

alt

经过上面步骤之后,这种Map<String , User>类型将会由MapXmlAdapter处理,MapXmlAdapter会自动完成Map<String, User>与StringMap之间的相互转换。而StringMap封装的是一个List集合,集合元素是一个Entry类——它是一个典型的Java Bean式的复合类,因此CXF可以自动处理它们。

经过上面处理之后,使用Endpoint来发布Web Service,当客户端调用proUsers(List<User> users)操作时,虽然服务器端返回的是Map<String,User>对象,但实际客户端得到的是StringMap对象——这就是MapXmlAdapter处理后的结果。因此客户端代码如下:

程序清单:codes\14\14.5\ComplexType_Client\src\lee\Client.java

alt

从上面粗体字代码可以看出,调用proUsers()方法返回的是StringMap对象,而StringMap是CXF可以自动处理的类型,这样就解决了复杂类型数据的传输问题。

14.5.6 使用拦截器

前面已经提到,CXF的拦截器可以很好地扩展CXF的功能,开发者可以在服务器端、客户端添加拦截器来进行额外的处理。例如,检查、修改Input、Output的SOAP消息包,通过检查、修改这些SOAP消息包即可进行权限控制。

CXF的拦截器示意图如图14.13所示。

alt

图14.13 CXF拦截器示意图

从图14.13所示示意图中可以看出:CXF拦截器分为In拦截器和Out拦截器,其中In拦截器用于拦截传入的SOAP消息包,Out拦截器用于拦截传出的SOAP消息包。

CXF内置了两个拦截器:LoggingInInterceptor和LoggingOutInterceptor,这两个拦截器用于拦截、打印传入、传出的SOAP消息包。

下面为前面介绍的Web Service的服务器端程序添加In拦截器、Out拦截器,程序如下:

程序清单:codes\14\14.5\Interceptor_Server\src\lee\Server.java

alt

下面为前面介绍的Web Service的客户端程序添加In拦截器、Out拦截器,程序如下:

程序清单:codes\14\14.5\Interceptor_Client\src\lee\DynaClient.java

alt

通过上面代码添加拦截器之后,无论在客户端,还是在服务器端都可以看到每次Web Service交互接收、发送的SOAP消息包。以sayHiToUser为例,调用sayHiToUser时传入的SOAP消息包如下:

alt

上面这份SOAP消息包与前面介绍WSDL所分析的SOAP消息包的结构完全一致,这就是调用sayHiToUser所需要的SOAP消息包。

调用sayHiToUser后返回的SOAP消息包如下:

alt

从两份SOAP消息包可以看出,这两份SOAP消息包的<Body…/>部分正是由WSDL文档规范所约定,而这两份SOAP消息包都不包含<Header…/>部分,这说明SOAP消息包的<Header…/>部分是可选的。

通过对SOAP包的分析不难看出,对于Java开发者来说,Web Service所提供的方法调用只是“假相”,实际上双方交互的信息只是SOAP消息包——也就是XML文档。

实际上,Web Service一次调用过程的本质是:

(1)客户端调用,先发送一个SOAP消息包(XML文档)。Web Service负责把Java对象“序列化”成XML文档(SOAP消息),该SOAP消息包里包含了所有的请求参数。

(2)SOAP消息包通过网络传到服务器。

(3)服务器读到XML文档(SOAP消息)。

(4)服务器的Web Service将XML文档(SOAP消息)“反序列化”成真正的值。

(5)服务器以真正的值作为参数,调用方法。

(6)服务器把方法返回值“序列化”成XML文档(SOAP消息包)。

(7)通过网络把XML文档(SOAP消息包)传到到客户端。

(8)客户端读到XML文档(SOAP消息包)。

(9)客户端的Web Service将XML文档(SOAP消息包)“反序列化” 成真正的值。

正因为Web Service的本质是以XML格式的SOAP消息包作为数据交换载体,而XML格式是一种与平台无关、语言无关的数据交换格式,这才可以实现与平台无关、与语言无关的数据交换。

14.5.7 使用拦截器进行权限控制

前面介绍了如何在服务器端、客户端添加拦截器,如果我们在客户端通过拦截器来修改SOAP消息,向SOAP消息的<Header../>部分添加授权的用户名、密码,这样客户端每次调用Web Service时都会带上用户名、密码信息,而服务器端则负责检查SOAP消息的<Header../>部分,检查调用者的用户名、密码是否符合要求,这样就可实现权限控制了。

CXF的拦截器需要实现Interceptor接口,但实际上都是实现它的子接口:PhaseInterceptor——它专门拦截特定阶段的SOAP消息。CXF为PhaseInterceptor 提供了一个抽象实现类:AbstractPhaseInterceptor,开发者一般通过扩展该抽象类来实现自己的拦截器。

下面为服务器端开发一个权限控制的拦截器,该拦截器会检查SOAP消息的<Header…/>部分,从中提取客户端的用户名、密码信息,并进行权限检查。该权限控制的拦截器代码如下:

程序清单:codes\14\14.5\Auth_Server\src\org\crazyit\cxfapp\auth\ AuthIntercetpr.java

alt

alt

上面拦截器中粗体字代码所实现的public void handleMessage(SoapMessage message)方法用于拦截、处理SOAP消息。上面程序中①号代码通过super显式调用了父类构造器,调用父类构造器时指定该拦截器在Phase.PRE_INVOKE阶段起作用——也就是在调用Web Service之前会起作用,这就可以在调用之前进行权限控制了。

实现上面拦截器后,通过以下代码在服务器端启用该拦截器:

程序清单:codes\14\14.5\Auth_Server\src\lee\Server.java

alt

由于上面权限控制拦截器会检查SOAP消息的<Header…/>部分,根据<Header…/>部分来验证调用者是否有权限来调用该Web Service,因此客户端代码需要通过拦截器来修改SOAP消息包,向SOAP消息包的<Header../>部分添加用户名、密码信息,下面是客户端拦截器的代码:

程序清单:codes\14\14.5\Auth_Client\src\lee\ AddHeaderInterceptor.java

alt

上面拦截器中粗体字代码重写了public void handleMessage(SoapMessage message)方法,该方法可拦截、修改SOAP消息。重写该方法时所做的操作完全就是DOM操作了,该DOM操作就是向SOAP消息的<Header…/>部分添加了相应的用户名、密码信息。程序中①号代码通过super显式调用了父类构造器,调用父类构造器时指定该拦截器在Phase.PREPARE_SEND阶段起作用——也就是在发送SOAP消息之前会起作用,这样就可以在发送SOAP消息之前对SOAP消息包进行修改了。

提供上面的拦截器之后,接下来可以在客户端启用该拦截器,然后再去调用服务器端的Web Service,此时可以看到调用Web Service的SOAP消息如下:

alt

上面SOAP消息包中粗体字代码就是AddHeaderInterceptor添加的<Header…/>头,该<Header…/>头里包含了调用该Web Service的用户名和密码信息。客户端每次调用时都会在SOAP消息的<Header…/>部分添加调用者的用户名和密码信息,而服务器端则可根据该用户名、密码信息来判断调用者是否被授权调用该Web Service。