blog.Toby

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  130 随笔 :: 2 文章 :: 150 评论 :: 0 Trackbacks
本文首先讨论了Web服务会话状态的保持方法,然后着重结合J2EE平台中Web服务核心技术--JAX-RPC来介绍怎么在Web服务调用过程中保持客户端的会话状态,并且提供了服务端和不同类型客户端的调用实例。

本文是J2EE Web服务开发系列文章的第九篇,本文首先讨论了Web服务会话状态的保持方法,然后着重结合J2EE平台中Web服务核心技术--JAX-RPC来介绍怎么在Web服务调用过程中保持客户端的会话状态,并且提供了服务端和不同类型客户端的调用实例。

阅读本文前您需要以下的知识和工具:

  • J2EESDK1.4(Sun已经发布了正式版),并且会初步使用;

  • 了解JAX-RPC的基本概念;
  • 能够使用JAX-RPC技术开发Web服务;
  • 一般的Java编程知识。

本文的参考资料见 参考资料

本文的全部代码在这里 下载

Web服务与会话


Web服务大多基于HTTP协议,而HTTP协议是一种无状态的协议。Web服务规范并没有定义客户端和服务端之间会话的保持方法。所以要在多个Web服务调用之间保持一些状态,需要使用一些额外的技术或者方法。

我们知道,基于HTTP的应用开发中,要在多个调用之间保持会话状态,通常可以采用以下几种方式:

  • URL重写,把要传递的参数重写在URL中;

  • 使用Cookie,把要传递的参数写入到客户端cookie中;

  • 使用隐含表单,把要传递的参数写入到隐含的表单中;

  • 使用Session,把要传递的参数保存在session对象中(其实Session机制基于cookie或者URL重写)。

上面几个方式有一个共同点:把要传递的参数保存在两个页面都能共享的对象中,前一个页面在这个对象中写入状态、后一个页面从这个对象中读取状态。特别是对于使用session方式,每个客户端在服务端都对应了一个sessionid,服务端维持了由sessionid标识的一系列session对象,而session对象用于保持共享的信息。

我们似乎从上面得到一些启发,是否可以在服务端标识每个Web服务客户端,并且把它们的状态保持在某个可以共享的位置,比如内存、文件系统、数据库。是的,许多Web服务开发工具正是这样实现的。如果使用Weblogic Workshop开发Web服务,它可以把Web服务的状态保持在实体Bean中。

如果Web服务的服务端能够访问HTTP会话对象,那么就可以通过HTTP会话来支持Web服务的会话。JAX-RPC就是采用了这种方式。

如果Web服务技术或者开发工具没有提供任何的支持,那么我们可以在服务端维持一个客户端状态池,这个状态池中的对象由客户端的id标识,客户端在每次调用时,都使用以下格式的SOAP消息:

												
														<soapenv:Envelope 
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <soapenv:Body>
   ...
   <client-id>001sf3242x-234234</client-id>
   <call-params>… </call-param>
   <call-method>
    getLogCount
   </call-method>
 </soapenv:Body>
</soapenv:Envelope>

												
										

服务端接收到这个SOAP消息时,可以通过这个<client-id>来获得对应状态池中的对象,然后进一步获得客户端预先设置的状态信息。

下面结合JAX-RPC技术来讨论具体的实现方法。





回页首


JAX-RPC和Web服务会话


概念回顾:在J2EE平台中,要开发Web服务,可以使用两种技术:JAX-RPC和JAXM。而对于JAX-RPC,又有两种不同类型的服务端点:Servlet服务端点和EJB服务端点。基于Servlet的服务端点运行在Servlet容器中,基于EJB的服务端点运行在EJB容器中。

我们知道,Servlet可以在客户端的多个调用之间保持会话状态,所以基于Servlet的JAX-RPC Web服务端点要保持客户的会话状态是可行的。但如果是EJB服务端点,由于这里的EJB是无状态会话Bean,所以要在多个调用之间保持状态必须通过其它机制实现,这里不讨论。

下面从JAX-RPC的生命周期和ServletEndpointContext接口来说明怎么保持和访问客户端的会话状态。

JAX-RPC的生命周期

根据JAX-RPC的规范,如果Web服务端点实现javax.xml.rpc.server.ServiceLifecycle接口,那么基于Servlet容器的JAX-RPC运行环境将管理这个端点的生命周期。javax.xml.rpc.server.ServiceLifecycle接口定义如下:

												
														例程1 ServiceLifecycle
