[读书笔记]实体Bean

主要内容:

  1. 实体bean如何描述向所有客户端提供服务的域对象;
  2. 两种类型的实体bean——bean管理的持久性(BMP)和容器管理的持久性(CMP);
  3. 除远程接口外,EJB如何提供本地接口;
  4. 指定、实现、配置和部署BMP实体bean;
  5. 从命令行而不是使用图形用户界面配置和部署EJB。

注:

  • 域对象:关系型数据库管理系统中的表。
  • 业务逻辑:指的是规则、约束、过程和实践的集合;

会话bean和实体bean都是对象,因此业务逻辑既可以处于会话bean中也可以处于实体bean中。

规则和约束通常应用于所有应用程序中。最好通过实体bean实现这种业务逻辑,因为实体bean是可以在多个不同应用程序中再次使用的域对象。

在业务领域中,过程和实践通常都是某类应用程序的表达,因此会话bean是实现这类业务逻辑的最佳选择。实际上,由于计算机可提供新的方法来实现这些任务,因此计算机化系统的引入常常会改变这些过程和实践(这些改变有时有利、有时有弊)。

实体bean与会话bean之间主要不同之处在于:

  • 实体bean实现的是javax.ejb.EntityBean而不是javax.ejb.SessionBean,因此两者的生命周期不同;
  • 实体bean使用EntityContext而不是SessionContext进行初始化。EntityContext向实体bean展示一个主键,而对于会话bean而言这是不可应用的。

javax.ejb接口的其他细节和会话bean是一样的。简要地说:

  • 实体bean的Home和远程接口分别通过扩展EJBHome和EJBObject来定义;
  • 本地Home和本地接口则分别通过扩展EJBLocalHome和EJBLocalObject进行定义;
  • EJBMetaData类提供对实体bean组件各部分的访问;
  • Handle和HomeHandle接口可序列化到远程bean或Home的引用。

实体bean的类型:

实体bean描述的是纯处在关系型数据库管理系统中的共享持久型数据或其他持久型数据存储集。如果数据存储集是关系型的,实际执行JDBC的职责可由bean本身或EJB容器承担。

前一种情况称为bean管理的持久性(BMP),后一种情况称为容器管理的持久性(CMP)。

注意:EJB规范主要面向的是关系型数据存储集。当然,可通过JDBC的javax.sql.DataSource对象执行容器管理的持久性,JDBC是基于ANSI SQL92的。

远程接口和本地接口

EJB2.0规范较之早期版本最为重要的一个改进就是本地接口和远程接口的调用。

能够调用bean的方法而无需考虑bean的位置对于会话bean而远是非常关键的,但是对于实体bean来说用处不大,甚至有不利的影响。客户端要完成一些工作常常必须处理多个实体bean,如果每一个实体bean都是通过远程的,就会导致网络拥塞。同时,为执行“通过值进行传递”语法,需要克隆人和序列化对象,这也会影响执行效率。

更令人沮丧的是,实体bean的客户端很可能是一个会话bean。实际上,使用非会话bean与实体bean进行交互通常被看作一种不好的习惯。时常,此会话bean作为正在使用的实体bean被协同定位。仅因为实体bean是远程的,EJB容器要通过网络负责完成全部会话——实体bean的调用,并克隆所有的序列化对象。

到目前为止,你可能以猜测到什么是本地接口,他们是实体bean可指定的非远程接口。同样这里使用Home和代理的概念,即本地接口从javax.ejb.EJBLocalHome扩展而来,而bean代理从javax.ejb.EJBLocalObject扩展而来。这些接口均为常规java接口,应用接口间传递的对象的“通过引用进行传递”语法。

一个实体bean可提供常规的Home/远程接口,或提供本地Home/本地接口。实际上,无法阻止实体bean提供这两种接口,尽管任何客户端使用远程接口都会导致前文提到的运行性能下降。本地接口并非实体bean专有,会话bean也可以提供本地接口。对于会话bean而言(尤其是无状态的会话bean),我们有理由为其提供远程和本地两种接口。通常我们希望这两种接口提供相同的性能,尽管EJB规范对此并未作强制性的要求。

本地接口不仅仅是EJB性能推动器,同时还是容器管理的持久性和容器管理的关系(CMR)建立的基础。

BMP实体Bean的生命周期

BMP和CMP实体bean的生命周期由bean必须实现的EntityBean接口规定。

然而,尽管方法名字是相同的,但这些方法的BMP和CMP实体bean承担的责任却不相同。

