JAVA的WebService支持


SOA(Service-OrientedArchitecture)面向服务架构是一种思想,它将应用程序的不同功能单元

通过中立的契约(独立于硬件平台、操作系统和编程语言)联系起来,使得各种形式的功能

单元更好的集成。目前来说,WebServiceSOA的一种较好的实现方式,WebService 采用

HTTP 作为传输协议,SOAPSimple ObjectAccess Protocol)作为传输消息的格式。但

WebService并不是完全符合SOA 的概念,因为SOAP 协议是WebService的特有协议,并

未符合SOA 的传输协议透明化的要求。SOAP 是一种应用协议,早期应用于RPC 的实现,

传输协议可以依赖于HTTPSMTP等。

SOA 的产生共经历了如下过程:

通常采用SOA 的系统叫做服务总线(BUS),结构如下图所示:

------------------------------------------------------------------------------------

JAVA中的Web服务规范:

JAVA 中共有三种WebService 规范,分别是JAXM&SAAJJAX-WSJAX-RPC)、JAX-RS

下面来分别简要的介绍一下这三个规范。

(1.)JAX-WS

JAX-WSJavaAPI For XML-WebService),JDK1.6 自带的版本为JAX-WS2.1,其底层支

持为JAXB。早期的基于SOAP JAVA Web服务规范JAX-RPCJavaAPI For

XML-RemoteProcedure Call)目前已经被JAX-WS 规范取代,JAX-WS JAX-RPC的演进

版本,但JAX-WS 并不完全向后兼容JAX-RPC,二者最大的区别就是RPC/encoded样式的

WSDLJAX-WS已经不提供这种支持。JAX-RPC API JAVAEE5 开始已经移除,如

果你使用J2EE1.4,其API位于javax.xml.rpc.*包。

JAX-WSJSR224)规范的API位于javax.xml.ws.*包,其中大部分都是注解,提供API

Web 服务(通常在客户端使用的较多,由于客户端可以借助SDK 生成,因此这个包中的

API 我们较少会直接使用)。

WS-MetaDataJSR181)是JAX-WS的依赖规范,其API 位于javax.jws.*包,使用注解配

置公开的Web 服务的相关信息和配置SOAP 消息的相关信息。

(2.)JAXM&SAAJ

JAXMJAVAAPI For XML Message)主要定义了包含了发送和接收消息所需的API,相当

Web 服务的服务器端,其API 位于javax.messaging.*包,它是JAVAEE 的可选包,因此

你需要单独下载。

SAAJSOAPWith Attachment API For JavaJSR 67)是与JAXM搭配使用的API,为构建

SOAP 包和解析SOAP包提供了重要的支持,支持附件传输,它在服务器端、客户端都需要

使用。这里还要提到的是SAAJ 规范,其API位于javax.xml.soap.*包。

JAXM&SAAJJAX-WS都是基于SOAPWeb服务,相比之下JAXM&SAAJ 暴漏了SOAP

更多的底层细节,编码比较麻烦,而JAX-WS 更加抽象,隐藏了更多的细节,更加面向对

象,实现起来你基本上不需要关心SOAP 的任何细节。那么如果你想控制SOAP 消息的更

多细节,可以使用JAXM&SAAJ,目前版本为1.3

(3.)JAX-RS

JAX-RS JAVA针对REST(RepresentationState Transfer)风格制定的一套Web 服务规范,

由于推出的较晚,该规范(JSR 311,目前JAX-RS的版本为1.0)并未随JDK1.6一起发行,

你需要到JCP 上单独下载JAX-RS 规范的接口,其API 位于javax.ws.rs.*包。

这里的JAX-WS JAX-RS规范我们采用Apache CXF 作为实现,CXF ObjectwebCeltix

Codehaus XFire 合并而成。CXF 的核心是org.apache.cxf.Bus(总线),类似于Spring

ApplicationContextBusBusFactory创建,默认是SpringBusFactory 类,可见默认CXF

是依赖于Spring 的,Bus都有一个ID,默认的BUSIDcxf。你要注意的是Apache CXF

2.2 的发行包中的jar 你如果直接全部放到lib 目录,那么你必须使用JDK1.6,否则会报

JAX-WS 版本不一致的问题。对于JAXM&SAAJ 规范我们采用JDK 中自带的默认实现。

------------------------------------------------------------------------------------

1.JAVAWebService规范 JAX-WS

Web 服务从前面的图中不难看出自然分为ServerClient 两部分,Server公开Web服务,

Client 调用Web服务,JAX-WS的服务端、客户端双方传输数据使用的SOAP 消息格式封

装数据,在后面我们会看到其实SOAP 信封内包装的就是一段XML 代码。

I.服务端示例:

我们先看一个服务器端示例:

(1.)公开Web服务的接口IHelloService

package net.ilkj.soap.server;

import javax.jws.WebService;

@WebService

public interface IHelloService {

Customer selectMaxAgeStudent(Customer c1, Customer c2);

Customer selectMaxLongNameStudent(Customer c1, Customer c2);

}

我们看到这个接口很简单,仅仅是使用类级别注解@WebService就标注了这个接口的方法

将公开为Web 服务,使用了这个注解的接口的所有方法都将公开为Web 服务的操作,如果

你想屏蔽某个方法,可以使用方法注解@Methodexclude=true。我们也通常把公开为Web

服务的接口叫做SEIServiceEndPoint Interface)服务端点接口。

(2.)实现类HelloServiceImpl

package net.ilkj.soap.server;

public class HelloServiceImpl implements IHelloService {

@Override

public CustomerselectMaxAgeStudent(Customer c1, Customer c2) {

if (c1.getBirthday().getTime()> c2.getBirthday().getTime())

return c2;

else

return c1;

}

@Override

public CustomerselectMaxLongNameStudent(Customer c1, Customer c2)

{

if (c1.getName().length()> c2.getName().length())

return c1;

else

return c2;

}

}

这个实现类没有任何特殊之处,但是如果你的实现类还实现了其他的接口,那么你需要在实

现类上使用@WebService 注解的endpointInterface属性指定那个接口是SEI(全类名)。

(3.)Customer类:

package net.ilkj.soap.server;

import java.util.Date;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Customer")

public class Customer {

private long id;

private String name;

private Date birthday;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public StringgetName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public DategetBirthday() {

return birthday;

}

public void setBirthday(Date birthday) {

this.birthday = birthday;

}

}

这个类是公开为Web 服务的接口中的参数类型和返回值,因此你需要使用JAXB 注解告诉

CXF 如何在XMLJavaObject 之间处理,因为前面说过SOAP 消息格式包装的是一段XML

代码,那么无论是服务器端还是客户端在接收到SOAP消息时都需要将XML 转化为Java

Object,在发送SOAP消息时需要将Java Object 转化为XML

(4.)发布Web服务:

package net.ilkj.soap.server;

import javax.xml.ws.Endpoint;

public class SoapServer {

public static void main(String[] args) {

Endpoint.publish("http://127.0.0.1:8080/helloService",

new HelloServiceImpl());

}

}

注意我们发布Web 服务使用的是javax.xml.ws.*包中的EndPoint 的静态方法publish()

(5.)查看WSDL

我们访问http://127.0.0.1:8080/helloService?wsdl 地址,您会看到很长的XML 文件(由于浏

览器的问题,如果你看到的是空白页面,请查看源代码),这就是WSDL(WebServiceDefinition

Language),对于你要访问的Web 服务,只要在其地址后加上,就可以在浏览器中查看用于

描述Web __________务的WSDL,这也是一种XMLWeb 服务能够被各种编程语言书写的程序访

问就是通过WSDL 这种通用的契约来完成的。

如果你已经看到WSDL,那么表示我们的Web 服务发布成功了。你可能会差异,我们没有

借助Tomcat 这样的Web服务器,直接运行一个main 方法是怎么发布的Web 服务呢?其实

CXF 内置了JettyServlet容器),因此你不需要将你的程序部署到Tomcat Web 服务器

也可以正常发布Web 服务。

------------------------------------------------------------------------------------

II.分析WSDL的构成:

下面我们来解释一下你所看到的WSDL 的结构,你可以对照你所生成WSDL(文件太长,

Word 里实在放不下)。

(1.)

这个是WSDL 的根元素,我们要关心的是三个属性,name 属性值为公开的Web 服务的接

口的实现类+Service(上例中为name="HelloServiceImplService",不同的JAX-WS

实现名字是不一样的);targetNamespace指定目标名称空间,targetNamespace 的值被后面

xmlns:tns 属性作为值, 默认是使用接口实现类的包名的反缀

targetNamespace="http://server.soap.ilkj.net/" …

xmlns:tns="http://server.soap.ilkj.net/"),

你可以使用@WebService 注解的targetNamespace属性指定你想要的名称空间。

(2.)

这个元素会通过声明几个复杂数据类型的元素。

一般首先你看到的是Web 服务中的方法参数、返回值所涉及的所有复杂(complex)类型的

元素定义,其中name属性值是这个复杂类型的JAXB 注解的name 属性值,

type 属性是tns:+JAXB注解的name属性值的全小写形式(上例中的方法参数、返回值只涉

及一个复杂类型CustomerCustomer@XmlRootElement注解的name属性值为Customer

因此你会看到<xs:elementname="Customer" type="tns:customer" />)。

再向下你会看到XXX 元素和XXXResponse元素,其中XXX 是方法名称(你可以使用

@WebMethodoperationName属性值指定XXX 的值),XXX 是对方法参数的封装,

XXXResponse是对返回值的封装,上例中你会看到

<xs:elementname="selectMaxAgeStudentMethod"

type="tns:selectMaxAgeStudentMethod" />

<xs:elementname="selectMaxAgeStudentMethodResponse"

type="tns:selectMaxAgeStudentMethodResponse" />

<xs:elementname="selectMaxLongNameStudent"

type="tns:selectMaxLongNameStudent" />

<xs:elementname="selectMaxLongNameStudentResponse"

type="tns:selectMaxLongNameStudentResponse" />

内容,

最 后你会看到一组元素, 这个元素通过name 属性关联到

,它为前面定义的元素指定封装的具体内容(通过子元素

定),上例中方法参数的复杂类型指定如下形式:

<xs:complexTypename="selectMaxAgeStudentMethod">

<xs:sequence>

<xs:elementminOccurs="0" name="arg0" type="tns:customer" />

<xs:elementminOccurs="0" name="arg1" type="tns:customer" />

xs:sequence>

xs:complexType>

我们看到方法参数名称为arg0arg1,如果你想指定方法参数的名字在方法参数前使用

@WebParamname属性指定值,同样,方法的返回值同样可以使用@WebResult 注解指定

相关的属性值。

例如:

@WebResult(name = "method")

CustomerselectMaxAgeStudent(@WebParam(name = "c1") Customer c1,

@WebParam(name = "c2") Customer c2);

(3.)

这个元素将输入参数(方法参数)和响应结果(方法返回值)、受检查的异常信息包装为消

息。

(4.)

这个元素指定Web 服务的端口类型(Web 服务会被发布为EndPoint 端点服务),它的name

属性默认为接口名称(你可以使用@WebService 注解的name 属性指定值)。这个元素包含

了一系列的子元素指定该端点服务包含了那些操作( 方法) ,

的子元素指定操作的输入输出(通过属性

message 绑定到前面声明过的消息)。

(5.)

这个元素将前面最终的端点服务绑定到SOAP 协议(你可以看出来WSDL 从上到下依次有

着依赖关系),其中的styleuse 分别可以使用SOAPBinding 注解的style

use 属性指定值、指定公开的操作(方法)。这部分XML 指定最终发布

Web 服务的SOAP 消息封装格式、发布地址等。

(6.)

这个元素的name 属性指定服务名称(这里与根元素的name 属性相同),子元素

name 属性指定port 名称,子元素location 属性指定Web 服务的地址。

------------------------------------------------------------------------------------

III.客户端调用示例:

我们从上面可以知道Web 服务只向客户端暴漏WSDL,那么客户端必须将WSDL 转换为自

己的编程语言书写的代码。JAX-WS 的各种实现都提供相应的工具进行WSDL JAVA

间的互相转换,你可以在CXF 的运行包中找到bin 目录,其中的wsdl2java.bat 可以将WSDL

转换为JAVA 类,bin 目录的各种bat 的名字可以很容易知道其作用,但要注意JAVA 类转

换为WSDL 最好使用前面的URL?wsdl 的方式获得,因为这样得到的是最准确的。

你可以在命令行将当前目录切换到CXF bin 目录,然后运行wsdl2java –h 查看这个批处理

命令的各个参数的作用,常用的方式就是 wsdljava –p包路径 –d 目标文件夹 wsdlurl

地址。现在我们将前面的WSDL生成客户端代码:

wsdl2java -p net.ilkj.soap.client –d E:\ http://127.0.0.1:8080/helloService?wsdl

你会在E 盘根目录找到生成的客户端代码,然后将它复制到Eclipse 工程即可使用。

如果你使用MyEclipse,可以按照如下步骤从WSDL 生成客户端代码:

New--->Other--->MyEclipse--->WebServices--->Web Services Client,然后依据设置向导即可

完成,但最好还是使用CXF wsdl2java 来完成,因为CXF2.2+版本开始支持JAX-WS2.1

规范,而MyEclipse 自带的好像是XFire wsdl2java,生成的客户端代码可能不是最新规范

的。

我们上面的WSDL 会生成如下所示的客户端代码:

Customer.java

HelloServiceImplService.java

IHelloService.java

ObjectFactory.java

package-info.java

SelectMaxAgeStudent.java

SelectMaxAgeStudentResponse.java

SelectMaxLongNameStudent.java

SelectMaxLongNameStudentResponse.java

其中package-info.javaObjectFactory.java JAXB 需要的文件;HelloServiceImplService.java

继承自javax.xml.ws.Service 类,用于提供WSDL 的客户端视图,里面使用的是大量

