摘要
本节将描述Session Bean的基本原理;开发过程中涉及的普遍过程;组件模型组成部分的开发规则及注意事项。
容器是为EJB组件提供运行时环境的系统。同一容器中可以部署多个EJB组件。容器提供客户端通过JNDI对已部署组件进行依赖注入(EJB3.0)或访问Home接口(EJB2.1)的能力,即客户端可请求容器注入会话Bean远程接口或通过JNDI查找指定EJB组件的Home接口。
客户端不能直接访问EJB组件的实例,只能通过JNDI查找指定组件的Remote接口,通过Remote接口取得对组件接口的引用。客户端对组件接口的方法调用,通过组件接口传播到容器中对应的EJB组件实例。
下图表示EJB组件及其接口与容器、客户端间的关系:
EJB组件及其接口与容器、客户端间的关系
容器为会话Bean提供运行时环境,并对会话Bean实例进行管理,因此,会话Bean实例的状态也由容器进行维护。根据状态管理模式,可以将会话Bean划分为有状态(Stateful)会话Bean和无状态(Stateless)会话Bean,有状态会话Bean的状态由容器负责维护,而无状态会话Bean则不需要容器进行状态管理。
对于当前容器中存在的有状态会话Bean实例,为提供更有效的管理,容器需要将某些空闲的实例状态从内存临时转移到存储机制中。这种转移称为实例的钝化(Passivation),相反地,将实例状态从存储机制恢复,则称为激活。(Activation)。
当实例被包含于某个事务中时,容器不能对实例进行钝化操作。
有状态会话Bean的会话状态指会话Bean实例中的域值,连同实例中的域通过Java对象引用指向的对象的传递闭包(transitive closure)。
注意 | |
---|---|
对象的传递闭包按照Java编程语言中的串行化协议定义,即是通过串行化对象实例,对实例的域进行保存。参考Sun的对象串行化文档。 |
某些情况下,会话对象的会话状态可能会包含打开的资源,如网络端口和数据库连接等。当会话对象被钝化时,容器不能钝化这些资源,因此,需要开发者在容器对实例发出钝化事件通知时(容器调用实例中注解为PrePassivate的方法(EJB3.0)或ejbPassivate方法),对上述资源进行关闭;在容器对实例发出激活事件通知时(容器调用实例中注解为PostActivate的方法或ejbActivate方法),重新打开上述资源。
因此在编写有状态的Session Bean时,开发者必须注意,保证实例的非transient域是下列类型之一,使容器可以在钝化实例时,可以完整保存对象的会话状态:
可串行化的对象;
null值;
EJB的业务接口引用;
EJB的远程接口引用,即使stub class为非序列化亦可
EJB的远程Home接口引用,即使stub class为非序列化亦可;
Entity Bean的本地接口引用,即使其为非序列化亦可;
EJB的本地Home接口引用,即使其为非序列化亦可;
对SessionContext对象的引用,即使其为非序列化亦可;
环境命名上下文(指“java:comp/env”JNDI上下文)及其任何子上下文;
UserTransaction的引用;
Resource manager连接工厂的引用;
对容器管理的EntityManager对象的引用,即使其为非序列化亦可;
通过依赖注入或JNDI查找获得的EntityManagerFactory引用,即使其为非序列化亦可;
对javax.ejb.Timer对象的引用
不可直接串行化的对象,但是通过在串行化对象期间,将对象的引用更改为对象的业务接口,Home接口和组件接口的引用、对象对SessionContext对象的引用、对象对“java:comp/env”JNDI上下文及其子上下文的引用、对象对UserTransaction接口的引用、对象对EntityManager与EntityManagerFactory的引用进行串行化,可将对象视为可串行化的对象。
对于会话Bean,其组件模型包含以下几部分:
Home接口,定义客户端创建、清除EJB实例的方法;(注:在EJB3.0组件模型中并不需要此接口)
组件接口(component interface),定义客户端可访问的组件的业务方法,在EJB3.0组件模型中称为业务接口(business interface);
组件类,提供对定义在组件接口中的方法的实现;
部署描述,包含此EJB的部署信息,如事务属性等,在EJB3.0中可使用注解来描述部署信息。
下面分别对开发这些单元时,涉及的普遍过程、规则及注意事项进行描述。
EJB2.1规范通过Home接口定义客户端创建、清除EJB对象的方法。Home接口有两种类型,本地Home接口和远程Home接口,分别提供给本地和远程客户端使用。在EJB3.0中,可通过依赖注入,JNDI查找,可选的生命周期回调方法注解来实现类似的功能。
远程Home接口使远程客户端可以:
创建新的Session对象;
清除一个Session对象;
取得此Session Bean的javax.ejb.EJBMetaData接口。javax.ejb.EJBMetaData接口用于表示此Session Bean的信息,降低客户端与服务器的绑定程度和用于支持客户端脚本;
取得远程Home接口的句柄(Handler),此句柄可串行化(serialization)到持久存储中,然后,可在其他虚拟机中,从持久存储中,对此串行化对象进行解串行化(deserialization),取得对此远程Home接口句柄的引用。
编写EJB的远程Home接口时,开发人员必须遵循如下规则:
远程Home接口必须扩展(extend)javax.ejb.EJBHome接口;
定义在接口中的方法的参数和返回值必须是合法的RMI类型,并且必须显式声明抛出java.rmi.RemoteException异常;
在继承关系上,远程接口可以扩展已有的接口,但继承的方法必须依从上一规则;
远程接口方法必须定义一个或多个create方法。无状态的Session Bean必须提供一个无参数的create方法,并且只能命名为“create()”的形式;每个create方法必须匹配一个定义在组件类中的ejbCreate方法,此组件类中的ejbCreate方法必须具有相同个数和类型的参数。无状态Session Bean的create方法在组件类中的匹配方法只能命名为“ejbCreate()”的形式。;
每个create方法名称开头必须是“create” 。
create方法的返回值类型必须是Session Bean的远程接口;
create方法必须声明抛出javax.ejb.CreateException异常;
Home接口通过定义一个或多个create方法,提供一种或多种创建session对象的方式。create方法的参数一般用于初始化被创建session对象的状态。
create方法的返回值类型为Bean的远程接口类型。
下面是一个范例Session Bean的Home接口的代码,定义了两个create方法:
public interface CartHome extends javax.ejb.EJBHome { Cart create(String customerName, String account) throws RemoteException, BadAccountException, CreateException; Cart createLargeCart(String customerName, String account) throws RemoteException, BadAccountException, CreateException; }
客户端可通过如下代码,对已部署在应用服务器上的Session Bean的Home接口进行查找,并使用CartHome接口中定义的create方法创建session对象:
Context initialContext = new InitialContext(); CartHome cartHome = (CartHome) javax.rmi.PortableRemoteObject.narrow( initialContext.lookup(“java:comp/env/ejb/cart”), CartHome.class); Cart shoppingCart = home.create("Duke DeEarl","123");
本地Home接口使本地客户端可以:
创建新的Session对象;
清除一个Session对象;
如果EJB需要提供本地客户端的访问,必须提供EJB的本地Home接口,开发人员必须遵循如下规则进行编写:
本地Home接口必须扩展(extend)javax.ejb.EJBLocalHome接口;
定义在接口中的方法不能抛出java.rmi.RemoteException异常;
在继承关系上,本地接口可以扩展已有的接口;
本地接口方法必须定义一个或多个create方法。无状态的Session Bean必须提供一个无参数的create方法,并且只能命名为“create()”的形式;每个create方法必须匹配一个定义在组件类中的ejbCreate方法,此组件类中的ejbCreate方法必须具有相同个数和类型的参数。无状态Session Bean的create方法在组件类中的匹配方法只能命名为“ejbCreate()”的形式。;
每个create方法名称开头必须是“create” 。
create方法的返回值类型必须是Session Bean的本地接口;
create方法必须声明抛出javax.ejb.CreateException异常;
会话Bean和实体Bean的客户端不能直接访问EJB组件类的实例,客户端程序通过业务接口访问EJB组件,开发者在业务接口中定义可供客户端访问的业务方法。业务接口分为两种类型,本地接口和远程接口。
如果EJB需要被远程客户端访问,必须提供EJB的远程接口,远程接口为组件提供以下支持:
定义EJB对象的业务逻辑方法, 远程接口把对业务方法的调用传播到Session Bean实例;
提供允许客户端取得远程Home接口实例引用的方法;
提供使客户端取得Session对象句柄的方法;
提供比较两个EJB实例是否相等的方法;
移除Session Bean实例的方法;
开发人员必须遵循如下规则编写远程接口:
远程接口必须使用@Remote注解标记(EJB3.0),或扩展(extend)javax.ejb.EJBObject接口(EJB2.1);
若使用EJB2.1规范,定义在接口中的方法的参数和返回值必须是合法的RMI类型,并且必须显式声明抛出java.rmi.RemoteException异常;
在继承关系上,远程接口可以扩展已有的接口,但继承的方法必须依从上一规则;
每个在远程接口中定义的方法,必须在组件类中有一个匹配的方法,组件类中的匹配方法必须与接口中定义的方法具有相同的名字、相同的参数个数、参数类型和相同的返回值,而且所有在远程接口中定义的抛出的异常也必须在匹配方法中定义;
远程接口方法不可暴露本地接口类型、本地Home接口类型,而且,对于容器管理持久性Entity Bean,也不可暴露作为Entity Bean中方法参数和返回值的受管理的集合类(collection)。
下面是一个EJB2.1的会话Bean的远程接口的代码,其中定义了三个业务方法:
public interface Cart extends EJBObject { public void addBook(String title) throws RemoteException; public void removeBook(String title) throws BookException, RemoteException; public Vector getContents() throws RemoteException; }
在EJB3.0中,可使用以下形式达到同样效果:
@Remote public interface Cart extends EJBObject { public void addBook(String title) throws RemoteException; public void removeBook(String title) throws BookException, RemoteException; public Vector getContents() throws RemoteException; }
在EJB2.1规范中,如果EJB需要被本地客户端访问,必须提供EJB的本地接口,本地接口为组件提供以下支持:
定义EJB对象的业务逻辑方法, 本地接口把对业务方法的调用传播到Session Bean实例;
提供允许客户端取得本地Home接口实例引用的方法;
提供比较两个EJB实例是否相等的方法;
移除Session Bean实例的方法;
在EJB3.0规范中,容器为远程接口提供了位置透明性,本地客户端亦可通过远程接口访问业务方法。但使用本地接口可以为EJB提供细粒度的业务方法,获得更好的可重用、与可维护性与运行效率。EJB3.0中的本地接口可以使用@Local注解来标记。
对于一个Session Bean组件,开发者在业务(组件)接口中定义的的业务方法和在Home接口中定义的create方法,需要在组件类中对这些方法提供实现。EJB2.1中的组件类必须实现javax.ejb.SessionBean接口,容器通过调用从javax.ejb.SessionBean中继承的管理方法,提供组件访问容器提供的服务的能力,并通过一些状态管理回调方法,向组件实例发送其生存周期中的关键事件的信息。在EJB3.0中的组件类则不必实现javax.ejb.SessionBean接口,而是通过依赖注入、JNDI查找、生存周期回调方法注解达到同样的功能。
EJB3.0的组件类不再强制实现javax.ejb.SessionBean接口,而是通过注解实现同样的功能,从而避免了严格的隐式命名约定。在组件中常用的注解有以下几个:
在组件类中可以使用@Resource注解通知容器对上下文资源进行依赖注入,特别地,可以注入SessionContext实例:
@Stateless public class EmployeeServiceBean implements EmployeeService{ @Resource SessionContext ctx; ... }
在组件类中可以使用@PostCreate注解标记初始化方法按照EJB3.0规范,容器将会在完成对会话Bean依赖注入之后,会话Bean的第一次方法被调用之前回调该方法。可以在该方法中对依赖注入后的会话Bean状态进行初试化。
在会话Bean的业务接口中使用@Remove注解标记客户端请求清除会话Bean的方法。该方法由客户端调用,若成功完成,容器将清除会话Bean。此注解适用于有状态会话Bean,无状态会话Bean的生存周期完全由容器管理,毋须在客户端显式删除。
在会话Bean组件类中可使用@PreDestroy注解标记容器清除会话Bean前的回调方法。容器将在会话Bean被清除前调用该方法。一般,实例可以在此方法中对实例占用的资源进行释放。
在实例将被容器钝化(Passivate)时,容器会调用被标记为PrePassivate的方法。同样,当实例将被再次激活(Activate)时,容器会调用被标记为PostActivate的方法。在实例钝化时,容器将会自动保存实例的状态信息;在激活实例时,容器也将恢复被保存的实例状态信息。一般的Session Bean都会忽略此事件,但是,对于使用某些不可串行化的资源,如与特定数据库的连接等,作为实例状态一部分的Session Bean,通常需要在ejbPassivate方法中释放资源,在ejbActivate方法中重新获取资源。请参考:第 44.2.1.2.1 节 “有状态Session Bean实例的钝化与激活”
在EJB2.1规范中,javax.ejb.SessionBean是容器与组件实例之间的协议。通过实现此接口,组件提供容器访问组件的能力,实际上,容器不需要组件提供任何服务,容器通过javax.ejb.SessionBean中定义的接口方法访问组件实例,主要是为了给组件提供访问容器提供的服务的能力,并向组件实例发送通知其生存周期中重要事件的发生信息。
javax.ejb.SessionBean中定义了以下方法。
容器通过调用setSessionContext方法,将由容器维护的Bean实例的上下文(context)与Bean实例进行关联。一般,Session Bean实例的上下文被作为实例会话状态的一部分,被实例所保存。
在实例将被容器钝化(Passivate)时,容器会调用ejbPassivate方法。同样,当实例将被再次激活(Activate)时,容器会调用ejbActivate方法。对于使用某些不可串行化的资源,如与特定数据库的连接等,作为实例状态一部分的会话Bean,通常需要在ejbPassivate方法中释放资源,在ejbActivate方法中重新获取资源。请参考第 44.2.1.2.1 节 “有状态Session Bean实例的钝化与激活”
对于会话Bean实例,容器将提供一个SessionContext对象,使实例可以访问由容器维护的实例的上下文环境。在SessionContext接口中,定义了如下方法:¹
getEJBObject方法,返回Session Bean的远程接口;
getEJBHome方法,返回Session Bean的远程Home接口;
getEJBLocalObject,返回Session Bean的本地接口;
getEJBLocalHome,返回Sesson Bean的本地Home接口;
getCallerPrincipal方法,返回标识调用此实例的调用者的java.security.Principal对象;
isCallerInRole方法,检查此Session Bean的调用者是否是某个特定的角色;
setRollbackOnly方法,当前事务将被永久标记为回滚,不会被提交。只有容器管理事务的Session Bean可被允许使用此方法;
getRollbackOnly方法,检查当前事务是否已被标记为回滚。例如,EJB实例可以通过此方法,判断是否继续在当前事务边界内进行计算。只有容器管理事务的Session Bean可被允许使用此方法;
getUserTransaction方法,返回javax.transaction.UserTransaction接口。EJB实例可通过此接口对事务边界进行划分,并取得事务的状态。只有容器管理事务的Session Bean可被允许使用此方法;
会话Bean的组件类可以选择是否实现javax.ejb.SessionSynchronization接口。此接口对会话Bean提供事务的同步通知。
afterBegin通知,标志会话Bean实例一个新事务的开始。当包含在事务中第一个的业务方法被调用之前,容器将调用此方法,后续的业务方法调用将会存在于此事务上下文中;
afterCompletion通知,标志一个会话Bean事务的提交操作结束,并通知实例提交操作是成功还是回滚;
beforeCompletion通知,通知会话Bean实例事务将进行提交操作;
EJB3.0规范规定,如果会话Bean直接或间接地实现了SessionSynchronization接口,容器必须回调afterBegin, beforeCompletion与afterCompletion方法。若会话Bean没有实现SessionSynchronization接口,则容器不会调用这些方法。
容器将对每个会话Bean实例的方法调用进行串行化。Apusic应用服务器中的EJB容器支持会话Bean的多个实例并发执行;但是,每个会话Bean实例只会看到依次进行的方法调用,因此开发Session Bean时,不需要将其以可重入(reentrant)的方式进行编写。
客户端不能对有状态的会话Bean对象进行并发调用。当某个客户端对某特定的会话Bean实例的业务方法调用正在执行中,从相同或不同的客户端发出了另一个客户端调用,容器将对第二个客户端调用抛出java.rmi.RemoteException或javax.ejb.EJBException,这取决于第二个客户端调用是通过远程或本地组件接口进行调用。
会话Bean的组件类中可以定义零到多个业务方法,其方法签名和命名必须遵守如下规则:
方法名是任意的,但不能使用“ejb”开头,以免与EJB组件架构中的容器管理回调方法发生冲突,如ejbPassivate方法;
业务方法必须被声明为public方法;
业务方法不能被声明为final或static;
如业务方法是对应于会话Bean远程接口中定义的业务方法,则方法参数和返回值必须是合法的RMI/IIOP类型;
可以抛出任意应用级异常;
注意 | |
---|---|
EJB1.0允许业务方法抛出java.rmi.RemoteException,以指出非应用级的异常。在EJB1.1和EJB2.1兼容的Enterprise Bean开发中,这种方式不建议使用(deprecated),EJB2.1兼容的Enterprise Bean开发中,不应从业务方法抛出java.rmi.RemoteException。 |
以下是一个会话Bean组件类的范例代码,其对应的组件接口和Home接口,请参考本章中关于组件接口和Home接口中的范例部分:
import java.util.*; import javax.ejb.*; public class CartBean implements SessionBean { String customerName; String customerId; Vector contents; public void ejbCreate(String person) throws CreateException { if (person == null) { throw new CreateException("Null person not allowed."); } else { customerName = person; } customerId = "0"; contents = new Vector(); } public void ejbCreate(String person, String id) throws CreateException { if (person == null) { throw new CreateException("Null person not allowed."); } else { customerName = person; } IdVerifier idChecker = new IdVerifier(); if (idChecker.validate(id)) { customerId = id; } else { throw new CreateException("Invalid id: " + id); } contents = new Vector(); } public void addBook(String title) { contents.addElement(title); } public void removeBook(String title) throws BookException { boolean result = contents.removeElement(title); if (result == false) { throw new BookException(title + " not in cart."); } } public Vector getContents() { return contents; } public CartBean() {} public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() {} public void setSessionContext(SessionContext sc) {} }
有状态Session Bean的生存周期
上图表示有状态Session Bean的生存周期。
有状态会话Bean实例的生命周期开始于用户端通过依赖注入或JNDI查找获得一个有状态会话Bean实例的引用,或者用户端调用了会话Bean的Home接口中的create<METHOD>方法。这将通知容器调用会话Bean类的newInstance方法创建一个新的会话Bean实例。然后容器将注入SessionContext实例(若可用),并执行其它由元数据注解标记的或在部署描述符中设定的依赖注入。然后容器将调用会话Bean的PostConstruct生存周期回调方法(若已定义)。如果会话Bean遵照EJB2.1规范,则容器调用实例中匹配的ejbCreate<METHOD>或Init方法。最后,容器返回会话Bean的对象引用。现在实例进入就绪状态,客户端可以调用其业务方法。
根据会话bean元数据注解中的事务属性或部署描述文件中的设定,以及客户端调用所关联的事务上下文,业务方法将会在一个特定的事务上下文环境或一个未指定的业务上下文环境中执行(在图中分别以“事务方法”与“非事务方法”表示)。
容器的缓冲算法决定会话Bean实例是否需要移出内存(例如采取最近最久未使用算法)。容器先回调Bean实例的PrePassivate生存周期回调方法(若已定义)。然后,容器把实例的状态信息保存到二级存储设备中,会话Bean进入钝化状态。在事务之中的会话Bean不能被钝化。
当会话Bean处于钝化状态,容器可能会在会话Bean超时后清除该实例,超时信息在部署描述文件中设置。此时所有对于该实例的引用都将失效。如果客户端试图通过业务接口调用任一方法,容器将抛出javax.ejb.NoSuchEJBException异常。如果使用EJB2.1规范,则容器分别为远程客户端抛出java.rmi.NoSuchObjectException异常,为本地客户端抛出javax.ejb.NoSuchObjectLocalException异常。
如果客户端调用了被标记为Remove的方法,或者home接口或组件接口中的remove方法,容器将在该方法成功完成后回调被标记为PreDestroy的生存周期回调方法(若已定义)。这将终结会话Bean实例的生存周期。此时所有对于该实例的引用都将失效。如果客户端试图通过业务接口调用任一方法,容器将抛出javax.ejb.NoSuchEJBException异常。如果使用EJB2.1规范,则容器分别为远程客户端抛出java.rmi.NoSuchObjectException异常,为本地客户端抛出javax.ejb.NoSuchObjectLocalException异常。要注意即使客户端没有显式调用Remove方法,容器也会在EJB对象实例超时后主动调用其PreDestroy方法并清除该实例。
如果客户端调用了钝化状态的会话Bean的方法,容器将激活该实例。此时容器将先从二级存储设备中恢复实例的状态信息,并调用实例的PostActivte方法(若已定义)。
如果Remove方法成功完成,或Remove方法中抛出了应用异常而retainIfException为假,或抛出了系统异常,SessionSynchronization方法将不会被调用。如果抛出了应用异常而retainIfException为真,则该会话bean实例既不会被清除也不会被丢弃,SessionSynchronization方法(若已定义)将会在事务结束时被调用。
客户端代码对有状态Session Bean生存周期的控制只能创建(使用依赖注入、JNDI查找或create<METHOD>方法)和清除(使用Remove方法)。其他方法由EJB容器进行调用。