package javax.xml.rpc.server;
public interface ServiceLifecycle {
void init(Object context) throws ServiceException;
void destroy();
}

												
										

JAX-RPC运行环境负责装载并实例化服务端点实例,装载和实例化可以在JAX-RPC运行环境启动时进行,也可以在服务端点处理SOAP RPC请求时进行。JAX-RPC运行环境使用Java类装载机制来装载服务端点,当成功装载目标类后,将实例化这个类。

当服务端点实例化后,在RPC请求到达之前JAX-RPC运行环境将初始化它们,这个初始化通过ServiceLifecycle.init方法来进行的。在初始化的过程中,可能需要设置一些访问外部资源的方法。在init方法中,有一个context参数,它用来访问由JAX-RPC运行环境提供的端点上下文(ServletEndpoint Context)。当初始化服务端点后,JAX-RPC运行环境就可以把多个远程调用派发到服务端点。在远程方法调用的过程中,JAX-RPC服务端点并不维持任何客户端的状态。所以JAX-RPC服务端点实例能够被池化(Pooling)。

当JAX-RPC运行环境决定移除服务端点实例时,它将调用服务端点实例的destroy方法。比如在系统关闭或者实例池中实例过多时,就可能发生这种操作。在destroy方法中,服务端点将释放占用的资源。当成功调用了destroy方法后,服务端点实例将被垃圾收集器收集。此时它不能处理任何远程方法调用。

ServletEndpointContext接口

在ServiceLifecycle.init(Object context)方法中,其中参数context就是ServletEndpointContext实例,ServletEndpointContext是由JAX-RPC运行环境维持的端点上下文。JAX-RPC规范规定了基于Servlet的服务端点的编程模型,但并没有为服务端点上下文(Endpoint Context)或者会话(Session)定义与组件模型、容器、绑定协议更通用的抽象,也就是说这些通用的抽象在JAX-RPC规范之外。

下面是ServletEndpointContext接口的代码。

												
														例程2 ServletEndpointContext接口
package javax.xml.rpc.server;
public interface ServletEndpointContext {
public java.security.Principal getUserPrincipal();
public javax.xml.rpc.handler.MessageContext getMessageContext();
public javax.servlet.http.HttpSession getHttpSession();
public javax.servlet.ServletContext getServletContext();
}

												
										

ServletEndpointContext接口对于会话的保持非常关键,因为它定义了getHttpSession方法,这个方法返回了和当前活动的客户端调用相关的HTTP会话。客户端的会话由JAX-RPC运行环境维持。如果没有相关的HTTP会话,那么这个方法返回null。

除了getHttpSession方法外,这个接口还定义了或者SOAP消息上下文,Servlet上下文方法,在这里不讨论了。

有了上面的理论,下面我们来开发一个能够使用HTTP会话的Web服务。





回页首


开发服务端


首先定义一个端点接口,它拥有几个交互操作的方法,如例程3所示。

												
														例程3 定义服务端点接口
package com.hellking.study.webservice.session;

import java.rmi.Remote;
import java.rmi.RemoteException;
/**
 *Web服务端点接口,它定义了三个服务方法。
 */
public interface SessionTestIF extends Remote {
    public String login(String id,String password) throws RemoteException;
    public String getLoginCount() throws RemoteException;    
    public void logout() throws RemoteException;
}

												
										

要想在Web服务中访问HTTP会话,那么必须拥有ServletEndpointContext实例,而这个实例必须通过ServiceLifecycle接口的init方法获得。也就是说,要使用HTTP会话,Web服务端点必须实现ServiceLifecycle接口。Web服务实现类如例程4所示。

												
														例程4 服务实现类
package com.hellking.study.webservice.session;

import java.rmi.Remote.*;
import javax.xml.rpc.server.ServiceLifecycle;
import javax.xml.rpc.server.ServletEndpointContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import java.util.Properties;
import java.io.FileInputStream;
import javax.servlet.http.HttpSession;
/**
 *SessionTestImpl是Web服务实现类,用于测试Web服务中Session的使用。
 *由于要使用Session,需要实现ServiceLifecycle接口。
 */
public class SessionTestImpl implements SessionTestIF,ServiceLifecycle{
 