生命周期如下:

  • 如果EJB容器需要一个实体bean实例(例如,如果实例迟太小),它会示例bean实例,并调用其setEntityContext()方法;
  • 汇总的实例可为查找方法在描述现有bean的持久性数据存储集中定位数据;
  • bean可采用下面两种方法的任何一种与EJBLocalObject代理(如果使用远程接口,则为EJBObject代理)进行关联。
  1. 第一种方法,通过ejbActivate()有容器激活bean。bean的代理此时存在,但并未与bean关联。如果先前曾钝化bean并在代理上调用过业务方法,则出现这种情况。如果bean的代理作为查找方法的结果被返回,也会出现这种情况。
  2. 第二种方法,客户端可能正在请求通过ejbCreate()和ejbPostCreate()创建一个实体bean。通常这意味着相应的数据已经插入到了持久性数据存储集。
  • 当bean已经与代理进行关联后,可在其上调用业务方法。在代理向bean委派业务方法之前,调用ejbLoad()生命周期方法,表明bean应该重新从事久性数据存储集中装载其状态。完成业务方法后,马上调用ejbStore()方法,表明bean应该使用自身状态的改变来更新持久性数据存储集。
  • bean可采用下述两种方法中的任何一种返回汇集的状态:
  1. 第一种方法,通过ejbPassivate()钝化bean。在生命周期中。通常不需要做什么事情,这是因为在先前的ejbStore()方法中bean的状态已经被保存之持久性数据存储集。因此,钝化仅仅表示EJBLocalObject代理和bean之间的连接已经被切断。
  2. 第二种方法,客户端可能正在请求通过ejbRemove()删除已创建的bean。这通常意味着持久性数据存储集中的相应数据已经被删除。
  • 最后,如果EJB容器需要,可通过首先调用unsetEntityContext()减少实例池的大小。

本地Home接口

注意bean的ejbCreate()和ejbFindXxx()方法的返回类型与本地Home接口种方法的返回类型是不同的。尤其是,当bean返回主键对象或主键对象集合时,本地Home接口方法返回(至客户端)本地代理或代理集合。

  1. 创建和删除方法
  • 本地方法抛出的异常列表和bean的对应方法应该是匹配的。对于createXXX()方法而言,列表应该是ejbCreateXXX()和ejbPostCreateXXX()所抛出的异常的联合。如果为实体bean提供Home和远程接口,则必须为Home接口的方法声明java.rmi.RemoteException异常。
  • 除了create()方法之外,本地Home接口还从javax.ejb.EJBLocalHome继承了Remove(Object o)方法,此方法对应于bean本身的ejbRemove()生命周期方法。

2. 查找方法

  • bean中的查找方法返回单一的主键(如单一的bean匹配底层查询)或主键的java.util.Collection(如果有多个匹配bean)。
  • 通常要求ejbFindByPrimaryKey()方法作为bean方法中的一个,尽管此方法并不是EntityBean接口的组成部分。这是因为参数的类型和返回的类型取决于bean。在Collections API引入J2SE1.2以前,该方法来自EJB1.0,还没有被使用。

3.定制主键类

当两个或多个域识别bean时,需要一个客户开发的主键类;否则使用bean来描述间的单一字段类型。

定制主键类需要遵循很多规则:

  • 类必须实现java.io.Serializable或java.io.Externalizable;
  • 类的值必须全部为基本的或是到序列化对象的引用;
  • 必须实现equals()和hashCode()方法;
  • 必须有一个无参构造器(也可以有其它带有参数的构造器,但提供这些构造器只是为了方便起见)。

换句话说,类必须被认为是值类型的。

例:一个主键类

1:package data;

2:

3:import java.io.*;

4:import javax.ejb.*;

5:

6:public class JobPK implements Serializable{

7: public String ref;

8: public String customer;

9:

10: public JobPK(){

11: }

12: public JobPK(String ref, String customer){

13: this.ref=ref;

14: this.customer=customer;

15: }

16:

17: public String getRef(){

18: return ref;

19: }

20: public String getCustomer(){

21: return customer;

22: }

23:

24: public boolean equals(Object obj){

25: if(obj instanceof JobPK){

26: JobPK pk=(JobPK)obj;

27: return(pk.ref.equals(ref) && pk.customer.equals(customer));

28: }

29: return false;

30: }

31: public int hashCode(){

32: return (ref.hashCode()^customer.hashCode());

33: }

34: public String toString(){

35: return "JobPK: ref" " + ref + "", customer=" "+customer + "" ";

36: }

37: }

