2008年(3500)
分类:
2008-05-04 19:53:40
在以后的日子里,将由Jackliu向大家陆续提供一系列EJB教程,有学习EJB的朋友请同步参考EJB相关书籍,实战系列将以例程的方式帮助你理解这些基本的概念,其中将包括:
所有章节完毕后将制作成pdf电子文档,供大家下载。
在前面的几篇文章里我介绍了如何开发会话Bean,下面将向大家介绍如何开发Entity Bean,首先充实一些关于Entity Bean的基本知识。
实体(entity) bean用来代表底层的对象,最常用的是用Entity Bean映射关系数据库中的记录。在一个Entity Bean中,关系型数据库的字段可以被一对一的映射到一个Entity Bean中,而表与表之间的关系就可以看成是Entity Bean之间的关系。一个Entity Bean的实例可能会对应表中一个特定的行记录描述或者对于一个查询结果。比如我们在数据库中设计了一个BOOK表,一个相对于BOOK表的Entity Bean就可以封装表中的部分或全部字段,当客户端获取一个Book Bean的实例引用时,就如同我们使用一个SELECT语句从数据库中检索了一条特定的关于一本图书的记录,并可以通过对象方法的方式去访问记录的值,当然你也可以使用remove方法去删除这条记录,用setXXX去改变某个字段的值,新的EJB 2.0查询语言(EJB QL,EJB 2.0 query language)使你可以通过SELECT的方式直接从组件池中查询Bean。
由于这种Bean对应于数据库中的记录,所以数据库记录的任何改变也应该被同步到我们的组件池中相关的Bean中,这个过程被成为持久性(persistencd),这是Entity Bean最重要的一个特征。根据持久性的管理者的不同分为:容器管理持久性(CMP,Container-Managed Persistence)和Bean管理持久性(BMP,Bean-Managed Persistence)。何谓容器管理者,就是在Bean与基础数据库表记录值之间负责同步工作的操作者。
CMP Bean的持久性由EJB容器负责持,Bean开发者不需要参与操作数据库的代码部分,与数据库的操作在部署EJB时由EJB部署者描述,由容器实现SQL操作和同步工作。BMP Bean的持久性由Bean负责,也就是由Bean开发者负责与数据库交互的代码部分。
Entity Bean支持EJB的1.1和2.0规范,并且不能同时支持两者,我们将按照规范1.1和2.0分别介绍BMP和CMP的特性,本节将主要介绍CMP 在EJB 1.1规范定义下的应用。当然上面的这些知识不足使你全部了解Entity Bean,你应该从相关的书籍或文章阅读有关的介绍。
在本节中你将了解到:
首先介绍一下容器持久性管理(CMP),然后介绍规范1.1中规定的CMP。
EJB结构的一个重要优点是EJB容器可以自动的为Entity Bean提供各种有用的功能,容器持久管理(CMP)可以使Bean开发者不用编写一行对数据库操作的代码就可以完成对数据库的基本操作,这样可以简化Bean的开发,使我们集中于纯业务逻辑部分,这也是EJB的一个目标。以为使用CMP方式编写的Bean对于数据库的操作是在部署时由部署者映射到实际的数据库字段的,所以这样就增强程序的移植性,CMP Bean的不会为某种特定的数据库去设计。如果你还对CMP不甚了解,下面可以帮助你迅速解答你一部分疑问:
如果你是一个Bean的开发者,打消这个念头吧,因为你已经不许要考虑这些问题了!这些工作将在在部署Bean时由部署者为CMP Bean指定一个数据库连接池的JNDI命名。Java应用服务器提供数据库连接池管理,并可以通过JNDI命名来获得一个引用。当我们要改变数据库类型或改变数据库的连接地址时,只需从新配置这个数据库资源即可。
这是一个值得考虑的问题,因为这是你在设计一个具体应用是考虑使用CMP还是使用BMP的依据之一。在设计一个CMP Bean时,Bean被固定映射一个实体表,表中的每个指定字段被映射成bean的一个public型类变量,在实际开发中,只需要在Bean的实现类中声明这些类变量,映射操作和SQL处理被交于部署者和容器自动完成。当然你可以迅速的开发出一个CMP Bean,但可能会因为复杂的数据逻辑处理而放弃使用CMP Bean而采用BMP Bean,至少在规范1.1版本,对CMP Bean规范的定义带来束缚还是比较大。
只有Entity Bean有主键,Session调用主键方法将抛出一个异常。Entity Bean是数据面向数据对象的表示,每个Bean的实例代表一行记录,所以就必须有一个主键来标识这个对象,以能够对其进行持久性操作。
CMP Bean由容器来负责实例的生成、装入、寻找、更新、删除等,所以主键也由容器来控制。对于CMP Bean,javax.ejb.EJBObject类已经为我们定义了一个默认的构造方法 public abstract Object getPrimaryKey(),并且不需要我们再为其改造。主键类型一般对应于数据表主关键字类型,比如在表BOOK中定义了一个Varchar2类型的关键字,那么应该告诉这个Bean 的PrimaryKey的类型应该是一个java.lang.String类型的。如果你仔细,会发现默认的getPrimaryKey()返回的是一个Object类,既然我们没有重新改造这个类,容器是如何知道的呢?EJB没有那么聪明,CMP Bean的主键是在部署者部署Bean时被指定的。比如我在部署Book这个Bean类时,就会告诉部署工具,我设计的这个类的主键对应数据表中哪个字段,同时其字段类型被映射一个Java的类型。在Bean的实现类中ejbCreate()方法里,CMP Bean返回一个NULL类型的值,BMP Bean返回一个主键类型对象;在Bean的远程主接口中,create方法用来插入一条数据,并根据ejbCreate()方法返回的值返回一个Bean的引用(组件接口)。
规范1.1定义了设计一个CMP Bean的接口、部署规范和CMP Bean的能力:
CMP Bean和会话Bean一样需要设计远程主接口、组件接口和Bean的实现类。远程主接口扩展了javax.ejb.EJBHome接口,组件接口扩展了javax.ejb.EJBObject接口,这两个接口的设计与会话Bean设计相似。组件实现类实现了javax.ejb.EntityBean接口,并用来实现一些组件的业务逻辑方法。
在一个实际的业务对象中,可能会要求一个Bean映射成多个数据表,在规范1.1中,可以由两种方法实现:
1. 另一个Entity Bean
可以用另一个Entity Bean来降低数据表的关系复杂度,比如对于一个定单 Bean,可能包括多条关于购买图书的定单项目的引用,可以设计另一个定单项目Entity Bean来解决这种问题。
2. 简单的Java类
可以使用一个简单的java数据结构,比如将数据存放到java.util.LinkedList类中,通过序列化把其保存到数据库表的一个字段值中。由于这种方法违背了关系型数据库的设计原则,并且容易在同一个事务总造成数据重影问题,所以不被推荐。
规范1.1规定CMP的查询方法在部署时被部署者指定, Bean开发者不需要实现,只需在组件接口中定义即可。
前几节介绍了会话Bean的寿命周期,与会话Bean不同的是,Entity Bean的寿命将超过创建它的客户端寿命,尽管客户在调用完一个Entity Bean释放其资源后,Entity Bean的实例本身仍然存在于组件池中,与映射的数据库记录保持持久性。图4-1画出了Entity Bean的状态图:
<图4-1>
从图4-1中看出,Entity Bean起初状态为"不存在,不引用"。当客户直接向数据库插入数据记录后,新的记录将被映射Bean的实例放到组件池中等待引用,并改变状态为"存在,不引用",此时可以通过主接口的find方法查找这些对象,也可以由主接口调用remove()方法将其删除。当客户通过远程主接口create()方法创建一个对象引用时,一个Entity Bean状态从"不存在,不引用"改变为"存在,引用",引用的句柄由create方法返回。只有Entity Bean处于"存在,引用"时,才可以调用组件的业务方法。将组件的引用指向为NULL将会释放该客户的引用资源,改变状态为"存在,不引用",当一个Entity Bean的状态被"存在且引用"时,使用主接口的remove方法或组件接口的remove方法将删除被映射的数据记录,释放Entity Bean实例资源,但引用资源仍未释放,所以此时的状态改变为"不存在,引用",将组件的引用设置成Null值,释放组件引用资源后,组件状态恢复到原来的"不存在,不引用"。分析图4-1的四个状态,可以得出,当一个Entity Bean处于不存在状态时,其映射的数据库记录也不存在,当数据库记录被其他应用程序或进程直接插入数据后,容器将自动维持其持久性特性,在组件池中为新添的记录创建尚未被引用的实例Bean,在客户端执行完一个Entity Bean的调用后一定要释放引用的资源,既设置引用为Null。
Entity Bean的寿命周期图如图4-2所示:
<图4-2>
在图4-2中显示了Bean的实现类在不同阶段所调用的方法,这些方法大部分实现了javax.ejb.EntityBean的接口。当一个远程客户调用远程主接口的create()方法时,容器调用newInstance()方法创建一个Bean实例,然后调用setEntityContext(..)方法将当前的情境传递给Bean,进入池共享阶段。如果调用来客户的create()方法,将调用组件的ejbCreate()方法和ejbPostCreate()方法,完全初始化Bean状态,并返回这个Bean的引用,此时Bean进入准备阶段,进入准备阶段的Bean业务逻辑方法可以被客户调用,在调用setXX或getXX等操作时,容器(CMP)或Bean(BMP)可能会多次调用更新(ejbStore()方法)和提取(ejbLoad()方法)来维护组件的持久性(persistence)。
我们计划要设计一个关于一个定单系统中描述商品:图书的CMP Bean,这个Bean里包括了图书的编号,书名和定价几个字段,通过findInPrice()方法,我们可以从数据库中查询符合某个定价范围的图书。为这个Bean起名为Cmp1Book,Cmp表示这个Bean是一个CMP,1代表使用了1.1规范,这样起名完全没有任何根据,只是因为我们接下来的几节还会以Book为例测试Entity Bean的特性,所以这样起名只是防止出现命名重复。
<图4-3>
设计一个CMP Bean至少包括四个步骤:
注意:本节假设你使用的Windows操作系统。如果使用其他操作系统,可能影响到存储路径和JDK命令,但这与程序代码和部署文件内容无关。
1.开发主接口(Cmp1BookHome.java):
开发主接口与开发Session Bean主接口相似,同样需要扩展javax.ejb.EJBHome接口,关于javax.ejb.EJBHome接口的定义和介绍请参考有关Session Bean开发的章节,此处不在详述。
一般情况下,习惯将主接口的命名规则规定为
大部分逻辑方法已经被EJBHome定义,在我们要设计的远程主接口中,不必再重新定义。值得注意的是,我们需要为这个接口定义一个create()方法,用来创建一个实例Bean的引用,返回的对象类型是组件接口类Cmp1Book。参数bookid、bookname、bookprice将在初始化一个Bean时被引用,并根据此值为数据库插入一条记录。记住session
bean的create()方法是创建并取得一个Bean的引用,而Entity Bean则是向数据库插入一条记录,并返回这条记录的映射对象。此外,我们还必须声明findByPrimaryKey()方法,这是在设计Session
Bean所不需要的,findByPrimaryKey()方法的入口参数类型是组件的关键字类型,通过给定的关键字值,可以从组件池中查询相关的组件实例,并返回对这个组件实例的引用。findInPrice()方法声明两个double型的参数,用来指定一个书的定价范围,按照范围值从组件容器中检索Bean的实例对象,将符合条件的Bean放到一个Collection
结构中。在CMP中,类似find
Cmp1BookHome.java代码:
import java.util.Collection; import java.rmi.RemoteException; import javax.ejb.*; //EJB CMP 1.1实战例子 public interface Cmp1BookHome extends EJBHome{ public Cmp1Book create(String bookid,String bookname,double bookprice) throws RemoteException,CreateException; //按主键[bookid字段]查找对象 public Cmp1Book findByPrimaryKey(String bookid) throws FinderException,RemoteException; //查找定价符合范围内的图书,将结果放到Collection中 public Collection findInPrice(double lowerLimitPrice,double upperLimitPrice) throws FinderException,RemoteException; } |
假设我们保存到D:\ejb\Cmp1Book\src\Cmp1BookHome.java
2.开发组件接口(Cmp1Book.java):
开发组件接口与开发Session Bean组件接口相似,同样需要扩展javax.ejb.EJBObject接口,关于javax.ejb.EJBObject接口的定义和介绍请参考有关Session Bean开发的章节,此处不在详述。
一般情况下,习惯将组件接口的命名规则规定为
组件接口类声明的接口方法必须在Bean实现类中实现。大家注意到接口方法中没有声明对bookid的操作是因为bookid作为组件的主键有其默认的操作方法,使用getPrimaryKey()方法可以获取组件的主键值,注意返回的是一个Object类型,但是在你的客户端程序中可以通过上溯造型成合适的类型。
Cmp1Book.java代码:
|
假设我们保存到D:\ejb\Cmp1Book \src\Cmp1Book .java
3.开发Bean实现类(Cmp1BookEJB.java):
这个类包含了业务逻辑的所有详细设计细节。Entity Bean的实现类实现了(implements)javax.ejb.EntityBean所定义的接口,首先我们先熟悉一下EntityBean的定义:
package javax.ejb; impot java.rmi.RemoteException; public interface EntityBean extends EnterpriseBean{ public abstract void ejbActivate() throws EJBException,RemoteException; public abstract void ejbLoad() throws EJBException,RemoteException; public abstract void ejbPassivate() throws EJBException,RemoteException; public abstract void ejbRemove() throws RemoveException,EJBException,RemoteException; public abstract void ejbStore() throws EJBException,RemoteException; public abstract void setEntityContext(EntityContext entitycontext) throws EJBException,RemoteException; public abstract void unsetEntityContext() throws EJBException,RemoteException; } |
EjbActivate()方法和ejbPassivate()方法在Bean激活或钝化时被调用,ejbLoad()方法从数据库中读取数据记录,ejbStore()方法提交当前数据状态到记录。EjbRemove()方法释放实例对象并删除相关映射的数据记录。SetEntityContext()方法可以使当前Bean实例访问Bean情境,unSetEntityContext()方法释放特定的情境资源。 Entity Bean激活时的调用顺序:ejbActivate()àejbLoad() Entity Bean钝化时的调用顺序:ejbStore()àejbPassivate()
一般情况下,习惯将组件实现类的命名规则规定为
类Cmp1BookEJB声明要实现EntityBean的定义,所以,我们必须完全实现EntityBean的接口定义。除此之外还必须实现ejbCreate()方法和ejbPostCreate()方法,ejbCreate()方法必须匹配主接口Cmp1BookHome对create()方法的声明,而ejbPostCreate()方法只需实现主接口create()方法的声明即可。
对于CMP 的ejbCreate()方法的声明返回类型为主关键字类型,但是由于是容器来实现,所以只需在此方法中关联相关的映射字段,然后返回NULL即可。必须在类中定义与数据库表相关联的映射字段,并声明成Public的类变量,因为这些类变量将被容器所引用。
这个类没有书写过多的业务逻辑,基本的存储逻辑已被容器所实现。
Cmp1BookEJB.java代码:
import java.util.*; import javax.ejb.*; //EJB CMP 1.1实战例子 public class Cmp1BookEJB implements EntityBean{ //映射bookid字段 public String bookid; //映射bookname字段 public String bookname; //映射bookprice字段 public double bookprice; public void setBookName(String bookname){ this.bookname=bookname; } public void setBookPrice(double bookprice){ this.bookprice=bookprice; } public String getBookName(){ return this.bookname; } public double getBookPrice(){ return this.bookprice; } public String ejbCreate(String bookid,String bookname,double bookprice) throws CreateException{ if(bookid==null) throw new CreateException("The bookid is required"); this.bookid=bookid; this.bookname=bookname; this.bookprice=bookprice; return null; } public void ejbPostCreate(String bookid,String bookname,double bookprice){} public void ejbLoad(){} public void ejbStore(){} public void ejbRemove(){} public void unsetEntityContext(){} public void setEntityContext(EntityContext context){} public void ejbActivate(){} public void ejbPassivate(){} } |
假设我们保存到D:\ejb\Cmp1Book\src\Cmp1BookEJB .java
到此为止我们的Bean程序组件已经编写完毕了,使用如下命令进行编译:
cd bean\Cmp1Book mkdir classes cd src javac -classpath %CLASSPATH%;../classes -d ../classes *.java |
如果顺利你将可以在..\Cmp1Book\classes目录下发现有三个类文件。
4.编写部署文件:
一个完整的ejb是由java类和一个描述其特性的ejb-jar.xml文件组成,部署工具将根据这些文件部署到容器中,并自动生成容器所需的残根类。 按照下面个格式编写一个ejb-jar.xml文件,对于DTD介绍此处省去。
ejb-jar.xml文件:
|
假设我们保存到D:\ejb\Cmp1Book\classes\META-INF\ejb-jar.xml(注意META-INF必须大写)
现在让我们看看当前的目录结构:
Cmp1Book <文件夹> classes<文件夹> META-INF<文件夹> ejb-jar.xml Cmp1Book .class Cmp1Book EJB.class Cmp1BookHome.class src<文件夹> Cmp1Book.java Cmp1BookEJB.java Cmp1BookHome.java |
在部署之前我们需要将这些类文件和xml文件做成一个jar文件,EJB JAR文件代表一个可被部署的JAR库,在这个库里,包含了服务器代码与EJB模块的配置。ejb-jar.xml文件被放置在JAR文件所指定的META-INF目录中。我们可以使用如下命令得到EJB JAR文件:
cd d:\ejb\Cmp1Book\classes (要保证类文件在这个目录下,且有一个META-INF子目录存放ejb-jar.xml文件) jar -cvf cmp1Book.jar *.* |
确保cmp1Book.jar文件包括的文件目录格式如下:
META-INF<文件夹> ejb-jar.xml Bmp1Book.class Bmp1BookEJB.class Bmp1BookHome.class |
部署工具一般由Java应用服务器的制造商提供,在这里我使用了Apusic应用服务器,并讲解如何在Apusic应用服务器部署这个组件。
注意,如果使用其他部署工具,原理是一样的。要使用Apusic应用服务器,可以到上下载试用版。
确定你的Apusic服务器已经被启动。
打开"部署工具"应用程序,点击文件->新键工程:
第一步:选择"新建包含一个 EJB组件打包后的EJB-jar模块"选项
第二步:选择一个刚才我们生成的cmp1Book.jar文件,
第三步:输入一个工程名,可以随意,这里我们输入cmp1Book
第四步:输入工程存放的地址,这里我们假设被存放到D:\ejb\cmp1Book\deploy目录下
完成四个步骤后,如果没有问题将出现cmp1BookBean的部署界面,基本的参数配置已经在我们刚才编写的ejb-jar.xml中定义,但是比部署SessionBean稍微复杂的是,需要提供一些EntityBean特性的配置:
选择cmp1Book的配置页,点击"实体EJB的持续性管理",点击"部署"按钮,进入如下画面:
<图4-4>
数据源的JNDI名:
必须由部署者给出当前组件的可用数据源JNDI名,一般每个应用服务器都会提供配置数据库连接池的功能。为了保证Cmp1Book组件最终能够在测试环境下运行,我介绍一下在Apusic服务器中配置数据库连接池的方法,其他应用服务器基本类似。编辑config目录下的datasources.xml文件,在
|
当然,可以根据你的实际需要把oracle数据库换为其他的数据员,这些改动不会影响到Bean。 配置完毕后,重新启动应用服务器。
选择对应的表名:
假定这个CMP Bean的数据表名为BOOK,添入这个值。
自动建表:
如果选定此项,在部署时,将由部署工具自动为你在数据库中创建所需的BOOK表结构。我不提倡这种懒的办法,因为自动创建的表结构对字段属性并没有优化,比如长度,类型,并且不会创建一些可以提高、优化数据库查询效率的各类索引,所以如果你是一个有经验的数据库开发人员不要养成这种习惯,当然如果你对关系型数据库不熟悉或这个逻辑对表结构关系要求不高,使用此项可以降低你的开发难度。假设我们手工在数据库中创建一个表结构如下:
CREATE TABLE BOOK(BOOKID VARCHAR2(5) NOT NULL,BOOKNAME VARCHAR2(64),BOOKPRICE NUMBER(12,2) ,PRIMARY KEY (BOOKID)); |
配置SQL子句:
我们已经知道在CMP中,所有的查询方法将在部署时被部署工具实现,对于findByPrimaryKey()方法,由于只有一个Where 关键字段=xx的WHERE子句,所以,不需要部署者特别的指定,但是在我们的设计的远程主接口Cmp1BookHome中还定义了一个findInPrice(double lowerLimitPrice,double upperLimitPrice)方法,需要部署者为这个方法指定一个SQL语句,填写下面的SQL:
SELECT "BOOKID" FROM "BOOK" WHERE "BOOKPRICE" BETWEEN ?1 AND ?2 |
其中?1表示接收lowerLimitPrice参数的值,并代入这个SQL语句 ?2表示接收upperLimitPrice参数的值,并代入这个SQL语句
上述步骤完成后就可以点击部署->部署到Apusic应用服务器完成部署工作。
EntityBean组件是没有任何运行界面的,组件的实例被容器所管理,所以我们要测试这个Bean组件,需要写一段测试程序。这里,我们写一段小服务程序(Java Servlet)。
关于如何编写Servlet我们这里不做介绍。使用InitialContext 对象用来获取当前servlet小应用程序的语境,方法lookup从组件池中查找一个JNDI对象,并取得一个远程主接口的引用。java:comp/env/ejb/Cmp1Book是我们刚才部署Cmp1Book组件的JNDI名,请参考ejb-jar.xml中的
下面是提供的代码: Cmp1BookServlet .java文件:
import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import javax.ejb.*; import javax.naming.InitialContext; import java.util.Collection; import java.util.Iterator; public class Cmp1BookServlet extends HttpServlet{ public void service(HttpServletRequest req,HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out =res.getWriter(); out.println(" "); } } |
假设我们将文件保存到D:\ejb\Cmp1Book\src\Cmp1BookServlet.java
使用如下命令编译Servlet
|
编译成功后将这个servlet部署到与Cmp1Book同一工程中,在部署前需要我们编写一个web.xml,并制作成一个Web模块文件(war文件) web.xml文件内容如下:
|
假设我们将文件保存到D:\ejb\Cmp1Book\test\WEB-INF\web.xml
J2EE Web应用可以包括Java Servlet类、JavaServer Page组件、辅助的Java类、HTML文件、媒体文件等,这些文件被集中在一个War文件中。其中War结构具有固定的格式,根目录名为WEB-INF,同一目录下应该有一个web.xml文件,用来描述被部署文件的部署信息,Jsp、html等文件可以放置在这个目录下,同时WEB-INF目录下可能存在一个classes目录用于存放Servlet程序,如果引用了一些外部资源,则可以被放置到WEB-INF\lib目录下。使用下面的命令生成这个Servlet测试程序的war文件:
cd D:\ejb\Cmp1Book\test\ jar -cvf cmp1Book.war *.* |
确保cmp1Book.war文件包括的文件目录格式如下:
|
成功编译后,将这个servlet一同部署到cmp1Book工程中,我们回到"部署工具",点击编辑à填加一个Web模块,选择我们刚刚编译成的cmp1Book.war文件 点击部署->部署到Apusic应用服务器完成部署工作。
打开浏览器,在浏览器中输入:
localhost-Web Server的主机地址 :6888-应用服务器端口,根据不同的应用服务器,端口号可能不同 /cmp1Book-部署servlet时指定的WWW根路径值 /servlet-ejb容器执行servlet的路径 /Cmp1BookServlet-测试程序 |
如果运行正常应该能够看到下面的结果
<图4-5>
下载本文示例代码