前言: 在《WIN2k环境下基于JBOSS平台的J2ee开发实践》系列文章中,我将通过不采用任何自动化工具带领读者来实现EJB开发,我们在全过程中,自己编写EJB类,自己配置容器环境和自己编写布署描述符文件等,使大家在学习EJB开发过程中深入地理解EJB的开发过程并掌握它。在前面三节中,我们带领大家配置好了开发环境和数据库连接池,并演示了如何开发SessionBean的方法。在这一节中,我们将学习如何开发实体Bean。 一、实体Bean编程基础 在J2ee的面向对象的多层配置架构中,我们通常会使用以下两种有明显区别的组件:
1、应用逻辑组件,它们是执行公共任务的方法提供者,它们完成的工作我们可以理解为是一个短暂的过程,如计算一个订单的价格、计算一个方程的解等。
会话Bean可以很好的完成这些应用操作,会话Bean常常包含用于执行应用任务的运算逻辑,它代表一个客户的一次操作或与用户工作流逻辑的一次用户会话过程。
2、持久数据组件,它是多层商务应用中的一个对象,如银行应用中的账户、商业客户等。这些对象代表的是你在商务应用中可能需要保存的数据。
如:你编写了一个Account类,它代表的是银行的账户,它的一个对象实例即代表银行中的一个具体的账户。也许这时你会怀疑这种对象组件是否有用,你可能会问:我为什么不可以直接把这些数据存储到数据库的表中呢?如把账户信息放入一个数据库表中,需要使用时,我们再把它读出来,为什么要使用这些对象组件呢?答案有两点:
一、 我们把这些数据视为对象能够使我们非常便利地操作和管理它们,并且它们表现为一个类形式的紧凑形式。
二、 我们可以利用这些中间层对象将数据存储到容器的缓存中提高访问数据的性能,同时我们能从容器那里获得更好的关联、事务和安全性。
实体Bean就是这些持久性数据组件,它们处于数据库和用户应用程序之间,由容器负责管理它们和数据库之间的数据交互。我们可以这样简单地理解为:实体Bean是数据库和用户应用程序之间的缓存数据,它处于WEB容器的内存中,它的表现形式不是像数据库中的一行一行的记录一样,它的表现形式是一个一个的对象实例,容器中的一个实体Bean类的对象实例代表的是数据库中的一条记录。
实体Bean不执行复杂的任务或工作流逻辑,因为他们本身代表的是数据,是持久状态的对象。例如:我们要创建一个银行账户,它有三个域:账号ID、账户户主的姓名、账户上的金额。在通常情况下,我们可以在数据库的accounts表中插入一行记录即可。采用了实体Bean后,我们先在内存中创建一个账户类,它有三个属性,然后我们实例化出该类的一个对象,它在内存中代表一个具体的账户。即我们首先把数据加载到内存中,然后,你可以处理这个内存中对象并改变它,这时你操作的是一个Java对象而不是一大堆的数据库记录,如:你改变了该账户的余额,此时,EJB容器会自动将你对该对象的改变映射到对应的数据库记录中去,这个过程是由容器负责执行的。即,我们负责改变这些代表数据的对象,EJB容器负责这些对象和数据库记录之间的同步。具体来说,它是怎么样同步实体Bean对象和数据记录的呢?
这个同步过程,需要我们了解实体Bean的生命周期,我们在此简要说明同步过程如下:当我们在EJB容器中创建(ejbCreate)一个实体Bean时,容器将一条数据记录读入(ejbLoad)到此实体Bean的属性域中形成一个特定的实体Bean对象(EJBObject),然后我们就可调用此实体Bean的一些方法来操作它的数据,当此Bean对象的某些数据值发生改变时,容器即调用实体Bean的一个叫ejbStore()的方法将改变后的数据更新到数据库的表中;当数据库表中的值发生改变后与当前内存中的实体Bean对象的数据值不一致时(如我们直接更改数据库记录),EJB容器即自动调用实体Bean的一个叫ejbLoad()的方法将数据库中的值重新装入到实体Bean的属性域中。当我们在EJB容器中删除一个实体Bean对象实例时,容器即调用实体Bean的一个叫ejbRemove()的方法,在此方法内,再将实体Bean的数据存储到数据库表中去。同时,由于数据库中可能有许多条记录,所有我们在EJB容器中也可能存在许多个对应的实体Bean的实例对象,那么我们用什么办法来区别这些实体Bean实例对象呢?就像数据库表中用主键来区别这些记录一样。答案是:我们也给每一个实体Bean来一个主键,用它的主键来区分这些实体Bean对象。如一个银行账户的对象我们可以用账户ID来区分它们。同样的,我们也可以像查找数据库记录一样的来查找实体Bean对象,这需要我们自己编写查找方法。
在实体Bean中,同样也存在如SessionBean中的钝化、活化的现象,这时容器分别调用实体Bean的ejbActivate()和ejbPassivate()方法。为了达到与数据库的记录一致性,在ejbPassivate()方法之前应调用ejbStore();在ejbActive()方法之后应立即调用ejbLoad()方法。
在EJB2.0规范中说明了,实体Bean(EntityBean)包括两种,即容器管理的实体Bean和Bean自身管理的实体Bean。二者区别是:Bean自身管理的实体Bean由我们Bean编写者去实现上面所讲的ejbLoa()和ejbStore()等方法,由我们控制如何去ejbLoad和ejbStore等。而容器管理的实体Bean则不用我们编写ejbLoad()和ejbStore()等方法,由容器去自动实现这些过程。 二、BMP实体Bean的编写方法与范例BMP实体Bean的编写
编写BMP实体Bean和编写SessionBean大体相同,但多了些东西,多的是:编写主键类和编写查找实体Bean的查找方法。同时,BMP实体Bean我们还要求自己来实现ejbLoad()和ejbStore等方法。下面我们就来实例来做一个BMP实体Bean,我们这个例子采用的是一个代表银行账户的Bean,我们定义一个银行账户有三个域:账户ID、户主姓名和账户金额,其中账户ID是主键。
首先我们在mySQL数据库中建一个数据库名为test,然后再在此库中建一个表accunts,如下的sql语句可以实现:
create table accounts( id varchar(64), ownerName varchar(64), balance double(13,2), primary key(‘id’));
接着,我们需配置JBOSS的数据库连接池,以便我们在BMP实体Bean中连接数据库,我们在这里采用mySQL数据库,配置方法详见本系列教程第二节,具体就是在JBOSS/Server/all/deploy/目录下新建一个mysql-ds.xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?> <datasources> <local-tx-datasource> <jndi-name>MySql</jndi-name> <connection-url>jdbc:mysql://10.0.0.18:3306/test</connection-url> <driver-class>org.gjt.mm.mysql.Driver</driver-class> <user-name>root</user-name> <password></password> </local-tx-datasource> </datasources>
注意上面文件中的IP和数据库用户名及密码与你自己的相同。
下面我们就开始来编写我们的实体Bean,同样,我们先建一个工作目录来存放我们这个实体Bean的所有发布前的文件。如上一节中所示,所有的目录均建立在JBOSS目录下的myproject目录中。
建立目录过程如下:
在JBOSS安装目录下建一个myproject 目录,如果你在上一节中己建了,可不建,然后在myproject目录下建一个AccountBMP目录存放我们这个Bean的所有的文件;然后再在AccountBMP目录下新建一个ejb目录和一个src目录,接着在ejb目录下新建一个account.jar目录和一个client目录。然后再在account.jar目录中新建一个account目录和一个META-INF目录及在account目录下面新建一个ejb目录;同样,在cient目录中新建一个account目录和在account下面再建一个ejb目录。
建好后的目录结构如下图1所示:
图1
接着,我们来编写我们的银行账户实体BEAN,进入src目录,所有的源文件都放在此目录中。
第一步编写Remote接口:
//Account.java package account.ejb; import javax.ejb.*; import java.rmi.RemoteException; public interface Account extends EJBObject{ //商务方法 public void storeinto(double amt) throws AccountException,RemoteException; public void getout(double amt) throws AccountException,RemoteException; //实体Bean上的获得器/设置器方法 public double getBalance() throws RemoteException; public void setBalance(double balance) throws RemoteException; public String getOwnerName() throws RemoteException; public void setOwnerName(String name) throws RemoteException; public String getAccountID() throws RemoteException; public void setAccountID(String id) throws RemoteException; }
大家看到,在这个方法中,我们抛出了一个自己定义的AccountException异常,我们在此给出它的代码如下:
//AccountException.java package account.ejb; //自定义异常类 public class AccountException extends Exception{ public AccountException(){ super(); } public AccountException(Exception e){ super(e.toString()); } public AccountException(String s){ super(s); } }
然后,我们编写Home接口,如下所示:
//AccountHome.java package account.ejb; import javax.ejb.*; import java.util.Collection; import java.rmi.RemoteException; public interface AccountHome extends EJBHome{ Account create(String accountID,String ownerName,double balance) throws CreateException,RemoteException; public Account findByPrimaryKey(AccountPK key) throws FinderException,RemoteException; public Collection findByOwnerName(String name) throws FinderException,RemoteException; //这个方法独立于所有的账户(即EJB实例) public double getTotalBankValue() throws AccountException,RemoteException; }
注意:上面的在Home接口中,我们定义了三个查找的方法,通常情况下,我们把查找实体Bean的方法都定义在Home接口中,把商务方法定义在Remote接口中,然后它们均在Bean类中实现。而且,我们必须将查找方法的名称均命为findByxxxx(),这种查找方法是与某一个实体Bean对象相关联的,如果我们需有其它的查找方法,可以自己定义名称,如上面的getTotalBankValue()方法,而且,我们在这里要注意:这里的命名与在Bean实现中的命名要对应起来。一般情况下,每一个BMP必须定义一个findByPrimaryKey()方法。
接着,我们来编写此实体Bean的主键类,主键类的格式比较固定,如下所示:
//AccountPK.java package account.ejb; import java.io.Serializable; //主键类 public class AccountPK implements java.io.Serializable{ public String accountID; public AccountPK(String id){ this.accountID = id; } public AccountPK(){ } public String toString(){ return accountID; } public int hashCode(){ return accountID.hashCode(); } public boolean equals(Object account){ return ((AccountPK)account).accountID.equals(this.accountID); } } 其中后三个方法是必须实现的。
接着,我们来实现我们的实体Bean类,它很长,希望不要认为它有占字数的嫌疑。这个类由四部分组成:setter/getter方法、容器调用的方法、商务方法和查找方法,它是一个自解释的类,所有的注解我都放在程序中了。
package account.ejb; //以下引入所必需的包 import java.sql.*; import javax.naming.*; import javax.ejb.*; import java.util.*; //BMP实体Bean,它代表银行的一个账户 public class AccountBean implements EntityBean{ //以下是第一部分,定义域和setter/getter方法 protected EntityContext ctx; //定义BMP管理的状态域,它和数据库中的字段对应的。 private String accountID;//主键 private String ownerName;//账户主姓名 private double balance;//账户上的金额 //构造器 public AccountBean(){ System.out.println("New Bank Account EntityBean Object created by JBOSS Container."); } //实体Bean域上的获得器/设置器方法 public double getBalance(){ return this.balance; } public void setBalance(double balance){ this.balance = balance; } public void setOwnerName(String name){ this.ownerName = name; } public String getOwnerName(){ return this.ownerName; } public String getAccountID(){ return this.accountID; } public void setAccountID(String id){ this.accountID = id; } public void setEntityContext(EntityContext ctx){ this.ctx = ctx; } public EntityContext getEntityContext(){ return this.ctx; } //以下是第二部分,实现由容器调用的方法 //EJB必要的方法,它们由容器调用 //活化时 public void ejbActivate(){ System.out.println("ejbActivate() called!"); } //钝化时 public void ejbPassivate(){ System.out.println("ejbPassivate() called!"); } //删除EJB实例时 public void ejbRemove() throws RemoveException{ System.out.println("ejbRemove() called!"); } //从数据库装入,以保持同步 public void ejbLoad(){ System.out.println("ejbLoad() called!"); //查询实体环境获得当前实体Bean对象的主键,以便我们知道要加载哪个实例 AccountPK pk = (AccountPK)this.ctx.getPrimaryKey(); String id = pk.accountID; PreparedStatement pstmt = null; Connection conn = null; try{ conn = getConnection();//这个是我们自己定义的方法,见后,它从连接池中获取 //通过用账号ID查询,从数据库得到账户记录 pstmt = conn.prepareStatement("select ownerName,balance from accounts where id =?"); pstmt.setString(1,id); ResultSet rs = pstmt.executeQuery(); rs.next(); //把查询的结果保存到实体Bean对象域中 this.ownerName = rs.getString("ownerName"); this.balance = rs.getDouble("balance"); }catch(Exception ex){ throw new EJBException("Account" + pk +"failed to load from database",ex); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } } //更新数据库,及时反映这个内存中的实体Bean实例的当前值 public void ejbStore(){ System.out.println("ejbStore() called!"); PreparedStatement pstmt = null; Connection conn = null; try{ conn = getConnection(); //把当前实体Bean的值放入到数据库中保存,及时更新数据库 pstmt = conn.prepareStatement("update accounts set ownerName =?,balance=? where id=?"); pstmt.setString(1,this.ownerName); pstmt.setDouble(2,this.balance); pstmt.setString(3,this.accountID); pstmt.executeUpdate(); }catch(Exception ex){ throw new EJBException("Account" +accountID + "failed to save to database",ex); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } } //容器调用,把这个Bean实例与一个特定的环境对象分离,固定的方法 public void unsetEntityContext(){ System.out.println("unsetEnetityContext() called!"); this.ctx = null; } //在ejbCreate调用之后调用,固定的方法 public void ejbPostCreate(String accountID,String ownerName,double balance){ } //这是对应于Home接口中的create()方法,返回这个账户的主键 public AccountPK ejbCreate(String accountID,String ownerName,double balance) throws CreateException{ PreparedStatement pstmt = null; Connection conn = null; try{ //在这里通入客户端调用时传入的参数创建一个实体Bean的实例 System.out.println("ejbCreate() called!"); this.accountID = accountID; this.ownerName = ownerName; this.balance = balance; conn = getConnection(); //插入这个账户到到数据库中 pstmt = conn.prepareStatement("insert into accounts(id,ownerName,balance) values(?,?,?)"); pstmt.setString(1,accountID); pstmt.setString(2,ownerName); pstmt.setDouble(3,balance); pstmt.executeUpdate(); //生成主键并返回它 return new AccountPK(accountID); }catch(Exception e){ throw new CreateException(e.toString()); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } } //第三部分,商务逻辑方法 //存入部分钱到用户账户中去 public void storeinto(double amt) throws AccountException{ System.out.println("storeinto("+amt+")called!"); this.balance += amt; } //从银行账户中取出一定的钱 public void getout(double amt) throws AccountException{ System.out.println("getout("+amt+")called."); //如果要取的钱大于账户余额 if(amt>balance){ throw new AccountException("Your balance is not enough!"); } this.balance -= amt; } //从连接池获得数据库的JDBC连接的方法 public Connection getConnection() throws Exception{ try{ Context ctx = new InitialContext(); javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup("java:/MySql");//这里要注意一致 return ds.getConnection(); }catch(Exception e){ System.out.println("Couldn't get dataSource!"); e.printStackTrace(); throw e; } } //第四部分,定义并实现Home接口中定义的查找方法,要注意它们的命名和Home接//口中的不同之处 //用主键查找一个账户 //注意,它的命名方式是前面加一个ejb然后把Home接口中对应的方法名的find改为//第一个字母大写 //每个实体EJB必须定义一个此方法 public AccountPK ejbFindByPrimaryKey(AccountPK key) throws FinderException{ PreparedStatement pstmt = null; Connection conn = null; try{ System.out.println("ejbFindByPrimaryKey() called!"); conn = getConnection(); pstmt = conn.prepareStatement("select id from accounts where id =?"); pstmt.setString(1,key.toString()); ResultSet rs = pstmt.executeQuery(); rs.next(); //没有错误发生,说明找到了,返回主键 return key; }catch(Exception e){ throw new FinderException(e.toString()); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } } //用它的姓名查找一个账户 //注意,它的命名方式是前面加一个ejb然后把Home接口中对应的方法名的find改为//第一个字母大写 public Collection ejbFindByOwnerName(String name) throws FinderException{ PreparedStatement pstmt = null; Connection conn = null; Vector v =new Vector(); try{ System.out.println("ejbFindByOwnerName("+name+") called!"); conn = getConnection(); pstmt = conn.prepareStatement("select id from accounts where ownerName =?"); pstmt.setString(1,name); ResultSet rs = pstmt.executeQuery(); while (rs.next()){ String id = rs.getString("id"); v.addElement(new AccountPK(id)); } return v; }catch(Exception e){ throw new FinderException(e.toString()); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } } //它返回银行中所有银行账号的余额总和 //这个方法独立于所有的账户(即EJB实例),可以认为它是一个所有EJB实例共有的方法 //注意:它的命名方式是在上面的情况下还多加了一个Home public double ejbHomeGetTotalBankValue() throws AccountException{ PreparedStatement pstmt = null; Connection conn = null; try{ conn = getConnection(); pstmt = conn.prepareStatement("select sum(balance) as total from accounts"); ResultSet rs = pstmt.executeQuery(); if(rs.next()){ return rs.getDouble("total"); } }catch(Exception e){ e.printStackTrace(); throw new AccountException(e); }finally{ try{ if(pstmt!=null) pstmt.close(); }catch(Exception e){} try{ if(conn!=null) conn.close(); }catch(Exception e){} } throw new AccountException("Error!"); } }
到此,我们就编写完成了我们的CMP实体Bean,它很长,关键长在我们要自己去实现大部分的同步数据库的方法。到了下一节,我们可以看到,这些方法在CMP中全不用自己实现,由容器来实现。下面我们来布署它并编写一个RMI-IIOP客户端来测试我们的实体Bean。
三、范例BMP实体的部署与测试
要布署我们的BMP,首先我们来编写布署描述符,它包括两个部分:一个是标准的ejb2.0规范的ejb-jar.xml文件,另一个是特定容器的如我们的JBOSS的布署描述符文件。
进入C:/JBOSS/myproject/AccountBMP/ejb/account.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> <entity> <ejb-name>Account</ejb-name> <home>account.ejb.AccountHome</home> <remote>account.ejb.Account</remote> <ejb-class>account.ejb.AccountBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>account.ejb.AccountPK</prim-key-class> <reentrant>False</reentrant> </entity> </enterprise-beans> <assembly-descriptor> <container-transaction> <method> <ejb-name>Account</ejb-name> <method-intf>Remote</method-intf> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
描述符中,元素persistence-type指明我们采用的是Bean管理的持久化的Bean,如果是容器管理的,则设置为Container。元素prim-key-class指明主键类。元素reentrant指明Bean是否可以通过另一个Bean来调用其自身,如BeanA调用BeanB,BeanB又返回调用BeanA,这种情况我们称为BeanA是可重入的,这是执行路径的自循环,如果我们想支持这种重入,此处就写为True,我们不想支持这种重入,所以写作False。元素assembly-descriptor将Bean和事务联系起业,我们在这里不讲。
同样,在C:/JBOSS/myproject/AccountBMP/ejb/account.jar/META-INF目录新建一个jboss-service.xml文件,这是JBOSS容器特有的描述符文件,它的内容如下:
<?xml version="1.0" encoding="gb2312"?> <jboss> <enterprise-beans> <entity> <ejb-name>Account</ejb-name> <jndi-name>Account</jndi-name> </entity> <secure>true</secure> </enterprise-beans> <reource-managers/> </jboss>
它描述了我们在客户端如何通过jndi来调用这个实体Bean的信息。
接着,我们进入src目录,将我们的Bean类编译出来,进入src目录,执行: com *.java (注意:com.bat是我们在此系列教程第一节中编写的编译批处理文件) 在src目录目录产生了一系列的.class文件,我们把这些文件全都移到: C:/JBOSS/myproject/AccountBMP/ejb/account.jar/account/ejb目录中准备发布它。
发布EJB,我们将accoun.jar目录整个拷贝到JBOSS的服务器布署目录:
C:/JBOSS/server/all/deploy中,启动JBOSS服务器,如果启动过程中没有抛出异常,则说明我们的实体Bean发布成功。 下面我们来编写一个客户端测试程序来测试我们的BMP实体BEAN,进入src目录,编写AccountClient.java文件,内容如下:它是自解释的。
package account.ejb; import javax.ejb.*; import javax.naming.*; import java.rmi.*; import javax.rmi.*; import java.util.*; import java.io.*; public class AccountClient{ public static void main(String[] args) throws Exception{ Account account = null; 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名为jboss-service.xml中的:jndi-name Object ref = ctx.lookup("Account"); System.out.println("Got reference"); AccountHome accountHome = (AccountHome)PortableRemoteObject.narrow(ref,account.ejb.AccountHome.class); //创建EJB对象 accountHome.create("987-654-321","abner chai",2000); account = accountHome.create("123-456-7890","John Smith",100); System.out.println("Total of accounts in bank is :"+accountHome.getTotalBankValue()); Iterator i = accountHome.findByOwnerName("John Smith").iterator(); if(i.hasNext()){ account = (Account)javax.rmi.PortableRemoteObject.narrow(i.next(),Account.class); }else{ throw new Exception("Could not find account"); } System.out.println("Initial Balance = "+account.getBalance()); account.storeinto(100); System.out.println("After storeinto 100,account balance = "+account.getBalance()); AccountPK pk = (AccountPK)account.getPrimaryKey(); //释放旧的对象引用,用ID来查 account = null; account = accountHome.findByPrimaryKey(pk); //取出50元 account.getout(50); System.out.println("after get out 50,account balance = "+account.getBalance()); //再取出200元 System.out.println("now get out 200"); //它应该抛出异常,因为钱己不够了。 account.getout(200); }catch(Exception e){ System.out.println("caught Exception"++e.toString()); }finally{ //删除实体Bean System.out.println("delete Account Bean"); try{ if(account!=null){ account.remove(); } }catch(Exception e){ e.printStackTrace(); } } }//end main }//end class
下面编译和布署客户端程序,进入src目录,运行:
com *.java,编译成功!
布署客户端,将编译后产生的Account.class、AccountClient.class、AccountException.class、AccountHome.class、AccountPK.class几个文件拷贝到:
C:/JBOSS/myproject/AccountBMP/ejb/client/account/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
它描述了jndi入口信息。 运行测试客户端,进入DOS方式,进入client目录,运行: runclient account/ejb/AccountClient,程序输出如下图2,表示运行成功! (注意:runclient是我们在本系列教程中第一节中编写的运行RMI-IIOP客户端的批处理程序)
图2
四、总结
在本节中,我们给大家展示了如何编写BMP实体BEAN,由于BEAN实体Bean在编写的过程中,我们Bean开发者需要实现大部分容器调用的方法,所以编写很麻烦,但同时,在编写的过程中,我们也明白了BMP实体Bean 的工作原理,为我们下一节学习CMP实体Bean打下了坚实的基础。
|