<script type="text/javascript"> google_ad_client = "pub-8800625213955058"; /* 336x280, 创建于 07-11-21 */ google_ad_slot = "0989131976"; google_ad_width = 336; google_ad_height = 280; // </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
实战EJB系列
在以后的日子里,将由Jackliu向大家陆续提供一系列EJB教程,有学习EJB的朋友请同步参考EJB相关书籍,实战系列将以例程的方式帮助你理解这些基本的概念,其中将包括:
所有章节完毕后将制作成pdf电子文档,供大家下载
实战EJB之五 开发实体BMP(EJB 1.1规范)
前一节介绍了EntityBean的有关介绍,并通过开发、部署实体CMP的例子介绍EJB1.1规范的CMP的有关特性,在这一节中你将了解如下内容:
- EJB 1.1规范中的BMP
- 编写一个EJB 1.1 的BMP程序
- 部署到应用服务器
- 开发和部署测试程序
- 运行测试程序
EJB 1.1规范中的BMP
根据规范中定义的EJB事务持久性(persistence)的特性被分为容器管理持久性(CMP)和Bean管理持久性(BMP)。虽然使用容器管理持久性给编程带来极大的方便,但是将事务持久性交于容器来控制降低了Bean的开发能力;BMP的Bean具有灵活的业务处理能力和更灵活的持久性控制能力,常用来映射一些复杂的数据视图或很难用CMP实现的复杂逻辑处理。
BMP的寿命周期和CMP的寿命周期管理机制是相同的,不同的是BMP的事务持久性管理机制交于Bean开发者,所以,在创建、更新、删除等数据库操作时,两种类型的Bean的顺序图是不一样的。为了说明这一点,可以从CMP和BMP在钝化/激活顺序图中分析,当然Bean的创建、查找、删除也是不同的:
通过图5-1和5-2的比较,我们很容易会发现:
CMP:当一个Bean实例被客户引用,并执行一个业务方法后,容器会自动读取Bean的实例字段(还记得我们在上一节在实现一个CMP时,为Bean定义了映射到数据库字段的Public型类字段吗),然后,通过容器与数据库发生关系,保存改变的数据,执行完毕后Bean被钝化,并调用ejbPassviate()方法通知Bean。当客户过一端时间又调用这个Bean的某业务方法时,被钝化的Bean又重新的激活,但是并不是马上执行这个业务方法,而是由EJB对象首先调用ejbActivate()方法通知Bean,Bean实例要激活,然后从数据库中提取数据,并自动将数据值映射到Bean实例,然后调用ejbLoad()方法,实例被再一次初始化,最后才开始执行要执行的业务方法,红色箭头和红色时间块做了明显的表示。
BMP:当一个Bean实例被客户引用,并执行一个业务方法后,容器会执行Bean的ejbStore()方法,并由这个方法把数据保存到数据库中(下面的例子你将会发现,我们不再为 Bean定义全局类变量,而是定义一些私有类变量),执行完毕后Bean被钝化,并调用ejbPassviate()方法通知Bean。当客户过一端时间又调用这个Bean的某业务方法时,被钝化的Bean又重新的激活,但是并不是马上执行这个业务方法,而是由EJB对象首先调用ejbActivate()方法通知Bean,Bean实例要激活,然后调用Bean的ejbLoad()方法,这个方法负责从数据库中提取数据,Bean实例被初始化,最后才开始执行要执行的业务方法。
<图5-1>
<图5-2>
BMP Bean要求所有的数据库操作都要由Bean实例完成,这些方法基本上包括:
setXXX():因为BMP不在为容器声明public类型的由容器来管理的映射字段,所以setXX方法需要开发者实现
getXXX(): 取得Bean字段值
ejbCreate():在CMP中,由容器实现,并返回一个NULL值,在BMP中必须由开发者自己实现,返回创建记录的主键值
ejbLoad(): 在CMP中,由容器实现,在BMP中必须由开发者自己实现,以实现组件非持久性状态缓存持久性信息
ejbStore():在CMP中,由容器实现,在BMP中必须由开发者自己实现,将信息从组件的非持久性状态转到持久性状态
ejbRemove():在CMP中,由容器实现,在BMP中必须由开发者自己实现
unsetEntityContext():在情境要求被释放时,释放在setEntityContext()中缓存的情境资源和取得的资源
setEntityContext():设置情境资源,初始化数据库连接对象
ejbActivate(): 在CMP中,由容器实现,在BMP中必须由开发者通过情境参数设置主键值
ejbPassivate():在CMP中,由容器实现,在BMP中必须由开发者取消Bean与数据库记录的持久性工作,进入钝化状态
ejbFindByPrimaryKey():在CMP中,由容器实现,在BMP中必须由开发者自己实现
ejbFindXXX():在CMP中,由容器实现,在BMP中必须由开发者自己实现
总体来看,在规范1.1中,CMP和BMP各有千秋,从机制上没有实质的差异,对于客户端的引用是不会察觉到两者的使用差异。不过是一个善于开发,灵活性小,且增加了部署工作(字段映射、编写SQL处理语句);另一个不善于开发,灵活性大,部署工作较少(没有了字段影射等麻烦,但却增加了配置外部引用资源[因为Bean会通过一个JNDI来查找数据库连接池],移植性较CMP差)。关于BMP的寿命周期请参看上一节介绍的EntityBean寿命周期
编写一个EJB 1.1 的BMP程序
上一节编写了一个CMP的例子,同样我们可以试者将它改写成一个BMP,假设功能需求不变化(功能介绍参看第四节的相关章节),为这个BMP起名为Bmp1Book 设计一个BMP Bean与CMP同样至少包括四个步骤:
- 开发主接口
- 开发组件接口
- 开发Bean实现类
- 编写部署文件
1.开发主接口(Bmp1BookHome.java):
主接口的设计与CMP的主接口设计一样,参照上一节主接口的设计,改动之处用黑体加粗显示。
Bmp1BookHome.java代码:
import java.util.Collection;
import java.rmi.RemoteException;
import javax.ejb.*;
//EJB BMP 1.1实战例子
public interface Bmp1BookHome extends EJBHome{
public Bmp1Book create(String bookid,String bookname,double bookprice)
throws RemoteException,CreateException;
//按主键[bookid字段]查找对象
public Bmp1Book findByPrimaryKey(String bookid)
throws FinderException,RemoteException;
//查找定价符合范围内的图书,将结果放到Collection中
public Collection findInPrice(double lowerLimitPrice,double upperLimitPrice)
throws FinderException,RemoteException;
}
|
假设我们保存到D:/ejb/Bmp1Book/src/Bmp1BookHome.java
2.开发组件接口(Bmp1Book.java):
组件接口的设计与CMP的组件接口设计一样,参照上一节组件接口的设计,改动之处用黑体加粗显示。
Bmp1Book.java代码:
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
//EJB BMP 1.1实战例子
public interface Bmp1Book extends EJBObject{
public void setBookName(String bookname) throws RemoteException;
public void setBookPrice(double bookprice) throws RemoteException;
public String getBookName() throws RemoteException;
public double getBookPrice() throws RemoteException;
} |
假设我们保存到D:/ejb/Bmp1Book /src/Bmp1Book .java
3.开发Bean实现类(Bmp1BookEJB.java):
最大的改动是Bean的实现类,这个类里将包括更多SQL实现的细节代码。首先要引用更多的开发包:java.sql.*;javax.sql.*;javax.naming.*; Bean不在声明全局的类变量,类变量的映射改较给Bean来管理。另外,还需要声明一个EntityContext情境变量,我们将通过这个变量的getPrimaryKey()方法得到保存在情境中的主关键字值,以便在Bean在激活时重新初始化Bean数据。因为要对数据库直接操作,所以我们要定义一个DataSource对象,在Bean初始化时从连接池中取得一个有效数数据库对象。定义的Connect对象将在获取一个数据库连接时被引用。dbjndi存放了一个获得数据库资源的JNDI名。改造后的Bmp1BookEJB如下: Bmp1BookEJB.java代码:
import java.util.*;
import javax.ejb.*;
//引入sql处理包
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
//EJB BMP 1.1实战例子
public class Bmp1BookEJB implements EntityBean{
//保存bookid字段值
private String bookid;
//保存bookname字段值
private String bookname;
//保存bookprice字段值
private double bookprice;
private EntityContext ctx;
private DataSource ds;
private String dbjndi="java:comp/env/jdbc/oadb";
private Connection con;
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;
}
//BMP需要Bean提供数据的插入
public String ejbCreate(String bookid,String bookname,double bookprice)
throws CreateException{
if(bookid==null)
throw new CreateException("The bookid is required");
try{
String sql="INSERT INTO BOOK VALUES(?,?,?)";
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setString(1,bookid);
stmt.setString(2,bookname);
stmt.setDouble(3,bookprice);
stmt.executeUpdate();
stmt.close();
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
this.bookid=bookid;
this.bookname=bookname;
this.bookprice=bookprice;
//由Bean负责事务持久性,Bean负责返回主键值
return bookid;
}
public void ejbPostCreate(String bookid,String bookname,double bookprice){}
//根据bookid值提取数据
public void ejbLoad(){
try{
String sql="SELECT BOOKID,BOOKNAME,BOOKPRICE FROM BOOK WHERE BOOKID=?";
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setString(1,this.bookid);
ResultSet rset=stmt.executeQuery();
if(rset.next()){
this.bookname=rset.getString("BOOKNAME");
this.bookprice=rset.getDouble("BOOKPRICE");
stmt.close();
}else{
stmt.close();
throw new NoSuchEntityException("BOOK ID:" this.bookid);
}
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
}
//保存被关联的数据记录
public void ejbStore(){
try{
String sql="UPDATE BOOK SET BOOKNAME=?,BOOKPRICE=? WHERE BOOKID=?";
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setString(1,this.bookname);
stmt.setDouble(2,this.bookprice);
stmt.setString(3,this.bookid);
if(stmt.executeUpdate()!=1){
stmt.close();
throw new EJBException("occount a error on saved");
}
stmt.close();
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
}
//删除关联的记录
public void ejbRemove(){
try{
String sql="DELETE FROM BOOK WHERE BOOKID=?";
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setString(1,this.bookid);
if(stmt.executeUpdate()!=1)
throw new EJBException("occount a error on remove");
stmt.close();
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
}
public void unsetEntityContext(){
this.ctx=null;
}
//初始化数据库连接,初始化情境参数
public void setEntityContext(EntityContext context){
this.ctx=context;
try{
InitialContext initial =new InitialContext();
ds=(DataSource)initial.lookup(this.dbjndi);
}catch(NamingException ne){
throw new EJBException(ne);
}
}
//在Bean激活时,从情境参数中获取Bean的主键值,然后会自动调用ejbLoad()
public void ejbActivate(){
this.bookid=(String)ctx.getPrimaryKey();
}
//解除当前Bean实例与数据库记录的关系
public void ejbPassivate(){
this.bookid=null;
}
//根据主键查找对象
public String ejbFindByPrimaryKey(String primarykey)
throws FinderException{
try{
String sql="SELECT BOOKID FROM BOOK WHERE BOOKID=?";
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setString(1,primarykey);
ResultSet rset=stmt.executeQuery();
if(!rset.next()){
stmt.close();
throw new ObjectNotFoundException();
}
stmt.close();
//查到数据库中存在此条记录
return primarykey;
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
}
//查找书单定价在指定范围内的Bean的集合
public Collection ejbFindInPrice(double lowerLimitPrice,double upperLimitPrice)
throws FinderException{
try{
String sql="SELECT BOOKID FROM BOOK WHERE BOOKPRICE BETWEEN ? AND ?";
System.out.println(sql);
con=ds.getConnection();
PreparedStatement stmt =con.prepareStatement(sql);
stmt.setDouble(1,lowerLimitPrice);
stmt.setDouble(2,upperLimitPrice);
ResultSet rset=stmt.executeQuery();
ArrayList booklist=new ArrayList();
while(rset.next())
booklist.add(rset.getString("BOOKID"));
stmt.close();
return booklist;
}catch (SQLException se){
throw new EJBException(se);
}finally{
try{
if(con!=null)
con.close();
}catch(SQLException se){}
}
}
}
|
假设我们保存到D:/ejb/Bmp1Book/src/Bmp1BookEJB .java
到此为止我们的Bean程序组件已经改写完毕了,使用如下命令进行编译:
cd bean/Bmp1Book
mkdir classes
cd src
javac -classpath %CLASSPATH%;../classes -d ../classes *.java
|
如果顺利你将可以在../Bmp1Book/classes目录下发现有三个类文件。
4.编写部署文件:
ejb-jar.xml文件:
<?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 BMP 1.1 Book EJB example
</description>
<display-name>Bmp1BookBean</display-name>
<enterprise-beans>
<entity>
<display-name>Bmp1Book</display-name>
<ejb-name>Bmp1Book</ejb-name>
<home>Bmp1BookHome</home>
<remote>Bmp1Book</remote>
<ejb-class>Bmp1BookEJB</ejb-class>
<persistence-type><b>Bean</b></persistence-type>
<prim-key-class>java.lang.String</prim-key-class>
<reentrant>False</reentrant>
<b><resource-ref>
<res-ref-name>jdbc/oadb</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref> </b>
</entity>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>Bmp1Book</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>NotSupported</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
|
假设我们保存到D:/ejb/Bmp1Book/classes/META-INF/ejb-jar.xml(注意META-INF必须大写)
现在让我们看看当前的目录结构:
Bmp1Book
<文件夹>
classes
<文件夹>
META-INF
<文件夹>
ejb-jar.xml
Bmp1Book .class
Bmp1Book EJB.class
Bmp1BookHome.class
src
<文件夹>
Bmp1Book.java
Bmp1BookEJB.java
Bmp1BookHome.java
|
部署到应用服务器
在部署之前我们需要将这些类文件和xml文件做成一个jar文件,EJB JAR文件代表一个可被部署的JAR库,在这个库里,包含了服务器代码与EJB模块的配置。ejb-jar.xml文件被放置在JAR文件所指定的META-INF目录中。我们可以使用如下命令得到EJB JAR文件:
cd d:/ejb/Bmp1Book/classes (要保证类文件在这个目录下,且有一个META-INF子目录存放ejb-jar.xml文件)
jar -cvf bmp1Book.jar *.*
|
确保bmp1Book.jar文件包括的文件目录格式如下:
META-INF
<文件夹>
ejb-jar.xml
InsufficientFundException.class
StatefulAccount.class
StatefulAccountEJB.class
StatefulAccountHome.class
|
部署工具一般由Java应用服务器的制造商提供,在这里我使用了Apusic应用服务器,并讲解如何在Apusic应用服务器部署这个组件。如果使用其他部署工具,原理是一样的。要使用Apusic应用服务器,可以到www.apusic.com上下载试用版。
确定你的Apusic服务器已经被启动。 打开"部署工具"应用程序,点击文件->新键工程:
第一步:选择"新建包含一个 EJB组件打包后的EJB-jar模块"选项
第二步:选择一个刚才我们生成的bmp1Book.jar文件
第三步:输入一个工程名,可以随意,这里我们输入bmp1Book
第四步:输入工程存放的地址,这里我们假设被存放到D:/ejb/bmp1Book/deploy目录下
完成四个步骤后,如果没有问题将出现bmp1BookBean的部署界面,基本的参数配置已经在我们刚才编写的ejb-jar.xml中定义,虽然我们在部署时免去了映射字段、编写SQL操作语句的要求,但是需要提供一些BMP特性的配置:
选择bmp1Book的配置页,点击"4.资源引用",画面上应该出现了我们在ejb-jar.xml文件中设置的数据库引用,我们设置一下共项范围和JNDI名,设置后的画面如下图5-3:
<图5-3>
上述步骤完成后就可以点击部署->部署到Apusic应用服务器完成部署工作。
开发和部署测试程序
对于客户端,引用BMP实例的方式和引用CMP实例的方式是一样的,所以我们不需要改动上一节的servlet程序,只需做少许改动: Bmp1BookServlet .java文件:
….
public class Bmp1BookServlet extends HttpServlet{
……
}
|
假设我们将文件保存到D:/ejb/Bmp1Book/src/Bmp1BookServlet.java
使用如下命令编译Servlet
cd D:/ejb/Bmp1Book
mkdir test
cd test
mkdir WEB-INF
cd WEB-INF
mkdir classes
cd D:/ejb/Bmp1Book/src
javac -classpath %CLASSPATH%;../classes/ -d ../test/WEB-INF/classes Bmp1BookServlet.java
|
编译成功后将这个servlet部署到与Bmp1Book同一工程中,在部署前需要我们编写一个web.xml,并制作成一个Web模块文件(war文件) 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>Bmp1BookServlet</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/Bmp1Book</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>Bmp1BookHome</home>
<remote>Bmp1Book</remote>
<ejb-link>Bmp1Book</ejb-link>
</ejb-ref>
</web-app>
|
假设我们将文件保存到D:/ejb/Bmp1Book/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/Bmp1Book/test/
jar -cvf bmp1Book.war *.*
|
确保bmp1Book.war文件包括的文件目录格式如下:
WEB-INF
<文件夹>
classes
<文件夹>
bmp1BookServlet.class
web.xml
|
成功编译后,将这个servlet一同部署到bmp1Book工程中,我们回到"部署工具",点击编辑à填加一个Web模块,选择我们刚刚编译成的bmp1Book.war文件 点击部署->部署到Apusic应用服务器完成部署工作。
运行测试程序
打开浏览器,在浏览器中输入:
http://localhost:6888/bmp1Book/servlet/Bmp1BookServlet
localhost-Web Server的主机地址
:6888-应用服务器端口,根据不同的应用服务器,端口号可能不同
/bmp1Book-部署servlet时指定的WWW根路径值
/servlet-ejb容器执行servlet的路径
/Bmp1BookServlet-测试程序
|
如果运行正常,运行的结果应该和上一节CMP的例子相同