WS-Addressing 初探
近日初学 WS-Addressing 协议,将探索过程及心得记下,以作分享。
学习的过程主要分为三个阶段:1)了解 WS-Addressing 协议;2)通过一个支持 WS-Addressing 的框架创建一个 WebService 实例;3)分析在 WS-Addressing 协议下 WebService 的 client 和 server 的消息交换过程。
一、WS-Addressing 简介
WS-Addressing 是 WebService 协议栈中的一项协议,从字面上看,Addressing 一词表明了其用途:web service 寻址。但这个描述显得过于宽泛,尚不足以理解该协议的内涵。由于任何一项技术都是在一定的背景下以解决某些特定问题为目标而产生的,WS-Addressing 也不能例外,我们通过了解其背景和目标问题场景将更有助于理解该协议。
先从 SOAP 说起,虽说SOAP协议定义了在Web Services之间传递消息的规范格式,在此基础上Services之间的消息交换将不再受到各种不同底层(传输层)的传输协议的影响,但是在SOAP协议中并没有定义如何寻址一个Web Services,寻址是通过底层传输协议的连接定义的。早先时候基于大多数的Web Services都是通过Http协议访问这一前提下,采用同步 request-response 的消息交换模式并不存在寻址的问题,因为 http 协议的特性,回复消息直接通过 http 请求本身的连接返回了。但是现实中 SOA 要面临的场景要复杂得多,光靠 http 解决不了所有的问题。这些问题可能包括:(1)分离的消息发送者和回复的接收者;(2)消息处理过程跨越多协议、多平台。
消息发送者和接收者的分离,可能的情况不仅包括连接的分离,时间的分离,甚至是协议的分离,例如:a、消息发送者不接收回复消息,而是指定了另外一个接收地址;b、消息的交换采用了无连接的协议;b、发送消息与回复消息采用不同的协议;c、消息需要经过数分钟、数小时甚至数天才处理完成产生回复。
消息处理过程是跨协议、跨平台的情形,例如,消息以 http 协议发送给服务 A ,被经过一定的处理后被以 smtp 发送给了属于另一平台的服务 B,而在处理完成之后,回复消息要求发送给消息原始发送者指定的回复地址。
针对特定的系统集成环境,要应对这些场景并非难事,每个系统开发者都会找到自己的解决方案。但要把它们集成在一起,就需要一起协商出一套共同的规则用于描述消息的寻址信息,这些内容包括:发送给谁、回复给谁、以及采用什么方式。而这正是 WS-Addressing 要解决的问题。WS-Addressing 是一些主要技术厂商(如Microsoft、Sun、BEA、IBM和SAP)协商的结果,并得到 W3C 组织的认可,并最终被 W3C 修订和确定为一个标准规范。
二、WS-Addressing 规范的消息格式
关于 WS-Addressing 规范内容在网上已经有很好的文章,我也是从中学习的,此处不再重复其中的内容,仅将其中精华的部分摘录如下,参考来源将在文末的“参考资料”中列举。
本质上,WS-Addressing 是 SOAP 消息头中如果描述寻址信息的一个规范。看看 SOAP 消息头的结构非常有助于我们的理解。以下是引用自 idior 相关文章的一些图片。
1、SOAP 消息头的结构:
MessageID :标识一个消息的 UUID 编号;
RelatesTo :回复的消息所应答的原始消息的 MessageID;
To :消息要发往的 URI ;
Reference Parameters :这是可选项,与 To 一起构成对 Endpoint Reference 的描述;
Action :定义了服务中处理消息方法,即WSDL中定义的操作的 Action ;
From :发送消息的 URL;
ReplyTo :回复消息发往的 URL;通过该项可以指定一个不同于 From 的地址,以指示服务将回复消息发往该地址;
FaultTo :消息处理失败时回复错误消息发往的 URL ;
(图1:SOAP消息头结构)
2、请求/回复消息的消息头对比
(图2:请求/回复消息)
三、基于 Apache CXF 框架创建支持 WS-Addressing 的 WebService
在 Apache CXF 2.4.1 版的示例中就有一个关于 WS-Addressing 的 WebService 的实例。下载 apache-cxf-2.4.1.zip ,在 samples 目录有个 ws_addressing 示例。该示例演示了创建 WS-Addressing 服务,访问WS-Addressing 服务的客户端。
1、服务的实现
服务采用面向 wsdl 的方式定义服务,其中包含了 4 个操作方法:sayHi、greetMe、greetMeOneWay、pingMe 。主要代码如下:
1 @WebService(name ="SoapPort",
2 portName ="SoapPort",
3 serviceName ="SOAPService",
4 targetNamespace ="http://apache.org/hello_world_soap_http",
5 wsdlLocation ="file:./wsdl/hello_world_addr.wsdl")
6
7 public class GreeterImpl implements Greeter {
8
9 private static final Logger LOG =
10 Logger.getLogger(GreeterImpl.class.getPackage().getName());
11
12 /* (non-Javadoc)
13 * @see org.apache.hello_world_soap_http.Greeter#greetMe(java.lang.String)
14 */
15 public String greetMe(String me) {
16 LOG.info("Executing operation greetMe");
17 System.out.println("Executing operation greetMe");
18 System.out.println("Message received: "+ me +"\n");
19 return"Hello "+ me;
20 }
21
22 /* (non-Javadoc)
23 * @see org.apache.hello_world_soap_http.Greeter#greetMeOneWay(java.lang.String)
24 */
25 public void greetMeOneWay(String me) {
26 LOG.info("Executing operation greetMeOneWay");
27 System.out.println("Executing operation greetMeOneWay\n");
28 System.out.println("Hello there "+ me);
29 }
30
31 /* (non-Javadoc)
32 * @see org.apache.hello_world_soap_http.Greeter#sayHi()
33 */
34 public String sayHi() {
35 LOG.info("Executing operation sayHi");
36 System.out.println("Executing operation sayHi\n");
37 return"Bonjour";
38 }
39
40 public void pingMe() throws PingMeFault {
41 FaultDetail faultDetail = new FaultDetail();
42 faultDetail.setMajor((short)2);
43 faultDetail.setMinor((short)1);
44 LOG.info("Executing operation pingMe, throwing PingMeFault exception");
45 System.out.println("Executing operation pingMe, throwing PingMeFault exception\n");
46 throw new PingMeFault("PingMeFault raised by server", faultDetail);
47 }
2、创建服务实例
CXF 框架通过为 Bus 指定 WSAddressingFeature 以支持 WS-Addressing 协议,主要代码如下:
1 SpringBusFactory bf = new SpringBusFactory();
2 URL busFile = Server.class.getResource("server.xml");
3 Bus bus = bf.createBus(busFile.toString());
4 bf.setDefaultBus(bus);
5
6 Object implementor = new GreeterImpl();
7 String address ="http://localhost:9000/SoapContext/SoapPort";
8 Endpoint.publish(address, implementor);
关于 WSAddressingFeature 的配置位于 server.xml 中,具体如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:cxf="http://cxf.apache.org/core"
xmlns:wsa="http://cxf.apache.org/ws/addressing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<cxf:bus>
<cxf:features>
<wsa:addressing/>
</cxf:features>
</cxf:bus>
</beans>
3、创建客户端
发送者和回复消息的接收者分离是 WS-Addressing 要解决的重要问题之一。要实现这一点需要在 SOAP 消息头中为 ReplyTo 项指定一个不同于发送者(from)的地址作为回复消息的地址。在示例中,CXF 通过 WSAddressingFeature 激活对 WS-Addressing 的支持,并通过 DecoupledEndpoint 指定一个消息回复的地址。主要代码如下:
URL wsdlURL;
File wsdlFile = new File(args[0]);
if(wsdlFile.exists()) {
wsdlURL = wsdlFile.toURL();
} else {
wsdlURL = new URL(args[0]);
}
SpringBusFactory bf = new SpringBusFactory();
URL busFile = Client.class.getResource("client.xml");
Bus bus = bf.createBus(busFile.toString());
bf.setDefaultBus(bus);
SOAPService service = new SOAPService(wsdlURL, SERVICE_NAME);
Greeter port = service.getSoapPort();
implicitPropagation(port);
explicitPropagation(port);
implicitPropagation(port);
其中,client.xml 中定义了 WS-Addressing 的相关配置,其内容如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cxf="http://cxf.apache.org/core"
xmlns:wsa="http://cxf.apache.org/ws/addressing"
xmlns:http="http://cxf.apache.org/transports/http/configuration"
xsi:schemaLocation="
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<http:conduit name="{http://apache.org/hello_world_soap_http}SoapPort.http-conduit">
<http:client DecoupledEndpoint="http://localhost:9990/decoupled_endpoint"/>
</http:conduit>
<cxf:bus>
<cxf:features>
<wsa:addressing/>
</cxf:features>
</cxf:bus>
</beans>
我们看到,配置中指定 DecoupledEndpoint=“http://localhost:9990/decoupled_endpoint” ,这其中的 URL 将作为消息中的 ReplyTo 的值。
我们在看看客户端示例中 implicitPropagation(port) 和 explicitPropagation(port) 这两个方法中的服务调用代码:
implicitPropagation 方法中对服务的调用方法和普通的调用并无二致,并没有针对 WS-Addressing 的特殊代码。调用中产生的 WS-Addressing 属性被隐式地自动加入到 SOAP 消息头中。为什么我能如此肯定这里面产生的 SOAP 消息含有 Addressing 的信息呢?在后面我将通过一个工具拦截 SOAP 消息,那时就能一览无遗了。
我们再看看 explicitPropagation 中的操作,代码如下:
private static AddressingProperties createMaps() {
// get Message Addressing Properties instance
AddressingBuilder builder = AddressingBuilder.getAddressingBuilder();
AddressingProperties maps = builder.newAddressingProperties();
//set MessageID property
AttributedURIType messageID = WSA_OBJECT_FACTORY.createAttributedURIType();
messageID.setValue("urn:uuid:"+ System.currentTimeMillis());
maps.setMessageID(messageID);
return maps;
}
/**
* A series of invocations with explicitly propogated
* Message Addressing Properties.
*/
private static void explicitPropagation(Greeter port) {
System.out.println();
System.out.println("Explicit MessageAddressingProperties propagation");
System.out.println("------------------------------------------------");
// associate MAPs with request context
Map<String, Object> requestContext =
((BindingProvider)port).getRequestContext();
requestContext.put(CLIENT_ADDRESSING_PROPERTIES, createMaps());
System.out.println("Invoking sayHi...");
String resp = port.sayHi();
System.out.println("Server responded with: "+ resp +"\n");
//set the RelatesTo property to the initial message ID, so that
// the series of invocations are explicitly related
//RelatesToType relatesTo = WSA_OBJECT_FACTORY.createRelatesToType();
//relatesTo.setValue(messageID.getValue());
//maps.setRelatesTo(relatesTo);
System.out.println("Invoking greetMe...");
requestContext.put(CLIENT_ADDRESSING_PROPERTIES, createMaps());
resp = port.greetMe(USER_NAME);
System.out.println("Server responded with: "+ resp +"\n");
System.out.println("Invoking greetMeOneWay...");
requestContext.put(CLIENT_ADDRESSING_PROPERTIES, createMaps());
port.greetMeOneWay(USER_NAME);
System.out.println("No response from server as method is OneWay\n");
// disassociate MAPs from request context
requestContext.remove(CLIENT_ADDRESSING_PROPERTIES);
}
从代码中可以看到,CXF 通过为 RequestContext 指定一个 AddressingProperties 来显式地设置 WS-Addressing 的相关属性。
4、总结
使用 CXF 创建一个 WS-Addressing 的 WebService 与创建的普通的 WebService 相比,不同之处在于:
(1)服务端启用 WSAddressingFeature ;
(2)客户端启用 WSAddressingFeature ;
(3)客户端指定一个 DecoupledEndpoint 地址作为 ReplyTo 的默认地址;或者通过 AddressingProperties 显式地设置 WS-Addressing 的各项属性;
但是,如果细心的话,你可能会对这些示例代码存在一些疑惑,至于你有没有,反正我有。我的疑惑是,既然 client 设置了独立的地址作为回复消息的地址,那也就是说,发送和接收返回值的操作应该是分离的。但是示例代码中对服务操作的调用和普通的方式一样(代码如下),都是同步返回的,并没有异步操作的代码。运行示例,根据调试跟踪和日志输出的结果来看,返回值的确是同步返回。操作的返回值是否真的是通过 DecoupledEndpoint 的 URL 返回的呢?关于这点将在第三节进行探索。
1 System.out.println("Invoking sayHi...");
2 String resp = port.sayHi();
3 System.out.println("Server responded with: "+ resp +"\n");
4
5 System.out.println("Invoking greetMe...");
6 resp = port.greetMe(USER_NAME);
7 System.out.println("Server responded with: "+ resp +"\n");
8
9 System.out.println("Invoking greetMeOneWay...");
10 port.greetMeOneWay(USER_NAME);
11 System.out.println("No response from server as method is OneWay\n");
12
13 try {
14 System.out.println("Invoking pingMe, expecting exception...");
15 port.pingMe();
16 } catch (PingMeFault ex) {
17 System.out.println("Expected exception occurred: "+ ex);
18 }
四、探查 WS-Addressing 的 WebService 的消息交换过程
示例中的 WS-Addressing 配置的消息交换模型是消息发送和回复接收是分离的,但是从编程角度来看,对服务操作的调用确是同步的过程,这两者是否矛盾?我想,这背后可能隐藏了什么细节。这时,该祭出 HttpAnalyzer 工具来对 client 和 server 的通讯过程来做一番探查了。
HttpAnalyzer 是一款可以捕获 http/https 通讯数据的工具,在网上有很多下载资源。下载安装后,继续。在此我装了 4.0 版本。
启动 HttpAnalyzer ,先配置进程过滤,过滤方式为"like",过滤字符串为“%javaw.exe%”,如下图。然后单击工具栏的 "start" 按钮启动 http 拦截,
启动 HttpAnalyzer 之后,我们以 debug 模式运行示例代码。先启动服务端,然后在客户端的 implicitPropagation(port) 打上断点,运行到此处后单步执行,观察 HttpAnalyzer 拦截到的隐式 Addressing 调用时的 SOAP 消息包。此时,拦截得到以下的输出:
分析拦截结果,我们可以看到以下几点信息:
1、除了 server 端启动了一个消息侦听(http://localhost:9000/SoapContext/SoapPort)之外,还有另一个消息侦听(http://localhost:9990/decoupled_endpoint),而这正是 client 配置的 DecoupledEndpoint 的 URL 。
2、根据http请求的编号上来看,http 9990端口上编号为 6、8、11 的请求都是分别接着 http 9000 端口上编号 5、7、10 的请求之后发生;
3、请求5 和请求 6 的关联
先看编号 5 请求的发送和收到的数据,如下
POST /SoapContext/SoapPort HTTP/1.1
Content-Type: text/xml; charset=UTF-8
Accept: */*
SOAPAction: ""
User-Agent: Apache CXF 2.4.1
Cache-Control:
no-cache
Pragma: no-cache
Host: localhost:9000
Connection: keep-alive
Content-Length: 652
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<Action xmlns="http://www.w3.org/2005/08/addressing">http://apache.org/hello_world_soap_http/Greeter/sayHiRequest</Action>
<MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:cde7280a-69db-4b9d-bc7d-90e0d0f0a676</MessageID>
<To xmlns="http://www.w3.org/2005/08/addressing">http://localhost:9000/SoapContext/SoapPort</To>
<ReplyTo xmlns="http://www.w3.org/2005/08/addressing">
<Address>http://localhost:9990/decoupled_endpoint</Address>
</ReplyTo>
</soap:Header>
<soap:Body>
<sayHi xmlns="http://apache.org/hello_world_soap_http/types"/>
</soap:Body>
</soap:Envelope>
发送的是一个 SOAP 消息,action 显示这是调用 sayHi 操作的请求,MessageID 被设置为一个 UUID 值;ReplyTo 的确被设置为 client 配置的 DecoupledEndpoint 的 URL 。再看这个请求收到的回复数据,如下:
HTTP/1.1200 OK
Content-Type: text/xml;charset=UTF-8
Content-Length: 578
Server: Jetty(7.4.2.v20110526)
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:9e7caac9-10b6-4d0f-934f-ec7c1b96863f
</MessageID>
<To xmlns="http://www.w3.org/2005/08/addressing">http://www.w3.org/2005/08/addressing/anonymous</To>
<ReplyTo xmlns="http://www.w3.org/2005/08/addressing">
<Address>http://www.w3.org/2005/08/addressing/none</Address>
</ReplyTo>
<RelatesTo xmlns="http://www.w3.org/2005/08/addressing">http://www.w3.org/2005/08/addressing/unspecified</RelatesTo>
</soap:Header>
<soap:Body/>
</soap:Envelope>
在请求5的连接中回复的 SOAP 消息是空的。我们再看看接下来在 9990 端口上请求6的数据。
POST /decoupled_endpoint HTTP/1.1
Content-Type: text/xml; charset=UTF-8
Accept: */*
User-Agent: Apache CXF 2.4.1
Cache-Control: no-cache
Pragma:
no-cache
Host: localhost:9990
Connection: keep-alive
Content-Length: 700
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<Action xmlns="http://www.w3.org/2005/08/addressing">http://apache.org/hello_world_soap_http/Greeter/sayHiResponse</Action>
<MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:98c83b3c-7035-4070-a264-5dccb6ed9d92
</MessageID>
<To xmlns="http://www.w3.org/2005/08/addressing">http://localhost:9990/decoupled_endpoint</To>
<RelatesTo xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:cde7280a-69db-4b9d-bc7d-90e0d0f0a676
</RelatesTo>
</soap:Header>
<soap:Body>
<sayHiResponse xmlns="http://apache.org/hello_world_soap_http/types">
<responseType>Bonjour</responseType>
</sayHiResponse>
</soap:Body>
</soap:Envelope>
请求6的接收数据:
HTTP/1.1202 Accepted
Transfer-Encoding: chunked
Server: Jetty(7.4.2.v20110526)
0
请求6发送了一个 SOAP 消息包到 9990 端口,从 action 中看出这是一个对 sayHi 操作的回复消息。RelatesTo 为 urn:uuid:cde7280a-69db-4b9d-bc7d-90e0d0f0a676,该值正好和请求5中发送的 SOAP 消息的 MessageID 相同。
如果是采用单步调试跟踪的话,会发现只有当 http 9990 端口收到回复请求后,port.sayHi() 方法才返回。
这也就是说,从通讯的层面来看,WS-Addressing 是一个异步通讯模型。但这与具体的实现框架在编程API上是异步还是同步并没有直接关系。在此示例中,CXF 显然是将底层的异步通讯和上层的同步API做了适配。
综合以上的分析,觉得 WS-Addressing 不再神秘:(1)它是一个通讯层的规范;(2)它定义了一种异步的通讯模型;(3)它定义了通讯地址信息的表示方法。
参考资料:
- WS-Addressing 从理论到实践 --- SOA基础规范介绍(http://www.cnblogs.com/idior/archive/2006/06/28/431017.html)
- Web 服务寻址(WS-Addressing)对 SOAP 的隐式影响(http://www.ibm.com/developerworks/cn/webservices/ws-address.html)
- Web 服务寻址 (WS-Addressing)(http://msdn.microsoft.com/zh-cn/library/ms951233.aspx#XSLTsection135121120120)
- Apache CXF (http://cxf.apache.org/)