注意ref和customer字段有公共的可视性,这是EJB规范的一项要求。每个字段——名字和类型——对应于bean自身的一个字段。这个要求看起来可能有点奇怪,但EJB容器需要他来管理CMP bean。

要实现equals()方法,检验对象中的所有字段是否与提供对象中的字段有着相同的值。对于原始值而言,应该使用常规操作符==,但对于对象引用而言,则必须调用equals()方法。

要实现hashCode()方法,需要生成一个完全基于这些字段的int值,如下所示:

if A.equals(B) then A.hashCode()==B.hashCode();

有许多方法可以实现这一要求。比较简洁快速的方法就是将主键类字段中全部的值转换成String,将它们连接成一个单一的String,然后对这个合成的字符串调用hashCode()。同样,所有字段的hashCode()值都可使用操作符连接在一起。在运行时,这种方式的执行速度要比串联方式快,但这却是意味着分发哈希码对于带有多个字段的主键不是件什么好事。

4.Home方法

处查找、创建和删除方法之外,还可以在本地Home接口种定义Home方法。Home方法可用来执行与一个bean及相关的某些业务类型功能。换句话说,Home方法是Java类方法的EJB等价物(使用static关键字进行定义)。

Home方法的一些常见用法包括:定义所有bean实例能够执行的批处理操作(例如降低所有商品的价格)或各种不同的方法,例如为toString()方法格式化bean的状态。

本地Home接口程序清单:JobLocalHome.java

package data;

import java.rmi.*;
import java.util.*;
import javax.ejb.*;

public interface JobLocalHome extends EJBLocalHome
{
JobLocal create (String ref, String customer) throws CreateException;
JobLocal findByPrimaryKey(JobPK key) throws FinderException;
Collection findByCustomer(String customer) throws FinderException;
Collection findByLocation(String location) throws FinderException;
void deleteByCustomer(String customer);
}

本地接口

和会话bean与远程接口之间的关系类似,本地接口定义实体bean的能力。由于实体bean最主要的是描述数据,因此在通过本地接口展现的方法中,毫无疑问多数应该是简单的获取和设置方法。

本地接口程序清单:JobLocal .java

package data;

import java.rmi.*;
import javax.ejb.*;
import java.util.*;

public interface JobLocal extends EJBLocalObject
{
String getRef();
String getCustomer();
CustomerLocal getCustomerObj(); // derived

void setDescription(String description);
String getDescription();

void setLocation(LocationLocal location);
LocationLocal getLocation();

Collection getSkills();
void setSkills(Collection skills);
}

注意:setLocation()方法接受的是LocationLocal引用,而不是包含位置名字的String。也就是说,Job Bean 定义本身与其它bean之间的关系,在此情况下,Location Bean 直接,有效地执行引用的完整性。这样需要Job实体bean的客户段提供有效位置。

这并不是说实体bean不能提供进一步的处理。常常引用的一个例子是用于SavingAccountBean的,它可提供withdraw()和deposit()方法。withdraw()方法确保余额永远不小于零。这个bean也可以提供applyInterest()方法,但几乎肯定不会提供setBanlance()方法

这些方法在bean中都有与之对应的方法,并且异常清单也严格匹配。这些方法的实现在后面“实现本地接口方法”一节进行讲述。

BMP实体bean的实现

实现一个实体bean包括:

  • 为javax.ejb.EntityBean中的方法提供实现;
  • 为本地Home接口中的每个方法对应方法;
  • 并为本地接口中的每个方法对应一个方法。

实现javax.ejb.EntityBean方法

setEntityContext()方法是执行JNDI查询的理想场所,例如获得一个JDBC数据源引用。

package data;

import javax.ejb.*;

import javax.naming.*;

import javax.sql.*;

public class JobBean implements EntityBean

{

public void setEntityContext(EntityContext ctx) {
this.ctx = ctx;
InitialContext ic = null;
try {
ic = new InitialContext();
dataSource = (DataSource)ic.lookup("java:comp/env/jdbc/Agency");
skillHome = (SkillLocalHome)ic.lookup("java:comp/env/ejb/SkillLocal");
locationHome = (LocationLocalHome)ic.lookup("java:comp/env/ejb/LocationLocal");
customerHome = (CustomerLocalHome)ic.lookup("java:comp/env/ejb/CustomerLocal");
}
catch (NamingException ex) {
error("Error looking up depended EJB or resource",ex);
return;
}
}

private Context ctx;

private DataSource dataSource;

//code omitted

}

unsetEntityContext()方法通常将这些字段设为null。

package data;

import javax.ejb.*;

