在以后的日子里,将由Jackliu向大家陆续提供一系列EJB教程,有学习EJB的朋友请同步参考EJB相关书籍,实战系列将以例程的方式帮助你理解这些基本的概念,其中将包括: 所有章节完毕后将制作成pdf电子文档,供大家下载。 会话Bean可以分为有状态会话Bean(stateful Bean)和无状态会话Bean(stateless Bean),有状态会话Bean可以在客户访问之间保存数据,而无状态会话Bean不会在客户访问之间保存数据。两者都实现了javax.ejb.SessionBean接口,EJB容器区通过部署文件ejb-jar.xml来判断是否为一个SessionBean提供保存状态的服务,另外,在程序实现上,无状态Bean不能声明实例变量,每个方法只能操作方法传来的参数,如果需要在引用期间维持一些数据状态,以在其他方法中可以引用,则可以把Bean设计成有状态会话Bean。在第二节中我们用一个SatatelessDate 在本节中你将了解到: 有状态会话Bean(Stateful Session Bean)就是在客户引用期间维护Bean中的所有实例数据的状态值,这些数据在引用期间可以被其他方法所引用,其他客户不会共享同一个Session 有状态Session Bean寿命周期由容器控制,Bean的客户并不实际拥有Bean的直接引用,当我们部署一个EJB时,容器会为这个Bean分配几个实例到组件池(component 有状态会话Bean的寿命周期比无状态会话Bean更加的复杂,有状态会话Bean有四种状态: 如图3-1所示: <图3-1> 有状态会话Bean的初始化状态为不存在,当有客户引用一个Bean时,按照顺序调用newInstance()、setSessionContext()和ejbCreate()方法,与第一节中讲到的无状态调用顺序相同。当处于方法现成状态时,如果客户调用remove()方法,则回到不存在状态,并触发Bean的ejbRemove()方法。如果客户长时间不调用Bean或服务器准备释放一些内存资源,则容器将这些Bean从组件池中钝化,钝化过程容器将调用Bean的ejbPassivate()方法,使程序员有机会在钝化Bean时释放分配的资源。当一个客户请求一个被钝化的Bean时,容器可以激活Bean,激活过程容器将调用ejbActivate()放,使程序员有机会在Bean转到方法现成状态时分配Bean所需的资源。 Bean本身可以管理事务(BMT Bean-Managed Transactions),也可以由容器管理事务(CMT Container-Managed 假设这次我们要为一个基金组织编写一个基金帐户的Bean组件,这个组件将为基金管理系统提供一个基金帐户的基本功能。为了能够描述清楚有状态会话Bean的特性,我们将之简化成提供三个业务逻辑接口:addFunds()方法为一个基金帐户添加基金,removeFunds()方法从基金帐户中取出基金,方法getBalance()为我们提供一个基金帐户的余额查询。我们为这个Bean起名为StatefulAccount 设计一个有状态的Session Bean至少包括四个步骤: 注意:本节假设你使用的Windows操作系统。如果使用其他操作系统,可能影响到存储路径和JDK命令,但这与程序代码和部署文件内容无关。 1.开发主接口(StatefulAccountHome.java): 是由Bean开发人员编写的一个Bean的主接口(interface)程序,负责控制Bean的生命周期(生成、删除、查找Bean)。只需要开发人员给出一个主接口类,类方法的实现由容器来完成。 主接口扩展了javax.ejb.EJBHome接口,参考avax.ejb.EJBHome接口定义如下: 一般情况下,习惯将主接口的命名规则规定为<bean-name>Home,所以我们把这个主接口类起名为StatefulAccountHome 大部分逻辑方法已经被EJBHome定义,在我们要设计的远程主接口中,不必再重新定义。值得注意的是,我们需要为这个接口定义一个create()方法,用来获得一个实例Bean的引用,返回的对象类型是组件接口类StatefulAccount。与第二节的StatelessDateHome类定义基本相同,不同的是Create()方法需要一个double类型的fund参数,当客户创建一个Bean引用时,我们将通过这个参数初始化基金帐户的余额。Fund数值的状态将由容器来维护。 StatefulAccountHome.java代码: 假设我们保存到D:ejbStatefulAccountsrcStatefulAccountHome 2.开发组件接口(StatefulAccount.java): 当远程用户调用主接口类生成方法(create(double))时,客户要得到一个组件的远程引用,因此EJB容器要求你为这个Bean的所有方法提供一个接口类,而类的实现则与远程主接口StatefulAccountHome 组件接口扩展了avax.ejb.EJBObject接口,参考avax.ejb.EJBObject接口定义如下: 一般情况下,习惯将组件接口的命名规则规定为<bean-name>,所以我们把这个组件接口类起名为StatefulAccount 大部分逻辑方法已经被EJBObject 定义,在我们要设计的组件接口StatefulAccount中,不必再重新定义,只要我们重申组件中有关业务逻辑的接口即可。为了使远程客户能够得到基金帐户的业务方法,我们必须对应Bean的实现类申明业务逻辑接口的方法。方法AddFunds(double)向一个帐户增加基金,removeFunds(double)方法从一个帐户中取出基金,getBalance()方法得到当前基金帐户的余额,注意这些类都被声名成接口方法,不需要我们在此实现。 在方法AddFunds和removeFunds中,如果参数不符合要求,抛出InsufficientFundException异常错误。 StatefulAccount.java代码: 假设我们保存到D:ejbStatefulAccountsrcStatefulAccount InsufficientFundException.java代码: 假设我们保存到D:ejbStatefulAccountsrcInsufficientFundException 3.开发Bean实现类(StatefulAccountEJB.java): 这个类包含了业务逻辑的所有详细设计细节。会话Bean的实现类实现了(implements)javax.ejb.SessionBean所定义的接口,首先我们先熟悉一下SessionBean的定义: 容器通过这些方法将相关信息通知给Bean实例,所有的方法都抛出RemoveException方法是为了与1.0规范兼容,之后版本编写的Bean只需要抛出EJBException即可。 setSessionContext()方法将会话的语境放到对象变量中,容器在结束会话Bean或自动超时死亡之前将会自动调用ejbRemove()方法,所以在此可以填入用来释放某些资源的代码。当实例被钝化或被激活时,调用ejbActivae()和ejbPassivate()方法。在钝化过程之前,容器调用ejbPassivate()方法,Bean的开发人员可以在这个方法里释放占用的资源,待Bean被激活时,在ejbPassivate()方法中再分配这些资源。 一般情况下,习惯将组件实现类的命名规则规定为<bean-name>EJB,所以我们把这个组件类起名为StatefulAccountEJB 类StatefulAccountEJB声明要实现SessionBean的定义,所以,我们必须完全实现SessionBean的接口定义。 StatefulAccountEJB.java代码: 假设我们保存到D:ejbStatefulAccountsrcStatefulAccountEJB 到此为止我们的Bean程序组件已经编写完毕了,使用如下命令进行编译: 如果顺利你将可以在..StatefulAccountclasses目录下发现有四个类文件。 一个完整的ejb是由java类和一个描述其特性的ejb-jar.xml文件组成,部署工具将根据这些文件部署到容器中,并自动生成容器所需的残根类。 按照下面个格式编写一个ejb-jar.xml文件,对于DTD介绍此处省去。 ejb-jar.xml文件: 假设我们保存到D:ejbStatefulAccountclassesMETA-INFejb-jar.xml(注意META-INF必须大写) 现在让我们看看当前的目录结构: 部署到应用服务器 在部署之前我们需要将这些类文件和xml文件做成一个jar文件,EJB JAR文件代表一个可被部署的JAR库,在这个库里,包含了服务器代码与EJB模块的配置。ejb-jar.xml文件被放置在JAR文件所指定的META-INF目录中。我们可以使用如下命令得到EJB 确保StatefulAccount.jar文件包括的文件目录格式如下: 部署工具一般由Java应用服务器的制造商提供,在这里我使用了Apusic应用服务器,并讲解如何在Apusic应用服务器部署这个StatefulAccount组件。 注意,如果使用其他部署工具,原理是一样的。要使用Apusic应用服务器,可以到www.apusic.com上下载试用版。 确定你的Apusic服务器已经被启动。 打开"部署工具"应用程序,点击文件->新键工程: 第一步:选择"新建包含一个 EJB组件打包后的EJB-jar模块"选项 第二步:选择一个刚才我们生成的StatefulAccount.jar文件 第三步:输入一个工程名,可以随意,这里我们输入StatefulAccount 第四步:输入工程存放的地址,这里我们假设被存放到D:ejbStatefulAccountdeploy目录下 完成四个步骤后,如果没有问题将出现StatefulAccountBean的部署界面,基本的参数配置已经在我们刚才编写的ejb-jar.xml中定义,可以点击部署->部署到Apusic应用服务器完成部署工作。 SessionBean组件是没有任何运行界面的,组件的实例被容器所管理,所以我们要测试这个Bean组件,需要写一段测试程序,这里,我们写一段小服务程序(Java 关于如何编写Servlet我们这里不做介绍。InitialContext 对象用来获取当前servlet小应用程序的语境,方法lookup从组件池中查找一个JNDI对象,并取得一个远程主接口的引用。java:comp/env/ejb/StatefulAccount是我们刚才部署StatefulAccount组件的JNDI名,请参考ejb-jar.xml中的 下面是提供的代码: StatefulAccountServlet.java文件: 假设我们将文件保存到D:ejbStatefulAccountsrcStatefulAccountServlet.java 使用如下命令编译Servlet 编译成功后将这个servlet部署到与StatefulAccount同一工程中,在部署前需要我们为部署编写一个web.xml,并制作Web模块文件(war文件),其中告诉部署工具这个servlet需要参考的一些资源和部署信息,这里我们将定义一个JNDI参考: <ejb-ref-name>告诉部署工具,这个Web模块需要参考的资源JNDI名称, 假设我们将文件保存到D:ejbStatefulAccount estWEB-INFweb.xml 下面我们要部署这个Servlet到J2EE服务器。J2EE Web应用可以包括Java Servlet类、JavaServer Page组件、辅助的Java类、HTML文件、媒体文件等,这些文件被集中在一个War文件中。其中War结构具有固定的格式,根目录名为WEB-INF,同一目录下应该有一个web.xml文件,用来描述被部署文件的部署信息,Jsp、html等文件可以放置在这个目录下,同时WEB-INF目录下可能存在一个classes目录用于存放Servlet程序,如果引用了一些外部资源,则可以被放置到WEB-INFlib目录下。使用下面的命令生成这个Servlet测试程序的war文件: 确保statefulAccount.war文件包括的文件目录格式如下: 成功编译后,将这个servlet一同部署到statefulAccount工程中,我们回到"部署工具",点击编辑-->填加一个Web模块,选择我们刚刚编译成的statefulAccount.war文件 打开浏览器,在浏览器中输入: 如果运行正常应该能够看到下面的结果 <图3-2>
实战EJB系列
实战EJB之三 开发会话Bean(有状态会话Bean)
Bean例子描述了开发无状态会话Bean的过程及特性,下面的一节将介绍关于有状态会话Bean的一些特性和寿命周期,并用一个例子来证明这些特性。
什么是有状态Session Bean?
Bean的实例。Bean的状态被保存到临时存储体中,因为Bean是可以被序列化的,所以同样也可以把一个Bean状态保存到文件系统或数据库中。因为在调用方法时需要维护状态(这部分是有开销的),所以只有需要维护客户状态时才使用有状态会话Bean。典型的会话Bean是购物车,当一个客户第一次打开购物车时,系统为他分配一个购物车的会话Bean,在以后,每当客户选购了商品将改变购物车的商品记录,而这些记录数据将保存到用户会话数据中。
有状态Session Bean寿命周期
pooling)中,当客户请求一个Bean时,J2EE服务器将一个预先被实例化的Bean分配出去,在客户的一次会话里,可以只引用一次Bean,就可以执行这个Bean的多个方法。如果又有客户请求同样一个Bean,容器检查池中空闲的Bean(不在方法中或事务中,如果一个客户长时间引用一个Bean但执行一个方法后需要等待一段时间再执行另一个方法,则这段时间也是空闲的),如果全部的实例都已用完则会自动生成一个新的实例放到池中,并分配给请求者。当负载减少时,池会自动管理Bean实例的数量,将多余的实例从池中释放。
Transation)。对于CMT,容器在方法开始时打开事务,在方法结束时实现事务。Bean开发人员可以通过afterBegin()、beforeCompletion()、afterCompletion(Boolean)来获取事务的各个状态,如果afterCompletion(Boolean)中Boolean变量为true表示事务完成,为false表示事务被撤消。
编写一个有状态Session Bean程序
package javax.ejb;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface EJBHome extends Remote{
public abstract EJBMetaData getEJBMetaData() throws RemoteException;
public abstract HomeHandle getHomeHandle() throws RemoteException;
public abstract void remove(Object obj) throws RemoteException,RemoveException;
public abstract void remove(Handle handle) throws RemoteException,RemoveException;
}
对象可以传递给另一个JVM,且不传递安全信息,这样新的应用可以不使用JNDI来查找对象既可以获得这个主接口,并来创建和获得Bean实例。
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
public interface StatefulAccountHome extends EJBHome{
public StatefulAccount create(double fund) throws RemoteException,CreateException;
}
.java
一样由容器在部署时自动生成。
package javax.ejb;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface EJBObject extends Remote{
public abstract EJBHome getEJBHome() throws RemoteException;
public abstract Handle getHandle() throws RemoteException;
public abstract Object getPrimaryKey() throws RemoteException;
public abstract boolean isIdentical(EJBObject ejbobject) throws RemoteException;
public abstract void remove() throws RemoteException,RemoveException;
}
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
public interface StatefulAccount extends EJBObject{
public void addFunds(double fund)
throws RemoteException,InsufficientFundException;
public void removeFunds(double fund)
throws RemoteException,InsufficientFundException;
public double getBalance()
throws RemoteException;
}
.java
public class InsufficientFundException extends java.lang.Exception{
public InsufficientFundException(){super();}
public InsufficientFundException(String msg){super(msg);}
}
.java
package javax.ejb;
impot java.rmi.RemoteException;
public interface SessionBean extends EnterpriseBean{
public void setSessionContext(SessionContext ctx) throws EJBException,RemoteException;
public void ejbRemove() throws EJBException,RemoteException;
public void ejbActivae()throws EJBException,RemoteException;
public void ejbPassivate()throws EJBException,RemoteException;
}
import javax.ejb.*;
public class StatefulAccountEJB implements SessionBean{
public void ejbCreate(double fund)throws CreateException{
if (fund<0)
throw new CreateException("Invalid fund");
this.fundBalance=fund;
}
public void ejbRemove(){}
public void ejbActivate(){}
public void ejbPassivate(){}
public void setSessionContext(SessionContext ctx){}
//实例变量,有状态的会话bean将在组件池中维护这个实例的值
private double fundBalance;
//向基金帐户增加基金
public void addFunds(double fund)throws InsufficientFundException{
if (fund<0)
throw new InsufficientFundException("Invalid fund");
this.fundBalance+=fund;
}
//从基金帐户撤除部分基金
public void removeFunds(double fund)throws InsufficientFundException{
if(fund<0)
throw new InsufficientFundException("Invalid fund");
if(this.fundBalance<fund)
throw new InsufficientFundException("the balance less than fund");
this.fundBalance-=fund;
}
//得到基金帐户的余额
public double getBalance(){
return this.fundBalance;
}
}
.java
cd beanStatefulAccount
mkdir classes
cd src
javac -classpath %CLASSPATH%;../classes -d ../classes InsufficientFundException.java
StatefulAccount.java StatefulAccountHome.java StatefulAccountEJB.java
4.编写部署文件:
<?xml version="1.0" encoding="UTF-8"?>
<!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>
<description>
This is StatefulAccount EJB example
</description>
<display-name>StatefulAccountBean</display-name>
<enterprise-beans>
<session>
<display-name>StatefulAccount</display-name>
<ejb-name>StatefulAccount</ejb-name>
<home>StatefulAccountHome</home>
<remote>StatefulAccount</remote>
<ejb-class>StatefulAccountEJB</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
StatefulAccount <文件夹 designtimesp=22185>
classes<文件夹 designtimesp=22187>
META-INF<文件夹 designtimesp=22189>
ejb-jar.xml
InsufficienFundException.class
StatefulAccount .class
StatefulAccount EJB.class
StatefulAccount Home.class
src<文件夹 designtimesp=22197>
InsufficientFundException.java
StatefulAccount.java
StatefulAccountEJB.java
StatefulAccountHome.java
JAR文件:
cd d:ejbStatefulAccountclasses (要保证类文件在这个目录下,且有一个META-INF子目录存放ejb-jar.xml文件)
jar -cvf StatefulAccount.jar *.*
META-INF<文件夹 designtimesp=22233>
ejb-jar.xml
InsufficientFundException.class
StatefulAccount.class
StatefulAccountEJB.class
StatefulAccountHome.class
开发和部署测试程序
Servlet)。
在create方法中,我们为基金帐户初始化一个数值,通过引用,把这个值传递给StatefulAccountEJB中定义的fundBalanced对象,注意这个对象的状态由容器维护。通过addFund、removeFund和getBalance方法我们可以测试fundBalanced状态改变所带来的影响。
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import javax.ejb.*;
import javax.naming.InitialContext;
public class StatefulAccountServlet extends HttpServlet{
public void service(HttpServletRequest req,HttpServletResponse res) throws IOException {
res.setContentType("text/html");
PrintWriter out =res.getWriter();
out.println("<html><head><title>StatefulAccountServlet</title></head>");
out.println("<body><h2>Test Result:<hr>");
try{
InitialContext ctx =new InitialContext();
Object objRef =ctx.lookup("java:comp/env/ejb/StatefulAccount");
StatefulAccountHome home=(StatefulAccountHome)
javax.rmi.PortableRemoteObject.narrow(
objRef,StatefulAccountHome.class);
//得到一个基金帐户对象的引用,并初始化帐户金额为100000
StatefulAccount bean=home.create(100000);
out.println("the account balance:"+bean.getBalance()+"<br>");
//向基金帐户增加5000.25
bean.addFunds(5000.25);
out.println("method of addFunds(5000.25) Result:"+bean.getBalance()+"<br>");
//从基金帐户调出1000.02
bean.removeFunds(1000.02);
out.println("method of removeFunds(1000.02) Result:"+bean.getBalance());
out.println("<hr>");
out.println("current account balance:"+bean.getBalance());
bean=null;
}catch(javax.naming.NamingException ne){
out.println("Naming Exception caught:"+ne);
ne.printStackTrace(out);
}catch(javax.ejb.CreateException ce){
out.println("Create Exception caught:"+ce);
ce.printStackTrace(out);
}catch(java.rmi.RemoteException re){
out.println("Remote Exception caught:"+re);
re.printStackTrace(out);
}catch(InsufficientFundException ie){
out.println("InsufficientFund Exception caught:"+ie);
ie.printStackTrace(out);
}
out.println("</body></html>");
}
}
cd D:ejbStatefulAccount
mkdir test
cd test
mkdir WEB-INF
cd WEB-INF
mkdir classes
cd D:ejbStatefulAccountsrc
javac -classpath %CLASSPATH%;../classes/ -d ../test/WEB-INF/classes StatefulAccountServlet.java
<ejb-ref>
<description></description>
<ejb-ref-name>ejb/StatefulAccount</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>StatefulAccountHome</home>
<remote>StatefulAccount</remote>
<ejb-link>StatefulAccount</ejb-link>
</ejb-ref>
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/dtd/web-app_2_3.dtd'>
<web-app>
<icon>
<small-icon></small-icon>
<large-icon></large-icon>
</icon>
<display-name>StatefulAccount</display-name>
<description></description>
<context-param>
<param-name>jsp.nocompile</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<param-name>jsp.usePackages</param-name>
<param-value>true</param-value>
<description></description>
</context-param>
<ejb-ref>
<description></description>
<ejb-ref-name>ejb/StatefulAccount</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>StatefulAccountHome</home>
<remote>StatefulAccount</remote>
<ejb-link>StatefulAccount</ejb-link>
</ejb-ref>
</web-app>
cd D:ejbStatefulAccount est
jar -cvf statefulAccount.war *.*
WEB-INF<文件夹 designtimesp=22451>
classes<文件夹 designtimesp=22453>
StatefulAccountServlet.class
web.xml
点击部署->部署到Apusic应用服务器完成部署工作。
运行测试程序
http://localhost:6888/statefulAccount/servlet/StatefulAccountServlet
localhost-Web Server的主机地址
:6888-应用服务器端口,根据不同的应用服务器,端口号可能不同
/statelessfulAccount-部署servlet时指定的WWW根路径值
/servlet-ejb容器执行servlet的路径
/StatefulAccountServlet-测试程序