看完上一篇blog:
Web服务搜索与执行引擎(九)——初看客户端如何调用Web服务
,我想我们对当前客户端如何调用Web服务的现状,无论是从高层接口的利用还是低层接口自己手动构建SOAP消息都有了最初步的印象了.接下来将要进行的是整个项目最核心的部分之一:服务的执行.我将为大家展示我们在项目中如何使用 SOAP with Attachments API for Java (SAAJ),简化创建和发送SOAP消息的详细过程。
Web 服务的基础是以标准格式发送和接收消息,这样所有系统都可以理解消息。通常情况下,这种标准格式是 SOAP。SOAP 消息可以手工生成和发送,但是如果说我们按上篇blog那样: 在客户端,首先创建一个HttpConnector对象,负责HTTP连接。设定Connector的一些头部信息,比如EndPoinURL和SoapAction等。如果网络连接需要使用代理服务器,那也要在这里设定相关的信息。接着创建SoapSerializer对象,用于生成Soap消息。按照WSDL里定义,把所有参数按顺序序列化,得到一个完整的SOAP请求消息。该Soap消息,作为Payload通过HttpConnector被发送到服务端。最后,生成一个SoapReader对象,负责读取服务端返回的SOAP消息,取得其中的返回值。
按上面这种SOAP消息的构建方法的话就会变得复杂起来, 所以说有必要借助于一些API来简化我们的操作.有几个基于Java的API可以用来构建低层SOAP消息来访问Web服务。这些API包括SAAJ、Web服务调用框架(WSIF)、上篇blog里提到的Axis等。
Ø 选择SAAJ的理由
我们最终选择了SAAJ,因为基于简单及适用性来考虑.SAAJ无疑很适合基于文档的同步或者异步Web Service。SAAJ使用简单,有助于在Java环境中集成各种Web Service,它扩展了对文档风格的Web Service通信的自然支持(natural support)。SAAJ还支持基于标准接口上的XML消息传递,并且这一点得到了供应商的广泛支持。另外SOAP with Attachments API for Java (SAAJ)—— Java API for XML Messaging (JAXM)的一个分支——能够使许多必需的步骤变得自动化,例如创建上面所说的连接,或者创建和发送实际消息。
Ø 什么是SAAJ
SAAJ是在松散耦合软件系统中利用SOAP协议实现的基于XML消息传递的API规范。顾名思义,SAAJ支持带附件的SOAP消息。
对于Java API for XML Messaging (JAXM),JAXM 1.0的理念是通过提供消息传递和SOAP API,允许开发人员根据SOAP编写支持消息传递标准的业务应用程序。随着JAXM 1.1版的推出,SOAP API (javax.xml.soap)被分割成了SAAJ1.1规范和JAXM1.1,JAXM1.1只包含基于消息传递的API(javax.xml.messaging)。目前,正在使用的SAAJ版本是1.2。
Ø 如何使用
回想那篇blog, Web服务搜索与执行引擎(八)——WSDL解析精髓,提到了为了使用 SAAJ构建SOAP消息调用该服务,我们将需要从 WSDL 收集下列最基本的信息:
目标名称空间
服务名称
端口名称
操作名称
操作输入参数
SOAP文档结构简单,利用SAAJ构建起来也比较方便。但我们需要将用户从网页中输入的数据作为SOAP的有效负载发送至异构平台的服务,如何来构建这个有效负载呢?按照什么样的格式将用户输入的数据放入SOAP的有效负载中呢?格式可以从解析WSDL文档过程中知道,即上面说的那些基本信息,但怎么来匹配用户的输入数据呢?这时就需要以WSDL中解析出来的参数名称作为用户从网页中输入的文本框的名字,从而匹配了每一个子参数的值,然后即可构建SOAP消息的有效负载。然后将SOAP消息发送至远程平台。
调用后,将返回一个SOAP消息返回值,我们需要解析,并将结果在网页中呈现给客户,我们利用了JDOM技术,根据从WSDL中解析出的返回信息来提取SOAP中的返回值,最终呈现给用户。
具体过程包括 5 个步骤:
1. 创建 SOAP 连接
2. 生成 SOAP 消息
3. 填充消息
4. 发送消息
5. 检索响应
SOPA 消息的结构
大家也可以去看看我的那篇blog: Web服务搜索与执行引擎(七)——重温WSDL与SOAP, 再次复习下WSDL 跟SOAP的有关知识.首先来看看消息自身的结构。一条基本的 SOAP 消息由带有两个主要部分的信封(envelope)构成:头部和主体。应用程序确定如何使用这些部分,但整个消息必须遵循特定的 XML 结构,例如:
清单1. 一条示例 SOAP 消息
这里,头部是空的,而主体包含了有效信息,或要传递的消息。在本例中,它是请求某本书价格的消息。
<
SOAP-ENV:Envelope
xmlns:SOAP-ENV
="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi
="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd
="http://www.w3.org/1999/XMLSchema"
>
<
SOAP-ENV:Header
/>
<
SOAP-ENV:Body
>

