本文不是JAX-WS的入门文档,如需了解JAX-WS,请参阅其它文章。
WebService为客户端与服务器沟通提供了非常良好的开发体验,由其在Java中体现尤为明显。JAX-WS是Java世界里一个非常优秀的WebService开发工具,通过采用JAX-WS,开发人员可以用非常简单的方式从客户端调用服务器开放的服务。
在采用C/S模 式开发的时候,一个客户与服务器经常经过好几次的交互过程才能完成一笔交易或者是一个请求的完成。由于这几次交互过程是密切相关的,服务器在进行这些交互 过程的某一个交互步骤时,往往需要了解上一次交互过程的处理结果,或者上几步的交互过程结果,这就需要在这几次交互过程中保持会话状态。
基于HTTP的应用开发中,要在多个调用之间保持会话状态,通常可以采用以下几种方式:
l URL重写,把要传递的参数重写在URL中;
l 使用Cookie,把要传递的参数写入到客户端cookie中;
l 使用隐含表单,把要传递的参数写入到隐含的表单中;
l 使用Session,把要传递的参数保存在session对象中(其实Session机制基于cookie或者URL重写)。
上面几个方式有一个共同点:把要传递的参数保存在两个页面都能共享的对象中,前一个页面在这个对象中写入状态、后一个页面从这个对象中读取状态。特别是对于使用session方式,每个客户端在服务端都对应了一个sessionid,服务端维持了由sessionid标识的一系列session对象,而session对象用于保持共享的信息
上述保持会话状态的方式,较常用的是服务器Session技术。服务端程序通过调用session.setAttribute(key,value) 和 session.getAttribute(key) ,在多次交互过程中保持会话状态。另外,为了保持会话,客户端也要做一些相应的工作。下面将从两个方面讨论在使用JAX-WS的项目中,如何才能保持会话状态。
服务器端
如果要保持会话状态,首要一点就是要获得本次请求的session。下面假设我们的WebService名称为HelloService,Port名为HelloPort。代码如下:
package cn;
@javax.jws.WebService(targetNamespace = "http://cn/", serviceName = "HelloService", portName = "HelloPort")
public class HelloDelegate {
@Resource
private WebServiceContext wsContext;
public void sayHello(){
MessageContext mc = wsContext.getMessageContext();
HttpSession session =
((javax.servlet.http.HttpServletRequest)mc.get(MessageContext.SERVLET_REQUEST)).getSession();
}
}
首先,通过在class HelloDelegate上方通过@WebService标签,说明HelloDelegate为一个WebService,然后在类的内部声明一个WebServiceContext类型的引用,该引用即为WebService上下文类型。每一次的WebService调用,在服务器端都有一个对应的WebService上下文对象。该对象的获取是通过在引用上方标注@Resource得到的。
得到WebService上下文后,读取其中的消息上下文,该消息上下文中包装了HttpServletRequest对象,而HttpSession对象正是在HttpServletRequest对象中。
到此为止,就可以在session中写入或者读取信息了。
客户端
客户端的会话保持可以分为两种需求,一种是对同一个Service的不同调用之间保持会话,另一种是在不同Service之间也保持会话,下面将分两种情况讨论。
1.在同一个Service的不同调用之间保持会话
这种情况比较简单,首先需要获得一个服务端点,相对上面的代码来说即获得一个HelloDelegate类型的对象,该对象是服务器端WebService的本地代理。
HelloService hs = new HelloService();
HelloDelegate hello = hs.getHelloPort();
现在,只要告诉hello对象保持会话就可以了。每一个客户端WebService代理内部都维护者一个请求上下文。该请求上下文是一个Map类型的对象,每次hello对象调用WebService,都会坚持这个请求上下文中的属性。其中有一个属性是 BindingProvider.SESSION_MAINTAIN_PROPERTY,类型为Boolean。如果该属性为true,那么该hello对象每次调用都会保持会话,否则每次调用都会发起一次新的会话,该值默认为false。现在通过设置该请求上下文,即可完成会话保持的设置。
((BindingProvider)hello).getRequestContext().put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true);
2.在不同的Service调用时保持会话
如果要在不同的WebService调用间保持会话,仅仅使用上述技术是不够的,要解决这个问题,就需要深入了解WebService内部实现机制。
通过对session方式的了解,我们知道,当客户端第一次向服务器发起请求时,服务器会在向客户端发送的HTTP应答中设置sessionid,在后继的HTTP请求中,只要客户端将sessionid设置到HTTP请求中,服务器就可以保持会话状态。
WebService的核心是通过一系列协议,如HTTP协议、SOAP协议等从客户端调用服务器端的代码,因此所有的WebService调用都是通过HTTP协议发起的。
现在问题的焦点是,如何在WebService调用中获得服务器发送的sessionid,以及如何将sessionid设置到多个Service调用的HTTP请求中,另外,要为与sessionid相关的代码在系统中找到一个比较合适的位置。这里首先要了解JAX-WS中的Handler机制。
2.1理解JAX-WS的handler机制
JAX-WS的handler机制提供插件式框架,通过handlers去加强运行的处理能力。它是在上层WebService代理与底层HTTP调用之间维护一个handler链,所有的调用请求与反馈都要经过handle链的处理,这样开发人员就有机会在发送请求前和接收反馈后进行定制处理。
JAX-WS定义了两种类型的handlers,分别为logical handlers and protocol handlers。在文章"A little about Handlers in JAX-WS"[1]中,Rama Pulavarthi 解释了logical和SOAP handlers的区别。
从上图可以看出,客户端与服务器的通信过程中,无论请求还是响应都要通过SOAP Handler处理。在SOAP Handler中,JAX-WS会提供一个SOAP MessageContext对象,这个对象正是我们需要的。
要实现一个Handler,首先需要写一个实现SOAP Handler接口的类,然后构造该类的对象,将该对象插入到指定的端点对象即可。
SOAP Handler接口说明如下,其中最关键的是要实现handleMessage方法,该方法的参数中提供了SOAPMessageContext类型的引用。
public Set<QName> getHeaders();
public void close(MessageContext context) ;
public boolean handleFault(SOAPMessageContext context) ;
public boolean handleMessage(SOAPMessageContext context) ;
由于在每一次连接的请求和响应时都要调用Handler,因此要获得Response上下文,首先需要知道当前调用的方向,是处于请求时还是响应时。
Boolean out =
(Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
对于客户端来说,out为true时表示请求,out为false表示响应。对于服务器端来说,恰恰相反,out为true是表示响应,out为false时,表示请求。
2.2 获得服务器发送的sessionid
要获得服务器发送的sessionid,因为服务器都是在HTTP Response中设置sessionid,因此首先要能够获得WebService调用中的Response上下文,只有获得该上下文对象,才能进一步的操作。SOAP Handler中的handleMessage方法提供的context参数,其中正包括了HTTP的响应上下文。
获得响应上下文之后,按照HTTP协议,只要从该上下文获取“Set-cookie”域即可,因为sessionid正是存在“Set-cookie”域中。
Map<String, List<String>> responseHeaders = (Map<String, List<String>>) context.get(MessageContext.HTTP_RESPONSE_HEADERS);
List<String> cookie = responseHeaders.get("Set-cookie");
检查得到的cookie对象,发现其中含有jsessionid项。
2.3 将sessionid设置到WebService调用的HTTP请求中
首先需要从SOAPMessageContext中获得HTTP请求上下文,然后按照HTTP协议,在该请求上下文中设置“cookie”属性即可。
Map<String,List<String>> requestHeaders = (Map<String, List<String>>) context.get(MessageContext.HTTP_REQUEST_HEADERS);
requestHeaders.put("cookie", cookie);
根据上述的方法,我们可以获得并设置sessionid,在此基础上,只要在所有的WebService调用中设置相同的sessionid,即可达成目的。为此,首先需要构造一个Handler,该Handler负责读取和设置sessionid,然后为所有的WebService调用设置该Handler即可。
2.4完整步骤
过程分为三步:
1.声明一个实现SOAPHandler接口的类:
class MyHandler implements SOAPHandler<SOAPMessageContext> {
private List cookie = null;
public Set<QName> getHeaders() { return null; }
public void close(MessageContext context) {}
public boolean handleFault(SOAPMessageContext context) {return false;}
public boolean handleMessage(SOAPMessageContext context) {
try {
//获得当前调用方向
Boolean out =
(Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if(!out){//响应
Map<String, List<String>> responseHeaders =
(Map<String, List<String>>) context.get(MessageContext.HTTP_RESPONSE_HEADERS);
List<String> c = responseHeaders.get("Set-cookie"); //sessionid在该域中
if(cookie==null && c!=null){ //这是第一次HTTP调用,cookie刚刚得到
cookie = c; //保存该cookie
}
}else{ //请求
//获取请求上下文
java.util.Map<String, List<String>> requestHeaders =
(Map<String, List<String>>) context.get(MessageContext.HTTP_REQUEST_HEADERS);
if(requestHeaders==null){ //请求上下文可能为空,构造一个新的即可
requestHeaders = new HashMap<String, List<String>>();
//将一个空的请求上下文设置到SOAPMessageContext中
context.put(MessageContext.HTTP_REQUEST_HEADERS, requestHeaders);
//设置该请求上下文全局有效
context.setScope(MessageContext.HTTP_REQUEST_HEADERS, MessageContext.Scope.APPLICATION);
}
//如果已经获得了sessionid,将该sessionid设置到请求上下文中即可
if(cookie!=null){
requestHeaders.put("cookie", cookie);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;//返回true,意味着其它Handler也可以被调用
}
}
2.构造一个MyHandler类型的对象
public static final MyHandler handler = new MyHandler();
3.对于所有的WebService调用,设置该Handler。
由于每一个WebService调用内部都维护着一个Handler链,因此只要找到该链并将该Handler插入其中即可。
class HandlerSettor{
public static final MyHandler handler = new MyHandler();
public static void setHandler(BindingProvider proxy) {
if (proxy != null) {
Binding binding = ((BindingProvider) proxy).getBinding();
List<Handler> handlerList = binding.getHandlerChain();//获得Handler链
if (!handlerList.contains(handler)) {//防止重复插入Handler
handlerList.add(handler);
binding.setHandlerChain(handlerList);
}
}
}
}
通过下面的代码来使用:
class Test{
public static void main(String[] args){
HelloService hs = new HelloService();
HelloDelegate hello = hs.getHttpPort();
HandlerSettor.setHandler(hello);
hello.sayHello();
}
}