javax.xml.ws.*包中的注解;剩下的类是Web 服务的接口、方法参数、响应值的类。

CXF 中使用JaxWsProxyFactoryBean 客户端代理工厂调用Web 服务,代码如下所示:

package net.ilkj.soap.client;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.GregorianCalendar;

import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;

import

com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendar

Impl;

public class SoapClient {

public staticvoid main(String[]args) throws ParseException {

JaxWsProxyFactoryBeansoapFactoryBean = new

JaxWsProxyFactoryBean();

soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService");

soapFactoryBean.setServiceClass(IHelloService.class);

Object o =soapFactoryBean.create();

IHelloServicehelloService = (IHelloService) o;

Customer c1 = new Customer();

c1.setId(1);

c1.setName("A");

GregorianCalendarcalendar = (GregorianCalendar)

GregorianCalendar

.getInstance();

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1989-01-28"));

c1.setBirthday(new XMLGregorianCalendarImpl(calendar));

Customer c2 = new Customer();

c2.setId(2);

c2.setName("B");

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1990-01-28"));

c2.setBirthday(new XMLGregorianCalendarImpl(calendar));

System.out.println(helloService.selectMaxAgeStudent(c1,

c2).getName());

}

}

这里要注意的就是Customer 的生日字段JAX-WS 在客户端映射为了XMLGregorianCalendar

类型。我们运行这个客户端,结果输出A,我们的Web 服务调用成功。你还要注意Web

务调用可能经常出现超时的问题,但你切不可以为只要WSDL 可以访问,就代表Web 服务

一定可以访问,因为是否可以访问与SEI 的实现类有关,而WSDL 仅是SEI 的一种XML

表示。

------------------------------------------------------------------------------------

IV.SOAP消息的格式:

我们从前面了解WebService 使用HTTP 协议传输消息,消息格式使用SOAP,那么在客户

端和服务器端传输的SOAP 消息是什么样子的呢?下面我们将服务端SoapServer.java 的代

码改为如下的形式:

package net.ilkj.soap.server;

import org.apache.cxf.interceptor.LoggingInInterceptor;

import org.apache.cxf.interceptor.LoggingOutInterceptor;

import org.apache.cxf.jaxws.JaxWsServerFactoryBean;

public class SoapServer {

public staticvoid main(String[]args) {

JaxWsServerFactoryBeansoapFactoryBean = new

JaxWsServerFactoryBean();

soapFactoryBean.getInInterceptors().add(new

LoggingInInterceptor());

soapFactoryBean.getOutInterceptors().add(new

LoggingOutInterceptor());

// 注意这里是实现类不是接口

soapFactoryBean.setServiceClass(HelloServiceImpl.class);

soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService");

soapFactoryBean.create();

}

}

我们注意到这里将javax.xml.ws.EndPoint 改为CXF 特有的API---JaxWsServerFactoryBean

并且我们对服务端工厂Bean 的输入拦截器集合、输出拦截器集合中分别添加了日志拦截器

(拦截器是CXF 的一项扩展功能,CXF 提供了很多拦截器实现,你也可以自己实现一种拦

截器),这样可以在Web 服务端发送和接收消息时输出信息。

现在我们再次运行服务器端和客户端,你会看到控制台输出如下信息:

2009-6-17 22:35:57org.apache.cxf.interceptor.LoggingInInterceptor

logging

信息: Inbound Message

----------------------------

ID: 2

Address:/helloService

Encoding: UTF-8

Content-Type:text/xml; charset=UTF-8

Headers:{content-type=[text/xml; charset=UTF-8],

connection=[keep-alive],Host=[127.0.0.1:8080], Content-Length=[367],

SOAPAction=[""],User-Agent=[Apache CXF 2.2.2], Content-Type=[text/xml;

charset=UTF-8],Accept=[*/*], Pragma=[no-cache],

Cache-Control=[no-cache]}

Payload:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

2:selectMaxAgeStudent

xmlns:ns2="http://server.soap.ilkj.net/">1989-01-28T00:

00:00.000+08:001A

1990-01-28T00:00:00.000+08:002B

--------------------------------------

2009-6-17 22:35:57

org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback

onClose

信息: Outbound Message

---------------------------

ID: 2

Encoding: UTF-8

Content-Type:text/xml

Headers: {}

Payload:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

2:selectMaxAgeStudentResponse

xmlns:ns2="http://server.soap.ilkj.net/">1989-01-28

T00:00:00+08:001A

ctMaxAgeStudentResponse>

--------------------------------------

Inbound Message 输出的是服务器端接收到的SOAP 信息,Outbound Message 输出的服务器

端响应的SOAP 信息,SOAP Headers:{}的前面是SOAP 消息的标识、编码方式、MIME

类型,Headers:{}熟悉HTTP 应该很容易看懂这里面的消息报头的作用,Headers:{}后面的

Payload(有效负载,也叫净荷)的XML 就是SOAP 消息的真正内容,我们看到SOAP

息内容被封装为信封,在信封之间的内容就是SOAP 消息正文,这

个元素还有一个子元素,如果你的某些注解的header=true,那么它将被放到

中传输,而不是SOAP 消息正文。

例如我们把服务端的IHelloService 接口改为如下的形式:

package net.ilkj.soap.server;

import javax.jws.WebParam;

import javax.jws.WebService;

@WebService

publicinterface IHelloService {

CustomerselectMaxAgeStudent(

@WebParam(name = "c1", header = true) Customer c1,

@WebParam(name = "c2") Customer c2);

CustomerselectMaxLongNameStudent(Customer c1, Customer c2);

}

我们注意第一个方法的第一个参数的header=true,也就是放在SOAP 的消息头中传输。然

后我们重新生成客户端的代码,SoapClient 的调用代码改为如下的形式:

package net.ilkj.soap.client;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.GregorianCalendar;

import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;

import

com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendar

Impl;

public class SoapClient {

public staticvoid main(String[]args) throws ParseException {

JaxWsProxyFactoryBeansoapFactoryBean = new

JaxWsProxyFactoryBean();

soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService");

soapFactoryBean.setServiceClass(IHelloService.class);

Object o =soapFactoryBean.create();

IHelloServicehelloService = (IHelloService) o;

Customer c1 = new Customer();

c1.setId(1);

c1.setName("A");

GregorianCalendarcalendar = (GregorianCalendar)

GregorianCalendar

.getInstance();

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1989-01-28"));

c1.setBirthday(new XMLGregorianCalendarImpl(calendar));

Customer c2 = new Customer();

c2.setId(2);

c2.setName("B");

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1990-01-28"));

c2.setBirthday(new XMLGregorianCalendarImpl(calendar));

SelectMaxAgeStudentsms = new SelectMaxAgeStudent();

sms.setC2(c2);

System.out.println(helloService.selectMaxAgeStudent(sms,c1)

.getReturn().getName());

}

}

我们注意到现在客户端的IHelloService 的第一个方法的第一个参数是SelectMaxAgeStudent

而不是Customer,运行之后控制台输出如下语句:

2009-6-17 23:02:29org.apache.cxf.interceptor.LoggingInInterceptor

logging

信息: Inbound Message

----------------------------

ID: 2

Address:/helloService

Encoding: UTF-8

Content-Type:text/xml; charset=UTF-8

Headers:{content-type=[text/xml; charset=UTF-8],

connection=[keep-alive],Host=[127.0.0.1:8080], Content-Length=[443],

SOAPAction=[""],User-Agent=[Apache CXF 2.2.2], Content-Type=[text/xml;

charset=UTF-8],Accept=[*/*], Pragma=[no-cache],

Cache-Control=[no-cache]}

Payload:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><

ns2:c1

xmlns:ns2="http://server.soap.ilkj.net/">1989-01-28T00:00:0

0.000+08:001A

xmlns:ns2="http://server.soap.ilkj.net/">1990-01-28T00:

00:00.000+08:002B

xAgeStudent>

--------------------------------------

2009-6-17 23:02:29

org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback

onClose

信息: Outbound Message

---------------------------

ID: 2

Encoding: UTF-8

Content-Type:text/xml

Headers: {}

Payload:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

2:selectMaxAgeStudentResponse

xmlns:ns2="http://server.soap.ilkj.net/">1989-01-28

T00:00:00+08:001A

ctMaxAgeStudentResponse>

--------------------------------------

我 们注意到Inbound Message 中的SOAP 信封将第一个方法的第一个参数放在

中传输。

------------------------------------------------------------------------------------

V.输入输出参数与@Oneway注解:

SOAP 中方法的参数是有流向的, @WebParam 注解的mode 属性由

javax.jws.WebParam.Mode 枚举指定,表示参数的流向,默认是IN,也就是输入参数,还可

以是OUTINOUT 类型。

如果是OUTINOUT 类型的参数类型,这样的方法参数将会被当做返回值在Web 服务调

用完成后返回给你,客户端生成代码时会被转变为javax.xml.ws.Holder类型,注意不要

导错包,不是javax.xml.rpc.Holder 类型(JDK1.6 已经没有这个类型,但是MyEclipse 中还

是会有,小心导入这个已经不使用的类型)。Holder 是一个泛型类,它持有类型T

@javax.jws.Oneway 注解是一个标识性注解,没有任何属性,它表示公开的Web 服务的方法

没有任何返回值,不允许有 OUT 类型的参数,不允许抛出非运行时异常。如果条件不符

JAX-WS 规范要求应该报告错误,但是CXF 的策略是如果方法存在返回值,生成客户端时

将被改为void;如果方法参数含有OUT 类型,生成客户端时将被忽略;如果方法含有INOUT

类型参数,生成客户端时将只作为IN 类型参数被保留。

例:

服务端SEI

@WebService

publicinterface IHelloService {

boolean selectMaxAgeStudent(@WebParam(name = "c1") Customer c1,

@WebParam(name = "c2") Customer c2,

@WebParam(name = "c3", mode = Mode.OUT)Holder c3);

CustomerselectMaxLongNameStudent(Customer c1, Customer c2);

}

客户端调用代码:

public class SoapClient {

public staticvoid main(String[]args) throws ParseException {

JaxWsProxyFactoryBeanclient = new JaxWsProxyFactoryBean();

client.setAddress("http://127.0.0.1:335/ws/services/helloService"

);

client.setServiceClass(IHelloService.class);

IHelloServicehelloService = (IHelloService) client.create();

Customer c1 = new Customer();

c1.setId(1);

c1.setName("A");

GregorianCalendarcalendar = (GregorianCalendar)

GregorianCalendar

.getInstance();

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1989-01-28"));

c1.setBirthday(new XMLGregorianCalendarImpl(calendar));

Customer c2 = new Customer();

c2.setId(2);

c2.setName("B");

calendar

.setTime(new

SimpleDateFormat("yyyy-MM-dd").parse("1990-01-28"));

c2.setBirthday(new XMLGregorianCalendarImpl(calendar));

Holderch = new Holder();

helloService.selectMaxAgeStudent(c1,c2, ch);

System.out.println(ch.value.name);

}

}

但是CXF 很聪明,并不是你的服务端使用OUTINOUT Holder之后,他就一定会生

成客户端时原样照做,例如:上面服务端方法如果将返回值改为void,那么CXF 将会在发

WSDL 时将c3 参数作为WebService 操作的返回值,因此客户端就会和服务端不一样了,

也就是客户端会生成如下的形式:

public net.ilkj.soap.client.Customer selectMaxAgeStudent(

@WebParam(name = "c1", targetNamespace = "")

net.ilkj.soap.client.Customerc1,

@WebParam(name = "c2", targetNamespace = "")

net.ilkj.soap.client.Customerc2

);

因为这确实是一个相等的转换,对于客户端来说,并不影响调用,不过这可能也与具体的

JAX-WS 的实现有关。

------------------------------------------------------------------------------------

VI.Web服务上下文:

javax.xml.ws.WebServiceContext 接口用于在Web 服务实现类中访问与服务请求有关的消息

上下文和安全信息,只需要使用javax.annotation.Resource 这个标准的注解(EJB JAVA EE

规范中的一些资源都使用这个注解注入)标注即可使用这个接口。

这个接口的getMessageContext()方法返回javax.xml.ws.handler.MessageContext 接口,这是一

个实现了Map 接口的接口,它包含了一组属性集。

例:

@Resource

private WebServiceContext context;

public void selectMaxAgeStudent() {

MessageContextmContext = context.getMessageContext();

Setset = mContext.keySet();

for (String key : set) {

System.out.println("***********" + key + "\t"+

mContext.get(key));

try {

System.out.println("+++++++++++" +

mContext.getScope(key));

} catch (Exception e) {

System.out.println("+++++++++++" + key + "is notexits");

}

}

}

上面的方法会打印出出消息上下文中的所有属性集及其范围,因为getScope(String name)

果没有name 存在会抛出异常, 我们这里捕获这个异常。范围的可选值是

javax.xml.ws.handler.MessageContext.Scope 中的APPLICATIONHANDLER 枚举,前者表

示属性对于处理程序(CXF)、客户端应用程序和SEI 都是可见的,后者只对处理程序可见。

这个接口的其他两个方法用于处理java.security.Principal 安全主体对象,这个对象用于处理

请求中的验证的角色对象,一般我们不会用到这种方式去做Web 服务的安全管理。

------------------------------------------------------------------------------------

VII.使用客户端视图:

在前面我们看到服务端发布Web 服务可以使用javax.xml.ws.Endpoint 接口发布Web 服务,

这样你可以在开发JAX-WS 的服务端时完全避开使用底层实现的API,统一使用标准的

JAX-WS 的接口、注解等。但是在客户端访问Web 服务时我们使用了CXF

JaxWsProxyFactoryBean 来进行操作,其实你也可以使用标准的JAX-WS API 完成客户端

调用。

例:

QName qName = new QName("http://server.soap.ilkj.net/",

"HelloServiceImplService");

HelloServiceImplServicehelloServiceImplService =

new HelloServiceImplService(

new URL("http://127.0.0.1:8080/ws/services/helloService?wsdl"),

qName);