 //服务端点上下文
 private ServletEndpointContext serviceContext;
    
    /**
     *ServiceLifecycle方法:初始化服务端点,或者要使用的资源。
     */
    public void init(java.lang.Object context)
    {
  serviceContext=(ServletEndpointContext)context;
 }
 /**
     *ServiceLifecycle方法:销毁服务端点实例。
     */
    public void destroy() 
    {
     this.serviceContext=null;
    //可能还有其它释放资源的方法。
    }
 /**
  *Web服务方法:登录,并且保存一些信息到HTTP会话中。
  */
    public String login(String id,String password) 
    {
     String ret=null;//返回值。
     Properties users=new Properties();
     try
     {
      //获得用户名、密码属性,一般是在数据库中,这里简化,把这些信息保存在一个文件中。
        users.load(com.hellking.study.webservice.session.SessionTestImpl.class
          .getResourceAsStream("password.properties"));
     }
     catch(java.io.FileNotFoundException e)
     {
      e.printStackTrace();
     }
     catch(java.io.IOException e)
     {
      e.printStackTrace();
     }
     try
     {
      String passwd=(String)users.getProperty(id);
      if(password.equals(passwd))
      {
       ret="登录成功,你可以执行其它操作!";
    HttpSession session = serviceContext.getHttpSession();
    //保存用户会话状态,它和一般的HTTP会话一样,都使用HttpSession来进行。
    session.setAttribute("isLogin",new Boolean(true));
    session.setAttribute("userId",id);    
    //更新登录次数,在这里省略。
      }
     }
     catch(Exception e)
     {
      ret="登录失败,请确认用户名和密码正确!";
      e.printStackTrace();
     } 
     return ret;
    }
    
    /**
     *Web服务方法:获得登录的次数,需要使用HTTP会话来获得当前user的id。
     */
    public String getLoginCount() 
    {
     String ret=null;//返回值
     HttpSession session = serviceContext.getHttpSession();
        Properties logcount=new Properties();
     try
     {
      //获得用户登录次数。
      logcount.load(com.hellking.study.webservice.session.SessionTestImpl.class
      .getResourceAsStream("login.properties"));
     }
     catch(java.io.FileNotFoundException e)
     {
      e.printStackTrace();
     }
     catch(java.io.IOException e)
     {
      e.printStackTrace();
     }
     try
     {
      //从HTTP会话中获得是否登录的属性。
      Boolean isLogin=(Boolean)session.getAttribute("isLogin");
      String userId=(String)session.getAttribute("userId");
      //如果已经登录,那么返回logcount属性值。
      if(isLogin.equals(Boolean.TRUE))
      {      
       ret=logcount.getProperty(userId);
      }
     }
     catch(Exception e)
     {
      e.printStackTrace();
     }
     return ret;     
    }
    /**
     *注销,使会话无效。
     */    
    public void logout() 
    {
     HttpSession session = serviceContext.getHttpSession();
     session.invalidate();
    }        
}

												
										

在上面代码中,SessionTestImpl 拥有一个ServletEndpointContext成员变量,这个成员变量在init方法初始化:

												
														serviceContext=(ServletEndpointContext)context;

												
										

在login方法中,通过:

												
														HttpSession session = serviceContext.getHttpSession();

												
										

方法来获得和当前客户端关联的HTTP会话,然后可以把一些交互的信息保存在session中,如:

												
														session.setAttribute("isLogin", new Boolean(true));

												
										

把用户已经登录的信息保存起来。在其它的Web服务方法中,如getLoginCount,可以通过:

												
														Boolean isLogin=(Boolean)session.getAttribute("isLogin");

												
										

之类的方法来获得原来保存的属性值。





回页首


编写描述符、部署


开发好以上两个类后,需要进行一些相关的描述,编写以下脚本:

												
														例程5 service-config.xml 
<?xml version="1.0" encoding="UTF-8"?>
<configuration 
  xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
  <service 
      name="MySessionTestService" 
      targetNamespace="urn:SessionTest" 
      typeNamespace="urn:SessionTest" 
      packageName="sessionTest">
      <interface name="com.hellking.study.webservice.session.SessionTestIF"/>
  </service>
</configuration>

												
										

通过:

												
														wscompile -define -d . -nd . -classpath . service-config.xml

												
										