import javax.naming.*;

import javax.sql.*;

public class JobBean implements EntityBean

{

private DataSource dataSource;
private SkillLocalHome skillHome;
private LocationLocalHome locationHome;
private CustomerLocalHome customerHome;

private String ref;
private String customer;
private String description;
private LocationLocal location;

private CustomerLocal customerObj; // derived

// vector attribute; list of SkillLocal ref's.
private List skills;

public void unsetEntityContext() {
this.ctx = null;
dataSource = null;
skillHome = null;
locationHome = null;
customerHome = null;
}

//code omitted

}

ejbLoad()ejbStore()方法确保bean状态和持久性数据存储集同步。


public void ejbLoad(){
JobPK key= (JobPK)ctx.getPrimaryKey();
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
stmt = con.prepareStatement(
"SELECT description,location FROM Job WHERE ref = ? AND customer = ?");

stmt.setString(1, key.getRef());
stmt.setString(2, key.getCustomer());
rs = stmt.executeQuery();

if (!rs.next()) {
error("No data found in ejbLoad for "+key,null);
}
this.ref = key.getRef();
this.customer = key.getCustomer();
this.customerObj = customerHome.findByPrimaryKey(this.customer); // derived
this.description = rs.getString(1);
String locationName = rs.getString(2);
this.location = (locationName != null)?locationHome.findByPrimaryKey(locationName):null;

// load skills
stmt = con.prepareStatement(
"SELECT job, customer, skill FROM JobSkill WHERE job = ? AND customer = ? ORDER BY skill");

stmt.setString(1, ref);
stmt.setString(2, customerObj.getLogin());
rs = stmt.executeQuery();

List skillNameList = new ArrayList();
while (rs.next()) {
skillNameList.add(rs.getString(3));
}

this.skills = skillHome.lookup(skillNameList);
}
catch (SQLException e) {
error("Error in ejbLoad for "+key,e);
}
catch (FinderException e) {
error("Error in ejbLoad (invalid customer or location) for "+key,e);
}
finally {
closeConnection(con, stmt, rs);
}
}

public void ejbStore(){
Connection con = null;
PreparedStatement stmt = null;
try {
con = dataSource.getConnection();
stmt = con.prepareStatement(
"UPDATE Job SET description = ?, location = ? WHERE ref = ? AND customer = ?");

stmt.setString(1, description);
if (location != null) {
stmt.setString(2, location.getName());
} else {
stmt.setNull(2, java.sql.Types.VARCHAR);
}
stmt.setString(3, ref);
stmt.setString(4, customerObj.getLogin());
stmt.executeUpdate();

// delete all skills
stmt = con.prepareStatement(
"DELETE FROM JobSkill WHERE job = ? and customer = ?");

stmt.setString(1, ref);
stmt.setString(2, customerObj.getLogin());
stmt.executeUpdate();

// add back in all skills
for(Iterator iter = getSkills().iterator(); iter.hasNext(); ) {
SkillLocal skill = (SkillLocal)iter.next();

stmt = con.prepareStatement(
"INSERT INTO JobSkill (job,customer,skill) VALUES (?,?,?)");

stmt.setString(1, ref);
stmt.setString(2, customerObj.getLogin());
stmt.setString(3, skill.getName());
stmt.executeUpdate();
}
}
catch (SQLException e) {
error("Error in ejbStore for "+ref+","+customer,e);
}
finally {
closeConnection(con, stmt, null);
}
}

在ejbLoad()方法中,JobBean使用JobSkill表中的数据作为skills字段的值,且必须从Job和JobSkill数据表中装载其状态。在ejbStore()方法中,对Job和JobSkill表会进行同样的修改。

当然,有时当bean保存自身时,他的数据已经被删除了。如果用户手工删除数据就有可能出现这种情况;EJB规范没有要求实体bean“锁定”底层数据。在此情况下,bean应该抛出javax.ejb.NoSuchEntityException异常;然后,此异常作为某种类型的java.rmi.RemoteException异常被返回值客户端。

更复杂的bean在ejbLoad()和ejbStore()方法中可执行其它的处理。例如,在关系型数据库中,由于性能方面的原因数据可能以反向规范化的形式进行存储。客户端无需了解这些持久性方面的问题。

另一种想法;可使用这些方法有效地处理文本。EJB规范建议压缩和解压缩文本,但也对文本中的关键字进行搜索,然后分开存储这些关键字,或将数据转化为XML格式。


当钝化或激活实体bean时,通常不需要做什么事情。