IHelloServicehelloService = (IHelloService) helloServiceImplService

.getPort(IHelloService.class);

首先我们使用Web 服务的WSDL 中的targetNamespace 中的name 属性

构建了javax.xml.namespace.QName 接口,然后我们调用生成的客户端代码中的客户端视图

类( 这个类继承javax.xml.ws.Service HelloServiceImplService ( 这个类名一般与

中的name 属性值相同)的构造方法传入WSDL URL 对象和QName

例,在获得客户端视图实例之后调用T getPort(T t)这个泛型方法找到要使用的端点服务接

口。

上面我们说的查找参数都是到WSDL 文件中去找,其实你打开客户端视图类同样可以找到

targetNamespace、服务名称、WSDL URL 地址等信息。

这里你要注意客户端视图中的相关信息其实无关紧要,例如:一个公司中的两个部门的系统

AB 要交互,那么在开发阶段,A 公开给B Web 服务的WSDL URL、名称空间等肯

定都是测试机的信息(尤其是URL IP 地址),这样你生成的客户端视图类中的WSDL

名称空间等都是针对测试的,那么在正式上线时,你将要访问A 的线上Web 服务,这时你

不必担心客户端视图类中的相关信息是客户机的,因为无论是你用JaxWsProxyFactoryBean

还是上面的方式,或者是下面的Spring 的方式,Web 服务地址等信息都是需要重新设置的,

当然你如果使用默认的构造方法(譬如上面的代码new HelloServiceImplService()是使用无参

的构造方法)或者是没有重新设置相关属性,那么将使用客户端视图类中的相关信息。

------------------------------------------------------------------------------------

VIII.JAX-WS的异常处理:

JAX-WS 中的服务端的自定义异常使用javax.xml.ws.WebFault 注解来完成,这样的异常会在

WSDL 文件中的中的子元素生成。下面我们来看一下一个

示例代码:

@WebFault(name = "HelloServiceException")

public class HelloServiceException extends Exception {

/**

*

*/

private staticfinal long serialVersionUID= 1562884941631450124L;

private HelloServiceFault details;

public HelloServiceException(String msg) {

super(msg);

}

public HelloServiceException(String msg, HelloServiceFaultdetails)

{

super(msg);

this.details = details;

}

public HelloServiceException(HelloServiceFault details) {

super();

this.details = details;

}

public HelloServiceFault getFaultInfo() {

return details;

}

@XmlRootElement(name = "HelloServiceFault")

public staticclass HelloServiceFault{

private String t;

public HelloServiceFault() {

}

public HelloServiceFault(String t) {

this.t = t;

}

public String getT() {

return t;

}

public void setT(String t) {

this.t = t;

}

}

}

这里需要注意一下几个问题:

(1.)自定义异常必须包含一个异常消息msg 和一个封装具体错误消息的Bean(这里是

HelloServiceFault),这个Bean 上必须使用JAXB 注解,这样他可以被转换为SOAP 消息中

XML 内容。

(2.)自定义异常中必须有一个getFaultInfo()的方法返回封装具体错误消息的Bean

------------------------------------------------------------------------------------

IX.使用MTOM传输附件:

MTOMSOAP Message Transmission OptimizationMechanismSOAP 消息传输优化机制,

可以在SOAP 消息中发送二进制数据,与SAAJ 传输附件不同,MTOM需要XOPXML-binary

Optimized Packing)来传输二进制数据。MTOM 允许将消息中包含的大型数据元素外部化,

并将其作为无任何特殊编码的二进制数据随消息一起传送。MTOM 消息会打包为多部分相

MIME 序列,放在SOAP 消息中一起传送。因此你可以看出MTOM 并不是将附件转为

Base64 编码,这样可以大大的提高性能,因为二进制文件转Base64 编码会非常庞大。MTOM

已经得到了大多数厂商的支持,包括微软等,所以使用这种方式处理SOAP 中的附件,可

以获得较大的通用性。

下面我们来看一个例子:

(1.)服务端的Customer 类:

import java.util.Date;

import javax.activation.DataHandler;

import javax.xml.bind.annotation.XmlAccessType;

import javax.xml.bind.annotation.XmlAccessorType;

import javax.xml.bind.annotation.XmlMimeType;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Customer")

@XmlAccessorType(XmlAccessType.FIELD)

public class Customer {

private long id;

private String name;

private Date birthday;

@XmlMimeType("application/octet-stream")

private DataHandler imageData;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Date getBirthday() {

return birthday;

}

public void setBirthday(Date birthday) {

this.birthday = birthday;

}

public DataHandler getImageData() {

return imageData;

}

public void setImageData(DataHandler imageData) {

this.imageData = imageData;

}

}

这里我们看到MTOM 方式中要传输的附件必须使用javax.activation.DataHandler 类,然后这

个类型还要使用@javax.xml.binding.annotation.XmlMimeType 进行注解,标注这是一个附件

类型的数据,这里我们标注imageData 是一个二进制文件,当然你也可以使用具体的MIME

类型,譬如:image/jpgimage/gif 等,但你要考虑客户端是否有对应的类型(因为JAVA

语言之外的客户端的特性未必是你完全了解的),而且javax.activation.*中的MIME 的相关

API 可以完成MIME 类型的自动识别。

这里你要注意的是必须在类上使用@XmlAccessorType(FIELD)注解,标注JAXB 在进行

JAVA 对象与XML 之间进行转换时只关注字段,而不关注属性(getXXX()方法),否则发

Web 服务时会报出现了两个imageData 属性的错误,不知道这是不是CXF 或者是底层

JAXB 实现的BUG,因为它并没有报其他的属性因为getXXX()方法的存在而视为重复属性。

这个时侯发布的WSDL 会包含如下的内容:

<xs:element minOccurs="0" name="imageData"

ns1:expectedContentTypes="application/octet-stream"

type="xs:base64Binary"

xmlns:ns1="http://www.w3.org/2005/05/xmlmime" />

接下来你要在服务端和客户端分别启用MTOM 支持,Spring 的配置文件如下所示:

<jaxws:properties>

<entry key="mtom-enabled"value="true"/>

jaxws:properties>

这段内容加到之间即可,也就是作

为他们的子元素存在。如果你想使用Java Code 实现,你可以在服务端、客户端获取

javax.xml.ws.soap.SoapBinding 实例,然后调用它的setMTOMEnabled(true)方法。其实从这

里你可以JAX-WS 是天然支持MTOM 的,只不过默认禁用了这一功能,因为在没有附件这

种大量数据要传输,MTOM 的优点并不会体现出来。

我们假设服务端SEI 的实现的一个方法如下所示:

public Customer selectMaxLongNameStudent(Customer c1, Customerc2) {

Customer rs = null;

if (c1.getName().length() > c2.getName().length())

rs = c1;

else

rs = c2;

rs.setImageData(new DataHandler(new FileDataSource(

new File("c:"+ File.separator + "18.jpg"))));

return rs;

}

我们看到DataHandler 需要DataSource 进行构造,这里我们用到了javax.activation.DataSource

的一个文件实现类来实现。

客户端调用代码如下所示:

… …

StringattachmentMimeType = helloService.selectMaxLongNameStudent(c1,

c2).getImageData().getDataSource().getContentType();

System.out.println(attachmentMimeType);

你可以看到控制台输出image/jpegMTOM 传输附件成功。如果你使用了日志拦截器,

你会看到服务端的控制台打印出了很多乱码,这些乱码就是传输的附件。

如果你使用的是MyEclipse 开发,你会看到在运行客户端时,有如下错误被抛出:

java.lang.NoClassDefFoundError:com/sun/mail/util/LineInputStream

这是由于MyEclipse 在新建Web 工程时给你添加的javaee.jar 中的javax.mail.*包与JDK1.6

自带的javax.mail.*包版本冲突,你只需要删除MyEclipse 中的javaee.jar 中的mail 包即可。

其实在JDK1.6 之后你会经常运行程序报出找不到某个类或者是某个类转型失败的异常,主

要原因是JDK1.6 中把JAVA EE5 中的许多规范都纳入进来,而且版本也比JAVA EE5 要高,

譬如JAX-WS2.1 就比早一些发布的JAVA EE5 JAX-WS2.0 要新,这种版本不一致的问题

要注意。另外的原因就是很多人根本不清楚JAVA EE 的各项规范,构建应用总是喜欢把一

jar 文件都弄进来,你会发现有些人即使在JDK1.6 环境中构建企业级应用,依然会加入

jaxb-api.jarjaxb-impl.jarxalan.jarxerces.jarjsr181.jar… …,要知道这些规范JDK1.6

中已经包含并附带默认实现,这样不分清楚的加进上面的jar 文件,极易造成与JDK1.6

带规范、默认实现冲突的问题,所以最好详细掌握JAVA EE 的体系结构。

关于SOAP消息中传输附件的规定:

在使用MTOM 传输附件时,控制台输出的信息我们摘入主要的如下所示:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

2:selectMaxLongNameStudentResponse

xmlns:ns2="http://server.soap.ilkj.net/">2B

ame>1990-01-28T00:00:00+08:00

ludexmlns:xop="http://www.w3.org/2004/08/xop/include"

href="cid:507f46da-6ff9-4174-8929-5448e128359b-1@http%3A%2F%2Fcxf.apa

che.org%2F"/>

nse>

--uuid:f9eb12c5-740a-4afc-8be2-c94654e787c1

Content-Type:image/jpeg

Content-Transfer-Encoding:binary

Content-ID:

<507f46da-6ff9-4174-8929-5448e128359b-1@http://cxf.apache.org/>

附件的二进制代码(乱码)

--uuid:f9eb12c5-740a-4afc-8be2-c94654e787c1

我们看到二进制文件使用了--uuid:***(相当于二进制文件与SOAP 其他消息部分的分隔符,

这个分隔符由Header 中的boundary 属性值指定,从这里你可以看到SOAP 消息的内容和

HTTP 中的各项规定没什么不同,熟悉HTTP 的应该知道表单提交附件也是这种形式)进行

包围,在顶端的-uuid:***之后跟着附件的MIME 头信息,这里最主要的就是Content-ID,如

果你在一个SOAP 消息中传输了多个附件,那么这个Content-ID 必须是唯一的。我们再看

SOAP 消息体中的元素中使用了如下结构:

MIME头中

Content-ID去除左右两侧的<>"/>

这段代码就是与下面的二进制文件关联的所在,也是上面我们说的那个XOP

那么我们如果禁用MTOM,还要传递附件,此时,附件会被编为BASE64 码进行传递,如

下所示:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

2:selectMaxLongNameStudent

xmlns:ns2="http://server.soap.ilkj.net/">1A

e>1989-01-28T00:00:00.000+08:00BASE64

2B1990-01-28

T00:00:00.000+08:00<

/soap:Body>

这种方式传递附件的缺点很明显,一个10KB 的图片的BASE64 码在WORD 里都可以用四

篇来显示,那么大一些的附件将会使得XML 的体积迅速膨胀,这与MTOM 的原样传输二

进制数据是没有可比性的。

------------------------------------------------------------------------------------

2.JAVAWebService规范 JAX-RS

REST 是一种软件架构模式,只是一种风格,不是像SOAP 那样本身承载着一种消息协议,

(两种风格的Web 服务均采用HTTP 做传输协议是因为HTTP 协议能穿越防火墙,JAVA

的远程调用RMI 等是重量级协议,不能穿越防火墙),因此你也可以叫做REST 是基于HTTP

协议的软件架构。REST 中重要的两个概念就是资源定位和资源操作,而HTTP 协议恰好完

整的提供了这两个要点,HTTP 协议中的URI 可以完成资源定位,GETPOSTOPTION

等方法可以完成资源操作,因此REST 完全依赖HTTP 协议就可以完成Web 服务,而不像

SOAP 协议那样只利用HTTP 的传输特性,定位与操作由SOAP 协议自身完成,也正是由于

SOAP 消息的存在,使得SOAP 笨重。你也可以说REST 充分利用了HTTP 协议的特性,而

不是像SOAP 那样只利用了其传输这一特性(事实上大多数人提到HTTP 协议就只会想到

它能用于数据传输)。

REST 对于HTTP 的利用分为以下两种:首先是资源定位,这就是URI,这本身并没有什么

特别的,但要注意REST HTTP 的资源定位理解更加到位,也就是你的Web 服务的URI

要能足够表意,例如:http://www.fetion.com.cn/fetionwap/baby/getBabyInfoById?id=1,从URI

上可以看出这个Web 服务定位到的资源是查询飞信WAP 宠物的信息,依据参数id 值查询。

那么可以继续出现以下层级:

http://www.fetion.com.cn/fetionwap/baby/storeroom/getStoreRoomById?id=1

http://www.fetion.com.cn/fetionwap/baby/storeroom/chicken/getCounts?id=1

我们看到REST 风格的URI 的目录层级足够表意,也就是资源定位,这种定位要求URI

唯一的。因为REST 流行于互联网,网上的资源应该有唯一的资源位置(例如:图片、视频)。

当然,如果你的服务越复杂,URI 可能就越长,越难理解,这也算是REST 风格的缺点。

第二种就是利用HTTP GETPOSTPUTDELETE 四种操作外加HEAD 请求报头完成

资源操作,你可以把前四种HTTP 的操作类比成数据库操作的SELECTUPDATEINSERT

DELETE 操作,有这几种最简单的操作任意组合就可以完成各种各样的复杂操作,当然这是

REST 的理念,事实上这样创建应用有点儿牵强。

REST 是一种软件架构理念,现在被移植到Web 服务上(因此不要提到REST 就马上想到

WebServiceJAX-RS 只是将REST 设计风格应用到Web 服务开发),那么在开发Web

务上,偏于面向资源的服务适用于REST,偏于面向活动的服务。另外,REST 简单易用,

效率高,SOAP 成熟度较高,安全性较好。