命令生成一个名为MySessionTestService.wsdl的Web服务描述文件。再通过:

												
														wscompile -gen -classpath . -d . -nd . -mapping mapping.xml service-config.xml

												
										

生成一个映射文件。另外,还需要编写几个描述符,如webservices.xml、web.xml等,在这里就不介绍了(这些描述符可以通过部署工具自动生成,见本文代码)。由于这个服务端点需要维持会话,所以在web.xml中特别描述了会话的保持时间,如例程6所示。

												
														例程6 web.xml描述符
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">

<web-app>
  <display-name>sessionTest-jaxrpc</display-name>
  <description>A web application containing a simple JAX-RPC endpoint</description>
  <servlet>
    <servlet-name>SessionTestServletImpl</servlet-name>
    <servlet-class>com.hellking.study.webservice.session.SessionTestImpl</servlet-class>
    <load-on-startup>0</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>SessionTestServletImpl</servlet-name>
    <url-pattern>/mysessionTest</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>60</session-timeout>
  </session-config>
</web-app>

												
										

可以看出,SessionTestServletImpl是作为Servlet运行的。为了在JAX-RPC环境启动时就实例化这个服务端点,需要设置<load-on-startup>元素值为0。另外,<session-config>元素值指定了客户端和JAX-RPC运行环境之间会话保持的时间。

关于打包和部署方法在这里就不赘述了,您可以参考本系列文章《使用EJB2.1无状态会话Bean作为Web服务端点》一文。





回页首


开发客户端


我们知道,JAX-RPC有三种不同类型的客户端:

  • 基于Stub;
  • 动态代理;
  • 动态调用。

下面讨论怎么在基于Stub和基于动态调用的客户端使用Web服务会话。

基于Stub的客户端

我们不得不从Stub接口的SESSION_MAINTAIN_PROPERTY属性说起,如果在客户端设置这个属性为Boolean.TRUE,那么在Web服务交互过程中,服务端将维持一个HTTP会话,否则不会维持HTTP会话。基于Stub的客户端代码如例程7所示。

												
														例程7 基于Stub的客户端
package com.hellking.study.webservice.session;
import javax.xml.rpc.Stub;
/**
 *Web服务调用客户端,测试Web服务会话。基于Stub的调用。
 */
public class SessionTestClientUseStub
{
  
   Stub stub;
   SessionTestIF sessionTest;
   //初始化Stub。
   public SessionTestClientUseStub()
   {
       stub = (Stub)(new MySessionTestService_Impl().getSessionTestIFPort());
       stub._setProperty(javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, 
                "http://127.0.0.1:8080/sessionTest/mysessionTest"); 
       stub._setProperty(Stub.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);
       sessionTest = (SessionTestIF)stub;
    }
   
    public static void main(String[] args) 
    {
  SessionTestClientUseStub test=new SessionTestClientUseStub();
  test.login();
  test.getLoginCount();
  test.logout();
        
    }
    /**
     *登录。
     */
    public void login()
    {
     try {           
             System.out.println("正在登录...");
            System.out.println(sessionTest.login("userid-001","abc"));
        } catch (Exception ex) {
            ex.printStackTrace();
        }  
   }
   /**
    *获得logincount属性值。
    */
   public void getLoginCount()
   {
      try
      {
       System.out.println("LoginCount的值为:");
       System.out.println(sessionTest.getLoginCount());
      }
      catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    /**
     *注销。
     */
    public void logout()
    {
   …//省略代码
    }
}

												
										

在服务调用之前,通过:

												
														stub._setProperty(Stub.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);

												
										

方法来设置SESSION_MAINTAIN_PROPERTY属性。

部署好服务端后,运行这个代码将获得如图1所示的结果。



图1 使用会话的测试结果

可以看出,上面的调用达到了预期的效果。因为在getLoginCount方法中并没有传入任何参数,但获得了前一个方法中登录id的LoginCount值。

如果我们屏蔽:

												
														stub._setProperty(Stub.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);

												
										

代码,或者更改为以下代码:

												
														stub._setProperty(Stub.SESSION_MAINTAIN_PROPERTY,Boolean.FALSE);

												
										

编译后再运行这个客户端,将获得如图2所示的结果。



图2 不使用会话的测试结果

可以看出,这里返回的LoginCount为null,说明客户端的会话并没有保持。

基于动态调用客户端

基于动态调用客户端主要是通过javax.xml.rpc.Call接口来进行的。和基于Stub的客户端一样,也必须先设置一个属性(Call.SESSION_MAINTAIN_PROPERTY)为Boolean.TRUE时才能使用HTTP会话。

我们看这个客户端的部分代码,如例程8所示。

												
														例程8 基于Call的客户端
package com.hellking.study.webservice.session;

… // imports
/**
 *测试Web服务会话的使用
 */
public class SessionTestClient {
    //一些调用参数。
    private static String qnameService = "MySessionTestService";
    private static String qnamePort = "SessionTestIFPort";