<
ns1:getPrice
xmlns:ns1
="urn:Book"

SOAP-ENV:encodingStyle
="http://schemas.xmlsoap.org/soap/encoding/"
>

<
isbn
xsi:type
="xsd:string"
>
123544111
isbn>

ns1:getPrice>

SOAP-ENV:Body>

SOAP-ENV:Envelope>
注意消息的结构。Envelope 包含 Header 和 Body 元素,这三者都是http://schemas.xmlsoap.org/soap/envelope/ namespace的一部分。应用程序使用 SOAPConnection 来发送消息。
创建连接和消息
清单2. 创建连接
创建一个SOAP连接,如上清单2所示。SAAJ客户机可以利用SOAP Connection Factory,通过创建SOAPConnection来建立点到点的同步连接。该连接提供了同步调用服务的方法。
其次,工厂还创建了消息自身:
清单3. 创建消息对象
import
javax.xml.soap.SOAPConnectionFactory;

import
javax.xml.soap.SOAPConnection;

import
javax.xml.soap.MessageFactory;

import
javax.xml.soap.SOAPMessage;

import
javax.xml.soap.SOAPPart;

import
javax.xml.soap.SOAPEnvelope;

import
javax.xml.soap.SOAPBody;


public
class
DynamicInvokeInterce
...
{

……

public List invokeOperation(Operation operation) throws Exception


try ...{

SOAPConnectionFactory soapConnFactory =

SOAPConnectionFactory.newInstance();

SOAPConnection connection =

soapConnFactory.createConnection();

MessageFactory messageFactory = MessageFactory.newInstance();

SOAPMessage message = messageFactory.createMessage();

SOAPPart soapPart = message.getSOAPPart();

SOAPEnvelope envelope = soapPart.getEnvelope();

SOAPBody body =envelope.getBody();

connection.close();

…

}
如清单3所示,使用 MessageFactory 创建消息自身。这一消息已经包含了空的基本部分,比如 envelope 和 header 。SOAPPart 包含了 envelope ,而 envelope 又包含了主体。从而创建了对所需对象(比如 SOAPBody)的引用。
接着,填充 SOAPBody:
清单4. 填充主体
...

import
javax.xml.soap.SOAPBody;

import
javax.xml.soap.SOAPElement;


public
class
DynamicInvokeInterce
...
{

public static final String XSI_NAMESPACE_PREFIX = "xsi";

public static final String XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance";

public static final String XSD_NAMESPACE_PREFIX = "xsd";

public static final String XSD_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema";

public List invokeOperation(Operation operation) throws Exception


try ...{

...

SOAPPart soapPart =message.getSOAPPart();

SOAPEnvelope envelope = soapPart.getEnvelope();

boolean isRPC = operation.getStyle().equalsIgnoreCase("rpc");

if(isRPC)


...{

//为envelope增加命名空间声明如果是RPC/encoded型

envelope.addNamespaceDeclaration(XSI_NAMESPACE_PREFIX, XSI_NAMESPACE_URI);

envelope.addNamespaceDeclaration(XSD_NAMESPACE_PREFIX, XSD_NAMESPACE_URI);

}

SOAPHeader header = envelope.getHeader();

header.detachNode();

SOAPBody body = envelope.getBody();

body.addNamespaceDeclaration("", operation.getNamespaceURI());

String targetObjectURI = operation.getTargetObjectURI();

if(targetObjectURI == null)


...{

targetObjectURI = "";

}

Name svcInfo = envelope.createName(operation.getTargetMethodName(), "", targetObjectURI);

SOAPElement svcElem=body.addChildElement(svcInfo);

//复杂类型的名称,需要添加子元素对应这个名称


if(operation.getComplextypename()!=null)...{

Name svc=envelope.createName(operation.getComplextypename());

svcElem=svcElem.addChildElement(svc);

}

if(isRPC)

//判断是encoding还是literal


...{

svcElem.setEncodingStyle(operation.getEncodingStyle());

}

//填充主体

Document doc=XMLSupport.readXML(operation.getInputMessageText());

if(doc.hasRootElement())


...{

buildSoapElement(envelope, svcElem, doc.getRootElement(), isRPC);

}

String soapActionURI = operation.getSoapActionURI();

if(soapActionURI != null && soapActionURI.length() > 0)


...{

MimeHeaders mimeHeaders = msg.getMimeHeaders();

mimeHeaders.setHeader("SOAPAction", """ + operation.getSoapActionURI() + """);

}

message.saveChanges();

}

}
需要说明的是,SAAJ顾名思义,SOAPMessag可以有用于二进制/文本附件的AttachPart。通过调用消息的getSOAPPart()方法可以得到SOAPPart。利用消息的createAttachPart()方法可以创建附件部分。并且可以利用AttachPart的 setContent()方法设置附件。第1个参数是附件,第2个参数是内容类型。如:
AttachmentPart attachPart = message.createAttachmentPart();
attachPart.setContent( "This is a text attachment demo", "text/plain");
但是因为SOAPHeader是可选的,且我们项目目前还没有带附件的SOAP消息这样的需求,所以说当构建一个SOAP请求消息时,我们从envelope里删掉了这个元素,就像清单4中的代码所示:
SOAPHeader header = envelope.getHeader();
header.detachNode();
现在可以有一个SOAPPart,它包含XML SOAP消息, SOAP 消息的主体就好像是另一个 XML 元素,可以在其中添加孩子元素,比如 getPrice。然后就可以像处理典型的 DOM 元素一样,为它添加 isbn 元素和文本节点。在这里,我们增加一个包含有我们将要调用的服务名的SOAPElement到SOAPBody:
Name
svcInfo=envelope.createName(operation.getTargetMethodName(),"", targetObjectURI);
SOAPElement svcElem=body.addChildElement(svcInfo);
SOAP消息中的一个片断:
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
在这个片断里: "urn:Book"就是createName中的第二个参数targetObjectURI,它是在WSDL的
里声明的(在解析WSDL时赋予的:
operationInfo.setTargetObjectURI(soapBody.getNamespaceURI());)
在准备了必要的元素后(操作名,命名空间,参数等),就可以开始填充主体了:
在这里有个细节之前一直没有提到,我们还用到了类BuildOperation,它的一个方法buildMessageText(com.swc.se.domain.Operation operationInfo, Message msg,Vector wsdlTypes)是用来当用户选择了一个操作之后,填写完所以的操作之后构建一个XML消息的(用一个String型的字符串来表示).
即通过调用buildMessageText方法后,
输出是一个xmlMsg字符串(Element rootElem = new Element(operationInfo.getTargetMethodName());
XMLSupport.outputString(rootElem);).
现在我们要反过来了,也就是说要还原字符串xmlMsg,变成XML结构,即:
buildSoapElement(envelope, svcElem, doc.getRootElement(), isRPC);如清单5所示.
清单 5构建SOAP消息子元素
protected
static
void
buildSoapElement(SOAPEnvelope envelope, SOAPElement soapElem, Element jdomElem,
boolean
isRPC)
throws
SOAPException


...
{

String elemText=jdomElem.getText();

if(elemText != null)


...{

if(isRPC == true)


...{

String type = jdomElem.getAttributeValue("type");

if(type != null)


...{

soapElem.addAttribute(envelope.createName(XSI_NAMESPACE_PREFIX + ":type"), XSD_NAMESPACE_PREFIX + ":" + type);

}

}

soapElem.addTextNode(elemText);

}

List attrs = jdomElem.getAttributes();

if(attrs != null)


...{

Iterator attrIter = attrs.iterator();

while(attrIter.hasNext())


...{

Attribute attr = (Attribute)attrIter.next();

Name attrName = envelope.createName(attr.getName(), attr.getNamespacePrefix(), attr.getNamespaceURI());


soapElem.addAttribute(attrName, attr.getValue());

}

}

List children = jdomElem.getChildren();

if(children != null)


...{

Iterator childIter = children.iterator();

while(childIter.hasNext())


...{

Element jdomChildElem = (Element)childIter.next();

SOAPElement soapChildElem = soapElem.addChildElement(jdomChildElem.getName());

buildSoapElement(envelope, soapChildElem, jdomChildElem, isRPC);

}

}

}
这些工作做完之后,message.saveChanges();可以调用SOAPMessage 的saveChanges()方法,来保存对消息的更改.
发送消息
对于同步消息,发送 SOAP 消息,并接收在同一步中发生的响应:
清单6. 发送消息
...


public
class
DynamicInvokeInterce
...
{


public List invokeOperation(Operation operation)throws Exception...{

...

URLEndpoint endpoint = new URLEndpoint(operation.getTargetURL());

SOAPMessage response = connection.call(msg, endpoint);

connection.close();

// 获得响应消息

Source responseContent = response.getSOAPPart().getContent();

// 把响应消息转化成一个JDOM模型

TransformerFactory tFact = TransformerFactory.newInstance();

Transformer transformer = tFact.newTransformer();

JDOMResult jdomResult = new JDOMResult();

transformer.transform(responseContent, jdomResult);

// 获得Document 对象

Document responseDoc = jdomResult.getDocument();

// 在控制台打印

String strResponse = XMLSupport.outputString(responseDoc);

System.out.println("SOAP Response from: " + operation.getTargetMethodName() +

": " + strResponse);

// Set the response as the output message

operation.setOutputMessageText(strResponse);

...

}

}
实际的消息是使用 call()方法发送的,该方法接收消息本身和目的地作为参数,并返回第二个 SOAPMessage 作为响应。call()方法将会阻塞,直到它接收到返回的 SOAPMessage 为止。
相应地,返回的 SOAPMessage 是一个与发送消息具有相同形式的 SOAP 消息,这样该消息操作时就可以像另一个 XML 消息一样.
清单7.读取并处理响应
...


public
class
DynamicInvokeInterce
...
{


public List invokeOperation(Operation operation)throws Exception...{

...

// 把响应消息转化成一个JDOM模型

TransformerFactory tFact = TransformerFactory.newInstance();

Transformer transformer = tFact.newTransformer();

JDOMResult jdomResult = new JDOMResult();

transformer.transform(responseContent, jdomResult);

// 获得Document 对象

Document responseDoc = jdomResult.getDocument();

// 在控制台打印

String strResponse = XMLSupport.outputString(responseDoc);

System.out.println("SOAP Response from: " + operation.getTargetMethodName() +

": " + strResponse);

//下面开始的代码均是使用JDOM处理返回的SOAP消息的细节,在此省略了

……
}
到这里,上面贴出来的部分代码只是到控制台打印所接收到的消息,但是后面还有大量地代码从 XML 文档中提取信息。
我想到这,大家应该对如何使用SAAJ来构建SOAP消息有一个初步的印象了吧.
另外,本系统还有一个模块是手机客户端的接入.这个模块的总结就留到下下次有时间再写了.
系列后面的这几篇(WSDL解析跟SAAJ的使用)可当会有点理论化了,初看的话可能会感觉有点晦涩,所以如果有时间的话,接下来的blog里写写用之前总结的这两个技术(WSDL解析,SAAJ)演示几个小例子的文章,那样的话一切看起来可能更加明朗.