REST 提供的网络服务叫做OpenAPI,它不仅把HTTP 作为传输协议,也作为处理数据的工

具,可以说对HTTP 协议做了较好的诠释,充分体现了HTTP 技术的网络能力。目前Google

Amazon、淘宝都有基于REST OpenAPI 提供调用。

------------------------------------------------------------------------------------

JAX-RS API javax.ws.rs.*包中,其中大部分也是注解。

我们先看一个较为简单的示例:

(1.)IStudentService.java

package net.ilkj.rest.server;

import javax.ws.rs.GET;

import javax.ws.rs.Path;

import javax.ws.rs.PathParam;

import javax.ws.rs.Produces;

import javax.ws.rs.QueryParam;

@Path(value = "/student/{id}")

@Produces("application/xml")

publicinterface IStudentService{

@GET

@Path(value = "/info")

StudentgetStudent(@PathParam("id") long id, @QueryParam("name")

String name);

@GET

@Path(value = "/info2")

StudentgetStudent(@QueryParam("name") String name);

}

说明:

1.这个REST 的服务接口的最终响应结果是XML@Produces 注解标注,这个注解可以包含

一组字符串,默认值是*/*,它指定REST 服务的响应结果的MIME 类型,例如:

application/xmlapplication/jsonimage/jpeg 等),你也可以同时返回多种类型,但具体生

成结果时使用哪种格式取决于ContentTypeCXF 默认返回的是JSON 字符串。

2.访问方法URI /student/1/info?name=Andrew-Lee/student/1/info2?name=Fetion,由@Path

注解组合而来;

3.@QueryParam 注解用于指定将URL 上的查询参数传递给使用这个注解的属性值;

4.@PathParam 注解用于指定将URL 上的路径参数作为使用这个注解的属性值。

5.@GET 注解指定方法对应于Http GET 请求。JAX-RS 提供javax.ws.rs.HttpMethod 注解

允许你增加除了@GET 等之外的方法,例如:你想定义一个新的HTTP 方法@PATCH,你

可以这样像下面这样编写代码:

importjava.lang.annotation.ElementType;

importjava.lang.annotation.Retention;

importjava.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@HttpMethod("PATCH")

public@interface PATCH {

}

但你要注意,你新增加的这个方法,Web 服务器一定要支持才可以,如果不支持,你就需

要配置你的Web 服务器,譬如上面定义的@PATCH 注解指定的PATCH 方法在标准的Http

方法中根本就不存在。

(2.)StudentServiceImpl.java

package net.ilkj.rest.server;

import java.text.ParseException;

import java.text.SimpleDateFormat;

public class StudentServiceImpl implements IStudentService {

public Student getStudent(long id, String name){

Student s = new Student();

s.setId(id);

s.setName(name);

try {

s.setBirthday(new SimpleDateFormat("yyyy-MM-dd")

.parse("1983-04-26"));

} catch (ParseExceptione) {

e.printStackTrace();

}

return s;

}

public Student getStudent(String name) {

Student s = new Student();

s.setId(1);

s.setName(name);

try {

s.setBirthday(new SimpleDateFormat("yyyy-MM-dd")

.parse("1983-04-26"));

} catch (ParseExceptione) {

e.printStackTrace();

}

return s;

}

}

(3.)Student.java

package net.ilkj.rest.server;

import java.util.Date;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="Student")

public class Student {

private long id;

private String name;

private Date birthday;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Date getBirthday() {

return birthday;

}

public void setBirthday(Date birthday) {

this.birthday = birthday;

}

}

因为最终的产出结果是XML,因此你需要对返回值Student 使用JAXB 注解,这样CXF

会知道如何转换。

(4.)发布REST 服务(Jetty):

package net.ilkj.rest.server;

import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;

import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;

public class RestServer {

public staticvoid main(String[]args) {

JAXRSServerFactoryBeansf = new JAXRSServerFactoryBean();

sf.setResourceClasses(StudentServiceImpl.class);

sf.setAddress("http://localhost:335/");

sf.create();

}

}

访问REST 风格的Web 服务很简单,你完全不需要像SOAP 使用SDK 生成客户端代码,因

REST 完全依赖HTTP 协议, 从这里可见REST 是轻量级的。我们访问

http://127.0.0.1:335/ws/services/student/1/info?name=AndrewLee 地址,可以看见页面如下所

示:

我们看到Web 服务访问成功,当然这个响应的XML 客户端通常要接到并处理,所以你可

以用((HttpURLConnection)new URL(“***”).openConnection()).getInputStream()获取,然

后使用程序处理这个接收到的结果。为了更加好的使用HTTP 访问REST 服务,推荐你使用

Apache HttpComponents-Client 组件进行HTTP 操作,因为这里使用的示例是GET 方法的请

求,你是用URL 直接访问或者使用java.net.URL 类来访问很容易,但是如果Web 服务的方

法使用@PUT 等注解,那么你就需要费一番头脑来在请求报头中加入要请求的方法类型等

信息,这些是很繁琐的事情。HTTP-Client 的访问代码如下所示:

package net.ilkj.rest.client;

import java.io.IOException;

import java.io.InputStream;

import org.apache.http.HttpResponse;

import org.apache.http.client.ClientProtocolException;

import org.apache.http.client.HttpClient;

import org.apache.http.client.methods.HttpGet;

import org.apache.http.impl.client.DefaultHttpClient;

public class RestClient {

public staticvoid main(String[]args) throws

ClientProtocolException,IOException{

HttpGet get = new HttpGet(

"http://127.0.0.1:8080/student/info2?name=Fetion");

HttpClienthttpclient = new DefaultHttpClient();

HttpResponseresponse = httpclient.execute(get);

InputStream ins= response.getEntity().getContent();

byte[] b = new byte[1024];

StringBuilder sb= new StringBuilder();

while (ins.read(b) != -1) {

sb.append(new String(b, "UTF-8"));

}

System.out.println(sb.toString());

}

}

注意上面只是简单示例,使用的是逐个字节读入的方式,因此最好不要在输出的XML 中出

现非ISO-8859-1 的字符。如果公开的Web 服务是PUT 方法,那么你可以使用HttpPut 类来

完成处理。

CXF 中的JAX-WS 的一些设置对于JAX-RS 也同样有效,例如日志拦截器,但不是所有的

都可以使用,例如:后面讲到的WSS4J 的拦截器就不能使用到JAX-RS 上,因为WS-*

SOAPWeb 服务的相关规范。下面是服务端被访问时输出的日志信息:

2009-6-2321:55:05 org.apache.cxf.interceptor.LoggingInInterceptor

logging

信息: Inbound Message

----------------------------

ID: 1

Address:/ws/services/student/info2

Encoding:UTF-8

Content-Type:

Headers:{connection=[Keep-Alive], host=[127.0.0.1:335],

user-agent=[Apache-HttpClient/4.0-beta2(java 1.5)],

Content-Type=[null]}

Payload:

--------------------------------------

2009-6-2321:55:06

org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback

onClose

信息: Outbound Message

---------------------------

ID: 1

Encoding:

Content-Type:application/xml

Headers:{Date=[Tue, 23 Jun 2009 13:55:05 GMT]}

Payload:

standalone="yes"?>1983-04-26T00:00:00+08:00

day>1Fetion

--------------------------------------

------------------------------------------------------------------------------------

I.JAX-RS的方法返回值:

JAX-RS 的接口方法可以返回javax.ws.rs.core.Response 接口或者是自定义类型(譬如上面的

Student 类型),Response 接口可以返回Http 的响应代码、响应头或者是一种实体,

javax.ws.rs.ext.MessageBodyWriter 类负责marshall 响应实体,响应实体被直接输出(譬如上

面的Student)或者是作为Response 的一部分。

那么与之对应的是MessageBodyReader 负责Unmarshall 响应实体,也就是读取实体。同样

的,前面提到的@Produces 用于指示一个资源类(服务接口)或者MessageBodyWriter 可以

产出的MIME 类型,@Consumes 用于指示资源类(服务接口)或者MessageBodyReader

以接受的MIME 类型。

下面我们举一个如何返回Response 的例子,上面的StudentServiceImpl 代码改造之后如下所

示:

(1.)服务接口:

@Path(value = "/student/{id}")

publicinterface IStudentService{

@GET

@Path(value = "/info")

@Produces("application/xml")

ResponsegetStudent(@PathParam("id") long id,

@QueryParam("name") String name);

@GET

@Path(value = "/info2")

ResponsegetStudent(@QueryParam("name") String name);

}

注意这里我们将@Produces 注解移到方法上,也就是你可以为每个服务方法单独指定输出类

型。

(2.)服务实现类:

public class StudentServiceImpl implements IStudentService {

public Response getStudent(long id, String name){

Student s = new Student();

s.setId(id);

s.setName(name);

try {

s.setBirthday(new SimpleDateFormat("yyyy-MM-dd")

.parse("1983-04-26"));

} catch (ParseExceptione) {

e.printStackTrace();

}

return Response.ok(s).build();

}

public Response getStudent(String name) {

return Response.status(Response.Status.BAD_REQUEST).build();

}

}

我们看到第一个方法返回的Response 包装了一个实体,第二个方法返回Http 的响应代码。

(3.)客户端访问代码:

HttpGet get = new HttpGet(

"http://127.0.0.1:335/ws/services/student/1/info2?name=Andrew-Lee

");

HttpClienthttpclient = new DefaultHttpClient();

HttpResponseresponse = httpclient.execute(get);

StatusLine st =response.getStatusLine();

if (st.getStatusCode() == HttpServletResponse.SC_OK) {

InputStream ins= response.getEntity().getContent();

byte[] b = new byte[1024];

StringBuilder sb= new StringBuilder();

while (ins.read(b) != -1) {

sb.append(new String(b, "UTF-8"));

}

System.out.println(sb.toString());

} else {

System.out.println(st.getStatusCode());

}

这里我们只有在响应代码是200 的时候才输出响应信息,其余都输出响应代码,这里你会看

到响应代码400 被输出,也就是服务实现类中设置的BAD_REQUEST

------------------------------------------------------------------------------------

II.关于Response接口:

这个类有两个静态的内部类,ResponseBuilder Status

ResponseBuilder 用于创建Response 实例,一般是Response 先通过自己的方法(ok()status()

notModified()等)获得ResponseBuilder 实例,然后ResponseBuilder 进行构建(设置响应头、

最后修改时间、Cookie、语言、MIME 类型等),你会发现这个类的所有方法都返回这个类,

这样你可以使用方法链编程调用多个方法,最后使用ResponseBuilder build()方法返回

Response 实例,也就是构建完毕。

(1.)ResponseBuilder 设置MIME 类型使用type(javax.ws.rs.core.MediaType type)方法,

MediaType 类中定义了大量的MIME 类型常量,以及几个简单的处理方法。

(2.)ResponseBuilder 中的方法tag(javax.ws.rs.core.EntityTagtag)用于设置HTTP 的响应头的

ETagETag HTTP 中一种与Web 资源关联的记号,具体请参考HTTP 文档。

(3.)ResponseBuilder 中的方法variant(javax.ws.rs.core.Variant)variants(List list)

用于简化设置响应头。Variant 的构造方法同时接受MediaTypeLocale、字符编码三个参数。

(4.)ResponseBuilder 中的方法cacheControl(javax.ws.rs.core.CacheControlcacheControl)方法用

于设置缓存实例,CacheControl 类的方法设置缓存在客户端的存在时间。

(5.)ResponseBuilder 中的方法cookie(NewCookie… cookies)用于设置HTTP Cookie,这里

接受的是一个可变参数。

Status 用于获取响应状态码,其中定义了大量的响应代码常量,以及几个简单的处理方法。

------------------------------------------------------------------------------------

II.JAX-RS中的异常处理:

JAX-RS 中你有如下三种方式处理异常:

(1.)直接使用Response 返回HTTP 响应代码,例如:400500 等表示错误的响应代码。

(2.)将异常或错误代码包装为javax.ws.rs.WebApplicationException

例如:throw new WebApplicationException(newMyException("***"));

throw new WebApplicationException(404);

这是一个运行时异常,不需要显示捕获处理。

(3.)将异常转换为响应结果:

public class StudentException extends RuntimeException{

private staticfinal long serialVersionUID= 1899623964871050278L;

public StudentException(String msg) {

super(msg);

}

}

import javax.ws.rs.core.Response;

import javax.ws.rs.ext.ExceptionMapper;

import javax.ws.rs.ext.Provider;

@Provider

public class StudentExceptionMapper implements

ExceptionMapper{

@Override

public Response toResponse(StudentException se) {

return

Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();

}

}

这里我们使用javax.ws.rs.ext.ExceptionMapper接口将异常映射为Response 类型,上面我

们看到将自定义的运行时异常StudentException 映射为500 Response 类型。注意这个

Mapper 必须使用@Provider 进行标注,这样你的服务实现类在抛出StudentExcpetion 之后一

律会被转换为Response 500 的响应结果。

------------------------------------------------------------------------------------

III.JAX-RS的参数处理:

前面我们看到@QueryParam@PathParam 注解用于获取查询参数、路径参数,另外,

@MatrixParam@FormParam@HeadParam@CookieParam 注解用于获取Martix 参数、

POST 方式提交的参数、HTTP 请求头中的参数、Cookie 参数。@DefaultValue 注解用于对

这六个注解设置在未取到值的情况下的默认值,@Encoded 用于对禁用前四种注解的自动解

码。

前面的例子都是一个一个字符串的传递,你也可以直接使用复合类型。

例:

@Path(value = "/student/{id}")

publicinterface IStudentService{

@GET

@Path(value = "/info/{id}/{name}")

ResponsegetStudent(@PathParam("") Student student);

}

我们看到@PathParam value 属性为空字符串,这表示将从路径上将和Student 中的属性名

相同的路径参数逐个传入。我们访问http://127.0.0.1:335/ws/services/student/1/info/2/m.j地址,

你可以看到Student id=1name=m.j,这里注意到如果有重复的路径参数,那么第一个起

作用。

现在我们再来看一下如何使用@MatrixParam 注解,这个注解用于取xxx;A=a;B=b;...的以;

隔的AB...的值。

例:

@Path(value = "/student/{id}")

publicinterface IStudentService{

@GET

@Path(value = "/info2/matrix")

ResponsegetStudent(@MatrixParam("") Student student);

}

然后我们访问http://127.0.0.1:335/ws/services/student/1/info2/matrix;id=2;name=m.j地址即可

接收到值。这个注解获取参数内部是使用javax.ws.rs.core.PathSegment 接口的实现类(CXF

中是org.apache.cxf.jaxrs.impl.PathSegmentImpl)来完成,用于获取 ; 分隔的路径片段中的

参数。

------------------------------------------------------------------------------------

IV.Web服务类的生命周期:

在没有Spring 的情况下,JAX-RS 的所有服务类都是在每次请求时重新创建,但大多数情况

下,公开Web 服务的类都是Service 层,由于Service 层都是无状态的,所以我们最好使用

单例模式,代码如下所示:

JAXRSServerFactoryBeansf = new JAXRSServerFactoryBean();

sf.setResourceClasses(StudentServiceImpl.class);

sf.setResourceProvider(StudentServiceImpl.class,

new SingletonResourceProvider(new StudentServiceImpl()));

sf.setAddress("http://localhost:8080/");

sf.create();

我们注意第三行使用了单例资源提供器,而不是默认的多实例资源提供器。

那么在Spring 容器中,默认情况下由于bean scope singleton,所以是单例模式的,如

果你想使用多例模式,可以设置scope prototype,但一般我们没有必要这样做。

------------------------------------------------------------------------------------

V.@Context注解:

你 可以使用@javax.ws.rs.core.Context 注解将UriInfo,SecurityContext, HttpHeaders,

Providers, Request, ContextResolver, HttpServletRequest,HttpServletResponse,

ServletContext, ServletConfig 实例注入到服务实现类,当然其实你也可以使用标准注解

@javax.annotation.Resource(但不建议这么做,否则某些情况下会出现引用错误)。

例:

public class StudentServiceImpl implements IStudentService {

@Context

private ServletContext servletContext;

@Resource

private MessageContext messageContext;

public Response getStudent(long id, String name){

System.out.println(servletContext.getContextPath() + "\t"

+ messageContext.getUriInfo().getPath() + "\t");

… …

}

}

上面的例子我们可以输出Web 应用上下文、访问当前方法的REST 路径。

------------------------------------------------------------------------------------

VI.资源路径嵌套:

首先我们看一个例子:

(1.)服务接口:

@Path(value = "/student/{id}/")

@Produces("application/xml")

publicinterface IStudentService{

@Path(value = "info/")

StudentgetStudent(@PathParam("id") long id,

@DefaultValue("AndrewLee") @QueryParam("name") String name);

@GET

@Path(value = "info/matrix")

ResponsegetStudent(@MatrixParam("") Student student);

}

(2.)Student 类:

@XmlRootElement(name = "Student")

public class Student {

private long id;

private String name;

private Date birthday;

… …

@GET

@Path("score/{name}")

public Score getScore(@PathParam("name") String name) {

Score score = new Score();

score.setName(name);

score.setNum(100);

return score;

}

}

然后我们访问http://127.0.0.1:8080/ws/services/student/1/info/score/math?name=AndrewLee

址,即可看到Score Json 的内容被输出,之所以输出的不是服务接口上的制定的XML

这是因为递归查找时会以最后找到的路径所在的类的@Produces 注解为主,而我们没有给

Student 定义产出类型,那么CXF 输出的就是默认的JSON 格式。这里我们要说的是JSON

XML 相比的优势是在大数据量的情况下,JSON 字符串要比XML 字符串在容量上小得

多得多,传输速度肯定也会快得多得多,也就是它更适合在网络上传输,但你会发现JSON

的字符串越多,可读性越差。

使用这种深度路径需要注意以下几点:

(1.)@GET HTTPMETHOD 注解必须放在递归的最内层的方法,因此你看到在服务接口中

的第一个方法上没有出现这个注解,这时会提示CXF 第一个方法的返回值中会有需要继续

递归查找的方法。

(2.)子资源(SubResourceStudent 中的getScore()方法的@Path 路径应该是一个相对路径,

也就是最好不要用 / 开头。

这种子资源的路径如果每次请求时都递归查找,很消耗资源,你可以像预编译正则表达式一

样,启用静态子资源解析,你可以使用如下的方式:

JAXRSServerFactoryBeansf = new JAXRSServerFactoryBean();

sf.setStaticSubresourceResolution(true);

Spring 配置文件中也可以如下配置:

......

其实这种SubResource 并不常用,因为这样会让关系变得复杂。但这里我们要说的是一个

REST 路径的命名习惯:

(1.)最顶层的根@Path 的路径要以 / 开头;

(2.)不是根@Path 的不要以 / 开头,如果其后还有子路径被递归嵌套,那么请以 / 结尾。

遵循这个原则,你可以看到第二个方法与之前不同,info 前面的 / 被删除了,matrix 后面也

没有 /

------------------------------------------------------------------------------------

VII.使用WebClient类:

前面我们都是使用了HTTP-Components-Client API 来访问REST 服务,这也是比较干净

的方式,所谓干净就是完全依赖HTTP API(其实更加干净的方式就是使用JAVA 自带的

HTTP API 操作) , 但是过于干净的调用方式总会很麻烦( 很明显使用

HTTP-Components-Client 要比java.net.*下面的API 要来得简单),其实CXF 自带的

org.apache.cxf.jaxrs.client.WebClient 用起来更加简单。

例:

WebClient client= WebClient

.create("http://127.0.0.1:8080/ws/services/student/1/");

Student student= client.path("info/matrix;id=2;name=m.j").accept(

"application/xml").get(Student.class);

System.out.println(student.getName());

WebClient API 大都返回这个类本身的实例,因此上面我们使用了方法链编程,可以连续

调用WebClient API

我们看到这种调用方式不仅更加符合REST 风格的路径组装的方式,也可以直接把接收到的

响应结果直接转换为JAVA 对象。你可能要问客户端是如何得到Student 类的呢?不要忘了

REST 是轻量级的,没有SOAP SDK 可以生成客户端代码,这个Student 类实际就是根据

对方公开给你的REST 服务的返回值说明文档自己构建出来的,因为公开REST 服务和

SOAP 服务的不同之一就是你要给出REST 服务的返回结果的说明,假设是XML,那么你

需要给出文档结果,以及XML 中的元素、属性的说明,通过这些返回值说明你就可以构建

出客户端用户接收它的类,当然转换过程是由JAXB 来自动完成的。具体如何公开REST

务可以参考阿里巴巴的OpenAPI 文档中的例子。

------------------------------------------------------------------------------------

3.CXF Spring

CXF 可以很好的与Spring 整合,这样可以为我们省去很多的代码,你需要的是简单的Spring

配置即可。

I.CXF发布在 Web服务器:

我们前面都是使用CXF 自带的Jetty 发布Web 服务,如果我们的Web 服务在Web 服务器中

编写,那么使用现有的Web 服务器自然是更好的选择。

通常CXF Web 服务器发布Web 服务都是通过Spring 容器完成,但是CXF 也可以完全脱

Spring 容器独立运行在Web 容器中, 你需要书写一个类覆盖

org.apache.cxf.transport.servlet.CXFNonSpringServletloadBus 方法指定BUS 以及发布你的

Web 服务。

我们的Web 应用上下文是/ws

(1.)web.xml

<servlet>

<servlet-name>CXFServletservlet-name>

<servlet-class>

net.ilkj.servlet.MyCXFNonSpringServlet

servlet-class>

<init-param>

<param-name>/helloServiceparam-name>

<param-value>

net.ilkj.soap.server.HelloServiceImpl

param-value>

init-param>

<load-on-startup>1load-on-startup>

servlet>

<servlet-mapping>

<servlet-name>CXFServletservlet-name>

<url-pattern>/services/*url-pattern>

servlet-mapping>

你可以配置多个初始化参数,指定你要发布的Web服务实现类。

(2.)MyCXFNonSpringServlet.java

package net.ilkj.servlet;

import java.util.Enumeration;

import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.xml.ws.Endpoint;

import org.apache.cxf.Bus;

import org.apache.cxf.BusFactory;

import org.apache.cxf.transport.servlet.CXFNonSpringServlet;

public class MyCXFNonSpringServlet extends CXFNonSpringServlet{

/**

*

*/