public void ejbPassivate(){
ref = null;
customer = null;
customerObj = null;
description = null;
location = null;
}

public void ejbActivate(){
}

实现本地Home接口方法

JobBean的ejbCreate()和ejbPostCreate()方法的实现


public JobPK ejbCreate (String ref, String customer) throws CreateException {

// validate customer login is valid.
try {
customerObj = customerHome.findByPrimaryKey(customer);
} catch(FinderException ex) {
error("Invalid customer.", ex);
}

JobPK key = new JobPK(ref,customer);
try {
// workaround; should call ejbHome.findByPrimaryKey, but
// ctx.getEJBHome() returns null...
/*
JobLocalHome jobHome = (JobLocalHome)ctx.getEJBHome();
System.out.println("jobHome = " + jobHome + ", key = " + key);
*/
ejbFindByPrimaryKey(key);
throw new CreateException("Duplicate job name: "+key);
}
catch (FinderException ex) {}

Connection con = null;
PreparedStatement stmt = null;
try {
con = dataSource.getConnection();
stmt = con.prepareStatement(
"INSERT INTO Job (ref,customer) VALUES (?,?)");

stmt.setString(1, ref);
stmt.setString(2, customerObj.getLogin());
stmt.executeUpdate();
}
catch (SQLException e) {
error("Error creating job "+key,e);
}
finally {
closeConnection(con, stmt, null);
}
this.ref = ref;
this.customer = customer;
this.description = description;
this.location = null;

}

 

public void ejbPostCreate (String ref, String customer) {}

此实现可有效地验证客户的存在与否(通过客户和唯一引用来识别职位),同时还可证实完整主键已经不存在于数据库中了。如果确实不存在,那么BMP bean抛出CreateException异常。否则(由抛出FinderException异常的ejbFindByPrimaryKey()调用所描述),方法继续。

另一种引用在关系型数据库管理系统中的Job表内设置唯一索引,然后捕获当试图插入复键时可能抛出的SQLException异常。

警告:这儿有一个紊乱的情况。用户有可能在复键检查和实际的SQL语句INSERT之间插入一条记录。在事物中调用ejbCreate()方法;关系型数据库管理系统隔离级别的更改(采用EJB容器制定的形式)可杜绝这种风险,尽管此时可能出现死锁现象。

注意skills字段被设置为一个空的ArrayList。它存储SkillLocal引用列表,此列表是到Skill bean的一个本地接口。当然,对新建的Job bean而言,这个列表是空的。skills字段之所以存储到SkillLocal对象的引用,而不是存储包含技能名称的String,这是经过深思熟虑的。如果技能名称被使用,那么查找技能的相关信息需要额外的步骤。这也是为CMP bean和容器管理的关系采用的方法,CMP中将详细讲述这方面的知识。

另外要注意的是customerObj字段。当Job被创建时,只被传递一个含有客户名字的String。此String是客户主键。customerObj字段借助CustomerLocal引用包含一个到父客户bean的引用。

skills和customerObj字段演示bean管理的关系。对于skills字段而言,这是一个多对多的关系(从Job到Skill)。对于customerObj字段而言,这是多对一的关系(从Job到customer)。

ejbCreate()和ejbPostCreate()方法都对应bean的本地Home接口中一个名为create()的单一方法。参数列表必须对应。同样,对于会话bean而言,也可有多个带有不同参数的创建方法,或使用createXXX()方法命名规则而不是超载create()的方法名字。

ejbRemove()方法与ejbCreate()方法是相对的;ejbRemove()方法从持久性数据存储集中删除一个bean的数据。JobBean中ejbRemove()方法的实现如下:


public void ejbRemove(){
JobPK key = (JobPK)ctx.getPrimaryKey();

Connection con = null;
PreparedStatement stmt1 = null;
PreparedStatement stmt2 = null;
try {
con = dataSource.getConnection();

stmt1 = con.prepareStatement(
"DELETE FROM JobSkill WHERE job = ? and customer = ?");

stmt1.setString(1, ref);
stmt1.setString(2, customerObj.getLogin());

stmt2 = con.prepareStatement(
"DELETE FROM Job WHERE ref = ? and customer = ?");

stmt2.setString(1, ref);
stmt2.setString(2, customerObj.getLogin());

stmt1.executeUpdate();
stmt2.executeUpdate();
}
catch (SQLException e) {
error("Error removing job "+key,e);
}
finally {
closeConnection(con, stmt1, null);
closeConnection(con, stmt2, null);
}
ref = null;
customer = null;
customerObj = null;
description = null;
location = null;
}

 
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值