问题:
我们需要这样一种J2EE应用,那就是具有相同用户id的客户端需要共享维持在服务器端的会话数据。除此之外,客户端并不仅仅是Web客户,也是applet和其他程序。我们怎样来实现它呢?
解决办法:
这是一个在编写J2EE应用程序时遇到的典型问题,因为J2EE API不能为之提供一种开箱即用的解决办法。
Servlet API通常使用Web客户所提供的较好方式来处理客户之间的会话及相关数据,但是不能为客户之间会话数据的共享提供一条直接的途径(出于安全原因)。仅对Web客户来说它也有些限制。
相反,Enterprise Java Beans (EJBs)就很容易做到这一点,因为他们能与任何类型的Java客户端一起工作。这看起来可能较复杂,但是在程序上看来是非常浅显易懂的。下面我们就来仔细分析一下。
我们很容易想到一个无状态会话 EJB,因为它能在服务器端保持客户机的状态。在客户会话之间的会话 EJB(或是他们的实例)也是可共享的,就像servlet一样,他们通过在应用程序中传送引用给客户机(或者给他们的远程接口)来实现这种共享。为了在会话之间传输引用,应用程序需要显式地为每个不同的客户机维持一个引用查询表,因此它需要知道把用户(及用户id)与一个已经存在的会话 EJB的合适实例相关联。
可是,当使用实体 EJBs时这些就需要WLS来做了 - 或者在这种情况下至少它们所做的有很多相似性。它能对存储器中实例化的每个实体EJB保持引用,然后再将它(或者对EJB的远程接口的引用)传送给客户机,这需要调用Home Interface的bean的 findByPrimaryKey方法。如果由于实体EJB根本不存在而找不到它的话,为与客户机和客户第一次登录时的会话相关联,它可以动态建。因此,实体EJB实例是很容易被共享的,即使是那些不在同一JVM中运行的客户机之间的实体EJB也是如此。
注意:一个实体EJB 实例通常不指数据库中的一行,它可以完全是虚拟的。但是因为实体EJB特别擅长于访问持久性数据,因此,万一遇到应用程序崩溃等情况,它也能提供一个保持会话(以及会话中的数据)的机会。
为了查找客户的会话引用,使用findByPrimaryKey方法是合理的,因为主键实际上是用户在此应用程序中的用户id。接下来应用程序唯一需要做的是,在客户们通过bean的findByPrimaryKey方法访问会话数据之前,对客户进行适当的身份验证。
创建示例应用程序
现在我们准备设计一个示例应用程序。首先,我们把应用程序分为四个部分:
1. UserSession:此类包含了保存在Hashtable里的用户会话数据以及一些别的信息如用户的用户id。为保持会话数据的持久性它使用Serializer类(见下面)。UserSession 类首先在本地被使用,然后通过实体EJB “包装器”很容易地被多种类型的多个客户(如JSP 和 Java 客户等)远程进行共享。
2. UserSessionSerializer:此类用于维持会话数据的持久性。为了使此过程尽可能地简单,第一步需要用使用Java序列化来代替JDBC。接下来,对分布式大规模应用程序使用基于JDBC的面向池的持久性技术。每个会话(即使由多个客户所共享)通常有一个磁盘上的文件和一个访问它的EJB 实例。
3. EJB 包装器:用于分布式UserSession对象。它是UserSession的一个子类。作为一个实体EJB,它由sharedSessionSample.remote包中的三个独立的类所组成(别的存储在“local”包中)。
4. 示例 JSP客户端:使用SessionData对象,首先本地使用,然后远程使用。
接下来,我们创建一个名为sharedSession-Sample.local的包,并且把两个类放在其中:UserSession类和UserSessionSerializer类。UserSession类有三个类成员(变量)和方法:
Members: protected String path; protected String userId; protected Hashtable sessionData; Methods (the throw definitions are not shown): public void create(String userId) public void load(String userId) public void save() public void remove() public Object getValue(String name) public void setValue(String name, Object object) public void removeValue(String name) public Hashtable getValues() public void setValues(Hashtable values)
类成员路径定义了序列化会话存储及装载地址的目录。通常这个路径对于所有会话都是相同的。用户id定义了会话的所有者,并且被用于在序列化时命名文件(如一个会话用户“John”将被序列化为名为“John.session”的文件,存储在特定目录的磁盘上)。注意:序列化时我们实际上不存储整个 UserSession ,只存储sessionData成员对象。
Getters 和 Setters
方法构建出来以后,能够很容易被扩展成作为一个实体EJB来工作。create()方法用来创建一个新的、空的会话,这个会话通过setters的setValue() 或setValues()方法被赋予一个或多个值后,通过save()方法所保存。这些setters之间的区别是显而易见的:前者当时设定了一个指定的值,然而后者为会话设定了所有的值(作为Hashtable来传递)。这些对于getters的getValue()和getValues()方法也是一样的。load()方法用于装载一个现有的会话以及来自磁盘的数据,并且在没找到会话的情况下(在这种情况下,会话可能会被创建),抛出一个异常,这与create()方法是相反的,create()方法是当会话已经存在的情况下抛出一个异常。
setters不能自动执行save()方法;save()方法必须显式地由客户来调用。可是,这一点对于实体EJB来说可以实现。
需要注意的是UserSession通过这种方式来执行,以至于getValues()方法和setValues()方法是“按值传递”而不是“按引用传递”来进行工作。在这里,这是一种首选的方法,它避免了在JSP上的会话数据的无意识的变化(我看到这种情况已经发生了很多次了,并且它还带有如Hashtable这样的对象)。清单 1展示了实际代码。
清单 1: UserSession的 getValues() 和 setValues() public Hashtable getValues() throws Exception { // Pass a copy instead of the reference to this.sessionData return (Hashtable) sessionData.clone(); } public void setValues(Hashtable values) throws Exception { // After setting, reference to the local copy instead of // pointing to client's hashtable object sessionData = (Hashtable) values.clone(); }
create()、load() 和save()方法使用了SessionSerializer类,这个类没有成员变量,只有静态的方法:
public static void serialize(String path, String userId, Hashtable sessionData) public static Hashtable deserialize(String path, String userId) public static void remove(String path, String userId)
serialize()方法用于把含有指定会话值的Hashtable保存(使用Java序列化)到磁盘上去。与serialize()方法相对立的Deserialize()方法用于在磁盘上现有会话值中实例化Hashtable对象。remove()用于从磁盘上删除序列化的会话值。
下面给出了一个SessionSerializer的serialize()方法和UserSession的save()方法的实现例子,它展示了怎样使用SessionSerializer类。
The SessionSerializer's serialize method: public static void serialize(String path, String userId, Hashtable sessionData) throws Exception { FileOutputStream ostream = new FileOutputStream(path + "/" + userId + ".session"); ObjectOutputStream p = new ObjectOutputStream(ostream); p.writeObject(sessionData); ostream.close(); } The UserSession's save() method: // Note that the path needs to be set first, before calling save public void save() throws Exception { UserSessionSerializer.serialize(path, userId, sessionData); }
接下来,这些文件被编译成Java类。一旦WLS开始运行并且这些文件被拷贝到类途径,作为例子第一部分的示例JSP页面能被创建起来,用于本地测试(即在相同JVM内)会话值的访问。它通常使用setValue()方法来为用户增量一个整形值“i”。如果在HTTP请求中的没有指明某个用户,那么它将会被看作“JohnDoe”。这里没有涉及到身份认证的问题,但是在实际生活中是需要的(清单 2显示了标准 Web 应用程序定义的使用)。
清单 2: 本地(同一JVM)访问会话值的示例 JSP 页面 <%@ page import=" java.util.*, sharedSessionSample.local.* " %> <% String user = request.getParameter("user"); if(user == null) user = "JohnDoe"; // First instantiate the session, then try to load it. // If not found (i.e. could not be loaded), let's create it. UserSession userSession = new UserSession(); userSession.setPath("./sessions"); try { userSession.load(user); } catch(Exception e) { userSession.create(user); } // Now increment it. // If null (not found), let's start from zero. int i = 0; if(userSession.getValue("i") != null) { i = ((Integer) userSession.getValue("i")).intValue(); i++; } userSession.setValue("i", new Integer(i)); %> The value of the 'i' for user '<%= user %>' is now <%= i %>. <% // Finally save the whole session data including the 'i'. userSession.save(); %>
作为一种选择,setValues()方法(而不是setValue()方法)可用于对变量“i”增值,如下所示:
int i = 0; Hashtable values = userSession.getValues(); if(values.get("i") != null) { i = ((Integer) values.get("i")).intValue(); i++; } values.put("i", new Integer(i)); userSession.setValues(values);
转换
既然本地访问会话值的类已经实现和测试过了,那么它们将准备通过转换成一个实体EJB由本地和远程客户来远程使用。这样也使得客户之间的会话共享变得容易,因为实体EJB控制着客户方法调用的同步。除此之外,通过使用bean管理的持久性(BMP) 和 Java 序列化,我们就能序列化Hashtable中的任何指定的值,而无需担心JDBC数据类型,对EJB也是如此。
然而,当同时从两个或多个客户机写入(更新)相同的会话值时,就需要应用普通的规则;在那些情况下EJB不可能防止对数据的重写。由于这个原因,应确保对它们增加一个标识符(举例来说,一个来自文件系统的事物id或时间戳等),这个标识符在客户机和服务器之间进行传送,从而指明当从多客户更新相同会话及其数据时,会话数据已经在别处(别的客户机)被更新了。
为了让local.UserSession用于远程使用,需要实现三个EJB类:
・ UserSessionHome: 实体EJB 的本地接口
・ UserSession: 实体EJB 的远程接口
・ UserSessionBean: 继承于本地包UserSession类的bean实现类
所有的类被放置在一个名为sharedSessionSample.remote的包中,以便把它们同一些本地类(有部分名字相同)区别开来。
UserSessionHome接口定义了两种方法来远程创建新的和本地现有会话:
public UserSession create(String userId) public UserSession findByPrimaryKey(String userId)
因为UserSessionBean(我们将马上看到)是从local.UserSession继承下来的子类,remote.UserSession的远程接口用于公布local.UserSession作为远程使用的方法(没有显示throw定义)。
public Object getValue(String name) public void setValue(String name, Object object) public void removeValue(String name) public Hashtable getValues() public void setValues(Hashtable values)
EJB的实现类remote. User-SessionBean只有唯一的一个成员变量(还有一些是继承于local. UserSession类的)以及下面的一些方法:
成员:
private transient EntityContext ctx;
方法(一些标准的EJB方法及throw定义没有显示出来):
public String ejbCreate(String userId) public String ejbFindByPrimaryKey(String userId) public void ejbLoad() public void ejbStore() public void ejbRemove() protected void setPath()
ejbCreate()方法用于在会话数据对象不存在的情况下创建一个新的会话数据对象。这个对象是通过调用继承于local.User-Session类的create()方法来创建的。如果创建成功了(即,对于给定用户id没有相应的会话存在),用户id被返回,EJB被WLS实例化,以用于共享及远程使用。如果会话已经存在,抛出一个标准的DuplicateKeyException(见清单 3)。
清单 3: EJB的 ejbCreate() 方法的实现 try { setPath(); create(userId); return userId; } catch(java.io.IOException ioe) { throw new DuplicateKeyException(userId); } catch(Exception e) { throw new CreateException(e.toString()); }
setPath()方法用于从EJB的属性中读取序列化目录路径,其实现方法如下所示:
try { InitialContext ic = new InitialContext(); setPath((String) ic.lookup("java:/comp/env/storagePath")); } catch (NamingException ne) { // defaults to '/sessions' directory under WebLogic // root dir, if not defined. For clustering, use a shared dir. setPath("./sessions"); }
注意,就群集来说,它是WebLogic的一个很好的特性,尤其适合于高负载站点的情况,目录路径应该是所有应用服务器实例所共享的资源,否则,群集将不能正常工作。
装载和存储方法
与ejbCreate()相反,findByPrimaryKey()方法用于查找一个现有服务器上的会话。首先,通过调用该方法自身继承于local.UserSession 类的load()方法,这个方法试图在磁盘上找到一个序列化的会话,并且在没找到时抛出一个ObjectNotFoundException。然而,如果会话被找到(即load()成功),它将返回这个用户id,这样服务器通过隐式调用ejbLoad()方法,实例化EJB,从而能够从磁盘上装载实际会话数据。
清单 4: findByPrimaryKey() 和 ejbLoad() 方法
public String ejbFindByPrimaryKey(String userId) throws ObjectNotFoundException { setPath(); try { load(userId); return userId; // Ok, now the ejbLoad() is called by the WebLogic } catch(Exception e) { throw new ObjectNotFoundException(userId); } } public void ejbLoad() throws EJBException { try { setPath(); load((String) ctx.getPrimaryKey()); } catch(Exception e) { throw new EJBException(e.toString()); } } 同样的load()方法用于两个目的:首先,当调用findByPrimary-Key()方法时,用于试图从磁盘上查找一个现有的会话,然后当EJB被服务器实例化(ejbLoad()方法)时,用于装载(反序列化)实际数据。这可能不是一种最佳实践,但是它能完成任务(本应该有一个单独的查找方法,但限于代码长度而省去了)。
当EJB的任何setter程序被调用时,服��器自动调用ejbStore()方法。因此,在EJB中显式使用save()方法毫无必要。后面我们可以看到,出于优化方面的考虑,可以很容易地添加save()方法,但这里不必使用它。因此,ejbStore()类似如下:
try { save(); } catch(Exception e) { throw new EJBException(e.toString()); } Finally, the ejbRemove() method, which can be used to delete existing session data: try { remove(); } catch(Exception e) { throw new EJBException(e.toString()); }
既然EJB已经就绪了,为了用EJB来取代本地的类,该修改JSP了。在清单 5中,为了能如清单 2一样完成相同的增值,UserSession设为可远程访问,但现在,它使得客户端(比如,一个独立的Java应用程序)能在服务器上一个单独的JVM中运行。
清单 5: 将 EJB 用于远程和共享访问的示例JSP页面 <%@ page import=" javax.naming.*, javax.ejb.*, java.rmi.RemoteException, java.rmi.Remote, java.util.*, sharedSessionSample.remote.* "%> <% String user = request.getParameter("user"); if(user == null) user = "JohnDoe"; Context ctx = new InitialContext(); UserSessionHome home = (UserSessionHome) ctx.lookup("sharedSessionSample.UserSession"); UserSession userSession = null; // Try to locate the session. If not found, create it. try { userSession = home.findByPrimaryKey(user); } catch(ObjectNotFoundException onfe) { userSession = home.create(user); } // Now increment the value, if found. // Otherwise, let's initialize it to zero. int i = 0; if(userSession.getValue("i") != null) { i = ((Integer) userSession.getValue("i")).intValue(); i++; } userSession.setValue("i", new Integer(i)); %> The value of the 'i' for user '<%= user %>' is now <%= i %>.
为了一次设置多个值,可以使用setValues()方法,如这个实现增量的例子所示:
int i = 0; Hashtable values = userSession.getValues(); if(values.get("i") != null) { i = ((Integer) values.get("i")).intValue(); i++; } values.put("i", new Integer(i)); // Put other values as well and finally set them userSession.setValues(values);
出于优化方面的考虑,对EJB的JNDI查找只能一次完成,并且存储在具有应用程序作用域的WebLogic Server的内存中(参见清单 6)。
清单 6: 缓存在应用程序作用域的 EJB // Try to locate the session. If not found, create it. UserSessionHome home = null; If(application.getAttribute("UserSessionHome") == null) { Context ctx = new InitialContext(); home = (UserSessionHome) ctx.lookup("sharedSessionSample.UserSession"); application.setAttribute("UserSessionHome", home); } else { home =(UserSessionHome) application.getAttribute("UserSessionHome"); }
除此之外,就JSP来说,为避免接下来的findByPrimaryKey()方法的调用,我们可以将UserSession对象本身缓存到以前找到的用户HTTP会话中。当然,对于Java客户端,如applets,并不需要缓存(作为本地对象来使用)。缓存的代码如清单 7所示:
Listing 7: Caching the EJB reference within session scope // Try to locate the EJB reference. If not found, create it. UserSession userSession = null; If(session.getAttribute("UserSession") == null) { try { userSession = home.findByPrimaryKey(user); } catch(ObjectNotFoundException onfe) { userSession = home.create(user); } session.setAttribute("UserSession", userSession); } else { userSession =(UserSession) session.getAttribute("UserSession"); }