    private static String BODY_NAMESPACE_VALUE =   "urn:SessionTest";
    private static String ENCODING_STYLE_PROPERTY =
         "javax.xml.rpc.encodingstyle.namespace.uri"; 
    private static String NS_XSD =   "http://www.w3.org/2001/XMLSchema";
    private static String URI_ENCODING =  "http://schemas.xmlsoap.org/soap/encoding/";
         
    ServiceFactory factory;
    Service service; 
    Call call;
    QName port;
   //初始化。 
   public SessionTestClient()
   {
        try
        {
          factory = ServiceFactory.newInstance();
          service =  factory.createService(new QName(qnameService));
          port = new QName(qnamePort);
          call = service.createCall(port);
          call.setTargetEndpointAddress("http://127.0.0.1:8000/sessionTest/mysessionTest");
          …//省略部分代码
    }
    catch(Exception e)
    {
     e.printStackTrace();
    }       
    
   }
   /**
    *测试login操作。
    */
   
   public void login()
    {
     try {
       
            QName QNAME_TYPE_STRING = new QName(NS_XSD, "string");
            call.setReturnType(QNAME_TYPE_STRING);
            call.setProperty(Call.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);
            call.setOperationName(new QName(BODY_NAMESPACE_VALUE,"login"));
            call.addParameter("String_1", QNAME_TYPE_STRING, 
                ParameterMode.IN);
            call.addParameter("String_2", QNAME_TYPE_STRING, 
                ParameterMode.IN);    
            String[] params = {new String("userid-001"),new String("abc")};
            String result = (String)call.invoke(params);
            System.out.println("正在登录...");
            System.out.println(result);

        } catch (Exception ex) {
            ex.printStackTrace();
       }
    }
    /**
     *和Web服务交互,获得LoginCount值。
     */
    public void getLoginCount()
    {
     try {
      
            call.removeAllParameters();         
            …//省略部分代码
             call.setProperty(Call.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);
             …//省略部分代码
            String result = (String)call.invoke(params);
            System.out.println(result);

        } catch (Exception ex) {
            ex.printStackTrace();
       }
     
    }
    /**
     *和Web服务交互,注销操作
     */
    public void logout()
    {
       try {
            call.removeAllParameters();    
              call.setProperty(Call.SESSION_MAINTAIN_PROPERTY,Boolean.TRUE);
            …//省略部分代码
       }
    public static void main(String[] args) {
     SessionTestClient test=new SessionTestClient();
     test.login();
     test.getLoginCount();
     test.logout();
    }       
}

												
										

它的运行结果如图3所示。



图3 基于Call的调用

可以看出,它同样获得了预期的结果。





回页首


总结


JAX-RPC以HTTP作为传输协议,那么会话的保持可以从HTTP应用入手。JAX-RPC两种服务端点中,只有基于Servlet的端点才能直接使用HTTP会话。要想在服务端点中访问HTTP会话,Web服务实现类必须实现javax.xml.rpc.server.ServiceLifecycle接口,实现了这个接口的服务端点的生命周期由JAX-RPC运行环境来管理。

通过ServletEndpointContext接口的getHttpSession来获得客户端的会话,这个会话由JAX-RPC运行环境维护。如果要在客户端使用HTTP会话,那么不论是Stub还是Call都必须设置SESSION_MAINTAIN_PROPERTY属性值为Boolean.TRUE。


来源:http://www-128.ibm.com/developerworks/cn/webservices/ws-session/index.html

posted on 2006-07-06 15:34 渠上月 阅读(750) 评论(0)  编辑  收藏 所属分类: java tips

只有注册用户登录后才能发表评论。


网站导航: