win2k环境下基于JBOSS的J2EE开发实践
----之三、有状态会话 Bean的开发及多种调用有状态会话Bean方法的实现
说明:此节是继上一节之后所写,在上两节中我们实现了 JBOSS 开发平台的配置及数据库连接池的配置,同时我们在第一节的示例中,给出了一个无状态会话 Bean 的实例 HelloWorld 。这一节中我们接着学习 EJB 开发。
注意:本系列文章发表于中国程序员网站( www.csdn.net ),所有内容均为作者本人(abnerchai)原著,任何网站和个人如需转载,请联系作者( josserchai@yahoo.com ),本教程系列是应广大网友要求所写,任何人不得用于商业用途,如果想收录本教程用于出版,请联系作者本人。否则视为非法。
一、有状态会话 Bean 的开发和部署
1、基础知识
在第一节中,我们给出了一个 HelloWorld 无状态的会话 Bean ,在 EJB2.0 规范中, EJB 包括三种 Bean ,第一种便是会话 Bean(Session Bean) ,然后还有实体 Bean(EntityBean) 和消息驱动 Bean 。
会话 Bean 包括无状态会话 Bean 和有状态会话 Bean ,二者的主要区别是前者在执行的过程中不保存 Bean 的状态信息,它没有状态域,也就是说:无状态会话 Bean 就只是一个执行过程,我们调用它完成我们的任务而己,它在容器池中无法保存 Bean 状态。而后者在容器中执行时会保存 Bean 的执行状态,相对应的,有状态会话 Bean 中也对应有状态域,即会话 Bean 在执行的过程中会将与固定客户端的会话状态保存起来以备后用。
然而,有状态会话 Bean 它没有主键类,客户端无法查找出它对应的在容器池中的 EJBObject ,那么我们如何重用我们的 EJBObject 呢?在这篇文章中,我们提供了两种方法,并给出了示例程序:
第一种方法是采用有状态会话 Bean 的 Handle( 句柄 ) ,为了取得句柄,可以调用 EJBObject 接口的 getHandle ()方法,返回一个 Handle 实例,为了重新构建对同一 EJBObject 的引用,可以使用 Handle 接口的 getEJBObject ()方法,此方法返回一个对应 Handle 的 EJB 对象,利用此对象,我们就可以重构出对应的 EJBObject ,如下示例:
Count count = counthome.create();// 产生 Remote 接口对象
javax.ejb.Handle handle = count.getHandle();// 获得 Remote 接口的句柄
…
Object obj = handle.getEJBObject();// 得获得 Handle 对应的在容器池中的 EJBObject 对象
Count recount= (Count)PortableRemoteObject.narrow(obj,Count.class);
// 将此对象重构为远程接口对象即可重新调用它的方法
第二种方法是采用有状态会话 Bean 的 HomeHandle ,它类似 handle ,但不能用于引用 EJBObject 。 HomeHandle 包含足够的信息,可以重建 EJBHome ()的引用。它的做法是调用 getHomeHandle ()方法 和 getEJBHome ()方法,此方法返回一个对应 Handle 的 EJBHome 对象,利用此对象,可以重新生成出对应的 EJBObject 对象,然后调用它的方法。如下示例:
Content ctx = new InitialContext();
Object h = ctx.lookup("CountHome");
CountHome home = (CountHome)PortableRemoteObject.narrow(h, CountHome.class);
HomeHandle homehandle = home.getHomeHandle();// 获取 HomeHandle
....
CountHome reHome = (CountHome)homeHandle.getEJBHome();
Count recount = rehome.create();
利用以上两种方法,我们可以自动的存储并重建引用所需的会话 Bean 中的 EJB 信息。
好了,了解了以上知识,下面我们就来一个真实的会话 Bean 来看看它的运行方式!
2、一个有状态会话Bean的开发和部署
开发一个会话 Bean ,基本的应遵守以下步聚,首先开发 Remote 接口,再编写 Home 接口,然后是 Bean 本身。
同时,为了区别,我们应默认遵守以下命名规则, Remote 接口直接用 xxx 命名, Home 接口用 xxxHome 命名, Bean 本身用 xxxBean 命名。
首先,我们手动建立开发环境,在 C:\JBOSS 目录(这里指 JBOSS 的安装目录,详见上一节)下新建一个存放我们项目的目录 myproject ,然后再在 myproject 下建一个存放此 Count 会话 Bean 的目录 CounterStatefullSessionBean ,用来存放我们的这个 Bean 的所有相关文件。然后,再在 CounterStatefullSessionBean 目录下建三个目录: ejb 、 jsp 和 src 分别用来存放 ejb 类、 Web 应用文件( jsp 文件及 Servlet 类)和我们的源程序。
接着,再在 EJB 目录下建一个 client 目录和一个 counter.jar 目录分别用来存放 client 端测试程序和服务器端类。然后再在 client 和 counter.jar 目录下同时各新建一个 counter 目录, counter 目录下再建一个 ejb 目录,这是我们的包名。然后在 counter.jar 目录下再建一个 META-INF 目录,用于存放我们的 ejb 配置文件。
接着,再在 jsp 目录中新建一个 counter.war 目录用于保存 WEB 发部的程序,再在 counter.war 下新建一个 WEB-INF 目录,同时在 WEB-INF 目录中新建一个 classes 目录,其下面再建包目录 counter 及 counter 目录下的 ejb 目录。
好了,我们的目录己建立成功,我们的目录结构见图 1 所示:
图 1
好了,我们利用上面的知识来建一个有状态会话 Bean 及它的测试程序。我们在这里引入《 Mastering EJB 》 (Second Edition) 中的一个会话 Bean ,即 Count 。这个 Bean 中有一个状态域命名为 val ,它用来保存相应客户端调用 EBJ 方法 count 的次数。
我们进入 src 目录,所有的类源程序我们都在此目录中编写和存放。
编写 Remote 接口,代码如下:
//Count.java
package counter.ejb;
import javax.ejb.*;
import java.rmi.RemoteException;
public interface Count extends EJBObject{
public int count() throws RemoteException;
}
编写 Home 接口,代码如下:
//CountHome.java
package counter.ejb;
import javax.ejb.*;
import java.rmi.RemoteException;
public interface CountHome extends EJBHome{
Count create(int val) throws RemoteException,CreateException;
}
编写 Bean 类,代码如下:
//CountBean.java
package counter.ejb;
import javax.ejb.*;
public class CountBean implements SessionBean{
// 当前的计数值就是对话状态
public int val;
private SessionContext ctx;
//EJB 方法
public int count(){
System.out.println("count()");
return ++val;
}
//EJB 必须的方法
public void ejbCreate(int val) throws CreateException{
this.val=val;
System.out.println("ejbCreate()");
}
public void ejbRemove(){
System.out.println("ejbRemove()");
}
public void ejbActivate(){
System.out.println("ejbActivate()");
}
public void ejbPassivate(){
System.out.println("ejbPassivate()");
}
public void setSessionContext(SessionContext ctx){
this.ctx = ctx;
}
public SessionContext getSessionContext(){
return this.ctx;
}}
好了,我们在 src 目录中产生了三个 java 文件,这是 EJB 必须的类,下面我们用在第一节中编写的 com.bat 文件来编译它们。
进行 src 目录,热行: com *.java 即会产生三个 .class 文件。
下面我们部署我们的 EJB 。首先编写部署描述符,进入:
C : \JBOSS\myproject\CounterStatefullSessionBean\ejb\counter.jar\META-INF 目录中,新建一个 ejb-jar.xml 文件,内容如下:
<?xml version="1.0" encoding="gb2312"?>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN"
"http://java.sun.com/dtd/ejb-jar_2_0.dtd">
<ejb-jar>
<enterprise-beans>
<session>
<display-name>Count</display-name>
<ejb-name>Count</ejb-name>
<home>counter.ejb.CountHome</home>
<remote>counter.ejb.Count</remote>
<ejb-class>counter.ejb.CountBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
此为 EJB 布署描述符文件,它里声明了此 CountBean 的一些属性,如声明了本地接口,远程接口及 Bean 的类型。其中 <session-type>Stateful</session-type> 一句声明此 Bean 是有状态会话 Bean ,如果是无状态会话 Bean( 见第一节中的 HelloWorld) ,则此处应该是 Stateless 。
然后,我们再在此目录中新建一个 jboss-service.xml 文件,它是 JBOSS 服务器特有的描述文件,它的内容如下:
<?xml version="1.0" encoding="gb2312"?>
<jboss>
<enterprise-beans>
<session>
<ejb-name>Count</ejb-name>
<jndi-name>Count</jndi-name>
</session>
<secure>true</secure>
</enterprise-beans>
<reource-managers/>
</jboss>
这个文件描述了我们的客户端调用此 EJB 时,在 jndi 树中查找此 EJB 的一些信息。如上面的声明,我们可以在客户端中采用 jndi 名为 Count 来查找此 CountBean ,并调用的它 EJB 方法,它对应于 <ejb-name>Count</ejb-name> 中描述的 Bean ,这里的 <ejb-name>Count</ejb-name> 中引用的 EJB 是 ejb-jar.xml 中定义的 <ejb-name>Count</ejb-name> 中定义的 Bean 。于是,客户端通过这一系列引用就可以通过 jndi 树查找到我们的 EJB 并调用它。
然后,我们把 src 目录中编译好的三个 class 文件拷贝到发布目录即:
C:\JBOSS\myproject\CounterStatefullSessionBean\ejb\counter.jar\counter\ejb 下。
然后把 counter.jar 整个目录拷贝到 C:\JBOSS\server\all\deploy 下,启动 JBOSS 服务器, JBOSS 自动会将 counter.jar 发布。在 JBOSS 启动过程中如果没有异常抛出,证明我们的 EJB 发布成功。
在下面,我们将编写客户端程序来测试此 EJB 。
二、 RMI-IIOP 客户端的开发
我们编写的 EJB 程序,通常在客户端有以下两种方法调用它:
• 采用 RMI-IIOP 的客户端 JAVA 程序
• 编写封装 EJB 的 JAVA Beans 调用 EJB ,然后再在 JSP 或 Servlet 中调用此 Beans 。也可以在 Servlets 中或 JSP 中直接调用 EJB 方法,但在实际运用中,我们常常不这么做。
这里,我们首先编写 RMI-IIOP 客户端来调用此有状会会话 Bean ,在下一小节中我们将编写一个在页面中调用 EJB 的示例。
进入 src 目录,新建一个 CountClient.java 文件,内容如下:
package counter.ejb;
import java.util.*;
import java.io.*;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import javax.naming.Context;
import javax.rmi.PortableRemoteObject;
public class CountClient
{
public static void main(String[] args)
{
//jndi 配置 , 应实现为外部属性文件
InitialContext ctx = null;
try{
Properties env = new Properties();
//config.properties 文件应该放在和 hello 包目录所在目录的同级目录中。即它和 hello 文件夹同在一个文件夹中。
env.load(new FileInputStream("config.properties"));
// Get a naming context
System.out.println(env);
ctx = new javax.naming.InitialContext(env);
System.out.println("Got context");
// Get a reference to the Interest Bean
//jboss 默认 jndi 名为 ejb-jar.xml 中的 :ejb-name 和 jndi-name 指定的
Object ref = ctx.lookup("Count");
System.out.println("Got reference");
CountHome counthome = (CountHome)PortableRemoteObject.narrow(ref, CountHome.class);
// Create an count object from the Home interface
int val=0;
Count count = counthome.create(val);
// call the count() method
for(int i=0;i<10;i++){
System.out.println(" 目前 SessionBean 中 count 的值为: "+count.count());
}
//remove the object
count.remove();
}catch(Exception e)
{
System.out.println(e.toString());
} finally{
if(ctx!=null){
try{
ctx.close();
}catch(javax.naming.NamingException ex){
}
}
}
}
}
然后,编译它,进入 src 目录,运行: com *.java ,即会将 CountClient.java 一起编译,编译后,我们的 src 目录中将产生四个 class 文件。
下面我们来部署和测试此客户端程序,将 src 目录中编译产生的 CountClient.class 、 Count.class 和 CountHome.class 三个文件拷贝到我们的客户程序目录中即,拷贝到:
C:\JBOSS\myproject\CounterStatefullSessionBean\ejb\client\counter\ejb 目录中。
然后再在 client 目录下面建一个 config.properties 文件,内容如下:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming.client
java.naming.provider.url=jnp://10.0.0.18:1099 // 这里是本机的 IP 地址。
这个文件提代 jndi 入口信息。在 DOS 方式中进入 client 目录,执行:
runclient counter/ejb/CountClient
即可看到正确的输出。
三、页面中调用有状态会话 Bean 及有状态会话 Bean 的重用方法
下面我们编写在 WEB 页面中如何调用我们的 EJB 示例程序,为了验证我们的 EJB 是有状态会话 BEAN ,它能保存我们的有状态会话 Bean 的状态,我们采用了以下方式:
在 Servlet 中调用 EJB 方法并运行它使有状态会话 Bean Count 中的 val 值达到一个值;然后,我们在 servlet 中将 URL 导向到 JSP 文件,再在 JSP 文件中查看会话 Bean 中的状态值是否保存。测试程序的状态转换图如下图 2 所示:
图 2
首先编写 Servlet 文件,进入 src 目录,新建一个 CountServlet.java 文件,内容如下:
package counter;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import javax.ejb.*;
import javax.naming.InitialContext;
import javax.naming.Context;
import java.util.*;
import javax.servlet.RequestDispatcher;
import counter.ejb.*;
public class CountServlet extends HttpServlet
{
private CountHome counthome = null;
public void init() throws ServletException {
Context ctx = null;
try {
String initCtxFactory = getInitParameter(Context.INITIAL_CONTEXT_FACTORY);
String providerURL = getInitParameter(Context.PROVIDER_URL);
Properties env = new Properties();
if(initCtxFactory!=null) {
env.put(Context.INITIAL_CONTEXT_FACTORY,initCtxFactory);
}
if(providerURL!=null) {
env.put(Context.PROVIDER_URL,providerURL);
}
ctx=new InitialContext(env);
Object objRef = ctx.lookup("Count");
// 主接口
counthome=(CountHome)javax.rmi.PortableRemoteObject.narrow(objRef,counter.ejb.CountHome.class);
}catch(javax.naming.NamingException ne){
System.out.println("Create Exception caught:"+ne);
throw new ServletException(ne.toString());
}catch(Exception e){
throw new ServletException(e.toString());
}finally {
if(ctx!=null){
try{
ctx.close();
}catch(javax.naming.NamingException ex){
}
}
}
}
public void service(HttpServletRequest req,HttpServletResponse resp) throws IOException,ServletException {
try{
// 组件接口
int val = 0;
Count countbean = counthome.create(val);
// 获得会话 Bean 的 handle ,以备后用
Handle countHandle = countbean.getHandle();
// 调用有状态会话 Bean 的方法
for(int i=0;i<10;i++)
{
// 调用 10 次 EJB 中的方法
countbean.count();
}
// 将有状态会话 Bean 的句柄保存到 session 中,以便在 JSP 重新使用该有状态会话 Bean
HttpSession session = req.getSession(true);
session.setAttribute("countHandle",countHandle);
// 重定向至 JSP 文件,在 JSP 文件中获得该有状态会话 Bean 的状态
RequestDispatcher disp = getServletContext().getRequestDispatcher("/page.jsp");
// 注意:在调用 disp.forward 方法之前,不能有任何的方法访问 ServletOutputStream 或 PrintWriter 即不能输出流被访问。
disp.forward(req, resp);
// 此处不要调用 remove ,否则就删除了容器池中的有状态会话 Bean , JSP 中就不能使用了。
//countbean.remove();
// 重新使用此会话 Bean 的方法如下
// 第一种方法,在上面用 EJBObject 的 getHandle() 方法获得 EJB 的 Handle(javax.ejb.Handle) 然后
// 再在使用的时候用下面的方法
//Count count = (Count)javax.rmi.PortableRemoteObject.narrow(countHandle.getEJBObject(),counter.ejb.Count.class);
//count.count();
//count.remove(); // 使用完了删除 EJB 实例
// 第二种方法,在上面用 Home 的 getHomeHandle() ,示例如下:
//HomeHandle homehandle = counthome.getHomeHandle();
//Object recount = homehandle.getEJBHome();
//CountHome newHomeRef = (CountHome)PortableRemoteObject.narrow(recount,counter.ejb.CountHome.class);
}catch(javax.ejb.CreateException ce){
System.out.println("Create Exception caught:"+ce);
}catch(java.rmi.RemoteException re){
System.out.println("Remote Exception caught:"+re);
}catch(javax.servlet.ServletException re){
System.out.println("Servlet Exception caught:"+re);
}catch(Exception re){
System.out.println("Exception caught:"+re);
}
}
}
接着,我们新建一个 page.jsp 文件,内容如下:
<%@page pageEncoding="GB2312"%>
<%@page contentType="text/html; charset=gb2312"%>
<%request.setCharacterEncoding("GB2312");%>
<%@page import = "javax.ejb.Handle"%>
<%@page import = "javax.naming.InitialContext"%>
<%@page import = "javax.naming.Context"%>
<%@page import = "java.util.*"%>
<%@page import = "counter.ejb.*"%>
<%
HttpSession mysession = request.getSession(false);
Handle countHandle = (Handle) mysession.getAttribute("countHandle");
int no=-1;
if(countHandle == null){// 直接请求此 JSP 文件时用以下代码
Context ctx = null;
CountHome counthome = null;
Properties env = new Properties();
ctx=new InitialContext(env);
Object objRef = ctx.lookup("Count");
// 主接口
counthome=(CountHome)javax.rmi.PortableRemoteObject.narrow(objRef,counter.ejb.CountHome.class);
Count count = counthome.create(0);
no = count.count();
count.remove();
ctx.close();
}else if(countHandle!=null){// 从 Servlet 中转过来的
Count count = (Count)javax.rmi.PortableRemoteObject.narrow(countHandle.getEJBObject(),counter.ejb.Count.class);// 重新构造出此会话 Bean
no = count.count();
count.remove();
mysession.removeAttribute("countHandle");// 删除 Session 中的 Attribute
}
%>
<html>
<head>
<title></title>
</head>
<body bgcolor="#FFFFFF" text="#000000" topmargin="5">
<p><%
if(no!=-1){
%>
有状态 SessionBean 中的当前的值为 <%=no%> !
<%}%>
</p></body></html>
然后,我们就可以部署我们的 WEB 客户端了,首先布署 Servlet ,将 CountServlet.class 拷贝到 :C:\JBOSS\myproject\CounterStatefullSessionBean\jsp\counter.war\WEB-INF\classes\counter 中,然后,编辑在 C:\JBOSS\myproject\CounterStatefullSessionBean\jsp\counter.war\WEB-INF 目录中新建一个 web.xml ,内容如下:
<?xml version="1.0" encoding="gb2312"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>CountServlet</servlet-name>
<display-name>CountServlet</display-name>
<servlet-class>counter.CountServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CountServlet</servlet-name>
<url-pattern>/CountServlet</url-pattern>
</servlet-mapping>
</web-app>
用于布署我们的 Servlet 。
接着,将 page.jsp 文件拷贝到 C:\JBOSS\myproject\CounterStatefullSessionBean\jsp\counter.war 目录中,让 page.jsp 和 WEB-INF 在同一个目录中,然后将 src 目录中的 Count.class 和 CountHome.class 拷贝到 :
C:\JBOSS\myproject\CounterStatefullSessionBean\jsp\counter.war\WEB-INF\classes\counter\ejb 目录中以供 JSP 调用 EJB 时使用。
好了,我们现在把整个 counter.war 目录拷贝到 C:\JBOSS\server\all\deploy 目录,重新启动 JBOSS , JBOSS 会布署我们的客户端。
在浏览器中输入 http://localhost:8080/counter/CountServlet ,即可以看到输出如下图 3 所示:
图 3
同时,在 JBOSS 的控制台上有如图 4 所示的输出:
图 4
到此,我们的有状态会话 Bean 己经编写和测试完毕。注意:我们一般情况下不会在 JSP 和 Servlet 中直接调用我们的 EJB ,我们通常采用一个 JavaBean 将 EJB 封装,然后再在 JSP 文件中调用它。这里是为了演示有状态会话 Bean 的状态保存情况,我们才这么做的。你一定要明白这一点。
在以后的文章里,应网友邀求,我会接着讲在 JBOSS 中如何在手动开发模式(即各种文件都是我们自己手动编写和部署)下开发和布署实体 Bean 。请大家关注。