JaxWs 基于消息编程
1 两种消息模式
2 三种数据类型
2.1 Source
2.2 SOAPMessage
2.3 DataSource
3 服务端访问底层信息
4 客户端访问底层消息
4.1 Dispatch的三种请求方式
5 统一入口
6 注意
6.1 MESSAGE和PAYLOAD的区别
通过SEI(Service Endpoint Interface)在服务端和客户端进行操作时,我们是直接使用的对应实现类或代理类对象。表面上看我们是使用的对象在服务端和客户端进行通讯,而实际上底层还是通过发送消息和解析消息进行的。有时候我们可能会希望或者需要直接访问这些消息,这个时候我们就可以通过Provider和Dispatch来实现了。Provider是应用在服务端的,而Dispatch是应用在客户端的。
1 两种消息模式
在使用Provider和Dispatch时,我们可以使用两种消息模式,MESSAGE和PAYLOAD。
当使用MESSAGE模式时我们可以访问整个的消息,包括绑定的任何header和wrapper。而使用PAYLOAD模式时我们仅仅可以访问payload的消息。如当我们的Dispatch在以PAYLOAD模式工作时,它只能访问到返回的SOAPMessage的body部分,而binding层将处理任何绑定的header和wrapper。
2 三种数据类型
Provider和Dispatch在进行信息传递时只能使用三种数据类型:
l javax.xml.transform.Source
l javax.xml.soap.SOAPMessage
l javax.activation.DataSource
2.1 Source
Source是一个接口,它持有一个XML文档对象。每一个Source接口的实现类都提供了一系列的方法来访问和操纵其持有的XML文档的内容。Source接口的实现类有DOMSource、SAXSource和StreamSource等。
2.2 SOAPMessage
SOAPMessage是一个抽象类,使用SOAPMessage的时候需要满足两个条件:
第一:Provider实现类使用的是SOAP绑定,即SOAPBinding;
第二:Provider实现类使用的是MESSAGE Mode。
SOAPMessage持有一个SOAP消息。
2.3 DataSource
DataSource是一个接口,使用时需要满足以下两个条件:
第一:Provider实现类使用的是Http绑定,即HttpBinding;
第二:Provider实现使用的是MESSAGE Mode。
DataSource是对数据集合的抽象,在适当的时候可以通过InputStream和OutputStream的形式提供对该数据的访问。其实现类有FileDataSource和URLDataSource。
3 服务端访问底层信息
服务端访问底层信息是通过Provider接口进行的。通过实现Provider接口并且把实现类发布为一个WebService,我们就可以在客户端发起请求时访问到其发送过来的底层消息对象,Source、SOAPMessage或者DataSource。Provider接口只定义了一个invoke方法,该方法接收一个消息对象,并返回一个同类型的消息对象,而且消息对象的类型只能是上面介绍的三种类型之一。
在使用Provider的时候我们需要在其实现类上使用@WebServiceProvider进行标记(使用@WebService标记好像也行),并且Provider<T>指定的消息对象类型必须是上面提到的三种数据类型之一。WebService使用的消息模式默认为PAYLOAD,我们可以在Provider实现类上使用@ServiceMode来指定其它值,如@ServiceMode(Service.Mode.MESSAGE)。
下面我们来看一个使用Provider的例子:
import javax.xml.soap.SOAPMessage; import javax.xml.ws.Provider; import javax.xml.ws.Service.Mode; import javax.xml.ws.ServiceMode; import javax.xml.ws.WebServiceProvider; @WebServiceProvider(serviceName = "SOAPMessageService", portName = "SOAPMessagePort", targetNamespace = "http://provider.jaxws.sample.cxftest.tiantian.com/") @ServiceMode(Mode.MESSAGE) public class SOAPMessageModeProvider implements Provider<SOAPMessage> { public SOAPMessage invoke(SOAPMessage request) { SOAPMessage response = null; try { System.out.println("客户端以SOAPMessage通过MESSAGE Mode请求如下: "); request.writeTo(System.out); response = MessageUtil.getInstance().create(null, "/provider/SOAPMessageResp.xml"); } catch (Exception ex) { ex.printStackTrace(); } return response; } }
在上面代码中,我们的SOAPMessageModeProvider:
l 实现了Provider接口;
l 通过Provider接口定义的泛型指定使用的消息对象数据类型为SOAPMessage;
l 通过@WebServiceProvider标注其为一个WebService,并指定了serviceName等属性;
l 通过@ServiceMode指定其使用的消息模式为MESSAGE;
l 在invoke方法中接收了一个SOAPMessage,并返回了一个SOAPMessage。
其中MessageUtil类的代码为:
import java.io.IOException; import java.io.InputStream; import javax.xml.soap.MessageFactory; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; public class MessageUtil { private static MessageUtil instance = new MessageUtil(); private MessageFactory factory; private MessageUtil() { try { factory = MessageFactory.newInstance(); } catch (SOAPException e) { e.printStackTrace(); thrownew RuntimeException(e); } } public static MessageUtil getInstance() { returninstance; } /** * 创建一个默认的SOAPMessage * @return * @throws SOAPException */ public SOAPMessage create() throws SOAPException { returnfactory.createMessage(); } /** * 根据MimeHeaders和soap格式文件路径创建一个SOAPMessage * @param headers * @param filePath * @return * @throws IOException * @throws SOAPException */ public SOAPMessage create(MimeHeaders headers, String filePath) throws IOException, SOAPException { InputStream is = MessageUtil.class.getResourceAsStream(filePath); SOAPMessage message = factory.createMessage(headers, is); is.close(); return message; } /** * 获取MessageFactory * @return */ public MessageFactory getMessageFactory() { returnfactory; } }
文件SOAPMessageResp.xml的内容为:
<?xml version="1.0" encoding="utf-8" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOAP-ENV:Body> <ns4:sayHiResponse xmlns:ns4="http://provider.jaxws.sample.cxftest.tiantian.com/"> <ns4:responseType>SOAPMessage Response</ns4:responseType> </ns4:sayHiResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
有了Provider实现类之后我们就可以把它发布为一个WebService了,如:
Object service = new SOAPMessageModeProvider(); Endpoint.publish("http://localhost:8080/test/jaxws/services/SOAPMessage", service);
4 客户端访问底层消息
客户端访问底层消息是通过Dispatch接口进行的,跟服务端的Provider接口一样,Dispatch接口中同样定义了一个invoke方法,该方法负责向服务端发送一种数据类型的消息,并返回一个对应类型的消息。不同的是Dispatch接口的实现类可以不需要我们自己定义和实现。我们可以通过创建代表服务端对应WebService对象的Service对象来创建一个Dispatch对象。Service类中定义了一系列的createDispatch重载方法,但比较常用的还是如下方法:
public <T> Dispatch<T> createDispatch(QName portName, Class<T> type, Mode mode)
该方法接收三个参数:
l 第一个参数QName类型的portName代表目标Service中对应的portName;
l 第二个参数表示底层发送和接收消息时使用的数据类型,根据配置的不同可以是前面提到的三种数据类型中的一种;
l 第三个参数表示使用的消息模式。
下面我们来看一个创建Dispatch,并使用它来与服务端进行交互的例子:
public static void main(String args[]) throws Exception { //定义serviceName对应的QName,第一个参数是对应的namespace QName serviceName = new QName("http://provider.jaxws.sample.cxftest.tiantian.com/", "SOAPMessageService"); //定义portName对应的QName QName portName = new QName("http://provider.jaxws.sample.cxftest.tiantian.com/", "SOAPMessagePort"); //使用serviceName创建一个Service对象,该对象还不能直接跟WebService对象进行交互 Service service = Service.create(serviceName); //创建一个port,并指定WebService的地址,指定地址后我们就可以创建Dispatch了。 service.addPort(portName, SOAPBinding.SOAP11HTTP_BINDING, "http://localhost:8080/test/jaxws/services/SOAPMessage"); //创建一个Dispatch对象 Dispatch<SOAPMessage> dispatch = service.createDispatch(portName, SOAPMessage.class, Mode.MESSAGE); //创建一个SOAPMessage SOAPMessage request = MessageUtil.getInstance().create(null, "/dispatch/SOAPMessageReq.xml"); //调用Dispatch的invoke方法,发送一个SOAPMessage请求,并返回一个SOAPMessage响应。 SOAPMessage response = dispatch.invoke(request); System.out.println("服务端返回如下: "); response.writeTo(System.out); }
在上面的代码中,我们先通过serviceName创建了一个Service对象,然后再通过addPort方法指定其对应的WebService地址。其实,我们也可以像下面这样,通过WebService对应的wsdl文件和serviceName创建对应的Service对象。
//指定wsdl文件的位置 URL wsdl = new URL("http://localhost:8080/test/jaxws/services/SOAPMessage?wsdl"); Service service = Service.create(wsdl, serviceName);
上述例子中对应的SOAPMessageReq.xml文件的内容如下:
<?xml version="1.0" encoding="utf-8" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOAP-ENV:Body> <ns4:sayHi xmlns:ns4="http://provider.jaxws.sample.cxftest.tiantian.com/"> <ns4:requestType>SOAPMessage Request</ns4:requestType> </ns4:sayHi> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
4.1 Dispatch的三种请求方式
同步请求
上述Dispatch的invoke方法请求是同步的,也就是阻塞式的,程序在调用了invoke方法之后会一直等待服务端的返回。
异步请求
异步请求是通过Dispatch的invokeAsync方法进行的。Dispatch的异步请求有两种方式,一种是通过定期轮询Dispatch调用invokeAsync方法返回的Response对象是否已经可以返回,另一种是通过回调函数的形式。所以,针对于这两种方式,Dispatch的invokeAsync有两个重载方法:
public Response<T> invokeAsync(T msg); public Future<?> invokeAsync(T msg, AsyncHandler<T> handler);
使用定期轮询的方式时,我们在执行完invokeAsync之后会返回一个Response对象,该对象会定期轮询判断invokeAsync方法是否已经完成。当invokeAsync方法调用完成之后,Response对象的isDone()方法会返回true,但是这种调用的完成并不一定是成功的完成,有可能是出异常了,或者其他什么问题。在调用完成,也就是isDone()方法的结果为true之后,我们就可以通过Response对象的get()方法尝试获取对应的返回对象了,之所以说是尝试获取,是因为我们的invokeAsync方法不一定是正常的完成了,如果没有正常完成,调用get()方法将抛出异常。上面Dispatch调用的例子如果我们把它改为定期轮询的异步请求的话,其调用过程的代码可以是这样子:
SOAPMessage request = MessageUtil.getInstance().create(null, "/dispatch/SOAPMessageReq.xml"); Response<SOAPMessage> response = dispatch.invokeAsync(request); System.out.println("开始判断调用是否已完成"); while (!response.isDone()) { Thread.sleep(200l); } SOAPMessage responseMsg = null; try { responseMsg = response.get(); } catch (Exception e) { System.out.println("调用失败"); } if (responseMsg != null) { System.out.println("服务端返回如下: "); responseMsg.writeTo(System.out); }
使用回调函数的方式时,我们需要给invokeAsync方法传递一个AsyncHandler接口的实现类作为回调对象。AsyncHandler接口中定义了一个handleResponse方法可以处理服务端返回的结果。当请求完成以后,Dispatch后端的线程会调用AsyncHandler对象的handleResponse方法。前面Dispatch调用的例子如果我们把它改为使用回调函数异步调用的话,其核心代码可以是如下这个样子:
SOAPMessage request = MessageUtil.getInstance().create(null, "/dispatch/SOAPMessageReq.xml"); Future<?> future = dispatch.invokeAsync(request, new AsyncHandler<SOAPMessage>() { @Override public void handleResponse(Response<SOAPMessage> res) { try { System.out.println("回调函数被调用了……"); SOAPMessage responseMsg = res.get(); responseMsg.writeTo(System.out); } catch (Exception e) { e.printStackTrace(); } } }); System.out.println("可以开始做其他事情了……"); while (!future.isDone()) { System.out.println("在请求完成之前,整个程序不能结束,否则回调函数不会被调用"); Thread.sleep(200l); }
注意,在使用回调函数方式使用Dispatch的异步请求时,请求结果未返回前整个程序不能停止。如果在请求结果返回以前,整个程序结束了,回调函数不会被调用。当然这种情况只会出现在如上单次执行的测试环境下,我们经常使用的Web环境是不会出现此问题的。由此看来,回调函数应该是被Dispatch内部的守护线程调用的。
一次请求
一次请求是通过invokeOneWay方法来进行的。它表示我们的客户端只需要发送请求,而不需要等待服务端的返回。
SOAPMessage request = MessageUtil.getInstance().create(null, "/dispatch/SOAPMessageReq.xml"); dispatch.invokeOneWay(request);
5 统一入口
JaxWs基于消息编程的一个好处是我们可以在服务端使用一个Provider来接收和处理所有的WebService请求,使用一个Dispatch或多个Dispatch来发送请求,从而达到对WebService的统一管理;另一个好处是客户端可以不定义或者说是不需要使用SEI接口及其相关的类。下面我们来看一个客户端和服务端之间直接通过消息编程的简单示例。
在服务端定义一个Provider<DOMSource>的实现类UniteServiceProvider。
@WebServiceProvider(serviceName="UniteService", portName="UniteServicePort", targetNamespace="http://provider.jaxws.sample.cxftest.tiantian.com/") @ServiceMode(Service.Mode.MESSAGE) @BindingType(HTTPBinding.HTTP_BINDING) public class UniteServiceProvider implements Provider<DOMSource> { @Override public DOMSource invoke(DOMSource request) { DOMSource response = null; MessageUtil.getInstance().printSource(request); Document requestDoc = (Document)request.getNode(); Element commandEle = (Element)requestDoc.getElementsByTagName("command").item(0); Element paramEle = (Element)requestDoc.getElementsByTagName("param").item(0); String command = commandEle.getTextContent(); try { response = this.getResponse(command, paramEle); } catch (Exception e) { e.printStackTrace(); } MessageUtil.getInstance().printSource(response); return response; } /** * 根据指令和对应的参数进行相关操作并返回对应的操作结果 * @param command * @param paramEle * @return * @throws Exception */ private DOMSource getResponse(String command, Element paramEle) throws Exception { String responseContent = "<response><product><id>1</id><name>Apple</name></product></response>"; InputStream is = new ByteArrayInputStream(responseContent.getBytes("UTF-8")); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is); DOMSource response = new DOMSource(doc); return response; } }
在上述代码中我们的服务端接收到一个DOMSource的request请求之后,通过DOMSource的getNode方法取到存放在其中的Document对象。之后我们对该Document的内容作了一个解析,然后把对应的返回结果封装成一个DOMSource进行返回。
上述使用到的MessageUtil类里面的printSource方法的代码为:
/** * 输出Source的内容 * @param source */ public void printSource(Source source) { StreamResult result = new StreamResult(System.out); try { TransformerFactory.newInstance().newTransformer().transform(source, result); System.out.println(); } catch (Exception e) { e.printStackTrace(); } }
发布上述Provider的过程这里就不再赘述了。接着来看一下客户端调用的代码:
public class UniteServiceClient { public static void main(String args[]) throws Exception { String requestContent = "<request><command>10001</command><parameter><id>1</id></parameter></request>"; InputStream is = new ByteArrayInputStream(requestContent.getBytes("UTF-8")); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is); DOMSource requestMsg = new DOMSource(doc); MessageUtil.getInstance().printSource(requestMsg); QName serviceName = new QName("http://provider.jaxws.sample.cxftest.tiantian.com/", "UniteService"); QName portName = new QName("http://provider.jaxws.sample.cxftest.tiantian.com/", "UniteServicePort"); Service service = Service.create(serviceName); //指定绑定方式为HTTPBinding service.addPort(portName, HTTPBinding.HTTP_BINDING, "http://localhost:8080/test/jaxws/services/UniteService"); Dispatch<DOMSource> dispatch = service.createDispatch(portName, DOMSource.class, Mode.MESSAGE); DOMSource responseMsg = dispatch.invoke(requestMsg); System.out.println("服务端返回来的信息是:"); MessageUtil.getInstance().printSource(responseMsg); } }
6 注意
6.1 MESSAGE和PAYLOAD的区别
在文章的开始部分介绍了两种消息模式,以及它们之间的区别。MESSAGE模式是访问的整个消息,而PAYLOAD模式访问的只是消息的部分内容。我们也知道,当我们使用DOMSource作为消息对象时我们可以使用MESSAGE和PAYLOAD这两种模式。所以接下来我们就来说说使用DOMSource作为消息对象时使用MESSAGE模式和PAYLOAD模式的区别。
为了具有可比性,我们指定使用的BindingType为SOAPBinding,(未指定BindingType时默认也为SOAPBinding),下面来看看使用MESSAGE模式和PAYLOAD模式的区别。
MESSAGE模式
使用MESSAGE模式,我们在发送DOMSource消息对象时,如果我们的DOMSource消息对象里面持有的Document不是一个SOAPPart(SOAPPart是一个实现了Document接口的抽象类),那么系统会先生成一个SOAPPart,然后把我们的DOMSource里面持有的Document作为SOAPPart关联的SOAPEnvelope对象的SOAPBody部分。然后再把该SOAPPart作为DOMSource持有的Document对象。这个时候如果我们只想获取到最原始的document,也就是SOAPBody包裹的那一段文档,我们得这样来取:
public DOMSource invoke(DOMSource request) { SOAPPart soapPart = (SOAPPart) request.getNode(); try { SOAPEnvelope soapEnvelop= soapPart.getEnvelope(); SOAPBody soapBody = soapEnvelop.getBody(); Document preDoc = soapBody.extractContentAsDocument(); } catch (SOAPException e1) { e1.printStackTrace(); } returnnull; }
如果DOMSource本身持有的Document对象就是一个SOAPPart的话就可以直接发送了,不需要再做转换了。当我们的DOMSource持有的不是一个SOAPPart时,系统在生成SOAPPart时很可能会抛出异常信息:HIERARCHY_REQUEST_ERR: 尝试在不允许的位置插入节点。所以当我们配合使用SOAPBinding、DOMSource消息对象和MESSAGE模式时,我们最好给DOMSource传入一个SOAPPart对象或者是SOAPPart格式的Document对象。
PAYLOAD模式
使用PAYLOAD模式时,我们发送的DOMSource消息会直接发送过去。对方接收到的内容和发送时的内容是一样的,注意只是内容是一样的,其持有的Document对象还是会当做一个普通的Document对象处理,如DocumentImpl。比如发送的时候DOMSource持有的是一个SOAPPart,那么接收的时候接收到的DOMSource里面的Document的内容还是发送时SOAPPart的内容,但是对象却是一个普通的Document对象,而不是发送时的SOAPPart对象;而如果发送的时候发送的是一个普通的Document对象,那么接收到的内容也只是一个普通Document的内容,不会像MESSAGE模式那样会有多余的SOAPHeader等信息。