private staticfinal long serialVersionUID= 1930791254280865620L;

@Override

public void loadBus(ServletConfig servletConfig) throws

ServletException{

super.loadBus(servletConfig);

Bus bus = this.getBus();

BusFactory.setDefaultBus(bus);

// 获取在web.xml中配置的要发布的所有的Web服务实现类并发布Web服务

Enumerationenumeration = getInitParameterNames();

while (enumeration.hasMoreElements()) {

String key =enumeration.nextElement();

String value =getInitParameter(key);

try {

Class clazz =Class.forName(value);

try {

Endpoint.publish(key,clazz.newInstance());

} catch (InstantiationExceptione) {

e.printStackTrace();

} catch (IllegalAccessExceptione) {

e.printStackTrace();

}

} catch (ClassNotFoundExceptione) {

e.printStackTrace();

}

}

}

}

为了测试结果,你可以暂时先删除lib 目录下Spring 的所有jar 文件,然后我们访问

http://127.0.0.1:335/ws/services/helloService?wsdl,我们看到Web 服务发布成功。

------------------------------------------------------------------------------------

II.使用Spring发布SOAP方式的Web服务:

使用Spring 发布Web 服务很简单,首先将web.xml 改为如下的形式:

<context-param>

<param-name>contextConfigLocationparam-name>

<param-value>/WEB-INF/beans.xmlparam-value>

context-param>

<listener>

<listener-class>

org.springframework.web.context.ContextLoaderListener

listener-class>

listener>

<servlet>

<servlet-name>CXFServletservlet-name>

<servlet-class>

org.apache.cxf.transport.servlet.CXFServlet

servlet-class>

<load-on-startup>1load-on-startup>

servlet>

<servlet-mapping>

<servlet-name>CXFServletservlet-name>

<url-pattern>/services/*url-pattern>

servlet-mapping>

我们看到CXFServlet 负责截获/services/*的请求,Web 服务的SEI Spring 的上下文加载监

听器来完成查找。那么下面我们来配置Springbeans.xml 如下所示:

xml version="1.0"encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:jaxws="http://cxf.apache.org/jaxws"

xmlns:jaxrs="http://cxf.apache.org/jaxrs"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://cxf.apache.org/jaxws

http://cxf.apache.org/schemas/jaxws.xsd

http://cxf.apache.org/jaxrs

http://cxf.apache.org/schemas/jaxrs.xsd">

<import resource="classpath:META-INF/cxf/cxf.xml"/>

<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"

/>

<import

resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml"/>

<import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>

<jaxws:endpoint id="helloServiceWs"address="/helloService"

implementor="#helloService"/>

<bean id="helloService"

class="net.ilkj.soap.server.HelloServiceImpl"/>

beans>

我们注意到这里引入了两个新的名称空间jaxwsjaxrs,因为CXF 实现了Spring

NamespaceHandler 接口,实现这个接口可以在Spring 中增加额外的配置。

那么jaxws 自然是配置SOAP 方式的Web 服务,你可以看到有jaxws:serverjaxws:endpoint

jaxws:client 三个元素,jaxws:server jaxws:endpoint 是等效的,都用于发布Web 服务,出

jaxws:endpoint 的原因是JAX-WS 规范中使用EndPoint 发布Web 服务(前面使用过这种

方式),CXF 为了和JAX-WS 对应,提供了这个与jaxws:server 功能一样的配置元素;jaxrs

REST 方式的Web 服务,有jaxrs:serverjaxrs:client 两个元素。

你也可以使用implementorClass 属性直接指向一个类,而不是像上面那样引用一个Bean

名字。

我们启动Web 服务器,访问http://127.0.0.1:335/ws/services/helloService?wsdl地址,如果你

看到WSDL,那么表示我们的Web 服务发布成功。你也可以使用下面的方式发布Web 服务:

<jaxws:server id="helloServiceWs"address="/helloService">

<jaxws:serviceBean>

<ref bean="helloService"/>

jaxws:serviceBean>

jaxws:server>

同样,你也可以使用serviceClass 属性指向一个类,而不是使用子元素jaxws:serviceBean

用一个Bean 的名字。无论是哪种方式,指向的目标都是SEI 的实现类。

------------------------------------------------------------------------------------

III.使用Spring开发SOAP方式的客户端:

<jaxws:client id="helloServiceClient"

address="http://127.0.0.1:335/ws/services/helloService"

serviceClass="net.ilkj.soap.client.IHelloService"/>

这里属性serviceClass 指向客户端生成的接口,这里就不能指向一个Bean 的名字,因为接

口是不能被实例化的。然后你就可以向访问Spring 容器中的Bean 一样去访问这个CXF

客户端代理(JaxWsClientProxy),下面是我们在JSP 中书写的调用代码:

IHelloServicehelloService =

(IHelloService)WebApplicationContextUtils

.getWebApplicationContext(application)

.getBean("helloServiceClient");

out.print(helloService.selectMaxAgeStudent(c1,c2).getName());

IIIII 的示例可以看出借助Spring Web 容器中发布、访问Web 服务极其简单。

关于具体的配置项,大多数都可以从元素、属性的名字上看出其作用,具体解释

请参看http://cwiki.apache.org/CXF20DOC/jax-ws-configuration.html

------------------------------------------------------------------------------------

IV.使用Spring发布REST风格的Web服务:

Spring 发布REST 风格的Web 服务与SOAP 方式的Web 服务没有什么区别,beans.xml 代码

如下所示:

<jaxrs:server id="studentServiceWs"address="/">

<jaxrs:serviceBeans>

<ref bean="studentService"/>

jaxrs:serviceBeans>

jaxrs:server>

<bean id="studentService"

class="net.ilkj.rest.server.StudentServiceImpl"/>

我们看到发布服务时使用了 / ,也就是在CXFServlet 拦截的/services/*之后直接跟

IStudentService @Path 的路径,不再额外追加其他的路径值。访问这个Web 服务可以直接

使用前面的方式访问。CXF 提供了标记访问REST 服务,但这个标记需要给

serviceClass 属性,然后内部使用JAXRSClientFactoryBean 类来完成创建,这样就似乎必

须得有服务接口才可以完成客户端调用配置,可是我一直没想通客户端怎么可能得到REST

服务的接口呢?好在WebClient 已经足够好用,所以我们就不必非得用这个

来完成客户端调用。

------------------------------------------------------------------------------------

4.SOAPWS-*规范:

基于SOAP Web 服务的WS-*规范有不下几十种,但对于JAX-WS 规范只定义了最基本

Web 服务的规范,并未定义通用的WS-*规范的通用接口,所以可能不同的JAX-WS

现提供的WS-*规范实现的数量是不一样的。目前CXF 支持如下几种WS-*的规范:

(1.)WS-Addressing:实现了与底层传输协议的隔离,寻址方式采取基于消息的路由,同时也

实现了对会话状态的保存机制(Web 服务SEI 本身应该是无状态、自包含的)。

(2.)WS-Reliable Messaging:这个规范实现了可靠的消息传递,也就是保证消息从发送者正

确传输到接受者。

(3.)WS-Security*WS-PolicyWS-Trust:这一系列规范实现了SOAP 的服务的安全策略、

信任机制。

这里我们讲一下较为常用的安全策略机制。

------------------------------------------------------------------------------------

I.传统的用户名令牌机制:

Apache WSS4JWebService Security For Java)实现了JAVA 语言的WS-SecurityCXF 中使

WSS4J 也是很容易的,WSS4J 依赖于SAAJ

CXF 中使用拦截器机制完成WSS4J 功能的支持,你只需要初始化WSS4JInInterceptor(对

应的还有一个WSS4JOutInterceptor)实例并添加相关信息即可,CXF2.1.x 之后的版本可以

完成SAAJInInterceptor(它也有一个对应的SAAJOutInterceptor)拦截器的自动注册,否则

你需要再注册一下SAAJ 拦截器。

下面我们看一个简单的示例:

(1.)服务器端:

<bean id="wss4jInInterceptor"

class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">

<constructor-arg>

<map>

<entry key="action"value="UsernameToken"/>

<entry key="passwordType"value="PasswordText"/>

<entry key="passwordCallbackClass"

value="net.ilkj.soap.server.security.ServerPasswordCallbackHandle

r" />

map>

constructor-arg>

bean>

<jaxws:server id="helloServiceWs"address="/helloService">

<jaxws:serviceBean>

<ref bean="helloService"/>

jaxws:serviceBean>

<jaxws:inInterceptors>

<ref bean="wss4jInInterceptor"/>

jaxws:inInterceptors>

jaxws:server>

这里我们使用构造方法参数初始化了一个输入的WSS4J 拦截器,到底有哪些参数的键值对,

可以在org.apache.ws.security.handler.WSHandlerConstants

org.apache.ws.security.WSConstants 中的产量列表中查找。例如:上面的第一组键值对action

UsernameToken 都是WSHandlerConstants 中的常量,表示验证机制是用户姓名令牌,也

就是使用传统的用户名和密码机制。第二组的键值对分别是WSHandlerConstants

WSConstants 中的常量,表示密码类型是文本,还可以是WSConstants.PASSWORD_DIGEST

(密码会被加密为MD5)。第三组键值对的键表示服务器端验证密码的回调处理类,这个

类必须JAVA 安全认证框架中的javax.security.auth.callback.CallbackHandler 类,你也可以用

passwordCallbackRef 指向一个Bean 的名字。

下面我们看一下服务器端的这个密码回调处理类,它负责接收并处理客户端提交的用户名和

密码,我们看到这个方法没有返回值,很显然,如果验证失败,你需要抛出异常进行表示。

package net.ilkj.soap.server.security;

import java.io.IOException;

import javax.security.auth.callback.Callback;

import javax.security.auth.callback.CallbackHandler;

import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ServerPasswordCallbackHandler implements CallbackHandler {

public finalstatic String USER = "Fetion2";

public finalstatic String PASSWORD = "Fetion";

@Override

public void handle(Callback[] callbacks) throws IOException,

UnsupportedCallbackException{

WSPasswordCallbackwspassCallback = (WSPasswordCallback)

callbacks[0];

System.out.println(wspassCallback.getIdentifier()+ "\t"

+wspassCallback.getPassword());

if (wspassCallback.getIdentifier().equals(USER)

&&wspassCallback.getPassword().equals(PASSWORD)) {

// undo

} else {

throw new WSSecurityException("No Permission!");

}

}

}

(2.)客户端:

<bean id="wss4jOutInterceptor"

class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">

<constructor-arg>

<map>

<entry key="action"value="UsernameToken"/>

<entry key="user"value="Fetion"/>

<entry key="passwordType"value="PasswordText"/>

<entry key="passwordCallbackClass"

value="net.ilkj.soap.client.security.ClientPasswordCallbackHandle

r" />

map>

constructor-arg>

bean>

<jaxws:client id="helloServiceClient"

address="http://127.0.0.1:335/ws/services/helloService"

serviceClass="net.ilkj.soap.client.IHelloService">

<jaxws:outInterceptors>

<ref bean="wss4jOutInterceptor"/>

jaxws:outInterceptors>

jaxws:client>

我们看到这里的区别是使用了WSS4J 的输出拦截器,因为你是要将用户名和密码输出到服

务端进行验证处理。另外的不同是多个一个user 参数初始化WSS4J 的输出拦截器,用于初

始化用户名,这是一个必选项,稍后我们将在下面的客户端密码回调处理类进行重新设定值。

package net.ilkj.soap.client.security;

import java.io.IOException;

import javax.security.auth.callback.Callback;

import javax.security.auth.callback.CallbackHandler;

import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ClientPasswordCallbackHandler implements CallbackHandler{

public finalstatic String USER = "Fetion2";

public finalstatic String PASSWORD = "Fetion";

@Override

public void handle(Callback[] callbacks) throws IOException,

UnsupportedCallbackException{

WSPasswordCallbackwspassCallback = (WSPasswordCallback)

callbacks[0];

wspassCallback.setIdentifier(USER);

wspassCallback.setPassword(PASSWORD);

}

}

然后我们访问这个Web 服务,从控制台查看日志,你可以看到在SOAP 信封的Header 中包

装了等元素,元素包括了WS-Security 的一些信息和我们设置的用户名和

密码(明文)。

下面我们看一下密码使用MD5 密文发送的代码,beans.xml 中只需要将passwordType 的值

变为PasswordDigest 即可,客户端回调类不需要变化,这里我们看一下服务端回调类。

… …

@Override

public void handle(Callback[] callbacks) throws IOException,

UnsupportedCallbackException{

WSPasswordCallbackwspassCallback =

(WSPasswordCallback)callbacks[0];

System.out.println(wspassCallback.getIdentifier()+ "\t"

+wspassCallback.getPassword());

if (WSConstants.PASSWORD_TEXT.

equals(wspassCallback.getPasswordType())){

if (wspassCallback.getIdentifier().equals(USER)

&&wspassCallback.getPassword().equals(PASSWORD)) {

// undo

} else {

throw new WSSecurityException("No Permission!");

}

} else {

System.out.println(wspassCallback.getIdentifier());

wspassCallback.setPassword(PASSWORD);

}

}

… …

你可以设置断点,你会发现Digest 密文密码情况下,WSPasswordCallback passwordType

属性和password 属性都为null,你只能获得用户名(identifier),一般这里的逻辑是使用这

个用户名到数据库中查询其密码,然后再设置到password 属性,WSS4J 会自动比较客户端

传来的值和你设置的这个值。你可能会问为什么这里CXF 不把客户端提交的密码传入让我

们在ServerPasswordCallbackHandler 中比较呢?这是因为客户端提交过来的密码在SOAP

息中已经被加密为MD5 的字符串,如果我们要在回调方法中作比较,那么第一步要做的就

是把服务端准备好的密码加密为MD5 字符串,由于MD5 算法参数不同结果也会有差别,

另外,这样的工作CXF 替我们完成不是更简单吗?

------------------------------------------------------------------------------------

II.数字签证方法:

除了UsernameToken 这种传统的安全机制,常用的验证动作还有一个就是使用数字签证技

术(X.509 Certificates),action 的取值为Signature 这需要你了解JAAS 中的相关内容,由

于商业证书都是收费的,这里我们采用自签名的方法,也就是JDK 自带的keytool 命令。

(1.)首先我们生成密钥文件(证书):

这个密钥的密码是myAliasPassword,使用RSA 算法生成密钥。

(2.)对密钥文件自签名:

注意各参数值的药使用上面输入的,例如:-storepass 是输入访问这个密钥文件的授权密码。

(3.)从签名后的私有密钥文件中导出公有密钥:

(4.)将公有密钥导入一个公有密钥文件:

上面简单演示了数字签证的制作过程,下面我们用批处理的方式生成一对服务端/客户端密

钥文件:

(1.)generateKeyPair.bat

rem @echo off

echo alias %1

echo keypass %2

echo keystoreName %3

echo KeyStorePass %4

echo keyName %5

keytool -genkey -alias %1 -keypass %2 -keystore %3 -storepass %4 -dname"cn=%1" -keyalg RSA

keytool -selfcert -alias %1 -keystore %3 -storepass %4 -keypass %2

keytool -export -alias %1 -file %5 -keystore %3 -storepass %4

(2.)generateServerKey

call generateKeyPair.bat apmserver apmserverpass serverStore.jkskeystorePass serverKey.rsa

pause

call generateKeyPair.bat apmclient apmclientpass clientStore.jkskeystorePass clientKey.rsa

pause

keytool -import -alias apmserver -file serverKey.rsa -keystoreclientStore.jks -storepass keystorePass

pause

keytool -import -alias apmclient -file clientKey.rsa -keystoreserverStore.jks -storepass keystorePass

我 们运行第二个批处理文件, 即可得到一对服务端/ 客户端密钥文件:

clientStore.jks/serverStore.jks

我们将这两个jks 放到类路径,并新建两个属性配置文件,如下图所示:

当然实际应用中这两对配置文件肯定是分开放在服务端和客户端的。

(1.) server_sign.properties

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.cry

pto.Merlin

org.apache.ws.security.crypto.merlin.keystore.type=jks

org.apache.ws.security.crypto.merlin.keystore.password=keystorePass

#org.apache.ws.security.crypto.merlin.alias.password=apmserverpass

org.apache.ws.security.crypto.merlin.keystore.alias=apmserver

org.apache.ws.security.crypto.merlin.file=serverStore.jks

(2.) client_sign.properties

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.cry

pto.Merlin

org.apache.ws.security.crypto.merlin.keystore.type=jks

org.apache.ws.security.crypto.merlin.keystore.password=keystorePass

#org.apache.ws.security.crypto.merlin.alias.password=apmclientpass

org.apache.ws.security.crypto.merlin.keystore.alias=apmclient

org.apache.ws.security.crypto.merlin.file=clientStore.jks

(3.)Spring 的配置文件:

<bean id="wss4jInInterceptor"

class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">

<constructor-arg>

<map>

<entry key="action"value="Signature"/>

<entry key="user"value="apmclient"/>

<entry key="passwordCallbackClass"

value="net.ilkj.soap.server.security.ServerPasswordCallbackHandle

r" />

<entry key="signaturePropFile"

value="server_sign.properties">entry>

map>

constructor-arg>

bean>

<jaxws:server id="helloServiceWs"address="/helloService">

<jaxws:serviceBean>

<ref bean="helloService"/>

jaxws:serviceBean>

<jaxws:inInterceptors>

<ref bean="wss4jInInterceptor"/>

jaxws:inInterceptors>

jaxws:server>

<bean id="wss4jOutInterceptor"

class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">

<constructor-arg>

<map>

<entry key="action"value="Signature"/>

<entry key="user"value="apmclient"/>

<entry key="passwordCallbackClass"

value="net.ilkj.soap.client.security.ClientPasswordCallbackHandle

r" />

<entry key="signaturePropFile"

value="client_sign.properties">entry>

map>

constructor-arg>

bean>

<jaxws:client id="helloServiceClient"

address="http://127.0.0.1:8080/ws/services/helloService"

serviceClass="net.ilkj.soap.client.IHelloService">

<jaxws:outInterceptors>

<ref bean="wss4jOutInterceptor"/>

jaxws:outInterceptors>

jaxws:client>

(4.)密码回调处理类就是把证书密码(记住不是keyStore 的密码)设置给WSS4J,我们这里

只给出客户端处理类,服务器端的处理类就是密码换成服务端的证书密码。

public class ClientPasswordCallbackHandler implements CallbackHandler {

@Override

public void handle(Callback[] callbacks) throws IOException,

UnsupportedCallbackException{

WSPasswordCallbackwspassCallback = (WSPasswordCallback)

callbacks[0];

wspassCallback.setPassword("apmclientpass");

}

}

(5.)访问Web 服务:

… …

helloService.selectMaxAgeStudent(c1, c2).getName()

… …

如果你使用了日志拦截器,你会看到SOAP 信封中包含了很多 的元素,里面封装了

数字签证的相关信息。

这个例子中的服务端的密码处理类可以省去,但你要把服务端的属性配置文件中的那行注释

放开,区别是密码放在回调类中比明文写在属性配置文件中安全,但是客户端只能使用密码

回调类的方式设置证书密码。

由于数字签证使用的是HTTPS 协议(SSL/TLS),你可以使用Transport 中的HTTP 通道的

元素http-conf:tlsClientParameters 配置SSL/TLS 的相关信息。

------------------------------------------------------------------------------------

III.混合验证方法:

WSS4J 支持如下几种验证模式:

XML Security

XML Signature

XML Encryption

这两种验证模式都是使用数字签证技术。

Tokens

Username Tokens

Timestamps

SAML Tokens

上面的两个示例我们分别使用了Username TokensXMLSignature 验证模式,这两种也是较

为常用的验证模式,但实际上WSS4J 也支持你混合使用上述的几种验证模式,例如:你可

以将Spring 中的WSS4J Interceptor action 属性指定为Timestamp Encrypt Signature,也

就是使用这三种验证模式。这也是为什么回调方法中的WSPasswordCallback 类型是一个数

组的原因,你可以依据getPasswordType()方法返回的密码类型为不同的验证模式设置密码。

不管是哪种验证模式,如果你想使用Java Code 方式实现,而不是Spring 的配置文件,那么

可以如下操作:

Map inProps= new HashMap();

... // configure the properties

WSS4JInInterceptor wssIn = new WSS4JInInterceptor(inProps);

然后将拦截器添加到拦截器列表即可完成操作。

------------------------------------------------------------------------------------

5.Transport

传输端口其实在前面的CXFServlet CXFNonSpringServlet 就是一种ServletTransportCXF

使用这两个传输端口发布Web 服务。下面我们看一下CXF 中最常需要配置的HTTP 传输端

口,另外,CXF 还支持JMS Transport,但不常使用。

HTTP Transport

Web 服务都是使用HTTP 作为传输协议,这个端口用于配置服务端、客户端在调用Web

务时的HTTP 的相关设置,例如:超时时间,SSL 相关设置、是否启用缓存等。

(1.)客户端调用:

<http-conf:conduit name="*.http-conduit">

<http-conf:client ConnectionTimeout="5000"

ReceiveTimeout="10000"/>

http-conf:conduit>

这里的*.http-conduit 是指对所有的Web 服务调用起作用,如果你想对一部分起作用,可以

使用{targetNamespace}serviceName.http-conduit 来设置。

http-conf:conduit 是客户端配置的顶级元素,它有如下几个子元素:

ElementDescription

http-conf:client 指定HTTP 的超时时间、是否启用持续连接、ContentType 等信息。

http-conf:authorization 指定HTTP 基本验证方式的相关配置。.

http-conf:proxyAuthorization 指定HTTP 基本验证方式时使用的代理服务器配置。

http-conf:tlsClientParameters 指定SSL/TLS 连接方式的配置。

http-conf:basicAuthSupplier 指定HTTP 基本验证方式的提供者信息。

http-conf:trustDecider 指定HTTP 连接的信任机制配置。

上面我们配置的是客户端连接Web 服务、接收返回值的超时时间设置,CXF 的默认设置为

30000ms 60000ms

(2.)服务端调用:

<http-conf:destination name="*.http-destination">

<http-conf:server ReceiveTimeout="10000"/>

http-conf:destination>

这里的*.http-destination 是指对所有的Web 服务发布起作用,如果你想对一部分起作用,可

以使用{targetNamespace}serviceName.http-destination 来设置。

http-conf:destination 是客户端配置的顶级元素,它有如下几个子元素:

ElementDescription

http-conf:server 指定HTTP 的连接设置信息。

http-conf:contextMatchStrategy 指定上下文匹配策略。

http-conf:fixedParameterOrder 指定是否固定参数拍学。

(3.)Java Code 调用:

如果你不使用Spring,那么使用Java Code,那么你需要借助CXF Front End API 操作。

服务端:

import org.apache.cxf.endpoint.Server;

import org.apache.cxf.frontend.ServerFactoryBean;

import org.apache.cxf.transport.http_jetty.JettyHTTPDestination;

import org.apache.cxf.transports.http.configuration.HTTPServerPolicy;

public class SoapServer {

public staticvoid main(String[]args) {

ServerFactoryBeanserverFactoryBean = new ServerFactoryBean();

serverFactoryBean

.setAddress("http://127.0.0.1:8080/ws/services/helloService");

serverFactoryBean.setServiceClass(HelloServiceImpl.class);

Server server =serverFactoryBean.create();

JettyHTTPDestinationdestination = (JettyHTTPDestination) server

.getDestination();

HTTPServerPolicyhttpServerPolicy = new HTTPServerPolicy();

httpServerPolicy.setReceiveTimeout(32000);

destination.setServer(httpServerPolicy);

}

}

客户端:

import java.net.MalformedURLException;

import java.net.URL;

import java.text.ParseException;

import javax.xml.namespace.QName;

import org.apache.cxf.endpoint.Client;

import org.apache.cxf.frontend.ClientProxy;

import org.apache.cxf.transport.http.HTTPConduit;

import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;

public class SoapClient {

public staticvoid main(String[] args)throws ParseException,

HelloException,MalformedURLException {

QName qName = new QName("http://server.soap.ilkj.net/",

"HelloServiceImpl");

HelloServiceImplServicehelloServiceImplService = new

HelloServiceImplService(

new

URL("http://127.0.0.1:8080/ws/services/helloService?wsdl"),

qName);

IHelloServicehelloService = helloServiceImplService

.getPort(IHelloService.class);

Client client =ClientProxy.getClient(helloService);

HTTPConduit http= (HTTPConduit) client.getConduit();

HTTPClientPolicyhttpClientPolicy = new HTTPClientPolicy();

httpClientPolicy.setConnectionTimeout(36000);

httpClientPolicy.setReceiveTimeout(32000);

http.setClient(httpClientPolicy);

//helloService.***

}

}

关于front-end and back-end

Front-endback-end是用于定义与用户相关的服务的程序接口和服务的。这里的用户可以是

人也可以是程序。front-end应用程序是由应用程序用户直接参与完成的,而back-end应用程

序却非直接支持front-end 服务,它们一般与需要的资源非常靠近,有能力与这些资源通信。

back-end应用程序可以直接与front-end应用程序通信,或者,更普遍的是,与被称为中间程

序的应用程序通信。

使用CXFFront EndAPI,你可以调用更多的方法,但是你也看到操作起来也是最繁琐的,

所以除非你有必要使用CXF的这么多特性,否则不要使用这种方式来操作JAX-WS

-----------------------------------------------------------------------------------

6.CXF的拦截器特征机制:

我们在前面看到 CXF通过拦截器(Interceptor)和特征(Feature)扩展自己的功能,例如:

WS-Addressing功能实用Feature实现,日志、WS-Security使用Interceptor实现。

我们也可以编写自己的拦截器注册到CXF中完成特定的功能。CXF中的所有拦截器都要事

org.apache.cxf.inrerceptor.Interceptor接口,

Message 接口可以获得SOAP 消息的相关信息。通过查看CXF API 文档,你会看到CXF

已经实现了很多种拦截器,很多已经在发布、访问Web 服务时已经默认添加到拦截器链。

一般情况下, 我们自己的拦截器只要继承AbstractPhaseInterceptor

org.apache.cxf.message.Message>类即可,这个类可以指定继承它的拦截器在什么阶段被启

用,阶段属性可以通过org.apache.cxf.phase.Phase 中的常量指定值。

例:

package net.ilkj.soap.server.interceptor;

import org.apache.cxf.interceptor.Fault;

import org.apache.cxf.message.Message;

import org.apache.cxf.phase.AbstractPhaseInterceptor;

import org.apache.cxf.phase.Phase;

public class HelloInInterceptor extends

AbstractPhaseInterceptor{

public HelloInInterceptor(String phase) {

super(phase);

}

public HelloInInterceptor() {

super(Phase.RECEIVE);

}

@Override

public void handleMessage(Message message) throws Fault {

System.out.println("*****************");

}

}

你要注意CXF 中的拦截器编写时不要只针对服务端或者客户端,应该是两者均可使用,另

外名字要见名知意。例如:使用InOut 标注这是一个输入时起作用还是输出时起作用的拦

截器。上面的HelloInInterceptor 由于在构造方法中指定在接收消息阶段有效,所以即使你把

它注册到OutInterceptor 的集合中也无效。具体关于CXF 中拦截器的内容需要时请参看

http://cwiki.apache.org/CXF20DOC/interceptors.html

同 样, 我们也可以通过继承AbstractFeature 类来实现一个新的特征, 只需要覆盖

initializeProvider 方法即可。其实Feature 就是将一组拦截器放在其中,然后一并注册使用。

例:

package net.ilkj.soap.server.feature;

import org.apache.cxf.Bus;

import org.apache.cxf.feature.AbstractFeature;

import org.apache.cxf.interceptor.InterceptorProvider;

import org.apache.cxf.interceptor.LoggingInInterceptor;

import org.apache.cxf.interceptor.LoggingOutInterceptor;

public class HelloFeature extends AbstractFeature{

@Override

protected voidinitializeProvider(InterceptorProviderprovider, Bus

bus) {

provider.getInInterceptors().add(new LoggingInInterceptor());

provider.getOutInterceptors().add(new

LoggingOutInterceptor());

}

}

这个特征类将日志拦截器捆绑在一起,你就可以将它注册到你要使用的地方,而不必一个一

个拦截器的注册使用。

CXF 除了允许在Spring 中的配置文件、硬编码注册拦截器和特征类,也允许你是用其自带

的注解@InInterceptors@OutInterceptors@InFaultInterceptors@OutFaultInterceptors

@Features 来直接注册到SEI,但是不推荐你这么做,因为这样你的类中就耦合和CXF 这个

具体的JAX-WS 实现的API

-----------------------------------------------------------------------------------

7.JAX-WS的异步调用:

Web 服务的调用默认都是阻塞调用,但是JAX-WS 也支持异步调用。

第一步,我们要在wsdl2java 上做文章,你需要生成支持异步调用的客户端存根代码,那么

我们需要新建文件async_binding.xml,内容如下所示:

xmlns:xsd="http://www.w3.org/2001/XMLSchema"

xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"

wsdlLocation="http://127.0.0.1:8080/ws/services/helloService?wsdl"

xmlns="http://java.sun.com/xml/ns/jaxws">

true

注 意红色部分是你的WSDL 的物理位置, 然后在wsdl2java 命令中追加–b

async_binding.xml,注意位置关系,这里我们假设运行的wsdl2java 命令与该文件在同一文

件夹下。Wsdl2java -b 参数指定多个以空格分隔的JAX-WSJAXB 绑定文件。

一个如下所示的服务端接口:

@WebService

publicinterface IHelloService {

CustomerselectMaxAgeStudent(@WebParam(name = "c1") Customer c1,

@WebParam(name = "c2") Customer c2);

CustomerselectMaxLongNameStudent(Customer c1, Customer c2);

}

生成的支持异步客户端的Stub 接口如下所示:

publicinterface IHelloService {

public Response

selectMaxAgeStudentAsync(Customerc1,Customer c2);

public Future selectMaxAgeStudentAsync(Customerc1,Customer c2,

AsyncHandlerasyncHandler);

public Customer selectMaxAgeStudent(Customer c1,Customer c2);

public Response

selectMaxLongNameStudentAsync(Customerarg0,Customer arg1);

public Future selectMaxLongNameStudentAsync(Customerarg0,

Customer arg1,

AsyncHandlerasyncHandler);

public Customer selectMaxLongNameStudent(Customer arg0,

Customer arg1);

}

这里我们为了看起来不混乱,删去了所有的标注。我们看到原有的SEI 中的两个方法分别增

加了两个以Async 结尾的新方法(现在一共是六个方法)。这两个增加的新方法一个是保

持参数不变,返回值为javax.xml.ws.Response<原方法对应的SOAP 响应结果类型>;另一个

是增加新的javax.xml.ws.AsyncHandler<原方法对应的SOAP 响应结果类型>参数,返回结果

变为java.util.concurrent.Future类型,熟悉JDK1.5 并法包的应该知道这个类是干什么用

的。

第二步,我们开始在客户端进行异步调用,很显然,你应该调用那两个以Async 结尾的方

法中的一个,那么为什么会有这两个异步调用方法呢?这是因为JAX-WS 中存在两种客户

端异步调用方式:

(1.)Polling:这种方式调用返回结果为Response的异步调用方法,它被称为轮询方式,

所谓轮询就是不断的查询结果是否返回,有点儿类似于Socket 监听。

(2.)Callback:这种方式调用返回结果为Future异步调用方法,它被称为回调方式,所谓

回调就是在有消息返回时调用回调方法。

其实Response 继承Future 类,因此两种方法都是使用JDK1.5 的异步计算类Future 来完成

的,只不过回调方法需要你再额外编写一个AsyncHandler 的回调方法,你可以在这里干一

些其他的事情。

HelloAsyncHandler 类:

public class HelloAsynchHandler implements

AsyncHandler{

private SelectMaxAgeStudentResponse reply;

@Override

public void handleResponse(Response

res) {

try {

System.out.println("handleResponse called");

reply = res.get();

} catch (Exception ex) {

ex.printStackTrace();

}

}

public Customer getResponse() {

return reply.getReturn();

}

}

客户端调用代码:

JaxWsProxyFactoryBeanclient = … …

IHelloServicehelloService = (IHelloService) client.create();

Customer c1 = … …

Customer c2 = … …

Customer resp;

// Callback

HelloAsynchHandlerhelloAsyncHandler = new HelloAsynchHandler();

Futureresponse = helloService.selectMaxAgeStudentAsync(c1, c2,

helloAsyncHandler);

System.out.println("Other Things...");

while (!response.isDone()) {

Thread.sleep(100);

}

resp =helloAsyncHandler.getResponse();

System.out.println("Server responded through callback with: "

+resp.getName());

System.out.println("-----------------------------");

// pollingmethod

ResponseselectMaxAgeStudentResponse =

helloService.selectMaxAgeStudentAsync(c1,c2);

System.out.println("Other Things...");

while (!selectMaxAgeStudentResponse.isDone()) {

Thread.sleep(100);

}

SelectMaxAgeStudentResponsereply = selectMaxAgeStudentResponse.get();

System.out.println("Server responded through polling with: "

+reply.getReturn().getName());

System.exit(0);

上面的Other Things … 是你要操作的其他业务逻辑,通常Future 的使用情景是将它放在代

码的最开始,然后去干其他的事情,因为在Future get()方法被调用之前,它不会产生阻

塞,这样你可以在方法执行的最后再去获取异步计算的结果。那么你可能会问,这样整个方

法还是不能算作完全的异步调用,因为最后要返回值的时候还是会阻塞哦!其实有返回值的

Web 服务调用本就不应该使用异步调用,因为返回值通常你都是要拿回来立即使用的。这

种异步调用机制适合于无返回值的Web 服务调用,这样你就不需要再调用Future get()

法了。

如果你想有返回值的时候也进行异步调用,那么就只能使用最原始的开启新的线程的方法,

也就是将Web 服务方法的调用放在run()方法中执行。

-----------------------------------------------------------------------------------

8.JAX-WSRPC/encoded问题:

首先我们看一下javax.jws.soap.SOAPBinding 注解,这个注解有如下三个属性:

属性取值1(默认) 取值2

style SOAPBinding.Style.DOCUMENTSOAPBinding.Style.RPC

use SOAPBinding.Use.LITERALSOAPBinding.Use.ENCODED

parameterStyleSOAPBinding.ParameterStyle.WRAPPED SOAPBinding.ParameterStyle.BARE

style 表示SOAP 消息样式,有文档和RPC 两种样式,每一种样式可以使用字符和编码两种

方式包含消息,对于参数可以使用包装模式(所有参数包装到一个Message)和赤裸模式(所

有参数独立存在)。至于它们具体的不同可以在SEI 上使用SOAPBinding 注解设置,然后

观察生成的WSDL 就可以看出区别。

那么对于JAX-WS 来说,前面多次提到,它不支持RPC/encoded,也就是你不能在SEI 上使

用如下的注解组合:

@SOAPBinding(style = SOAPBinding.Style.RPC, use = SOAPBinding.Use.ENCODED)

CXF 在发布Web 服务时会忽略掉ENCODED,所以你也不会看到任何错误发生(这个与

JAX-WS 的具体实现有关,其他的实现可能会抛出异常)。

但是试想一下,如果发布Web 服务的一方是较早构建的,那么它可能公开给你的就是

JAX-RPC 的服务,一旦又是RPC/encoded 方式的,那么CXF 将无法访问,如果你是用

wsdl2java.bat 你将会看到RPC/encoded not supported by JAX-WS 2.0 的错误提示。

这种情况你通常不大可能会说服对方改换JAX-WS,那么我们就要要使用JAX-RPC Web

服务实现来访问对方的Web 服务,这里简单说一下使用Apache Axis 1.4 访问RPC/encoded

Web 服务,注意我们使用的Axis,不是Axis 2,这是两个差别很大的Web 服务实现,Axis

2 实现的是JAX-WSJAX-RS,而Axis 实现的是JAX-RPC

我们在安装完Axis 之后,将其自带的lib 目录中的jar 添加到classpath,运行如下命令:

java org.apache.axis.wsdl.WSDL2Java –p 包路径 wsdlURL

在得到生成的客户端代码之后,你可以在Spring 的配置文件配置如何访问这个JAX-RPC

Web 服务。

I.WSDL样例(部分):

…>

... ...

II.生成的客户端代码:

你会发现JAX-RPC 生成的客户端没有任何注解,这是因为JAX-RPC J2EE1.4 中的Web

规范,依赖于JDK1.4JAVA JDK1.5 才开始支持注解,因此发布一个JAX-RPC Web

服务业显然没有它的后继版本JAX-WS 使用注解来得简洁。

III.Spring的配置文件:

<bean id="rpcClient"

class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactory

Bean">

<property name="serviceInterface"

value="net.ilkj.soap.rpc.client.WapLinklServerImpl"/>

<property name="wsdlDocumentUrl"

value="http://10.0.1.72:8080/ibmp/service/WAPLink?wsdl"/>

<property name="namespaceUri"

value="http://10.0.1.72:8080/ibmp/service/WAPLink"/>

<property name="serviceName"value="WapLinklServerImplService"/>

<property name="portName"value="WAPLink"/>

bean>

这 里使用Spring JaxRpcPortProxyFactoryBean 来访问JAX-RPC Web 服务,

serviceInterface 指定客户端生成的接口,wsdlDocumentUrl 指定WSDL 的地址,namespaceUri

指定WSDL 中的targetNamespace 属性值,serviceName 指定WSDL 中的

name 属性值,portName 指定WSDL 中的的子元素name

属性值。然后我们可以像使用普通的Bean 一样在使用时将它强制转换为serviceInterface

定的接口类型,就可以调用Web 服务的方法了。从这里你也可以看出Spring 的强大易用,

Spring 提供的对RMIJAX-RPCJAX-WSHTTPINVOKERJMS 的整合调用都极为简洁。

-----------------------------------------------------------------------------------

9.JAXMSAAJ

前面我们就说过,JAXM&SAAJJAX-WS 都是基于SOAP 消息的Web 服务规范,并且前

者暴漏了太多的细节,编码非常的繁琐,那么干嘛还要用它呢啊?你可以设想这样一种情况,

你访问的Web 服务传回来的SOAP 消息中的XML 可能无法正确解析成你的客户端对象,

或者你要对SOAP 消息中的XML 做一些处理,那么你该怎么办呢?因为JAX-WS 暴漏的细

节极少,几乎都是自动完成的,你根本无法实现这个逻辑(或许CXF 的拦截器可能会有提

供这种打断自动处理机制,允许你在XML 解析成JAVA 对象之前半路插入,自己解析XML

但这也只是CXF 的功能,在JAVA 面向接口的规则下,不能保证其他的JAX-WS 实现也提

供这种入口)。这个时侯SAAJ 就派上用场了,其实SAAJ 提供的API 就是用于组装和解构

SOAP 消息的。

我们回忆前面介绍过SOAP 消息的构成,可以用下图表示:

这些部件在SAAJ 中都有对应的接口,对于客户端来说,SAAJ 的调用过程通常如下所示:

1.创建 SOAP 连接

2.创建 SOAP 消息

3.SOAP 消息里增加数据

4.发送消息

5.SOAP 应答进行处理

I.SAAJ组装和结构SOAP消息:

现在我们访问前面的那个传输附件的CXF 公开的Web 服务接口:

(1.)服务器端:

public Customer selectMaxAgeStudent(Customer c1, Customer c2)

throws HelloServiceException {

try {

System.out.println("********************************"

+c1.getData().getContentType());

// 输出接收到的附件

InputStream ins= c1.getData().getInputStream();

byte[] b = new byte[ins.available()];

OutputStream ous= new FileOutputStream("c:/temp.jpg");

while (ins.read(b) != -1) {

ous.write(b);

}

ous.close();

} catch (IOException e){

e.printStackTrace();

}

// 传递附件到客户端

c1.setData(new DataHandler(new FileDataSource("c:/18.jpg")));

c2.setData(new DataHandler(new FileDataSource("c:/18.jpg")));

if (c1.getBirthday().getTime() >c2.getBirthday().getTime())

return c2;

else

return c1;

}

(2.)客户端:

import javax.xml.soap.*;

// 获取SOAP连接工厂

SOAPConnectionFactoryfactory = SOAPConnectionFactory.newInstance();

// SOAP连接工厂创建SOAP连接对象

SOAPConnectionconnection = factory.createConnection();

// 获取消息工厂

MessageFactorymFactory = MessageFactory.newInstance();

// 从消息工厂创建SOAP消息对象

SOAPMessagemessage = mFactory.createMessage();

// 创建SOAPPart对象

SOAPPart part =message.getSOAPPart();

// 创建SOAP信封对象

SOAPEnvelopeenvelope = part.getEnvelope();

// 创建SOAPBody对象

SOAPBody body =envelope.getBody();

// 创建XML的根元素

SOAPBodyElementbodyElementRoot = body.addBodyElement(new QName(

"http://server.soap.ilkj.net/", "selectMaxAgeStudent",

"ns2"));

// 创建Customer实例1

SOAPElementelementC1 = bodyElementRoot

.addChildElement(new QName("c1"));

elementC1.addChildElement(new QName("birthday")).addTextNode(

"1989-01-28T00:00:00.000+08:00");

elementC1.addChildElement(new QName("id")).addTextNode("1");

elementC1.addChildElement(new QName("name")).addTextNode("A");

// 创建附件对象

AttachmentPartattachment = message

.createAttachmentPart(new DataHandler(new

FileDataSource(

"c:/18.jpg")));

// 设置Content-ID

attachment.setContentId("<" + UUID.randomUUID().toString() + ">");

attachment.setMimeHeader("Content-Transfer-Encoding", "binary");

message.addAttachmentPart(attachment);

SOAPElementelementData = elementC1.addChildElement(new QName("data"));

// 添加XOP支持

elementData.addChildElement(

new QName("http://www.w3.org/2004/08/xop/include", "Include",

"xop")).addAttribute(new QName("href"),"cid:"

+attachment.getContentId().replaceAll("<", "")

.replaceAll(">", ""));

// 创建Customer实例2

SOAPElementelementC2 = bodyElementRoot

.addChildElement(new QName("c2"));

elementC2.addChildElement(new QName("birthday")).addTextNode(

"1990-01-28T00:00:00.000+08:00");

elementC2.addChildElement(new QName("id")).addTextNode("2");

elementC2.addChildElement(new QName("name")).addTextNode("B");

AttachmentPartattachment2 = message

.createAttachmentPart(new DataHandler(new

FileDataSource(

"c:/18.jpg")));

attachment2.setContentId("<" + UUID.randomUUID().toString() + ">");

message.addAttachmentPart(attachment2);

SOAPElementelementData2 = elementC2.addChildElement(new

QName("data"));

elementData2.addChildElement(

new QName("http://www.w3.org/2004/08/xop/include", "Include",

"xop")).addAttribute(new QName("href"),"cid:"

+attachment2.getContentId().replaceAll("<", "")

.replaceAll(">", ""));

// 访问Web服务地址

SOAPMessagereMessage = connection.call(message, new URL(

"http://127.0.0.1:8080/ws/services/helloService"));

// 控制台输出返回的SOAP消息

reMessage.writeTo(System.out);

// 输出SOAP消息中的第一个子元素的元素名称

System.out.println("<<<<<<<<<<<<<<<<<<<"

+reMessage.getSOAPBody().getFirstChild().getLocalName());

// 输出SOAP消息中的附件

Iteratorit = reMessage.getAttachments();

while (it.hasNext()) {

InputStream ins= it.next().getDataHandler().getInputStream();

byte[] b = new byte[ins.available()];

OutputStream ous= new FileOutputStream("c:/temp2.jpg");

while (ins.read(b) != -1) {

ous.write(b);

}

ous.close();

}

从上面的代码可以看出,SAAJ 完全暴漏了SOAP 消息的处理细节,使得你可以更加自如的

控制从服务端获取来的SOAP 消息,而不必像JAX-WS 一样完全自动处理。但是参照WSDL

的内容,使用SAAJ 组装对应的SOAP 消息,确实难度比较大,尤其是对方公开的Web

务比较复杂的时候,你需要熟练的掌握WSDL 的各部分内容。一个较为偷懒的办法是首先

使用JAX-WS 访问服务端,然后把CXF 拦截器捕获的向服务端发送的SOAP 消息复制出来,

照样组装。

另外,要说明的是SAAJ 只支持传输二进制形式的附件,不能解析传输的BASE64 码的附件,

因此,服务端的JAX-WS 需要启用MTOM,这样传输回来的附件SAAJ 才会自动解析,否

则你要自己将传回的BASE 码转换成二进制文件。

SAAJ API 位于JAVA SE6 的文档中,大部分方法从名字上就可以看出是干什么用的,另

外,JDK1.6 API 也是中文的,这里不去详细说明SAAJ 的各个API 的用法。

-----------------------------------------------------------------------------------

II.使用JAXM发布Web服务:

JAXM API 实际上就是用来将一个Servlet 发布成WebService 的地址,它要求你的Servlet

继承javax.xml.messaging.JAXMServlet 并实现javax.xml.messaging.ReqRespListener 接口(如

Web 服务是单向的, 也就是没有返回值给客户端, 那么可以实现

javax.xml.messaging.OnewayListener 接口),其实这一个抽象类和两个接口是我们可以使用

的三个JAXM API,其余的基本上都是JAXM 底层实现需要使用的API,我们并不需要

关心。

例:

public class MyJAXMServlet extends JAXMServlet implements

ReqRespListener{

/**

*

*/

private staticfinal long serialVersionUID= 1870615968744239558L;

@Override

public SOAPMessage onMessage(SOAPMessage soapMessage) {

// 处理参数soapMessage,这是客户端传送过来的SOAP消息

// 返回SoapMessage,这是响应给客户端的SOAP消息

return null;

}

}

在实际应用中,你可以把你的业务层放在onMessage()方法中调用,相当于把业务层的方法

用这个Servlet 包装成了Web 服务,这个Servlet 不可以使用普通的方式访问,只能使用

SoapConnection call()方法调用。

实际上你可以看出来,JAXM 发布的Web 服务比较简单,完全省略了WSDL,这也就是说,

你用这种方式发布Web 服务,必须把要接收的Soap 消息的内容说明发布出来(有点儿类似

REST 风格的OpenAPI),这样客户端才知道如何组装你想要的SOAP 消息。从这里你也

可以看出来,HTTP 协议与SOAP 消息是基于SOAP 的基本组成,WSDL 是完全可以没有的,

WSDL 的作用是异构平台为了方便使用自己的语言特性的中间桥梁。__


你可能感兴趣的:(Java)