EJB3(中文版)参考

jinkunaier

贡献于2013-04-07

字数:0 关键词: EJB Java开发

译者序 在学习了一段时间的 Hibernate 之后,在朋友徐小献那里得知了 EJB3 的实体 Bean 借鉴了 Hibernate, 转而学习 EJB3,在网上查了很 多资料,在 JBoss 网站上看到了这本书,觉得不错,可是没有完整版 的下载,不久,在国内论坛上找到了此书,但是,是英文版的,对于 刚才始学习 EJB3 的新手来说,找点相关资料太难了,只好在学习之 余,将所学过的重点章节进行了简单的翻译,这是初稿,未经校对, 供大家学习交流,其中有很多的错误和我没有理解的地方原版放在上 面,我使用的开发环境是 Eclipse+JBoss+MySQL. 读者发现严重错误可 以发送邮件到 liuyuhui_007@yahoo.com.cn 我会加以更正,我的 QQ:495585885. 我将会在我的博客 http://hi.baidu.com/vsandjava 上陆续发表相关信息以及 Eclipse 工程. 刘玉辉 2006 年10 月18 日于哈尔滨师范大学 Enterprise JavaBeans 3 5.3 包装一个持久化单元 一个持久化管理映射是一个固定的类集合到一个详细的数据库 ,这个类的集 合被称作持久化单元,在你能够建立与查询实体使用实体管理时 ,你必需学会怎 样打包一个持久化单元使用 J2SE 或者J2SE 环境,一个持久化单元定义在 Persistence.xml 文件,这个部署描述必需符合 Java 持久化规范,一个 Persistence.xml 文件能够定义一个或多个持久化单元 ,这个文件存在于本地的 META-INF 目录中: .一个简单的 JAR 文件包含在一个正规的 J2SE 程序的路径中 .一个 EJB-JAR 文件,持久化单元被包含在 EJB 描述中 文件 Persistence.xml 部署描述定义特性和配置属性包含的每一个持久化单元, 每一个持久化单元必需有一个唯一的身份,尽管空字符串也是一个有效的名. 这些类的集合在持久化单元中被详细指定,你的开发环境的持久化提供者自动扫 描JAR 文件中的类文件集合部署成实体 .当扫描完成,持久化提供者将查找 JAR 文件中的每一个类文件确定其中是否包含有元数据注释 @javax.persistence.Entity,是否这些实体的集合能够被映射. 每一个持久化单元绑定一个且仅有一个数据源,在J2SE 环境中,EJB3 规范规定必 需配置和定义数据源,在J2EE 开发环境中,规范化的 XML 元素定义这种关联. Persistence.xml 文件的根元素,包含 一个或多个 元素,每一个有两个属性:name(必需) 和transaction-type(可选的).它的子元素包括: (可选), (可选), (可选), (可选), (可选), (可 选), (可选), (可选), and (可选). 这里有一个 persistence.xml 文件的例子: java:/OracleDS update Name 属性定义了一个名子供单元引用的,这个名子用来注入元数据和 XML 部署描 述元素来引用这个单元,这个属性是必需的. transaction-type 属性定义是否你的持久化单元管理被集成在 J2EE 的JTA 或者 你想使用本地资源(RESOURCE_LOCAL) javax.persistence.EntityTransaction API 用来管理完整的 EntityManager 实 例,这个属性默认为 J2EE 环境中的 JTA 和RESOURCE_LOCAL 在SE环境中 元素是一个注释用来描述持久化单元,它不是必需的. 元素是非常重要的,一个类实现 javax.persistence.PersistenceProvider 接口,在J2EE 和J2SE 环境中,持久化 执行一个插件,由卖主提供适当的执行,常常不需要你去定义这个元素,依赖默认 值即可. 如果你使用了JTA或者RESOURCE_LOCAL持久化单元,你需要定义一个元素,分别的,这些元素的详细说明一个数据 的卖主规范的特性,通常, 这个字符串是一个全局的 JNDI 名来引用此数据源.如 果没有定义,由容器提供默认的引用. 元素定义集合详细的属性传递到持久化提供者,这些详细配置明确 的由卖主来执行,以后不需要注册或 JNDI 服务在 J2SE,这常常由卖主提供配置数 据源,代替使用元素. 这个持久化单元 JAR 文件或许随意的包含映射 XML 部署描述被称作 Orm.xml 在 META-INF 目录中的描述 ,这个文件用来定义映射在类集合中包含持久化单元和 数据库的映射,另外映射文件被引用通过 元素,这个元素的值是 一个类路径不是硬编码的 URL.你可以按需求写多个元素.第6章 和第八章将会介绍. 5.3.1.持久化单元类集 一个持久化单元映射一个固定的类集合到关系型数据库 ,在默认的情况下,你的 清单没有其它的元数据在 Persistence.xml 文件中,这个JAR 文件包含 persistence.xml 将要被检查通过根元素到其它类的注解包含 @javax.persistence.Entity,.这些类是额外的类集合由持久化单元进行管理 , 你必需详细指明其它的 JAR 文件你想要检查的使用元素 ,这个元素的值是一个JAR文件路径,被包含到persistence.xml文件 中: java:/OracleDS .../lib/customer. jar update 扫描 JAR 文件来确保工作在 J2EE 环境中,但是不是在轻量级的 J2SE 应用程序中, 理论上,不能确定 JAR 文件集合被杯查,实际上,这不是一种情况,所有主要的 EJB3.0 提供商的专家组剪掉了这种不正式的说法 ,他们无问题 的支持这种特性 在J2SE 中,你是否做这种信赖性 JAR 文件检查,类集能够被明确的列出使用 元素. java:/OracleDS com.titan.domain.Cabin com.titan.domain.Customer update 类Cabin 和Customer 在元素中被列出并附加到持久化单元,如果你不想 在Persistence.xml 的JAR 文件中被检查,你可以使用元素 java:/OracleDS com.titan.domain.Cabin com.titan.domain.Customer update 最终确定的类集合是所有的元数据的合并. .类注释使用@Entity 在Persistence.xml 文件的 JAR 文件 (除清单) .类注释使用@Entity 包含在任何 JAR 文件使用元素 .类映射文件 META-INF/orm.xml 如果它存在 .类映射在任何 XML 文件引用时使用元素 .类列表使用元素 通常,你发现不需要使用, , or 元素,一种情 况是这些类已经被映射在两个或更多个持久化单元中. 5.4 获取一个 EntityManager 现在你要打包和部署一个持久化单元,你需要获取方问一个 EntityManager 所以你在能够 持久化,更新,移除和查询实体 beans 在数据库中,在J2SE 中,实体管理的建立使用一个 javax.persistence.EntityManagerFactory,尽管你可以使用工厂接口在 J2EE 中,这个平台提供了一些额外的特性使得它变得容易和减少冗长来管理实体实例 5.4.1EntityManagerFactory 实体的管理需要建立或获取从 EntityManagerFactory,在J2SE 应用程序中,你必 需使用 EntityManagerFactory 来建立一个 EntityManager 的实例. 使用工厂不是必需的在 J2EE 中 package javax.persistence; public interface EntityManagerFactory { EntityManager createEntityManager( ); EntityManager createEntityManager(java.util.Map map); Void close( ); Boolean isOpen(); } 方法 createEntityManager()返回 EntityManager 的实例来管理一个明确的长久 性的持久化上下文.你可以传递 java.util.Map 类型的参数覆盖或扩充任何容器 提供者提供的详细的属性,不需要定义 persistence.xml 文件.当你完使用 EntityManagerFactory 你必调用 close()方法来关闭它(除非是注入,后面将讨 论).方法 isOpen()用来检查是否 EntityManagerFactory 的引用是有效的. 5.4.1.1 获取一个 EntityManagerFactory 在J2SE 中 在J2SE 中,类javax.persistence.Persistence 信靠 EntityManagerFactory:: Public class Persistence { Public static EntityManagerFactory createEntityManagerFactory( String unitName); Public static EntityManagerFactory createEntityManagerFactory( String unitName,java.util.Map,properties); javax.persistence.Persistence 类查找 persistence.xml 描述文件在 Java classpath 中, unitName 参数传递将要执行的持久化 ,用来定位 EntityManagerFactory 是否与 给定的名相匹配.另外,你可以覆盖或添加任何详细的属性的定义通过 persistence.xml 文件中的元素,传递一个 java.util.Map 作为第二个参 数. EntityManagerFactory factory = Persistence.createEntityManagerFactory("CRM"); ... factory.close( ); 在J2SE 中,这种做法是被推荐的,这样可以释放被工厂所使用过的资源. 5.4.1.2 获得一个 EntityManagerFactory 在J2EE 中 在J2EE 中,很容易得到 EntityManagerFactory.可以通过注入直接到字段或 setter 方法到你的 EJB 组件中去,使用@javax.persistence.PersistenceUnit, 注释. package javax.persistence; @Target({METHOD, FIELD, TYPE}) @Retention(RUNTIME) public @interface PersistenceUnit { String name( ) default ""; String unitName( ) default ""; } 方法 unitName()是唯一的标识在 PersistenceUnit 中,当PersistenceUnit 被使 用,它不仅仅注入 EntityManagerFactory,同时也注册一个引用到 JNDI ENC 的EJB 组件.(JNDI ENC 在11 和14 章中讨论). EJB 容器通知@PersistenceUnit 注释和注入正确的工厂: Import javax.persistence.*; Import javax.ejb.*; @Stateless public MyBean implements MyBusinessInterface { @PersistenceUnit(unitName="CRM") private EntityManagerFactory factory; private EntityManagerFactory factory2; @PersistenceUnit(unitName="CUSTDB") public void setFactory2(EntityManagerFactory f) { this.factory2 = f; } 当一个无状态会话 Bean 被建立时,EJB 容器设置这个工厂字段来识别“CRM”在 持久化单元中.它也在调用 setFactory2 方法的同时使用了“CUSTDB”持久化单元. 不同于 J2SE,一个注入 EntityManagerFactory 自动关闭在容器中的实例被丢弃的同 时,事实上,你可以调用 close(0 方法在一个注入 EntityManagerFactory,一个 IllegalStateException异常将会被抛出.@PersistenceUnit注释和XML是等价 的隐藏了更多的细节在第14 章中会有介绍 5.4.2.获取一个持久化上下文 一个持久化上下文件建立可以通过 EntityManagerFactory.createEntityManager()方法的调用.返回一个 EntityManager 实例来描述一个长期的持久化上下文 . 如果 EntityManagerFactory 是JTA-enabled (java 事务激活),你必需明确 EntityManager 实例在事务中调用 EntityManager.joinTransaction()方法,如 果你不支持 EntityManager 在JTA 事务中,你所做的改变实体 Bean 将不会与数 据库同步. 注意: EntityManager.joinTransaction( )是必需被调用的,当一个 EntityManager 被建立,明确的使用 EntityManagerFactory 如果你使用的是容器 管理的持久化上下文,你不需要完成这些额外的步骤 使用 EntityManagerFactory API 是有一些冗长和笨拙当你使用嵌套 EJB 调用.幸运的是 EJB 和Java 持久化规规范精确的集成。一个 EntityManager 能够被直接的注入到 EJB 中使用@javax.persistence.PersistenceContext.注释(或等价的 XML 文件将在 第14 章中进行讨论): package javax.persistence; public enum PersistenceContextType { TRANSACTION, EXTENDED } public @interface PersistenceProperty { String name( ); String value( ); } @Target({METHOD, TYPE, FIELD}) @Retention(RUNTIME) public @interface PersistenceContext { String name( ) default ""; String unitName( ) default ""; PersistenceContextType type( ) default TRANSACTION; PersistenceProperty[] properties( ) default {}; } @PersistenceContext 注释的工作方式类似于 @PersistenceUnit 除了实体 Bean 管理的实例被注入代替一个 EntityManagerFactory: @Stateless public class MySessionBean implements MySessionRemote { @PersistenceContext(unitName="titan") private EntityManager entityManager; ... } unitName()属性唯一标识持久化,默认情况下,一个 transaction­scoped(事务范围)的 持久化上下文是被注释注入的 ,你可以覆盖默认的 ,而使用 type()属性,当你使用事务范围的 Entity-Manager,一个持久化上下文关联一个事务,直到这个事务结束. 意思就是说实体 Bean 管理的内部事务上下文相互影响,没关系,如果使用不同的 实例注入到不同的 Beans,同样的上下文将会被使用 ,这个注释的细节将会在 14 章中进行讨论,持久化上下文的移植将会在第十六章中进行讨论. 你可能从来没有调用过 close()方法在一个注入实体管理中.清除的处理被应用 服务器所管理.如果你关闭一个实体管理,一个 IllegalStateException 将抛出. 一个 EXTENDED 实体管理只能被注入到有状态会话 Bean 中;无状态会话 Bean 和 消息驱动 Bean 是被 Pooled,再没有其它方法来关闭持久化上下文和释放任何被 管理的实体实例.为了获取一个扩展的上下文, 一个有状态会话 Bean 使用 @javax.persistence.PersistenceContext 注释和一个类型为 Extended, @Stateful public class MyStatefulBean implements MyStatefulRemote { @PersistenceContext (unitName="titan", type=PersistenceContextType.EXTENDED) private EntityManager manager; ... } 当MyStatefulBean 被建立时,一个持久化上下文也被建立通过注入管理的字段. 这个持久化上下文有同样的生命周期和这个 Bean.当有状态会话 Bean 被移除时, 这个持久化上下文被关闭 .意思是实体对象的实例仍旧与有状态会话 Bean 绑定 在有状态会话 Bean 被激活期间. 第11 章会有更详细的介绍 注意:当你使用@PersistenceContext 注释或等价的 XML 文件做为 EJB 的持久 化时,这些生气勃勃性的定义使开发者很容易与实体 Bean 交互,实体管理的建立 使用 Entity-ManagerFactory 有更多的错误倾向因为应用开发都不再考虚更多 的事情,实事上,开发者可能忘记关闭实体管理器和泄露资源.优势是 EJB 容器使 其变得更加简易. 5.5 交互使用一个 EntityManager 现在我们来学习怎样部署和获得引用一个实体管理,你已经准备学习交互的语义用 它.EntityManager API 方法来插入和移除实体从一个数据库中又合并更新从一个已经分 离的实体实例.也有很多查询的 API 供你来使用建立查询对象从确定的 EntityManager 中 的方法: package javax.persistence; public interface EntityManager { public void persist(Object entity); public T find(Class entityClass, Object primaryKey); public T getReference(Class entityClass, Object primaryKey); public T merge(T entity); public void remove(Object entity); public void lock(Object entity, LockModeType lockMode); public void refresh(Object entity); public boolean contains(Object entity); public void clear( ); public void joinTransaction( ); public void flush( ); public FlushModeType getFlushMode( ); public void setFlushMode(FlushModeType type); public Query createQuery(String queryString); public Query createNamedQuery(String name); public Query createNativeQuery(String sqlString); public Query createNativeQuery(String sqlString, String resultSetMapping); public Query createNativeQuery(String sqlString, Class resultClass); public Object getDelegate( ); public void close( ); public boolean isOpen( ); } 5.5.1 持久化实体 持久化一个实体实际上是向数据库中做一个插入的动作.持久化的实体不再被建 立在数据库中,建立一个实体之前,首先要分配一个实例给它 ,并设置它的属性, 和确定它与其它对象可能建立的关联,初始化一个实体 Bean 同初始化一个 Java 对象一样,当上面这些任务都完成之后,你可以调用 EntityManager.persist( ) 方法进行持久化: Custom cust = new Customer( ); cust.setName("Bill"); entityManager.persist(cust); 当这个方法被调用后,实体管理队列将 Customer 插入到数据库中,对象的实例被 管理.那时插入了很少的变量,如果 persist()被调用在一个事务中,这个插入是 立即发生的,或者将被加入到队列中直到事务结束.依赖刷新模式(后面讲) 你可以手动的强制插入,调用 flush()方法.你也可以调用 persist()在事务外如 果仅仅是为了实体管理一个 EXTENDED 的持久化上下文.这个插入队列直到持久 化上下文关联一个事务,一个注入扩展的持久化上下文是自动关联一个 JTA 事务 被EJB 容器.其它的扩展上下文建理管理通过 EntityManagerFactor API,你必需调用 Entity.Manager.joinTransaction( ) 执行事务的关联. 如果实体有各种各样的关系和其它实体,这些实体也同时被建立在数据库中,是否有适当的层 叠策略设置,在第七单日中讨论层叠,在第六章我们来看一下 Java 持久化自动生成主键在 Persist()方法被调用.所以,在前面的几个例子中,如果你有自动键生成激活,你可能认为生成键 是在 Persist()方法完成后. 方法Persist()抛出IllegalArgumentException 异常,如果参数不是一个实体类 型.TRansactionRequiredException 抛出如果方法调用或事务范围的持久化上 下文.无论如何,如果实体管理是一个扩展的持久化上下文 ,它是支的调用 Persist()方法在事务范围之外 ;这个插入队列直到持久化上下文交互同一个事 务. 5.5.2 查找实体 实体管理器提供两种机制来定位对象在你的数据库中.一种方法是通过简单的实 体管理主法来定位一个实体用主键.别一种是建立执行的查询语句. 5.5.2.1 find() and getReference( )方法 EntityManager 有两种不同的方法允许你查找一个实体通过主键: public interface EntityManager { T find(Class entityClass, Object primaryKey); T getReference(Class entityClass, Object primaryKey); } 两个方法使用实体类作为参数,以实例的实体主键.它们使用 Java 类所以你不需 要任何修释.这两个方法有什么不同呢?方法 find()返回 null 如果实体没有在 数据库中找到.它的初始化状态每一个属性是在 lazy-loading 策略中(下一章讨 论lazy loading) Customer cust = entityManager.find(Customer.class, 2); 在这个例子中,我们定位一个 Customer 通过主建 ID为2.那么 find()方法的第二 个参数的类型又是怎样被预编译的? 好的,Java5 有一个特性叫做自动制动器由它来转换原始的类型到 Object 类型, 所以,常量 2被转换成 java.lang.Integer; Customer cust = null; try { cust = entityManager.getReference(Customer.class, 2); } catch (EntityNotFoundException notFound) { // recovery logic } getreference( )方法不同于 find()方法之处在于如果实体没有在数据库中找 到会抛出 javax.persistence.EntityNotFoundException 并且不能保证实体状 态被初始化. 方法 find( ) 和 getreference( )都抛出 IllegalArgumentException 如果参数 类型不是实体类型.你可以允许调用它们在事务范围外,既然这样,任何对象被返 回是分离的如果 EntityManager 是transaction­scoped 但是仍管理发果它是一个扩展的 持久化上下文. 5.5.2.2 查询 持久化对象也能够被 EJB QL 所定位,不同于 EJB2.1,没有 finder 方法,并且你必需建立一个查 询对象来调用EntityManager's createQuery( ), createNamedQuery( ), or createNativeQuery( ) 方法: public interface EntityManager { Query createQuery(String queryString); Query createNamedQuery(String name); Query createNativeQuery(String sqlString); Query createNativeQuery(String sqlString, Class resultClass); Query createNativeQuery(String sqlString, String resultSetMapping); } 建立并执行一个 EJB QL查询是非常类似于建立和执行 JDBC PreparedStatement: Query query = entityManager.createQuery("from Customer c where id=2"); Customer cust = (Customer)query.getSingleResult( ); 查询和 EJB QL将在第九章中讨论 所有的对象实例返回通过方法 find( ), getResource( ),或查语句在持久化上下文 中你访问仍活跃的,意思是更远的调用 find()将返回同样的实体对象实例. 5.5.3.更新实体 一旦你查找一个实体 Bean 通过调用 find(),getreference()方法或建立和执行一个查询,这个实 体Bean 的实例仍可以管理通过这个持久化上下文,直到上下文被关闭,在这期间,你能改变实 体Bean 实例的状态像其他对象一样,和更新也将自动同步(依赖Flush Mode)或你调用flush 方法直接: @PersistenceContext EntityManager entityManager; @TransactionAttribute(REQUIRED) public void updateBedCount(int id, int newCount) { Cabin cabin = entityManager.find(Cabin.class, id); cabin.setBedCount(newCount); } 在上面的代码中,Cabin 实体通过 find()方法被查找到,仍然被 EntityManager 管理,因为当前的持久化上下文与事务的关联 .意味着你可以编辑对象实例和数 据将要自动被更新当 EntityManager 决定 flush 改变从内存到数据库中. 5.5.4 合并实体 JAVA 持久化规范允许你合并状态,改变从已经分离的实体持久化存储,使用 merge()方法.考虑到一个不定的客户端.这个客户调用一个方法在远程 TravelAgent 会话 Bean 查找一个 cabin 在数据库中: @PersistenceContext EntityManager entityManager; @TransactionAttribute(REQUIRED) public Cabin findCabin(int id) { return entityManager.find(Cabin.class, id); } 在这个例子中持久化上下文在调用完 findCabin()方法后结束,同样它是一个单 JTA 事务,当一个 Cabin 实例是序列化的,它被分离从实体管理和发送到不确定 的远程客户.这个 Cabin 实例是 Java 对象和不再与任何实体管理相关联.你可以 调用它的 getter 和setter 在此对象上,像其它古老的 Java 对象,不定的客户做 了少量的改变后 Cabin 实例又被发送到服务器端. Cabin cabin = travelAgent.findCabin(1); cabin.setBedCount(4); travelAgent.updateCabin(cabin); TRavelAgentBean.updateCabin( )方法使 Cabin 参数合并到当前的持久化上下 文管理的实体管理器中调用 merge()操作: @PersistenceContext EntityManager entityManager; @TransactionAttribute(REQUIRED) public void updateCabin(Cabin cabin) { Cabin copy = entityManager.merge(cabin); } 远程客户所做的改变将被重新获得持久化存储,当实体管理器决定刷新到数据库 中,下面的规则应用于合并,在updateCabin( )方法的 Cabin 参数中: .如果实体管理器没有准备管理一个 Cabin 实例使用同样的 ID,一个完全拷贝的 Cabin 参数和返回从 merge()方法,这个拷贝管理通过实体管理器 ,任何附加的 setter 方法被调用在这个拷贝将要与数据库进行同步当 ntityManager 决定刷 新.Cabin 参数将分离并且没有被管理. .如果实体管理器已经管理 Cabin 实例通过相同的主键,那么 Cabin 参数的内容 将被拷贝到管理的实例 .merge( )操作将返回这个管理的实例 ,Cabin 参数将分 离并且没有被管理. merge( )方法将抛出 IllegalArgumentException 如果参数不是实体类 型.transactionRequiredException 将会被抛出如果方汉被调用在 transaction-scoped 持久化上下文.无论如何,如果实体管理是一个扩展的持久 化上下文,它是合法的调用这个方法在事务范围和更新将要被队列直到持久化上 下文与一个事交互. 5.5.5 移除实体 一个实体能够被移除从数据库中,通过调用 EntityManager.remove( ) 方 法.remove( )操作并不是立即就删除 Cabin 从数据库中.当实体管理器决定刷新 时,才会移除,一个 DELETE 将要执行: @PersistenceContext EntityManager entityManager; @TransactionAttribute(REQUIRED) public void removeCabin(int id) { Cabin cabin = entityManager.find(Cabin.class, id); entityManager.remove(cabin); } 在remove()调用之后,Cabin 实例将不再被管理和将要被分离.同样的,如果实体 还有其它关联到其它实体对象也将被移除,信赖于层叠规则,(将在第七章进行讨 论)remove()操作能够销毁由 persist()方法持久化的实体实例. Remove() 方法抛出 IllegalArgumentException 如果参数不是实体类型. TRansactionRequiredException 抛出如果方法调用是在 transaction­scoped 持久化的 上下文.无论如何,如果 EntityManager 是一个扩展的持久化上下文,它是合法的调用 方法在事务范围并且 remove 将排队直到持久化上下文与一个事务进行交互. 5.5.6 refresh( ) 方法 如果你关新当前的管理的实体不是最新的同数据库,你可以使用 EntityManager.refresh( )方法, refresh( )方法更新实体的状态从数据库中, 重写改变的实体: @PersistenceContext EntityManager entityManager; @TransactionAttribute(REQUIRED) public void removeCabin(int id) { Cabin cabin = entityManager.find(Cabin.class, id); entityManager.refresh(cabin); } 如果实体 Bean 存在关联,那些实体也将被更新,信赖于层叠策略设置实体映射的 元数据. refresh( )方法抛出 IllegalArgumentException 如果参数不是一个被当前实体 管理器管理的实例.如果方法调用是在事务范围内的持久化上下文,将会抛出 transactionRequiredException,它是合法的调用在事务范围外 .如果对象不再 存在于数据库中,因为别一个过程调用了 removed,将会抛出 EntityNotFoundException. 5.5.7. contains( ) and clear( )方法 contains( )方法用一个实体的实例做为参数 ,如果这个实例被当前的持久化上 下文所管理,返回True,如果参数的类型不是实体类型,将抛出 IllegalArgumentException. 如果你需要分离所有的实体实例从当前的持久化上下文,你可以调用 EntityManager 的clear()方法 ,你要意识到,当调用 clear()方法时你对实体 Bean 所做的改变将丢失.它将调用 flush()在clear()调用之前,所以,不要丢失 你的改变. 5.5.8 flush( ) and FlushModeType 方法 当调用 persist( ), merge( ), 或 remove( )时,所做的改变并没有同步到数据库中直 到实体管理器决定刷新时.你可以强制同步在任意时刻,调用 flush()方法,默认情况下,刷新 是自动发生的在一个相互关联的查询被执行(在每次执行完查询之后就执行 flush 交率会很 低下)和事务提交时.除了find() 的默认规则.一个flush 不需要在发生 find() 或 getreference( )时调用,因为通过主键查找没有对数据产生任何更新的影响. 你可以控制和改变默认的行为通过 javax.persistence.FlushModeType 列举: public enum FlushModeType { AUTO, COMMIT } AUTO 是默认的行为描述处理代码片断,COMMIT 的含意是改变更新在事务提交时, 不是在任何查询之前,你可以设置 FlushModeType通过 setFlushMode( )方法在 EntityManager 中. 为什么要改变更新模式呢 ?默认的更新行为有很多种理解 .如果你在数据库中做 一个查询,你想确定任何更新是否应用到数据库中时 .如果实体管理器没有刷新 到数据库时,将不会影响到数据库中,显然,你想做的改变要在事务提交之后. FlushModeType.COMMIT 执行的原因.最好的方法来调整数据库应用程序去移除 不是必需去调用数据库的.由一些提供者来执行当执行 JDBC 绑定的更新请求时. 如果updateBeds()方法使用默认 . FlushModeType.AUTO,一个执行的查询 SQLUpdate,使用 COMMIT 允许实体管理器执行批量更新,同时,一个更新常常结束 在一个写锁结束.使用 COMMIT 在事务之间的提交. 5.5.9.锁 EntityManager API 支持读和写锁,因为锁接近事务的概念,在第十六章中进行讨论 5.5.10.getDelegate( )方法 getdelegate( )方法允许你获得一个引用在持久化提供对象来实现 EntityManager 接口,很多提供者都对 EntityManager 接口进行子扩 充,getdelegate( )方法提供了一种方法访问卖主的 APIS. 5.6.资源的本地处理 在J2EE 环境中,一个实体管理的持久化上下文通常由一个 JTA 事务进行管理,当 你运行在一个非 J2EE 环境中,JTA 是不可用的,所以 JAVA 持久化规范提供一个事 务API 通过EntityTransaction 接口.你可以获得访问一个事务通过 EntityManager.getTransaction( )操作: public interface EntityTransaction { public void begin( ); public void commit( ); public void rollback( ); public boolean isActive( ); } Begin() 方法抛出 IllegalStateException 如果已经存在一个活动的 EntityTransaction, commit( ) 和 rollback( )方法抛出 IllegalState- Exception 如果事务不是活动的. 注意:在J2EE 环境中 EntityTransaction API 由卖主执行,但是应用程序的开发者, 不鼓励使用 JTA. 你不能使用 EntityTransactions如果事务类型是持久化单元 JTA. 让我们看一下第四章的例子,并转换成容器外测试使用 javax.persistence.Persistence API 和 EntityTransaction: import javax.persistence.*; public class StandaloneClient { public static void main(String[] args) throws Exception { EntityManagerFactory factory = Persistence.createEntityManagerFactory("titan"); EntityManager manager = factory.createEntityManager( ); try { createCabin(manager); Cabin cabin_2 = manager.find(Cabin.class, 1); System.out.println(cabin_2.getName( )); System.out.println(cabin_2.getDeckLevel( )); System.out.println(cabin_2.getShipId( )); System.out.println(cabin_2.getBedCount( )); } finally { manager.close( ); factory.close( ); } } public static void createCabin(EntityManager manager) { Cabin cabin_1 = new Cabin( ); cabin_1.setId(1); cabin_1.setName("Master Suite"); cabin_1.setDeckLevel(1); cabin_1.setShipId(1); cabin_1.setBedCount(3); EntityTransaction transaction = manager.getTransaction( ); transaction.begin( ); manager.persist(cabin_1); transaction.commit( ); } } 这个例子中,首先获取一个引到 EntityManager-Factory 描述持久化单元 .我们 使用javax.persistence.Persistence 类来查找 titan 持久化单元通过 createEntity-ManagerFactory( )静态方法描述在这章 .一旦工厂被找到 .我们 可以建立一个 EntityManager 来交互. createCabin( )方法接受 EntityManager 类型的参数,为了能够将 Order 插入到 一个新的 Cabin 实体数据库,我们需要与实体管理器进行交互在事务单元内 .直 到我们运行在容器外,我们不能使用 JTA 并且使用 EntityTransaction API 来开始 和提交一个工作单元. 这个例子非常类似 JDBC 操作,使用 JDBC,我们将获取一个 java.sql.Connection 从数据 源,正象我们获得一个 EntityManager 从EntityManagerFactory,我们过去使用 java.sql.Connection commit( ) and rollback( )方法来完成工作 .正象我们使 用EntityTransaction 在前几章. 第六章 映射持久化对象 6.1 设计模型 实体是一种古老的 JAVA 类在 JAVA 持久化规范中.你可以定义和分配这些类像其 它的 JAVA 对象一样.可以与实体管理服务进行交互来持久化,更新,移除,查找和 查询实体组件.实体管理服务负责自动管理实体组件的状态 .这种服务来管理实 体组件在事务和持久化状态到数据库中,在第五章中已经进行了详细的讨论. 6.1.1.Customer 组件 Customer 组件是一个简单的实体 Bean 来自巡游的消费者或旅客模型.它的设计 和使用可以跨跃多个商业领域.JAVA 持久化是关于关系型数据库的.本章将介绍 Customer 实体的设计与实现.这个实体将通过多种途径构造在本章中 ,我们将演 示多种方法来映射 Customer 实体到关系型数据库. 6.1.2 组件类 Customer 组件是一个古老的 Java 对象映射到关系型数据库.通过字段来处理状 态,随意的,它通过getter和setter方法来访问组件的状态.它有最小的,无参的 构造方法. package com.titan.domain; import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @Id public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 当我们建立一个持久化类的时候必需有两个元数据注释: @javax.persistence.Entity 指示该类将要被映射到数据 库,@javax.persistence.Id 指示类的那一个属性将会被用作主键.持久化属性 将假设所有的其它属在类中的将与数据库中的栏的名和类型相同.表名将默认的 和类名相对应.这里是表的定义,持久化提供者将假定你映射到: create table Customer( id long primary key not null, firstName VARCHAR(255), lastName VARCHAR(255) ); @javax.persistence.Entity 元数据通知持久化提供者这个类能够被持久化. package javax.persistence; @Target(TYPE) @Retention(RUNTIME) public @interface Entity { String name( ) default ""; } @Entity 元数据有一个 name 属性.这个属性用来供 EJB QL参考.如果你不提供这 个属性值.默认的名是一个不合法的组件类. 怎样应用@javax.persistence.Id 元数据注释来确定是否你将要使用 Java 组件类型来定义你的持久化属性或者放置@Id 元数据注释到 getter 方法上.像下 例子一样你可以应用任何其它的映射元数据注释到getter和setter方法在组件 类中,提供者将假设其它的getter和setter在你的类中代表持久化属性.将自动 映射它们在名字和类型的基础之上. @Entity public class Customer implements java.io.Serializable { @Id private long id; private String firstName; private String lastName; public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 这里,我们有一个地方放置了 @Id 元数据在类的字段上 .持久化的提供者将假设 类中的其它字段变成持久化属性 ,自动映射其名和类型.在这个例子中其它成员 元数据注释放置在字段上,不在 Getter 和Setter 方法上. 我们真正定义的访问 类型是,关系映射定义在字段和方法中在一个类中. 6.1.3 XML 映射文件 如果你不想要使用元数据来识别和映射实体组件,你可以有选择的使用 XML 映射 文件来定义这个元数据.默认情况下,持久化提供者将首先查找 META­INF 目录下 的orm.xml 或者 persistence.xml 文件中的元素,这里是一个 Customer 实体映射的 XML 文件. 映射文件的顶部元素是.元素定义实体类和访问类 型,PROPERTY or FIELD . 元素是元素的一个子元素 ,定义那一 个属性为主键.像有注解的类,持久化提供者将假设其它属性在你的注释类中是 一个持久化属性,如果你没有明确的定义它们. 6.2.基本的关系映射 开发者可以通过两种方法来实现实体组件 ,一些应用程序是通过 Java 对象模型 来确定数据库模型的.其它的应用程序从已存在的数据库 schema 来确定 Java 对象模 型. Java 持久化规范提够足够的弹性来开始程序在两种方法中.如果你建立的数据库 schema 是从 Java 对象模型中,很多 Java 提供商的工具能够自动生成 schema 通过元数据或 XML 元素,在你 提供的代码中.在这个环节中,程序的原型非常的快和简单 ,如果你没有定义更多的元数据为 持久化引挚自动生成 Schema,当你想调整你的映射时,Java 持久化规范规定必需的元数据和 XML 映射文件. 如果已存在数据库的 schema,很多提供商的工具能够自动直接生成 Java 实体代码,有时,虽然 生成的代码不是对象导向和映射到数据库中并不是很理想,幸运的是 Java 持久化规范提供了 必要的映射能力来简化这一问题. 你将会发现你使用的注释和 XML 映射将依赖说明,如果是自动生成的 schema 通过 实体类,你也许不需要@Table 和 @Column 两个注释,它的明确定义依赖于规范中的 默认值.如果你有一个已存在的 schema,有将会发现有很多的元数据将会被列出. 6.2.1.基本的 Schema 映射 我们不喜欢默认的表和栏的映射在原先的 Customer 实体类中,或者已存在表我们将要映射它, 或者 DBA 强制名的转换,在数据库的 Schema 上,我们实际上定义的关系表映射到 Customer 实体使用@javax.persistence.Table,和@javax.persistence.Column 注释来进行 映射,下面是表定义的 SQL: create table CUSTOMER_TABLE ( CUST_ID integer primary key not null, FIRST_NAME varchar(20) not null lastName varchar(255) not null, ); 我们想要改变表名和 id 和 firstName 两个属性的栏位名 ,我们想在 firstName 属性加上非空约束,和设置 VARCHAR 长度为 20,让我们修改原始的 Customer 实体 类和增加元数据注释: package com.titan.domain; import javax.persistence.*; @Entity @Table (name="CUSTOMER_TABLE") public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @Id @Column(name="CUST_ID", nullable=false, columnDefinition="integer") public long getId( ){ return id; } public void setId(long id) { this.id = id; } @Column(name="FIRST_NAME", length=20, nullable=false) public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 6.2.1.1.@Table 注释 @javax.persistence.Table 注释告诉 EntityManager 服务关系表映射成组件类, 你不需要详细指定这个注释,如果你不想映射,表名是无限制的组件类名.让我们 看一下这个注释的完整定义: package javax.persistence; @Target({TYPE}) @Retention(RUNTIME) public @interface Table { String name( ) default ""; String catalog( ) default ""; String schema( ) default ""; UniqueConstraint uniqueConstraints( ) default {}; } catalog( ) 和 schema( )属性是自省的,同样的它们识别有关表的 catalog 和 schema 属于: public @interface UniqueConstraint { String[] columnNames( ); } @Table.uniqueConstraints( )属性允许指定唯一的栏限制那将被包含在数据库 定义语言(DDL)中,一些提供商的工具能够创建 DDLs 通过实体类集合,或自动表 生成,当组件被部署时,UniqueConstraint 注释非常的有用在使用特定提供商定 义额外的约束时,如果你不使用 schema 生成工具,你不需要定义这片元数据. @Table 和 @UniqueConstraint 注释有一个等价的 XML 映射元素: SOME_OTHER_ATTRIBUTE
元素是元素的子元素,如果你需要可以指定多个元素,一个唯一性约束可以包含多个栏,倒不如多个唯一性约束. 6.2.1.2.@Column 注释 使用@Column 注释,我们需要设置 id 属性的栏位名为 CUST_ID 并且是非空,数据库 中的类型是 integer.firstName 属性,我们改变了栏位名和 VARCHAR 类型的长度映 射成 20. @javax.persistence.Column 注释描述详细的字段和属性信息映射到表中的指 定栏: public @interface Column { String name( ) default ""; boolean unique( ) default false; boolean nullable( ) default true; boolean insertable( ) default true; boolean updatable( ) default true; String columnDefinition( ) default ""; String table( ) default ""; int length( ) default 255; int precision( ) default 0; int scale( ) default 0; } name( )属性很明显的指定栏位名,如果你没有指定栏位名,栏位名默认的为字段 或属性,table( )属性用来多表映射,在本章后将会介绍,其它的属性用来在厂商 提供的工具自动生成 Schema 时使用.如果映射到一个已存在的 schema,你不需要定 义任何的这些属性,unique( ) and nullable( )属性定义约束在栏位上,你可以指定 是否想要栏位被包含在 SQL INSERT or UPDATE 使用 insertable( ) and updatable( ), 分别的.columnDefinition( )属性允许你定义准确的 DDL 来定义 栏位类型,length( )属性确定VARCHAR类型的长度当有一个String类型的属性. 为了数值型属性你可以定义 scale( ) 和precision( )属性. @Column 注释有一个等价的 XML 文件中的元素,这个元素是属性映射类 型, , , 的子元素,元素的描述将会 在本章后进行. 元素的属性有相同和行为和默认值与@column 注释. 6.2.1.3.XML 让我们看一个完整映射的例子和怎样进行映射在 XML 文件中:
6.3 主键 主键用来标识一个已存在的实体组件的身份.每个实体 Bean 必需有一个主键,并 且是唯一的.主键能够被映射到一个或多个属性并且可以被映射到以下类型中的 一种:任何Java 原始数据类型(包含包装类),java.lang.String,or a primary- key class composed of primitives and/or strings.让我们重新认识一个属 性主键. 6.3.1. @Id 注释 @javax.persistence.Id 注释标识一个或多个属性,来组成表中的主键: package javax.persistence; @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface Id { } 你可以通过手动或厂商提供的工具来生成主键,当你使用厂商提供的工具生成时 你需要使用@javax.persistence.GeneratedValue,注释: package javax.persistence; @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface GeneratedValue { GenerationType strategy( ) default AUTO; String generator( ) default ""; } public enum GenerationType { TABLE, SEQUENCE, IDENTITY, AUTO } 持久化提供商要求提供键生成原始主键,你可以定义主键类型来生成通过 strategy( )属性.GeneratorType.AUTO 策略是一般的配置: package com.titan.domain; import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @Id @GeneratedValue public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } AUTO 策略告诉持久化提供者允许为你提供键.IDENTITY 策略使用特殊栏类型,支 持多种数据库,来建立主键.让我们看一下下面的例子; 元素是的子元素,如果你想使用 IDENTITY 来代替,将 AUTO 换成 IDENTITY 即可. 6.3.2. 表生成器 TABLE 策略指明一个用户定义的关系表从那个生成的数字键,被使用的关系表的 逻辑结构如下: create table GENERATOR_TABLE ( PRIMARY_KEY_COLUMN VARCHAR not null, VALUE_COLUMN long not null ); PRIMARY_KEY_COLUMN 所保存的值是为了匹配你生成的主键.VALUE_COLUMN 保存 值的数量. 如果想使用这个策略,你必需定义表生成器,使用 @javax.persistence.TableGenerator,注释,这个注释能够被应用在类或方法的 主键. package javax.persistence; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface TableGenerator { String name( ); String table( ) default ""; String catalog( ) default ""; String schema( ) default ""; String pkColumnName( ) default ""; String valueColumnName( ) default ""; String pkColumnValue( ) default ""; int allocationSize( ) default 50; UniqueConstraint[] uniqueConstraints( ) default {}; } name( )属性定义@TableGenerator 的名子并且参照@Id.generator()属性. table( ), catalog( ), and schema( )描述生成表的表定义,pkColumnName( ) 属性是栏位名用来标识指定的实体主键你将要成生的. valueColumnName( )属 性保存将指定栏位名的数量为了生成主键.pkColumnValue( )属性用来匹配将要 生成的主键. 元数据属性说明: name:生成器的唯一名字 ,可以被Id元数据使用 . table:生成器用来存储 id值的Table定义. pkColumnName:生成器表的主键名称 . valueColumnName:生成器表的ID值的列名称. pkColumnValue:生成器表中的一行数据的主键值 . initialValue:id值的初始值 . allocationSize:id值的增量 . allocationSize( )属性是计数器的增量当持久化提供者查询表的新值时.它允 许提供者将数据存入缓存块中并不是每次都写入数据库每次都许要一个新的 ID. 如果你是自动生成的表,你也可以定义一些约束条件使用 uniqueConstraints( ) 方法. 让我们看一个实际的例子,怎样使用生成器在 Customer 实体上: package com.titan.domain import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @TableGenerator(name="CUST_GENERATOR" table="GENERATOR_TABLE" pkColumnName="PRIMARY_KEY_COLUMN" valueColumnName="VALUE_COLUMN" pkColumnValue="CUST_ID" allocationSize=10) @Id @GeneratedValue (strategy=GenerationType.TABLE, generator="CUST_GENERATOR") public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 现在如果你分配和Persist()一个Customer实体,id属性将自动生成当 persist() 方法被调用后. 让我们看一下怎样定义 XML 文件: 元素是的子元素,它的属性和注一样精确,所以我们 不再解说,需要记住的是元素,你必需指定 generator 属性, 如果你使用@GeneratedValue 注释,要带上这个属性. 6.3.3. 序列生成器 一些关系型数据库,如Oracle,有一个高效的、内置的结构用来生成 ID序列.这 就是 SEQUENCE 生成策略.生成的类型定义是通过 @javax.persistence.SequenceGenerator 注释: package javax.persistence; @Target({METHOD, TYPE, FIELD}) @Retention(RUNTIME) public @interface SequenceGenerator { String name( ); String sequenceName( ) default ""; int initialValue( ) default 1; int allocationSize( ) default 50; } 元数据属性说明 : name:生成器的唯一名字 ,可以被Id元数据使用 . sequenceName:数据库中,sequence对象的名称 .如果不指定 ,会使用提供商指定的默认名称 . initialValue:id值的初始值 . allocationSize:id值的增量 . 下面的代码定义了一个使用提供商默认名称的 sequence生成器. @SequenceGenerator(name="EMP_SEQ", allocationSize=25) name( )属性指定@SequenceGenerator 怎样引用在@Id 注释中.使用 sequenceName( )属性定义那张序列表将要被使用在数据库中. initialValue( ) 是用于主键的初始值,allocationSize( )是用于访问后的增长值.让我们再一次 应用 SEQUENCE 策略在 Customer 实体组件上: package com.titan.domain import javax.persistence.*; @Entity @Table(name="CUSTOMER_TABLE") @SequenceGenerator(name="CUSTOMER_SEQUENCE", sequenceName="CUST_SEQ") public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="CUSTOMER_SEQUENCE") public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 这个例子有一点不同于 Table 策略的例子,生成的定义在组件类上代替直接在属 性上,表和序列生成器能够定以在类上和属性上两个位置的任一处.同样的表生 成类型,当EntityManager.persist( )执行过后主键将自动生成 让我们看一下等价的 XML 映射文件: entity-mappings> 元素是元素的子元素,它的属性类似于 @SequenceGenerator 注释.注意元素参照序列生成器的定 义. 6.3.4.主键类和复合类 有时关系映射要求一个主键由多个持久化属性复合而成,例如,我们的关系模型 明确指明实体 Customer 的唯一标识通过 last name 和安全码代替自动生成的数 字键.这些被称作复合主键.Java 持久化规范提供了多种途径映射这种类型的模 型。其中的一种是通过@javax.persistence.IdClass.注释;其它的是通过 @javax.persistence.EmbeddedId 注释. 6.3.4.1. @IdClass 注释 第一种方法定义主键类(和复合主键)使用@IdClass 注释.组件类不用使用主键 类在其中.但是不使用它来与实体管理器交互当查找一个持久化对象通过它的主 键.@IdClass 是类级的注释并且指定那个键类将被使用当与实体管理器交互时. @Target(TYPE) @Retention(RUNTIME) public @interface IdClass { Class value( ); } 在你的组件类中,你指明一个或多个属性做为主键,使用@ID 注释.这些属性将映 射, 成准确的属性在@IdClass.让我们看以下将 Customer 组件类改成复合主键 为name 和安全码.首先,让我们定义一个主键类: package com.titan.domain; public class CustomerPK implements java.io.Serializable { private String lastName; private long ssn; public CustomerPK( ){} public CustomerPK(String lastName, long ssn) { this.lastName = lastName; this.ssn = ssn; } public String getLastName( ){ return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public long getSsn( ){ return ssn; } public void setSsn(long ssn) { this.ssn = ssn; } public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof CustomerPK)) return false; CustomerPK pk = (CustomerPK)obj; if (!lastName.equals(pk.lastName)) return false; if (ssn != pk.ssn) return false; return true; } public int hashCode( ) { return lastName.hashCode( ) + (int)ssn; } } 主键类必需满足下列条件: ※必需被序列化 ※必需有一个公共的无参构造方法 ※必需实现 equals()和hashCode()方法 Customer组件必需有同样要求的属性同CustomerPK类的属性被加上@Id注释的. package com.titan.domain; import javax.persistence.*; @Entity @IdClass(CustomerPK.class) public class Customer implements java.io.Serializable { private String firstName; private String lastName; private long ssn; public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Id public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Id public long getSsn( ){ return ssn; } public void setSsn(long ssn) { this.ssn = ssn; } } 注意:主键自动生成不支持复合键和主键类.你需要手工创建键值. 让我们看一下等价的 XML 文件: com.titan.domain.CustomerPK 元素是的子元素,并且它的值是完全有资格做主键类的类名, 注意,多个元素映射成主键类. 主键类用于当你查询 Customer 组件时: CustomerPK pk = new CustomerPK("Burke", 9999999); Customer cust = entityManager.find(Customer.class, pk); 无论你何时调用EntityManager的方法像find()或getreference(),你必需使用 主键类才能识别这个实体. 6.3.4.2. @EmbeddedId 注释 一种不同的方法来定义主键类和复合主键是直接嵌入到组件类中. @javax.persistence.EmbeddedId 注释用于此目的并且与 @javax.persistence.Embeddable 注释联用. package javax.persistence; public @interface EmbeddedId { } public @interface AttributeOverrides { AttributeOverride[] value( ); } public @interface AttributeOverride { String name( ); Column[] column( ) default {}; } public @interface Embeddable { } 在你的表中有两种方法来映射主键类的属性到栏位上.一种是详细说明@Column 映射在你的主键类的源代码中.另一种是使用@AttributeOverrides.让我们来看 一下它的形式: package com.titan.domain; import javax.persistence.*; @Embeddable public class CustomerPK implements java.io.Serializable { private String lastName; private long ssn; public CustomerPK( ){} public CustomerPK(String lastName, long ssn) { this.lastName = lastName; this.ssn = ssn; } @Column(name="CUSTOMER_LAST_NAME") public String getLastName( ){ return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Column(name="CUSTOMER_SSN") public long getSsn( ){ return ssn; } public void setSsn(long ssn) { this.ssn = ssn; } public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof CustomerPK)) return false; CustomerPK pk = (CustomerPK)obj; if (!lastName.equals(pk.lastName)) return false; if (ssn != pk.ssn) return false; return true; } public int hashCode( ) { return lastName.hashCode( ) + (int)ssn; } } 我们改变 Customer 组件类直接使用 CustomerPK,使用@EmbeddedId 注释. IdClass 与EmbeddedId 的区别在于 IdClass 在一个实体类中使用所有与数据表 对应的字段,而EmbeddedId 可以分在两个文件里. CustomerPK 主键类用在 EntityManager APIs 调用 Customer 实体时. CustomerPK pk = new CustomerPK("Burke", 9999999); Customer cust = entityManager.find(Customer.class, pk); 只要你调用 EntityManager 的方法,像find()或getreference(),你必需使用主 键类来标识这个实体. 如果你不想使用@Column 来映射主键类,或者你想覆盖它们,你可以使用 @AttributeOverrides 定义它们直接在组件类中: @Entity public class Customer implements java.io.Serializable { private String firstName; private CustomerPK pk; public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @EmbeddedId @AttributeOverrides({ @AttributeOverride(name="lastName", column=@Column(name="LAST_NAME"), @AttributeOverride(name="ssn", column=@Column(name="SSN")) }) public PK getPk( ){ return pk; } public void setPk(CustomerPK pk) { this.pk = pk; } } @AttributeOverrides 注释是@AttributeOverride 注释的集合.name( )属性指 定类内部的属性名在你将要映射的类中. column( )属性名允许你描述将要映射 的栏位. 让我们现在看一下等价于@IdClass 的XML 文件: 属性是元素的子元素,并且描述嵌入类. 元素标识嵌入类的持久化属性.在内部,你可以定义, , , 和 栏位的映射.这些子元素是可选的. 元素嵌入在元素内标识嵌入主键属性的实体. 元素,用来指定嵌入类的被覆盖属性的栏位的详细信息. 6.4 属性映射 到现在为止,我们仅仅显示了映射指定栏位到简单的原始类型.有一些元数据能 够用来调整你的映射.在本章中,你将学会更多的用于复杂的属性映射的元数据. Java 持久化映射为 JDBC 的Blobs 和Clobs,序列化对象,和内嵌对象,同版本属性 做了同样的优化.我们将讨论它们. 6.4.1@Transient注释 在我们第一个例子中的 Customer 组件类中,我们演示的持久化管理假定每一个 属性(getter/setter 或field,依赖于访问类型)为非瞬间的在组件类中被持久 化,即使属性没有任何关联映射元数据与之关联.这是非常重要的对于持久化对 象的原型,特别是持久化卖主支持自生成,然而,你可能有一些属性不想要持久化, 并且,这种默认的方式是不相应的.例如,我们做过的@EmbeddedId 的例子.你获 得的 CustomerPK 类的实例的界面如果你想要观察 last name 或安全数字在 Customer 实体组件中.它将非常有用在 Customer 组件的 Getter 方法上,这样能 够直接提供信息组别。这就是@javax.persistence.Transient.注释的由来: @Entity public class Customer implements java.io.Serializable { private String firstName; private CustomerPK pk; public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Transient public String getLastName( ){ return pk.getLastName( );} @Transient public long getSsn( ){ return pk.getSsn( );} @EmbeddedId public PK getPk( ){ return pk; } public void setPk(CustomerPK pk) { this.pk = pk; } } 当你使用@javax.persistence.Transient 注释时,持久化管理器将忽略此字段 并且不对其进行持久化.这里有一个等价的 XML 文件如下: 元素在元素声明内,等同于@transient 注释的属性. 6.4.2.@Basic注释和 FetchType 枚举类型 @Basic 注释是最简单的映射形式对原持久化的属性来说.也是默认的属性映射 类型是最原始的,原始的包装类型, java.lang.String, byte[], Byte[], char[], Character[], java.math.BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time , and java.sql.Timestamp.你不需要很明确的告诉持久化管理器映射成基本属性 因为它通常计算出 JDBC 所使用的属性类型. public @interface Basic { FetchType fetch( ) default EAGER; boolean optional( ) default true; } public enum FetchType { LAZY, EAGER } 通常,不需要在持久化属性上指明这个注释.可是,有时你需要详细指明 fetch() 属性,当持久化对象第一次从数据库中取出时你需要详细指明装载的属性是延迟 的还是热切的.这个属性在查询数据时允许持久化提共者优化数据库的访问使用 最小的数据量进行装载.所以,如果 fetch()属性是 LAZY 时,详细属性将不被初始 化直到实际访问到这个字段时.所有的其它注释有同样的属性.这种不自然的事 情在规范中,尽管 fetch()这个属性是一个提示,即使你标记了属性为 LAZY 的 @Basic 类型,持久化的提供者仍然允计装载属性为热切的.实际上这是预期的这 种特性是类级的工具.它将同样的被著名那个延迟装载的即不真正的有用也不是 一个重要的执行优化.事实上最好是热切的装入基本属性. optional( )属性是非常有用的当你的持久化提供者生成数据库的 Schema 时.当 这个属性被设置成 True 时,这个属性可以被空处理.让我们感受一下 Customer 实体并显示怎样使用@Basic,注释: package com.titan.domain import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; @Id @GeneratedValue public long getId( ){ return id; } public void setId(long id) { this.id = id; } @Basic(fetch=FetchType.LAZY, optional=false) public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Basic(fetch=FetchType.LAZY, optional=false) public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } 在上面的代码中,我们提示了 firstName 和lastName 属性是延迟装载的,同样, 这些属性在数据库的 Schema 中都是非空的. 与@Basic 注释等价的 XML 配置文件: 6.4.3.@Temporal注释(与时间相关的注释) @Temporal 注释提供额外的信息到持久化提供者关于映射 java.util.Date 或 java.util.Calendar 类型的属性.这个注释允许你映射这些种对象类型到一个 日期,一个时间,或一个时间戳字段在数据库中.默认情况下,持久化提供者假定 时间类型为一个时间戳: package javax.persistence; public enum TemporalType { DATE, TIME, TIMESTAMP } public @interface Temporal { TemporalType value( ) default TIMESTAMP; } 这个注释能够与@Basic 注释联合使用.例如,说我们想要增加一个 time-created 属性到 Customer 实体.如下所做: package com.titan.domain; import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; private java.util.Date timeCreated; @Id @GeneratedValue public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Temporal(TemporalType.TIME) public java.util.Date getTimeCreated( ){ return timeCreated; } public void setTimeCreated(java.util.Date time) { timeCreated = time; } } timeCreated 属性存储到数据库中是 TIME 这种 SQL 类型.让我们看一下等介的 XML 配置文件: TIME 元素用在元素内定义,并且有同样的枚举类型在前面已经讨 论过. 6.4.4.@Lob注释 有时持久化属性需要很大的内存.字段中的一个代表一幅图片或一个非常大的文 本.JDBC 有特殊的类型与这些非常大的对象对应. java.sql.Blob 类型代表二进 制数据,和java.sql.Clob 代表字符数据.@javax.persistence.Lob 注释用来映 射那些大的对象类型.JAVA 持久化允许你映射一些基本类型到@Lob 和持久化管 理器处理他们内部的任一个 Blob 或Clob 类型,依赖属性的类型: package javax.persistence; public @interface Lob { } @Lob 注释的属性持久化一个: ◆Blob 如果 Java 类型是 byte[], Byte[], 或 java.io.Serializable ◆Clob 如果 Java 类型是 char[], Character[],或java.lang.String @Lob 注释常同@Basic 注释联合使用提示属性将被延迟加载.让我们修改 Customer 组件来增加代表一个 JPEG 图象的属性: package com.titan.domain; import javax.persistence.*; import com.acme.imaging.JPEG; @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; private JPEG picture; @Id @GeneratedValue public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Lob @Basic(fetch=FetchType.LAZY) public JPEG getPicture( ){ return picture; } public void setPicture(JPEG picture) { this.picture = picture; } } 当你使用@Lob 类型时,最好标记上到达类型为 lazy,因为你不经常访问大对象当 与Customer 组件进行交互时. 让我们看一下与@Lob 等价的 XML 配置文件: 元素用在元素内,并且用于标识@Lob 类型的属性. 6.4.5.@Enumerated 注释 @Enumerated 注释映射 Java 枚举类型到数据库中.它与@Basic 注释联合使用,让 我们看一下它的详细使用方法: package javax.persistence; public enum EnumType { ORDINAL, STRING } public @interface Enumerated { EnumType value( ) default ORDINAL; } 一个 Java enum 属性能够被映射成字符串代表的类型或是由数值组成的枚举类 型值.例如,我们想让一个 Customer 实体有一个可以预定房间的属性,并且被 Java 的Enum 所描述叫做 CustomerType,使用 UNREGISTERED, REGISTERED, 或者 是 BIG_SPENDAH 等枚举值,我们需要做如下工作: package com.titan.domain; import javax.persistence.*; public enum CustomerType { UNREGISTERED, REGISTERED, BIG_SPENDAH } @Entity public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; private CustomerType customerType; @Id @GeneratedValue public long getId( ){ return id; } public void setId(long id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName( ){ return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Enumerated (EnumType.STRING) public CustomerType getCustomerType( ){ return customerType; } public void setCustomerType(CustomerType type) { customerType = type; } } 你并不是一定要使用@Enumerated 注释来映射属性的,如果你忘了这个注 释,ORDINAL EnumType 值是假设的, 等价的 XML 配置文件如下: STRING 元素用在元素内,它有 ORDINAL 或 STRING 两种值. 6.5.@SecondaryTable注释与多表映射 有时你要处理一个逻辑实体它存在于两个不同的表中.你希望实体组件类能够描 述出你需要的对象,但是它是映射到两张不同的表的因为你工作的逻辑数据库模 型。Java 持久化允许你映射实体组件类到一个或多个表使用 @javax.persistence.SecondaryTable.注释.例如,定义 Customer 组件类的属性 Address,但是地址相关的数据存储在分开的表中,下面演示为什么会这样: create table CUSTOMER_TABLE ( CUST_ID integer Primary Key Not Null, FIRST_NAME varchar(20) not null, LAST_NAME varchar(50) not null ); create table ADDRESS_TABLE ( ADDRESS_ID integer primary key not null, STREET varchar(255) not null, CITY varchar(255) not null, STATE varchar(255) not null ); 使用@SecondaryTable 注释,Address_table 的主键栏位将连接一个或多个 Customer_table 表中的栏位. public @interface SecondaryTable { String name( ); String catalog( ) default ""; String schema( ) default ""; PrimaryKeyJoinColumn[] pkJoinColumns( ) default {}; UniqueConstraint[] uniqueConstraints( ) default {}; } public @interface PrimaryKeyJoinColumn { String name( ) default ""; String referencedColumnName( ) default ""; String columnDefinition( ) default ""; } @SecondaryTable 注释与@Table 注释非常的相像,除了它的 pkJoinColumns( )属 性的定义.在Customer 组件类中,你将定义这个的注释和详细指明 Address_table 的主键使用嵌入的@PrimaryKeyJoinColumn 注释. @PrimaryKeyJoinColumn 注释的 name()属性代表 Address_table 中使用连接. referencedColumnName( )属性代表 Customer_table 表中的栏名将与 Address_table 进行连接. package com.titan.domain; import javax.persistence.*; import com.acme.imaging.JPEG; @Entity @Table(name="CUSTOMER_TABLE") @SecondaryTable(name="ADDRESS_TABLE", pkJoinColumns={ @PrimaryKeyJoinColumn(name="ADDRESS_ID")}) public class Customer implements java.io.Serializable { ... @PrimaryKeyJoinColumn 指定 Address_table 表中的栏位将与 Customer_table 表中的主键进行连接.在这种情况下,它是 Address_Id.我们不需要指明注释的 referencedColumnName( )属性,因为它默认是 Customer 实体的主键栏位. 下一步是映射street, city, 和 state属性到Address_table表中的栏位.如果 你还记着@Column 注释的全部的话,我们没有认真的学习过 table()属性,你可以 使用这个映射 Address 属性到第二张表: package com.titan.domain; import javax.persistence.*; import com.acme.imaging.JPEG; @Entity @Table(name="CUSTOMER_TABLE") @SecondaryTable(name="ADDRESS_TABLE", pkJoinColumns={ @PrimaryKeyJoinColumn(name="ADDRESS_ID")}) public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; private String street; private String city; private String state; ... @Column(name="STREET", table="ADDRESS_TABLE") public String getStreet( ){ return street; } public void setStreet(String street) { this.street = street; } @Column(name="CITY", table="ADDRESS_TABLE") public String getCity( ){ return city; } public void setCity(String city) { this.city = city; } @Column(name="STATE", table="ADDRESS_TABLE") public String getState( ){ return state; } public void setState(String state) { this.state = state; } ... 如果是三张表以上应该怎么做呢?例如,你要在其中嵌和信用卡的属性,但是信 息已经存储在另一张表中了,在这种情况下,你可以使用@SecondaryTables 注 释: package com.titan.domain; import javax.persistence.*; import com.acme.imaging.JPEG; @Entity @Table(name="CUSTOMER_TABLE") @SecondaryTables({ @SecondaryTable(name="ADDRESS_TABLE", pkJoinColumns={@PrimaryKeyJoinColumn (name="ADDRESS_ID")}), @SecondaryTable(name="CREDIT_CARD_TABLE", pkJoinColumns={@PrimaryKeyJoinColumn (name="CC_ID")}) }) public class Customer 你要匹配属性同@Column.table( )属性集. 让我们看一下多表映射的 XML 配置文件:
... 元素可以被多次定义在元素内. 元素是元素的子元素.并且有 name, referenced-column-name, 和 column-definition 属性.你可以有选择的使用 table 属性映射到第二张表. 6.6.@Embedded Objects Java 持久化规范允许你插入不存在的 Java 对象在实体组件中并且映射的嵌入 对象属性什到实本表中的栏位.这些对象没有任何标识并且被包含的实体组件类 所专有.@EmbeddedId 主键的规则非常的简单如同前面的例子.我们首先定义一 个内部对象: package com.titan.domain; import javax.persistence.*; @Embeddable public class Address implements java.io.Serializable { private String street; private String city; private String state; public String getStreet( ){ return street; } public void setStreet(String street) { this.street = street; } public String getCity( ){ return city; } public void setCity(String city) { this.city = city; } public String getState( ){ return state; } public void setState(String state) { this.state = state; } } 内部的 Address 类有@Column 映射定义在里面.其次,使用 @javax.persistence.Embedded 注释在 Customer 组件类内的嵌入实例 Address, 类: package javax.persistence; public @interface Embedded {} 同样的@EmbeddedId,@Embedded 注释能够被联合同@AttributeOverrides 注释一 同使用如果你想覆盖指定嵌入类的信息.下面的例子显示如果覆盖.如果你不想 覆.省去@AttributeOverrides 注释. package com.titan.domain; import javax.persistence.*; @Entity @Table(name="CUSTOMER_TABLE") public class Customer implements java.io.Serializable { private long id; private String firstName; private String lastName; private Address address; ... @Embedded @AttributeOverrides({ @AttributeOverride (name="street", column=@Column(name="CUST_STREET")), @AttributeOverride(name="city", column=@Column(name="CUST_CITY")), @AttributeOverride(name="state", column=@Column(name="CUST_STATE")) }) public Address getAddress( ){ return address; } ... } 在本例中,我们映射 Address 类属性到 Customer_table 表中的栏位.如果你不想 指定@Embedded 注释和 Address 类是序列化的,那么持久化提供者假定它是@Lob 类型与序列化的字节流到 Customer_table 表中的栏位. 看一下 XML 映射文件: 这个看起来非常准确的映射,除了映射一个指定的属性使用 元素,其它的都相同. 简而言之,都是关于基本的属性映射.在下一章中,我们将讨论怎样映射复杂的关 在多个实体组件之间. 7.1.七种关系类型 实体组件间存在七种关系.集中的说有四种:一对一、一对多、多对一、和多对 多.另外每种关系都有单向和双向之分.这样似乎会有八种可能.但是如果你他细 考虑,你将意识到一对多和多对一的双向关系实际上是等价的.因而只有七种关 系类型.想要明白关系,通过下面一些简单的例子: 一对一单向关系 在顾客与地址的关系中,你很明确的是可以获取一个顾客的地址,但是也许工不 关心一个地址的顾客. 一对一双向关系 在顾客与信用卡数字的关系中,给你一个顾客,你很明确的查找到他的信用卡的 卡号.给你一个信用卡的卡号,你很可能想到那个顾户持有此卡. 一对多单向关系 在顾客与电话号码的关系中.一个顾客可以有多个电话号码(单位、家庭、手机、 等等)你想查找一个顾客的电话号码.你可能没有使用这些号码中的一个而找到 这个顾客. 一对多双向关系 在巡游和预约之间的关系中.给定一个预约,你需要查找巡游的保留.(记录多对 一双向关系是别一种描述的相同概念) 多对一单向关系 在一次巡游和一条船的关系中.你想查找船将要使用巡游的详细细节,很多巡游 共享同一条船,虽然在不同次.通过查找船来看与巡游的关联不那么有用,如果你 想有这种功能,你可以实现多对一的双向关联. 多对多单向关联 在预定和小屋之间的关系.有可能预约多个小屋,很明确的是你想要关心那一个 小屋被预定.可是,你可能不会关心被预定的小屋的详细细节(如果你需要这样做, 可以实现双向的关联) 多对多双向关联 巡游和消费者之间的关系中.一个消费者可能预约在多次巡游中,并且每次巡游 有多个消费者.你有可能查找每次巡游都先预定的消费者和每次特定的巡游都去 的消费者. 注意这些关系描述了在适航性的域模型.使用 EJB QL,能够返回未被标记的关联 (例如,返回巡航所使用的船甚至连接到多对一的单向巡航所使用的船).再一次, 元数据描述的域对象导航相关的定义. 在本章中,我们将计论应用元数据的实体组件这间的特定关系.我们也将讨论一 些不同的通用数据库 Schema,并且将学习怎样映射他们到注释关系中. 7.1.1.一对一单向关系 一对一单向映射关系的例子是在Customer实体与Address实体之间.在这个例子 中.每一个消费者有一个联细的地址信息,并且每一个地址对应一个消费者.那一 个组件被参考那一个确定导航方向.当消费者参考地址时,地址不需要参考消费 者组件.关系是单向的仅仅从消费者到地址,没有其它方法通过对象导航.换句话 说,一个地址实体不知道是那一个消费者拥有它,下图显示其关系 7.1.1.1.关系数据库的 Schema 如Figure 7-2 所示,一对一单向关联关系通常用在一个表参照别一个包含外键 的数据库 Schema 中.在这种情况下,Customer 表包含一个外键到 Address 表,但 是ADDRESS 表不包含外到 Customer 表.这样允许 Address 表中的记录被其它表所 共享, 7.1.1.2.程序模型 在单向关联关系中(只能通过一种方式进行导航),实体组件所定义的一个属性 能够被关系中的其它组件进行获取和设置.因而,在Customer 组件类中你能够调 用getAddress( )/setAddress( )方法访问 Address 实体,但是没有方法在 Address 类中可以访问 Customer 实体.让我们看一下怎样标记 Customer 组件实 现一对一关联到 Address: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private Address address; ... @OneToOne(cascade={CascadeType.ALL}) @JoinColumn (name="ADDRESS_ID") public Address getAddress( ){ return homeAddress; } public void setAddress(Address address) { this.homeAddress = address; } 一对一关联中使用@javax.persistence.OneToOne 注释连同 @javax.persistence.JoinColumn 注释.让我们 看一下@JoinColumn 注释: public @interface JoinColumn { String name( ) default ""; String referencedColumnName( ) default ""; boolean unique( ) default false; boolean nullable( ) default true; boolean insertable( ) default true; boolean updatable( ) default true; String columnDefinition( ) default ""; String table( ) default ""; } name 声明外键列的名字,referencedColumnName 声明外键指向列的列名. @JoinColumn 注释非常的恰当如同@Column 注释.它所定义的栏在 Customer 表中 参照 Address 表的主键我们定义在 Figure 7­2.如果你连接的东西不同于 Address 表的主 键,你必需使用 referencedColumnName( )属性.referencedColumnName( )属性必 需是叭一的,因为是一对一的关系. 如果你映射的一对一关系的实体有复杂的主键,使用@JoinColumns注释来定义多个外键 栏: public @interface @JoinColumns { JoinColumn[] value( ); } 让我们学习@OneToOne 注释 public @interface OneToOne { Class targetEntity( ) default void.class; CascadeType[] cascade( ) default {}; FetchType fetch( ) default EAGER; boolean optional( ) default true; String mappedBy( ) default ""; } optional 声明关系是否是必须存在的,即是否允许其中一端为 null cascade 声明级联操作. targetEntity( )属性声明有关系的实体类.通常,你不需要初始化这个属性,持 久化提供者能够通过属性类型找出其设置.Fetch()属性的描述我们在第六章中 已经学过.它允许你指定装载方式是 lazily 或eagerly.在第八章中,将演示怎样 热切的取一个关系同 EJB QL,即使你已经标记 FetchType 的类型为 LAZY. optional( )属性指定关系是否可以为空,如果是 False,那么非空关系必需存在 于两个实体之间. Cascade()属性有一点复杂,我们将在本章后面讨论这种关系类型的属性. mappedBy( )属性用于双向关系我们会在下一部分中进行讨论. XML 配置文件如同注释一样有相同准确的属性,让我们看一下: ALL 7.1.1.3.主键连接栏 有时两个相关实体的主键被指定的栏所代替.在这种情况下,关系实体的主键是 相同的,并且不需要指定连接栏,如Figure 7-3 所示没有指定外键的栏映射的关 系,同样表的连接使用主键. 在这个映射过程中,你要使用一个可选的注释来描述这个映射 @javax.persistence.PrimaryKeyJoinColumn : public @interface PrimaryKeyJoinColumn { String name( ) default ""; String referencedColumnName( ) default ""; String columnDefinition( ) default ""; } (在三种情况下会用到 PrimaryKeyJoinColumn. • 继承(详见 Inheritance). • entity class 映射到一个或多个从表.从表根据主表的主键列(列名为 referencedColumnName 值的列),建立一个类型一样的主键列,列名由 name 属性 定义. • one2one 关系,关系维护端的主键作为外键指向关系被维护端的主键,不 再新建一个外键列(与JoinColumn 不同). 元数据属性说明: • name:列名. • referencedColumnName:该列引用列的列名 • columnDefinition: 定义建表时创建此列的 DDL) name( )属性查询实体的主键栏元数据被应用的话,除非你的实体有一个复杂的 主键,你可以将其空放着并且由持久化提供者来领会它. referencedColumnName( )属性确定实体关系的栏连接,如果它的左边是空的,它 将假设关系实体的主键将被使用. columnDefinition( )将会被使用当持久化提供者生成 Schema 时,并且它将指明 referencedColumnName( )的SQL 类型. 如果主键连接变成一个合成的自然问题,那么 @javax.persistence.PrimaryKeyJoinColumns,注释对你是有用的: public @interface PrimaryKeyJoinColumns { PrimaryKeyJoinColumn[] value( ); } 所以,让我们使用这个注释来映射 Customer/Address 实体一对一关系,如 Figure7-3 所示: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private Address homeAddress; ... @OneToOne(cascade={CascadeType.ALL}) @PrimaryKeyJoinColumn public Address getAddress( ){ return homeAddress; } public void setAddress(Address address) { this.homeAddress = address; } 因为我们连接的是Customer和Address实体的主键并且不是复合主键,我们能简 单的注释 Customer 组件的 Address 属性使用@PrimaryKeyJoinColumn 注释. 7.1.1.4.一对一单向 XML 映射文件 7.1.1.5.默认关系映射 如果持久化提供者支持自动生成 Schema,你不需要指明@JoinColumn 或 @PrimaryKeyJoinColumn 元数据.自动 Schema 的生成非常的强大在你快速成生原 型的过程中: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private Address address; ... @OneToOne public Address getAddress( ){ return homeAddress; } public void setAddress(Address address) { this.homeAddress = address; } 当你没有指定任何一个数据库映射为单向一对一关系,持久化提供者将自动生成 必要的外键映射为你.在Customer/Address 关系的例子中,下表将自动生成: CREATE TABLE CUSTOMER ( ID INT PRIMARY KEY NOT NULL, address_id INT, ... ) ALTER TABLE CUSTOMER ADD CONSTRAINT customerREFaddress FOREIGN KEY (address_id) REFERENCES ADDRESS (id); 对于单向一对一的关系,默认的映射建立一个外键栏名从你映射的属性的组和中 跟随在参照表的主键栏与字符相连接. 7.1.2.一对一双向关系 我们可以扩展 Customer 实体包含对 CreditCard 实体的引用.将维持信用卡的信 息.消费者将继续引用它的信用卡,并且信用卡将继续向回引用消费者的判断力. 因为信用卡能够意识到它的持用者. 每个信用卡有一个返回到消费者的引用同 时每一个消费者引用一个信用卡,有一个一对一的双向关系. 7.1.2.1.数据库 Schema 信用卡与 CREDIT_CARD 表相通,所以我们需要增加一个 CREDIT_CARD 外键到 Customer 表: CREATE TABLE CREDIT_CARD ( ID INT PRIMARY KEY NOT NULL, EXP_DATE DATE, NUMBER CHAR(20), NAME CHAR(40), ORGANIZATION CHAR(20), ) CREATE TABLE CUSTOMER ( ID INT PRIMARY KEY NOT NULL, LAST_NAME CHAR(20), FIRST_NAME CHAR(20), ADDRESS_ID INT, CREDIT_CARD_ID INT ) 一对一双向关联的数据库Schema模型与一对一单向关系的数据库Schema存在相 同的方法,在另一个表的外键参照其它.记住在数据库关系模型中,没有此定向性 的概念,所以同样的数据库 Schema 将使用单向的和双向的对象关系。Figure 7-4. 显示怎样的数据库 Schema 将实现 Customer 和Credit_card 表的行. 在Customer 和 CreditCard 的实体关系模型中,我们需要定义一个关系属性名 customer 在CreditCard 组件类中: @Entity public class CreditCard implements java.io.Serializable { private int id; private Date expiration; private String number; private String name; private String organization; private Customer customer; ... @OneToOne(mappedBy="creditCard") public Customer getCustomer( ){ return this.customer; } public void setCustomer(Customer customer) { this.customer = customer; } ... } mappedBy()属性是新的,这个属性设置双向关系并且通知持久化管理映射的详细 信息在 Customer 组件类中,明确 Customer 的CreditCard 属性. 我们需要添加一个关系属性到 Customer 组件类为 CreditCard 关系: @Entity public class Customer implements java.io.Serializable { private CreditCard creditCard; ... @OneToOne (cascade={CascadeType.ALL}) @JoinColumn(name="CREDIT_CARD_ID") public CreditCard getCreditCard( ){ return creditCard; public void setCreditCard(CreditCard card) { this.creditCard = card; } ... } 下面是设立双向关系的例子: Customer cust = new Customer( ); CreditCard card = new CreditCard( ); cust.setCreditCard(card); card.setCustomer(cust); entityManager.persist(cust); 我们设置 cascade()属性为 ALL.当我们讨论层叠操作时,你将看到这个属性设 置使 CreditCard 被创建通过层叠当 Customer 实体被持久化时. 双向关系中有很多特性.同所有的双向关系类型中,包括一对一,它常常是关系有 者端的概念.尽管 setCustomer( )方法在 CreditCard 组件类中是可用的,如果我 们设置它并不能引起改变在持久关系中.当我们标记@OneToOne 关系在 CreditCard 组件类中使用 mappedBy()属性,这样指定 CreditCard 实体成为关系 中的相反面.这样的意思是 Customer 实体是关系的拥有方.如果你想关联一个 CreditCard 实例同一个不同的 Customer,你可以调用旧的 Customer 的 setCreditCard( )方法,传递 null,并且调用 setCreditCard( )在新的 Customer 实例中: Customer newCust = em.find(Customer.class, newCustId); CreditCard card = oldCustomer.getCreditCard( ); oldCustomer.setCreditCard(null); newCust.setCreditCard(card); 注意:当修正关系的时候,总是要通知双向关系是的两端. 实体像其它的 Java 对像一样有一个关联到另一个对象.你需要设置关系两端的值在内存中为关系更 新. 如果消费者取消了他的信用卡,你可以设置消费者的 CreditCard 属性为空并且 移除信用卡实体从数据库中: Customer cust = em.find(Customer.class, id); em.remove(cust.getCreditCard( )); cust.setCreditCard(null); 因为实体组件是 POJOs,它如同应用程序的开发者管理关系,不是持久化的提供 者.当应用程序处理并分离对象时,这是至关重要的. 7.1.2.2.一对一双向 XML 映射文件 让我们看一下 Customer/CreditCard 实体一对一双向关系的 XML 配置文件: 7.1.2.3.默认关系映射 如同我们先前所看到的,如果持久化提供者持 Schema 的自动生成,你不需要指定 像@JoinColumn 元数据: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private CreditCard creditCard; ... @OneToOne public CreditCard getCreditCard( ){ return homeAddress; } ... } @Entity public class CreditCard implements java.io.Serializable { ... private Customer customer; ... @OneToOne(mappedBy="creditCard") public Customer getCustomer( ){ return this.customer; } ... } 当你没有指定任何数据映射到双向一对一关系,持久化提供者将自动为你生成必 需的外键映射.在Customer/CreditCard 关系的例子中,下表将自动生成: CREATE TABLE CUSTOMER ( ID INT PRIMARY KEY NOT NULL, creditCard_id INT, ... ) ALTER TABLE CUSTOMER ADD CONSTRAINT customerREFcreditcard FOREIGN KEY (creditCard_id) REFERENCES CREDITCARD (id); 对于双向性一对一的关系,默认映射建立一个外键栏从你映射的属性组合中,跟 在后面的是一个-‘_’字符,用来连接参照表的主键栏名. 7.1.3.一对多单向关联 实体组件能够维持多数关系,这就意味着一个实体组件能够集合或包含多个其它 的组件.例如,一个消费者可能对应多个电话号码.这就不同于简单的一对一关联, 就那样来说,从多样的一对一关系用相同的组件类型.一对多和多对多关系要求 开发者使用集合代替单个参考当访问关系字段时. 7.1.2.1.关系数据库的 Schema 为了演示一对多单向关系,我们将使用一个新实体组件,Phone,我们需要定义一 个表: CREATE TABLE PHONE ( ID INT PRIMARY KEY NOT NULL, NUMBER CHAR(20), TYPE INT, CUSTOMER_ID INT ) 在Customer 和Phone 表之间的一对多单向关系实现有多种方式,如例,我们选择 Phone 表包含一个外键到 Customer 表,事实上,一个单向一对多关系常常映射同 一个连接表. 集合数据表能够维持一个无唯一外键的集合表的栏.在这种情况下的消费者和电 话实体中,PHONE 表维持一个外键到 CUSTOMER 表,并且一个或多个 PHONE 记录可 能包含外键到相同的 CUSTOMER 记录,换句话说,在数据库中,PHONE 记录指向 CUSTOMER 记录.在程序模型中,它指向多于两个被颠倒的电话的客户实体.它是 怎样工作的呢?容器系统隐藏反转的指针,以便消费者意识到它的 Phone,并且 没有其它的方法,当你要求容器返回一个 Phone 的集合时(调用 getPhoneNumbers()方法返回一个集合),它查询 PHONE 表的所有记录同一个外键 匹配的 Customer 实体的主键.这类型的关系的反面指针的使用在图 7-5 被举 例. 数据库的 Schema 演示了结构和实际数据库的关系可以不同于程序模型定义的关 系.在这种情况下表的设置被反转,但是持久化管理器将管理组件参照组件开发 的规范.当你处理旧数据库时(例如,数据库在 EJB 应用程序建立前已经创建), 指针反转非常普通如同图示,所以支持这种类型的关系映射是非常重要的. 7.1.3.2.程序模型 你可以定义一对多关系使用@javax.persistence.OneToMany 注释: public @interface OneToMany { Class targetEntity( ) default void.class; CascadeType[] cascade( ) default {}; FetchType fetch( ) default LAZY; String mappedBy( ) default ""; } 属性的定义如同@OneToOne 的定义. 在程序模型中,我们描述实体组件关系的多样性通过定义关系属性指向多个实体 和使用@OneToMany注释. 为了支持这种类型的数据, 我们将会使用来自 java.util 软件包的一些数据结构,Collection, List, Map, 和 Set. 集合维护同一种类型实体对象的引用,这意味着它包含多种引用到一种实体组件. Collection类型能够包含重复引用到相同实体组件,Set类型则不能. 例如,一个消费者可能同多个电话号码之间存在关系(一个家庭电话,一个工作电 话,传真,等等),每一个代表一个电话实体.代替了每一个电话的不同关系字 段,Customer实体保持所有的Phones在集合关系中: @Entity public class Customer implements java.io.Serializable { ... private Collection phoneNumbers = new ArrayList( ); ... @OneToMany(cascade={CascadeType.ALL}) @JoinColumn (name="CUSTOMER_ID") public Collection getPhoneNumbers( ){ return phoneNumbers; } public void setPhoneNumbers(Collection phones) { this.phoneNumbers = phones; } } @JoinColumn注释引用CUSTOMER_ID栏在PHONE表中,我们使用一般Java类到定义 的Phones集合.使用一个普通的不仅是好的程序实践因为它给了我们一个具体的 类型到你的集合,但是,它也允许持久化管理器计算出准确的关联Customer实体 将要映射的.如果你不使用一般的集合,你将指定@OneToMany.targetEntity( ) 属性.同样,因为这是一个单向关系,@OneToMany注释的mappedBy( )属性不需要 被设置.mappedBy( )属性仅用在双向关系中. Phones组件类将在下一个列表中被显示.注意Phone没有为Customer提供关系属 性.它是一个单向的关系;Customer实体维持一个关系到多个Phones,但是Phones 不维持关系返回到Customer.只有Customer知道关系. package com.titan.domain; import javax.persistence.*; @Entity public class Phone implements java.io.Serializable { private int id ; private String number; private int type; // required default constructor public Phone( ){} public Phone(String number, int type) { this.number = number; this.type = type; } @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } public String getNumber( ){ return number; } public void setNumber(String number) { this.number = number; } public int getType( ){ return type; } public void setType(int type) { this.type = type; } } 举例说明一个关体组件使用集合类为基础的关系,让我们看一些与 EntityManager交互的一些代码: Customer cust = entityManager.find(Customer.class, pk); Phone phone = new Phone("617-333-3333", 5); cust.getPhones( ).add(phone); 因为Customer实体是关系的持有者,一个新的Phone将自动在数据库中创建,因为持久 化管理器将把它的主键看作是0,生成一个新的ID(GeneratorType 是 AUTO),并 且插入到数据库中. 如果你想移除一个Phone从这个关系中,你需要移除Phone从集合和数据库中: cust.getPhones( ).remove(phone); entityManager.remove(phone); 移除Phone从消费者集合中,不用移除Phone从数据库中.你必需明确的移除Phone, 以别的方式,它将被孤立 . 7.1.3.3.一对多单向映射XML文件 7.1.3.4.连接表映射 另一个 Customer/Phone 实体关系的关系数据库映射是一个关联表维持两个外键 栏指向 Customer 和Phone 记录.可以放置一个约束在 Phone 表的外键栏上在关联 表上到确定它约束的叭一实体(例如,每一个电话仅有一个消费者).当允许 CUSTOMER 外键栏包含副本时,这个关系表的最大好处是它不强加关系在 Customer 和Phone 表的记录之间.这是当使用单向关系的时候,最普遍的映射. create table CUSTOMER_PHONE ( CUSTOMER_ID int not null, PHONE_ID int not null unique ); 使用这种类型的映射,我们需要改变@JoinColumn 而使用 @javax.persistence.JoinTable 注释: public @interface JoinTable { String name( ) default ""; String catalog( ) default ""; String schema( ) default ""; JoinColumn[] joinColumns( ) default {}; JoinColumn[] inverseJoinColumns( ) default {}; UniqueConstraint[] uniqueConstraints( ) default {}; } (JoinTable 在many-to-many 关系的所有者一边定义.如果没有定义 JoinTable, 使用 JoinTable 的默认值. 元数据属性说明: • table:这个 join table 的Table 定义. • joinColumns:定义指向所有者主表的外键列,数据类型是JoinColumn数 组. • inverseJoinColumns:定义指向非所有者主表的外键列,数据类型是 JoinColumn 数组.) @JoinTable 注释非常像@Table 注释,除了增加的 joinColumns( ) 和 inverseJoinColumns( )属性. joinColumns( )属性定义一个外键映射到关系拥 有者的主键上. inverseJoinColumns( )属性映射非拥用端.如果关系的两端有 一个复合主键,我们仅增加更多的@JoinColumn 注释即可: @Entity public class Customer implements java.io.Serializable { ... private Collection phoneNumbers; ... @OneToMany(cascade={CascadeType.ALL}) @JoinTable(name="CUSTOMER_PHONE"), joinColumns={@JoinColumn(name="CUSTOMER_ID")}, inverseJoinColumns={@JoinColumn(name="PHONE_ID")}) public Collection getPhoneNumbers( ){ return phoneNumbers; } public void setPhoneNumbers(Collection phones) { this.phoneNumbers = phones; } } 由这个定义,在CUSTOMER_PHONE 表中 Customer 的主键映射到 CUSTOMER_ID 连接 栏.Phone 实体的主键映射到 PHONE_ID 连接栏在 CUSTOMER_PHONE 表中.因为消费 者和电话是一对多的关系,一个唯一约束将被放置在 Customer_PHONE 表的 PHONE_ID 栏上被持久化提供者如果它支持并且激活 DDL 生成.依照关系的定义, 一个消费者可以有多个电话,但是一个电话只属于一个消费者,这个唯一约束强 加在其上面. 7.1.3.5 一对多单向连接表的 XML 映射文件 7.1.3.6.默认的关系映射 如果持久化提供者支持自动生成 Schema,你不需要指定@JoinColumn 元数据.自 动生成 Schema 是非常,重要的当你做快速原型时: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private Collection phoneNumbers = new ArrayList( ); ... @OneToMany public Collection getPhoneNumbers( ){ return phoneNumbers; } ... } 当你没有指定任何数据库映射为一对多单向关系,持久化提供者将使用默认映射 在连接表上,前面已讨论过,在Customer/Phone 关系例子中,下面的连接表将被 生成: CREATE TABLE CUSTOMER_PHONE ( CUSTOMER_id INT, PHONE_id INT ); ALTER TABLE CUSTOMER_PHONE ADD CONSTRAINT customer_phone_unique UNIQUE (PHONE_id); ALTER TABLE CUSTOMER_PHONE ADD CONSTRAINT customerREFphone FOREIGN KEY (CUSTOMER_id) REFERENCES CUSTOMER (id); ALTER TABLE CUSTOMER_PHONE ADD CONSTRAINT customerREFphone2 FOREIGN KEY (PHONE_id) REFERENCES PHONE (id); 外键表的名字的建立连接有实体表的表名使用’_’作为分隔,外键栏连接每一 个实体表名后跟一个‘-’接着是实体主键名.唯一限制被放在关系多的一方, 外键约束应用在栏的两端. 7.1.4. Cruise, Ship, 和 Reservation 实体 到现在为止,我想你可能对所有这些电话号码,信用卡和地址实体感到很无聊了. 为了使它更加有趣,我们要介绍更多一些的实体组件,以便于我们能很好的介绍 剩下的四种关系:多对一单向;一对多双向;多对多单向;和多对多双单向. 在泰坦的预定系统中,每一个消费者能够预定一次或多次巡航. 每个预约需要预 定.一次预定可能是为一个或多个 (通常二) 乘客.每次巡航需要一艘船,但是每 艘船在一整年中可以巡航多次. 图 7-6 举例说明这些关系 7.1.5.多对一单向关系 多对一单向关系的结果是当多个实体组件参考一个实体组件时,被参考的实体组 件并不知道它与其它组件之间的关系. 在泰坦巡航的业务中,例如,巡航的概念能被 一个巡航实质组件取得.如同图7-6所示,巡行与船的关系是多对一.这种关系是 单向的:巡航实体维持和船实体的关系,但是船这个实体并不保存它被用作巡航 的信息. 7.1.5.1.关系数据库的 schema 巡航与船实体关系的数据库 Schema 是非常简单的; 它需要 CRUISE 表为 SHIP 表 提供一个外键栏,同时 CRUISE 表中的每一行指向 SHIP 表中的一行,CRUISE 和 SHIP 表的定义见下面的代码片断;如图 7-7 显示在数据库中的它们两个之间的关 系. 描述一艘客轮的相关信息需要大量的数据,我们在这里使用一个 SHIP 表的简单 定义: CREATE TABLE SHIP ( ID INT PRIMARY KEY NOT NULL, NAME CHAR(30), TONNAGE DECIMAL (8,2) ) CRUISE 表维持巡行的名子,船,和其它信息不是讨论很密切的信息.(其它表,如 同RESERVATIONS, SCHEDULES , 和 CREW,通过关联表将会与 CRUISE 表存在关 系.), 在本书中我们将会使它保持简单,并且定义成一个有用的例子 CREATE TABLE CRUISE ( ID INT PRIMARY KEY NOT NULL, NAME CHAR(30), SHIP_ID INT ) 7.1.5.2.程序模型 多对一关系通过@javax.persistence.ManyToOne annotation 注释来描述: public @interface ManyToOne { Class targetEntity( ) default void.class; CascadeType[] cascade( ) default {}; FetchType fetch( ) default EAGER; boolean optional( ) default true; } 属性定义非常类同于@OneToOne 注释.程序模型对我们来说是很简单.我们要增 加一个 Ship 属性到 Cruise 实体组件类并使用@ManyToOne 注释: @Entity public class Cruise implements java.io.Serializable { private int id; private String name; private Ship ship; // required default constructor public Cruise( ){} public Cruise(String name, Ship ship) { this.name = name; this.ship = ship; } @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } public String getName( ){ return name; } public void setName(String name) { this.name = name; } @ManyToOne @JoinColumn (name="SHIP_ID") public Ship getShip( ){ return ship; } public void setShip(Ship ship) { this.ship = ship; } } 即使我们有一个使用名子和船作为参数的构造,Java 持久化规范仍要求一个默 认的一个无参的构造函数.@JoinColumn 注释指定 Cruise 实体表增加一个 SHIP_ID 的栏,它是一个外键到 Ship 实体表.如果你使用持久化提供者提供的 Schema 自动生成工具,你不需要指定@JoinColumn@JoinColumn 注释,提供者对它 提供默认值. 在Cruise 和Ship 实体之间的关系是单向的,所以 Ship 组件类没有定义任何关系 到Cruise,下为持久化属性: @Entity public class Ship implements java.io.Serializable { private int id; private String name; private double tonnage; // required default constructor public Ship( ){} public Ship(String name,double tonnage) { this.name = name; this.tonnage = tonnage; } @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } public String getName( ){ return name; } public void setName(String name) { this.name = name; } public double getTonnage( ){ return tonnage ;} public void setTonnage(double tonnage) { this.tonnage = tonnage ; } } 现在这些对于你来说应该是很平常的了. 交换Ship的引用在Cruise实体间,有同 等的明显的影响.如同前面的Figure 7­7,所示,每次巡行可能仅需要一艘船,但是每艘船可 能引用多个Cruise实体.如果你坐在船A上,巡行1,2和3使用船A,并且通过它到巡行4,巡行1到 巡行4都将使用船工,如图7­8所示. Figure 7­8. Sharing a bean reference in a many­to­one unidirectional relationship 7.1.5.3.多对一单向映射 XML 文件 7.1.5.4.默认的关系映射 如果持久化提供者支持自动 Schema 生成,你不需要指定@JoinColumn 元数据. 应用程序能够很快的创建: @Entity public class Cruise implements java.io.Serializable { ... @ManyToOne public Ship getShip( ){ return ship; } 默认的数据库映射到多对一关系类似于一对一单向映射.当你没有指定任何数据 库映射时,持久化提供者将为你生成必要的外键映射.在Cruise/Ship 关系的例 子中,下表将被自动生成: CREATE TABLE CRUISE ( ID INT PRIMARY KEY NOT NULL, ship_id INT, ... ) ALTER TABLE CRUISE ADD CONSTRAINT cruiseREFship FOREIGN KEY (ship_id) REFERENCES SHIP (id); 对于多对一单向关系,默认映射创建一个外键栏,它的名子是由映射的属性的组 合跟一个-字符,来连接引用表的主键栏. 7.1.6.一对多双向映射关系 一对多和多对一双向关系听起来好像是不同,但是不是这样的.一个一对多双向 关系发生在一个实体组件维持一个以集合为基础关系的特性同另一个实体,并且 每个集合中的实体单个引用回它所聚集的组件.例如,在泰坦的巡行系统当中,每 个Cruise 实体维持所有乘客预定的一个集合为每次巡航.而且每次预定维持它 关于巡航的引用.关系是一对多双向关系,从巡行的角度看,关系是多对一双向关 系从预定的角度看. 7.1.6.1 关系数据库 Schema 我们需要的第一张表是 RESERVATION,它的定义在下面列出. 注意:RESERVATION 表包含其它的信息,一个栏作为参照 CRUISE 表的外键. CREATE TABLE RESERVATION ( ID INT PRIMARY KEY NOT NULL, AMOUNT_PAID DECIMAL (8,2), DATE_RESERVED DATE, CRUISE_ID INT ) 当RESERVATION 表包含一个到 CRUISE 表的外键时,CRUISE 表不把一个外键维持 回到 RESERVATION 表.持久化管理器通过查询 RESERVATION 表能够确定 Cruise 和Reservation 实体之间的关系.如此明确的指出从 CRUISE 表到 RESERVATION 表是没有必要的.这个例子说明实体的持久化关系的检验和数据库中的实际关系 是分离的. RESERVATION 和 CRUISE 表之间的关系如图 7-9 所示. Figure 7-9. One-to-many/many-to-one bidirectional relationship in RDBMS 7.1.6.2 程序模型 为了做Cruises 和 Reservation实体之间的关系,我们先定义Reservation实体, 它维持一个关系字段到 Cruise: @Entity public class Reservation implements java.io.Serializable { private int id; private float amountPaid; private Date date; private Cruise cruise; public Reservation( ){} public Reservation(Cruise cruise) { this.cruise = cruise; } @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } @Column(name="AMOUNT_PAID") public float getAmountPaid( ){ return amountPaid; } public void setAmountPaid(float amount) { amountPaid = amount; } @Column(name="DATE_RESERVED") public Date getDate( ){ return date; } public void setDate(Date date) { this.date = date; } @ManyToOne @JoinColumn(name="CRUISE_ID") public Cruise getCruise( ){ return cruise; } public void setCruise(Cruise cruise) { this.cruise = cruise ;} } 我们需要增加一个以集合为基础关系的属性到 Cruise 组件类中以便它能引用所 有建立的 Reservation 实体: @Entity public class Cruise implements java.io.Serializable { ... private Collection reservations = new ArrayList( ); ... @OneToMany(mappedBy="cruise") public Collection getReservations( ){ return reservations; } public void setReservations(Collection res) { this.reservations = res; } } Cruise 和 Reservation 实体之间的相互依赖产生一些有趣的结果.如同一对一 双向关系,关系的两侧必需有一端是拥有者.现在的 Java 持久化要求多对一关系 端有这种关系.它就是 Reservation 实体.什么意思呢?好的,它需要你去调用 Reservation.setCruise( )每当你增加或移除一个巡行的预定.如果你不调用 Reservation.setCruise(),关系在数据库中将不会改变.这可能似乎非常困惑, 但是如果你服从关系两端的主要配置,那么你将会没有问题了. 总是在你的 Java 代码中看到双向关系的两端都存在配置 在我们的泰坦巡航系统中,Reservation 实体从不改变当关系被建立后.如果一 个消费者想要预定不同的巡航时,它需要删除旧的预定和建立一个新的.所以,代 替设置 Reservation.setCruise( )方法为 null. 应用程序码会仅仅移除 Reservation: entityManager.remove(reservation); 因为 Reservation 实体是关系的拥有端,巡航的预定属性更新同移除在下次一同 被载入从数据库中. 7.1.6.3.一对多双向映射 XML 配置文件 entity-mappings> 7.1.6.4.默认映射关系 像其它的映射类型,如果持久化提供者支持自动生 Schema,你就不需要指定 @JoinColumn 元数据; @Entity public class Reservation implements java.io.Serializable { ... @ManyToOne public Cruise getCruise( ){ return cruise; } 默认的数据库映射为一对多双各关纱类似于多对一单向关系.当你没有指定任何 的数据库映射时,持久化提供者将生成必要的外键映为你.在 Reservation/Cruise 关系的例子中,下表会被产生: CREATE TABLE RESERVATION ( ID INT PRIMARY KEY NOT NULL, cruise_id INT, ... ) ALTER TABLE RESERVATION ADD CONSTRAINT reservationREFcruise FOREIGN KEY (cruise_id) REFERENCES CRUISE (id); 因为是双向一对多映射关系,默认映射建立一个外键栏,取名是由你映射的属性 的组合跟着一个_字符连接被参照表的主键栏位名. 7.1.7.多对多双向映射关系 多对多双向映射关系发生在,当多个实体用另一个实体来保持一个以集合为基础 的关系属性的时候.并且每个组件的引用在集合中,把一个以集合为基础关系的 组件维持加到原来的集合组件.举例来说,在泰的巡行中,每个预定实体可能被多 个客户引用(一系列可以使用单个预定),而且每个消费者能有多个预定(一个人 可预有多于一个的预定).在这个多对多双向映射关系中,消费者掌握它的合部预 定信息,并且每个预定可以为多个消费者. 7.1.7.1.关系数据库的 Schema RESERVATION 和 CUSTOMER 表已经建立.要建立多对多双向关系,我们需要建 RESERVATION_CUSTOMER 表.这个表来维护两个外键栏:一个是 RESERVATION 表的 别一个是 CUSTOMER 表的: CREATE TABLE RESERVATION_CUSTOMER ( RESERVATION_ID INT, CUSTOMER_ID INT ) 表CUSTOMER, RESERVATION, 和 RESERVATION_CUSTOMER 之间的关系如下图: Figure 7-10. Many-to-many bidirectional relationship in RDBMS 多对多双向关系总是要求一个连接表在规范的关系数据库中. 7.1.7.2.程序模型 多对多关系的定义使用@javax.persistence.ManyToMany 注释: public @interface ManyToMany { Class targetEntity( ) default void.class; CascadeType[] cascade( ) default {}; FetchType fetch( ) default LAZY; String mappedBy( ) default ""; } 为了模拟多对多双向关系在Customer和Reservation实体间,我们需要包含集为 基础的关系属性在它们里: @Entity public class Reservation implements java.io.Serializable { ... private Set customers = new HashSet( ); ... @ManyToMany @JoinTable (name="RESERVATION_CUSTOMER"), joinColumns={@JoinColumn(name="RESERVATION_ID")}, inverseJoinColumns={@JoinColumn(name="CUSTOMER_ID")}) public Set getCustomers( ){ return customers; } public void setCustomers(Set customers); ... } Customers 关系定义成 java.util.Set. Set 类型包含唯一的消费者并且无重复. 重复的 Customers 会引起一些有趣的但是不是需要的副作用在泰坦的预定系统 中.为了维持一个有效的乘客数,和避勉消费者超载,泰坦要求一个客户在相同的 预定公能一次.集合类型能够表示出这种限制.集合类型的作用主要依赖于建立 的底层数据库的约束. 所有的双向关系,都必需有一个拥用端.在这个例子中,是Reservation 实体.困 为Reservation 是关系拥有端,它的组件类中定义了@JoinTable 映射. joinColumns( )属性定义外键在 RESERVATION_CUSTOMER 表,用来参照 RESERVATION 表. inverseJoinColumns( )属性标识 RESERVATION_CUSTOMER 表中 的外键参照 CUSTOMER 表. 如同@OneToMany 关系,如果你使用持久化提供者的 Schema 自动生成工具,你不 需要指定一个@JoinTable 注释.Java,持久化规范为@ManyToMany 映射关系提供 了默认的映射并且为你建立连接表. 我们已经修改消费者让他维持一个以集合为基础关系的预定. Customer 组件类 现在包含一个 reservations 关系属性: @Entity public class Customer implements java.io.Serializable { ... private Collection reservations = new ArrayList( ); ... @ManyToMany(mappedBy="customers") public Collection getReservations( ){ return reservations; } public void setReservations(Collection reservations) { this.reservations = reservations; } ... 关于一对多双向关系,mappedBy( )属性标识定义关系的 Reservation 组件类的 属性.这也确认 Customer 实体是关系的相反面. 当改变和影响关系属性时,相同的所有权规则的应用如同我们看到的一对多双向 关系的例子.Customer/Reservation 实体关系被建立之后,泰坦巡行应用程序可 能要修改它在建立之后.消费者可能要把 relative 或 nanny 加入预定,或者移 除一个不太舒服而无法来巡行的一个朋友: Reservation reservation = em.find(Reservation.class, id); reservation.getCustomers( ).remove(customer); 因为预定是关系的拥有端,你可以把一个消费者从预定客户属性中移除.如果你 改为移除预定的消费者.不会有数据库的更新为消费者实体关系的非持有端. 7.1.7.3.多对多双向映射 XML 文件 7.1.7.4.默认关系映射 如同其它关系类型,双向多双多关系支持 Schema 的自动生成使用最少的元数据: public class Reservation implements java.io.Serializable { ... private Set customers = new HashSet( ); ... @ManyToMany public Set getCustomers( ){ return customers; } public void setCustomers(Set customers); ... } 当你没有指定任何数据库映射为双向多对多关系,持久化提供者将建立一个连接 表为你.在Reservation/Customer 实体关系的例子中,下面的连接表将被生成: CREATE TABLE RESERVATION_CUSTOMER ( RESERVATION_id INT, CUSTOMER_id INT, ); ALTER TABLE RESERVATION_CUSTOMER ADD CONSTRAINT reservationREFcustomer FOREIGN KEY (RESERVATION_id) REFERENCES RESERVATION (id); ALTER TABLE RESERVATION_CUSTOMER ADD CONSTRAINT reservationREFcustomer2 FOREIGN KEY (CUSTOMER_id) REFERENCES CUSTOMER (id); 建立的连接表的名子是由拥用实体关系的表使用_字符连接关系表的表名.外键 栏是实体表的名跟一个_加上实体主键栏名的每个实体的连接组成.外键约束应 用在两个栏位上. 7.1.8.多对多单向关系 多对多单向关系发生成当多个组件维持一个以集合为基础系,同其它组件,但是 组件引用在 Collection 不保持一个以集合基础关系返回到聚合组件.在泰坦预 定系统中,每个预定分配一个船舱在船在.这样允许消费者预定指定的船舱(例如, 一个豪华客遍及和一个船舱)在船上.在这种情况下,每个预定可能超过一个船舱, 因为每个预定可能是给超过一个消费者的.例如,一个家庭中有五个人预定两个 相邻的小屋(一个为孩子一个为父母),当Reservation 实体保持它预定的 Cabins 信息的时候,它不需要追踪巡行中所有预定的小屋.预定参考小屋组件的集合,但 是小屋组件不保持引用到预定. 7.1.8.1.关系数据库 Schema 我们首先在定单中定义一个 Cabin 表: CREATE TABLE CABIN ( ID INT PRIMARY KEY NOT NULL, SHIP_ID INT, NAME CHAR(10), DECK_LEVEL INT, BED_COUNT INT ) CABIN表有一个外键到SHIP表的引用. 这关系很重要,我们不讨论它因为我们在前面 学过一对多双向关系.为了调节RESERATION表和CABIN表之间的关系,我们需要一个 CABIN_RESERVATION 表: CREATE TABLE CABIN_RESERVATION ( RESERVATION_ID INT, CABIN_ID INT ) 小屋记录和预定之间的关系记录过 CABIN_RESERVATION 表在图 7-11 被举例. Figure 7­11. Many­to­many unidirectional relationship in RDBMS 这个多对多单向关系类似于前面讨论过的 Customer/Phone 连接表映射的一对多 单向关系,最大的不同在于 CABIN_RESERVATION 表不没有唯一性约束:多个小屋 对应多个预定,反之亦然. 7.1.8.2. 程序模型 为了模拟这个关系,我们需要增加一个 Cabin 集合类型的字段到 Reservation 组 件中: @Entity public class Reservation implements java.io.Serializable { ... @ManyToMany @JoinTable(name="CABIN_RESERVATION", joinColumns={@JoinColumn(name="RESERVATION_ID")}, inverseJoinColumns={@JoinColumn(name="CABIN_ID")}) public Set getCabins( ){ return cabins; } public void setCabins(Set cabins) { this.cabins = cabins; } ... } 另外我们需要定义一个Cabin组件.注意:Cabin组件不所关系维护到Reseration. 预定栏们的缺少告诉我们关系是单向的: @Entity public class Cabin implements java.io.Serializable { private int id; private String name; private int bedCount; private int deckLevel; private Ship ship; @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } public String getName( ){ return name; } public void setName(String name) { this.name = name; } @Column(name="BED_COUNT") public int getBedCount( ){ return bedCount; } public void setBedCount(int count) { this.bedCount = count; } @Column(name="DECK_LEVEL") public int getDeckLevel( ){ return deckLevel; } public void setDeckLevel(int level) { this.deckLevel = level; } @ManyToOne @JoinColumn(name="SHIP_ID") public Ship getShip( ){ return ship; } public void setShip(Ship ship) { this.ship = ship; } } 虽然 Cabin 组件没有定义一个关系字段为 Reseration,它定义了一个一对多双向 关系为 SHIP.交换关系字段在多对多关系中的效果等同于多对多双向关系. 7.1.8.3.多对多单向映射 XML 文件 7.1.8.4.默认关系映射 如果持久化提供者支持自动 Schema 生成,你不需要指定@JoinTable 元数据: @Entity public class Reservation implements java.io.Serializable { ... @ManyToMany public Set getCabins( ){ return cabins; } public void setCabins(Set cabins) { this.cabins = cabins; } ... } 当你没有指定任何数据库映射为单各多对多关系,持久化提供者将建立一个连接 表为你.像Reservation/Cabin 例子,下面的连接表将被生成: CREATE TABLE RESERVATION_CABIN ( RESERVATION_id INT, CABIN_id INT, ); ALTER TABLE RESERVATION_CABIN ADD CONSTRAINT reservationREFcabin FOREIGN KEY (RESERVATION_id) REFERENCES RESERVATION (id); ALTER TABLE RESERVATION_CABIN ADD CONSTRAINT reservationREFcabin2 FOREIGN KEY (CABIN_id) REFERENCES CABIN (id); 连接表的名子是由实体表连接另一个关系实体中间使用_作为分隔.外键栏的连 接每个实体表名,使用_连接实体的主键栏名.外键约束被应用到两个栏位上. 7.2. 映射以集合为基础的关系 到目前为止,我们学过的一对多和多对多的例子中使用了 java.util.Collection and java.util.Set 类型,Java 持久化规范也允许使用 java.util.List or a java.util.Map 类描述关系. 7.2.1.订购 List 为基础的关系 java.util.List 接口可以表达以集合为基础的关系.如果你想要使用一个 List 并非是一个 Set 或Collection 类型,你不需要任何特别的元数据.(在这种情况 下,List 实际上给你一个与语义相关的袋子,允许重复集合).一个 List 类型可 以给你另外的能力到返回一个特定标准的集合关系.这就要求另外的元数据,由 @javax.persistence.OrderBy,注释提供: package javax.persistence; @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface OrderBy { String value( ) default ""; } Value()属性允许你定义部分 EJB QL来指定你想要排序的关系当它从数据库中 加载出来时.如果 Value()属性为空,List 在以主键值为基础,进行长幂排排序. 让我们看一下预定/客户关系,它们是多对多双向关系,并且预定的消费者属性返 回一个 List,是按消费者姓的字母顺序排序进行分类的. @Entity public class Reservation implements java.io.Serializable { ... private List customers = new ArrayList( ); ... @ManyToMany @OrderBy ("lastName ASC") @JoinTable(name="RESERVATION_CUSTOMER"), joinColumns={@JoinColumn(name="RESERVATION_ID")}, inverseJoinColumns={@JoinColumn(name="CUSTOMER_ID")}) public List getCustomers( ){ return customers; } public void setCustomers(Set customers); ... } "lastName ASC"告诉持久化提供者按照消费者的姓进行升序排列,你也可以使用 ASC 进行升序,DESC 进行降序排列.你也可以指定另外的约束像 @OrderBy('lastname asc, firstname asc").在这个例子中,列表将使用 lastname 进行排序,并且重姓的,将使用名子进行排序. 7.2.1.1.列了 XML 映射 lastName ASC ... 7.2.2.以映像为基础的关系 java.util.Map 接口能够表示以集合为基础的关系,在这种情况下,持久化提供 者建立以一个映射作为相关实体的一个属性和它的值作为实体本身的键.如果你 使用 java.util.Map,你必需使用@javax.persistence.MapKey,注释: package javax.persistence; @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface MapKey { String name( ) default ""; } name( )属性是持久化属性的名字,你想要映射对像的键字段.如果它的值为空, 它将假定你使用关系实体的主键作为映像键. 例如,在本章前面已经讨论过的 Customer/Phone 之间的一对多单向关系所描述 的: @Entity public class Customer implements java.io.Serializable { ... private Map phoneNumbers = new HashMap( ); ... @OneToMany(cascade={CascadeType.ALL}) @JoinColumn(name="CUSTOMER_ID") @MapKey(name="number") public Map getPhoneNumbers( ){ return phoneNumbers; } public void setPhoneNumbers(Map phones) { this.phoneNumbers = phones; } } 在这个例子中,消费者的电话属性将返回一个 java.util.Map,当Phone 实体的键 是数值属性并且值也是,当然,是Phone 实体自身.没有额外的栏来保存映像键, 因为映像键被 Phone 实体占用. 7.2.2.1 映像的 XML 映射 ... 7.3.实体的分离和提取 在第五章中,我们讨论了怎样管理实体实例,因为,当持久化上下文结束时,实体 将会与上下文分离.因为这些实体的实例不再被持久化上下文所管理,她们有没 有被初始化的属性或关系.如果你在用户端和服务器间传送这些分离的实体给你 的客户和主要使用它的数据传送对象.你需要完全了解任何没有被初始化关系的 效果. 当实体的实例被分离出来时,它的状态没有被完全初始化,因为一些持久化属性 或关系可能被标记成 lazy 装载在映射元数据中.每个关系元注释有一个 fetch() 属性,用来指定当实体被查询时是否装载.如果 fetch()属性被设置成 FetchType.LAZY,关系没有被初始化,直到代码被调用: Customer customer = entityManager.find(Customer.class, id); customer.getPhoneNumbers( ).size( ); 调用 phoneNumbers 集合的 size()方法引起关系从数据库中的装载. 注意:重要的是,除非实体是被持久化上下文所管理,否则 lazy 的初始化不会发 生.如果实体被分离,规范并不清楚持久化提供者将要执行什么样的动作当访问 一个没有加载的关系中的一个分离的实体.当你调用访问关系时,大多数的持久 化提供者会抛出 lazy 实例异常,或者当你试图调用一个关系中的分离实体: Cruise detachedCruise = ...; try { int numReservations = detachedCruise.getReservations( ).size( ); } catch (SomeVendorLazyInitializationException ex) { } 在这个代码中,应用程序接收到一个分离实 Cruise 实体的实例并且尝试访问 @OneToMany 预定关系.因为关系属性的 fetch()是LAZY,大多数的厂商将会抛出 厂商指定的异常.lazy 初始化问题有两种方法能够克服,明显的方式是导航到需 要的关系,在实体的实例仍然被持久化上下文所管理时.第二种方法是当你查询 实体的时候,使用 eagerly 装载.在第九章中,你将会看 EJB QL查询语言有一个 FETCH JOIN 操作,当你调用一个查询的时候,允许你预先初始化选择的关系. 持久化提供者如何抛出异常在访问关系时,当Cruise类是一个简单的Java类时? 虽然没有在规范中定义,厂商有一些方法实现它.一是通过字节码处理 Cruise 类. 在J2EE 中,应用程序服务器要提供钩子给持久化提供者处理字节码.在J2SE 中, 持久优提供者要另外的编辑步骤在代码的基础上.另一种方法是让厂商来实现, 建立一个代理类继承 Cruise,并且重新实现所有的访问方法和增加 lazy 初始化 检查.因为以集合为基础的关系,持久化提供者能提供集合的自实现并且做 lazy 的检查.任何的实现,注意发现持久化提供者将会在分离的 lazy 初始化中设定, 所以你的代码要采取适当的措施来处理这异常. 7.4 层叠 有一个到目前为止我们忽略的一个注释属性:@OneToOne,@OneToMany, @ManyToOne , 和 @ManyToMany 关系的 cascade()属性注释.这章中我们将详细 讨论 cascade() 属性的应用. 当你执行一个实体管理操作在一个实体的实例上,你可以自动在实体上的任何关 系属性上执行相同的操作.这就需要调用层叠操作,例如,你要持久化一个新的 Customer 实体同一个新的 Address 和Phone 号码,你需要做的是连接对象,实体 管理能够自动的创建 customer 和它的相关实体,所有的这些在 persist( )方法 被调用后: Customer cust = new Customer( ); customer.setAddress(new Address( )); customer.getPhoneNumbers( ).add(new Phone( )); // create them all in one entity manager invocation entityManager.persist(cust); 就JAVA 持久化规范,层叠可以被应用到多种实体操作,包括 persist(),merge(),remove(),和refresh(),这个功能是由 javax.persistence.CascadeType 关系注释的 cascade()属性来设定, CascadeTypep定义为 JAVA 的一个枚举类型: public enum CascadeType { ALL, PERSIST, MERGE, REMOVE, REFRESH } ALL 值代表所有的层叠操作.剩下的代表个别的层叠操作.cascade()属性是一个 层叠操作的排列应用在你的关系实体上.让我们再看一下前面计论过的 Customer和Address实体间的一对一单向关系.如你所看到的,它有CascadeType 枚举类型的 REMOVE 和PERSIST: package com.titan.domain; @Entity public class Customer implements java.io.Serializable { ... private Address homeAddress; ... @OneToOne(cascade={CascadeType.PERSIST, CascadeType.REMOVE}) @JoinColumn(name="ADDRESS_ID") public Address getAddress( ){ return homeAddress; } public void setAddress(Address address) { this.homeAddress = address; } 只要你持久化 Customer 和Address 实体相关的 Customer 也是新的,address 也 被持久化了,如果你 remove()一个 Customer,与之相联系的 Address 实体也将被 从数据库中移除.让我们看一下本例的 XML 映射: 元素定义成元素的子元 素用来指定层叠策略.你也可以使用. 被使有时,是你想要应用所有的串联级别到关系时.我们将在这个 部分解释层叠的详细信息使用 Customer/Address 关系的例子. 7.4.1.持久化 PERSIST 处理数据库中的实体的创建.如果你使用 CascadeType 的 PERSIST 在一 对一关系的 Customer 上,恐怕你将不能很好的持久化你建立的 Address.它将为 你创建.持久化提供者也将执行适当的 SQL INSERT 语句在适当的情况下: Customer cust = new Customer( ); Address address = new Address( ); cust.setAddress(address); entityManager.persist(cust); 如果没有 Cascade 策略的 PERSIST,那么你需要调用 EntityManager.persist( ) 在address 对象上. 7.4.2.合并 MERGE 处理实体同步,意味着插入和更重要的更新.这些不是传统的的更新.如果 你还记着前面的章节,我们提及对象分离从持久化管理器中和序列化到远程客户, 在本地的更新到远程用户,对象实例将被发送并返回到服务器,并且改变将被合 并到数据库中.合并把分离的对象实例同步状态到持久化存储中去. 因此,MERGE 的意思是同 PERSIST 类似.如果存在 MERGE 层叠策略,那么你不需要 调用 EntityManager.merge( )为包含的实体: cust.setName("William"); cust.getAddress( ).setCity("Boston"); entityManager.merge(cust); 在这个例子中,当cust 变量被实体管理器合并时,实体管理将层叠我含苞欲放的 address 和city 合并,同时更新数据库. 另外有趣的关于 MERGE 是,如果你增加了新实体到数据库建立的关系,当调用 merge()时,他们将会被持我化和建立: Phone phone = new Phone( ); phone.setNumber("617-666-6666"); cust.getPhoneNumbers( ).add(phone); entityManager.merge(cust); 在这个例子中,我们分配一个 Phone 和增加一个 Customer 的Phone 号码列表.我 们调用merge()同customer,并且,因为我们让MERGE CascadeType设置这个关系. 持久化提供者将把一个 Phone 实体在数据库中建立. 记住:只有被合并操作管理的状态返回图表,不是传递一个参数. 7.4.3.移除 REMOVE 是简单的,在我们的 Customer 例子中,如果你删除一个 Customer 实体,它 的address 将同时被删除.这个功能与 EJB2.1 完全相同. Customer cust = entityManager.find(Customer.class, 1); entityManager.remove(cust); 7.4.4.更新 REFRESH 类似于 MERGE.不同于合并,尽管,这公适用于当 EntityManager.refresh( )调用时隔不久.Refreshing 不更新数据库同改变对 象的实例.相反的,它刷新的是从数据库中读出对象的状态.再一次,被包含的实 体也将被更新. Customer cust ...; entityManager.refresh(cust); // address would be refreshed too 所以,如果你改变消费者的地址信息被不同事务所提交,cust 变量的 address 属 性连同改变一同更新.在实际中非常有用当一个实体有多个属性通过数据库生成 (被引发,例子).你可以更新实体来读取生成的属性.在这种情况下,确定那些生 成的属性是只读的从持久化提供者的视图,例如,使用@Column (insertable=false, updatable=false). 7.4.5.所有 ALL 包含前面的所有策略和用于简单的目的. 7.4.6.什么时候使用重叠 你不总是想要使用重叠为每一个关系.例如,你不想在移除 Cruise 或Customer 实体关系时移除 Reservation 实体从数据库中因为这些实体的生命周期常常是 高于Reservation的生命周基期.你可能不想层叠merges因为你有可能从数据库 中取出的数据是过期的或者是没有被填充的一个特别的业务操作关系.因为执行 的原因,你不想更新所有的关系实体,因为这将产生较多的循环旅行到数据库中. 在你决定层叠策略之前,你要知道你的实体将会如何使用.如果你不确定它的使 用,你应该关闭所有的层叠策略.记住,层叠是一个方便而简单的工具用于减少 EntityManager API 的调用. 第八章 实体继承 为了完成一个对象到关系映射的能力,必需支持实体继承.Java 持久化规范支持 实体继承,多种关联关系,和多种查询.这些特性在 EJB CMP 2.1 规范中是没有 的. 在本章中,我们将修改 Customer 实体,那么我们定义一个前几章中做过的来适应 实体继承.我们将扩展基类 Person 和定义 Employee 类来扩展成 Customer 类. 在泰坦巡航的时候,职员将会有特殊的优惠待遇. 图 8-1 所示的继承图表. Figure 8-1. Customer class hierarchy ` Java 持久化规范提供三种不同的方式来映射实体继承到关系型数据库: 一个表对应类的继承层次(数据库中只存在一个最终子类对应的表) 一个表拥有类的所有属性在继承中. Person Customer Employee 一个表对应具体的类(有几个实体就生成几张表,而且以继承字段递增的方式存 在于数据库中) 一个类与一个表对应,同时所有的属性和它的子类的属性映射到这张表 每个表都是最终实体的子类(实体间存在继承,在数据库中中存在实体差集) 每个类有它自已的表.每个表将有它定义的唯一的属性.这些表没有任何子类或 超类的属性. 在这章中,我们使用这三种策略来映射 Customer 类. 8.1. 一个表对应类的继承层次 在这个映射策略中,一个数据库中的表代表一个给定类的层次.在我们的例子 中,Person,Customer,和Employee 实体代表同样的表,如下所示代码: create table PERSON_HIERARCHY ( id integer primary key not null, firstName varchar(255), lastName varchar(255), street varchar(255), city varchar(255), state varchar(255), zip varchar(255), employeeId integer, DISCRIMINATOR varchar(31) not null ); 如你所看到的,Customer 类的所有属性层次被保存在 PERSON_HIERARCHY 表.单表 类层次映射要求一个额外的鉴别栏.这栏用于识别实体类型用于存储 PERSON_HIERARCHY 表中的一行的细节.让我们看一下类中如何使用注释来映射 这种继承策略: @Entity @Table(name="PERSON_HIERARCHY") @Inheritance(strategy=InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="DISCRIMINATOR", discriminatorType=DiscriminatorType.STRING) @DiscriminatorValue("PERSON") public class Person { private int id; private String firstName; private String lastName; @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } public String getFirstName( ){ return firstName; } public void setFirstName(String first) { this.firstName = first; } public String getLastName( ){ return lastName; } public void setLastName(String last) { this.lastName = last; } } @javax.persistence.Inheritance 注释用来定义继承关系的持久化策略: package javax.persistence; @Target(TYPE) @Retention(RUNTIME) public @interface Inheritance { InheritanceType strategy( ) default SINGLE_TABLE; } public enum InheritanceType { SINGLE_TABLE, JOINED, TABLE_PER_CLASS } Strategy()属性定义我们要使用的继承映射.因为我们要使用单表类层次, SINGLE_TABLE 被应用.@Inheritance 注释仅能被放在类层次的根上,除非你改 变子类的映射策略: package javax.persistence; @Target(TYPE) @Retention(RUNTIME) public @interface DiscriminatorColumn String name( ) default "DTYPE"; DiscriminatorType discriminatorType( ) default STRING; String columnDefinition( ) default ""; int length( ) default 10; } 因为一个表代表一个完整的类层次,持久化提供者需要一些方法来标识那个类在 数据库中分类映射.由它来确定读取鉴别栏的值. @javax.persistence.DiscriminatorColumn 注释标识表中的那一栏来存储辨别 栏的值.name()属性标识栏位名.它可以是 STRING,CHAR,或者是 INTEGER.对于我 们的 Customer 类层次的映射,你没有指定 discriminatorType( ),它将是默认的 STRING.如果你对默认的栏名感到很好,你完全可以移除@DiscriminatorColumn. package javax.persistence; @Target(TYPE) @Retention(RUNTIME) public @interface DiscriminatorValue { String value( ) } @javax.persistence.DiscriminatorValue 注释定义鉴别栏的值,将可存储的 Person 类的实例的行.你也可以不加这个属性如果你不想的话.那样的话,持久 化管理将自动生成值.这个持将由提供商指定,DiscriminatorType 是CHAR 或者 是INTEGER.当类型指定为 STRING 时实体的名子使用默认.指定值为 CHAR 和 INTEGER 是很好的习惯. 其它的类层次是很简单的.仅需要指定继承元数据,你可指定鉴别值,如果你不想 使用默认值: @Entity @DiscriminatorValue ("CUST") public class Customer extends Person { private String street; private String city; private String state; private String zip; public String getStreet( ){ return street; } public void setStreet(String street) { this.street = street; } ... } // use the default discriminator value @Entity public class Employee extends Customer { private int employeeId; public int getEmployeeId( ){ return employeeId; } public void setEmployeeId(int id) { employeeId = id; } } 所以,在这个例子中,Customer 实体设置鉴别栏的值为 CUST,使用 @DiscriminatorValue 注释.对于 Employee 实体,鉴别栏值默认值为 Employee, 这是实体组件的实体名. 现在你对映射类型有了一个很好的了解,让我们看一下映射的 XML 文件: PERSON CUST 8.1.1.优点 SINGLE_TABLE 映射策略是最简单的实现和执行好于所有策略的继承策略.只有 一个表管理和下理.当载入实体时,持久化引挚没有做任何复杂的连接,合并或 (子选择),否则当使用多种关系时,所有的数据存储在一张表中. 8.1.2.缺点 这种方式的最大缺点是所有子类属性的对应的栏必需允许为空.所以,如果你需 要或想有任何非空值约束在栏目位上,是不可以的.同时,因为子类属性栏没有被 使用,SINGLE_TABLE 策略不是规范化的. 8.2. 一个表对应具体的类 在这种策略中,在层叠关系中每个数据库中表对应一个具体的类.每个栏对应一 个属性,和所有超类的属性. create table Person ( id integer primary key not null, firstName varchar(255), lastName varchar(255), ); create table Customer ( id integer primary key not null, firstName varchar(255), lastName varchar(255), street varchar(255), city varchar(255), state varchar(255), zip varchar(255), ); create table Employee ( id integer primary key not null, firstName varchar(255), lastName varchar(255), street varchar(255), city varchar(255), state varchar(255), zip varchar(255), employeeId integer, ); 和SINGLE_TABLE 策略最大的不同是不再需要鉴定栏在数据库的 Schema 中.也要 注意每个表包含全部的级联持久化属性.让我们看一下怎能样使用注释映射这种 策略: @Entity @Inheritance(strategy=InheritanceType .TABLE_PER_CLASS) public class Person { ... } @Entity public class Customer extends Person { ... } @Entity public class Employee extends Customer { ... } 注意:需要继承元数据 InheritanceType,并且需要唯一的 Person 基类. 让我们看一下这种策略类型的 XML 映射文件: 同其它注释类似,如果你依于赖默认属性映射,元数据描述的内容非常少. 元素的定义连同 TABLE_PER_CLASS 在类的根上. 8.2.1.优点 对于 SINGLE_TABLE 策略,这种方式允许你定义约束条件在子类的属性上. 另一个附加的优点是很容易映射遗留下来的类,先前存在的 Schema 也使用这种 策略. 8.2.2.缺点 这种策略不是规范化的,同样的,每个表存在多余的栏位对于基类的属性来说.同 时,为了支持这种类型的映射,持久化管理需要做一些谨慎(不确定)的事情. 一种方式是容器实现通过多种查询,当载一个个实体或多种关系时.因为容器需 要伤脑筋多个循环查询到数据库,所以这是一个极大的性能上的攻击.另一种方 式是容器实现这个策略使用 SQL UNION.这样仍不像 SINGLE_TABLE 策略那样快, 但是它会比一个多选择的实现好的多.对一个 SQL UNION 的缺点是并不是所有的 关系数据库都支持这样的 SQL 特性.当你开发实体组件时,选取这样的一个策略 或许是不明智的,除蜚你你必需使用它. 8.3. 每个表都是最终实体的子类 在这种实体继承方法中,每一个子类有自已的表,这个表只包含在那个特殊类上 被定义的属性.换句话来说, 它与 TABLE_PER_CLASS 策略类似,除了 Schema 是 规范化的.这也被称作 JOINED 策略. create table Person ( id integer primary key not null, firstName varchar(255), lastName varchar(255), ); create table Customer ( id integer primary key not null, street varchar(255), city varchar(255), state varchar(255), zip varchar(255), ); create table Employee ( EMP_PK integer primary key not null, employeeId integer ); 当持久化管理装载一个实体那是一个子类或经过多种关系,分类在所有表上做一 个SQL 的连接.在这种映射中,必需有一个栏在每一个表中,供连接每个表时使用. 在我们的例子中, EMPLOYEE, CUSTOMER, 和 PERSON 表共享主键值.注释非常的 简单: @Entity @Inheritance(strategy=InheritanceType.JOINED) public class Person { ... } @Entity public class Customer extends Person { ... } @Entity @PrimaryKeyJoinColumn (name="EMP_PK") public class Employee extends Customer { ... } 当一个使用 JOINED 继承策略的实体被载和时,持久化管理需要知道每个标中的 那一个栏用于进行连接.@javax.persistence.PrimaryKeyJoinColumn 注释可 以被使用来描述这个元数据. package javax.persistence; @Target({TYPE, METHOD, FIELD}) public @interface PrimaryKeyJoinColumn String name( ) default ""; String referencedColumnName( ) default ""; String columnDefinition( ) default ""; } Name()属性查找在当前表中包含将要执行连接的栏.它的的默认值是超类表的主 键栏. referencedColumnName( )是将要被使用执行连接的超类表中的栏. 它可 以是超类表中的任意栏,但是它的默认值是主键.如果主键栏的名子在基类和子 类中是相同的,那么就不再需要此注释.举例来说,Customer 实体不再需要 @PrimaryKeyJoinColumn 注释.Employee 类有不同于超类的主键栏,所以 @PrimaryKeyJoinColumn 注释是必需的.如果类的层次使用复合键,有一个 @javax.persistence.PrimaryKeyJoinColumns 注释能够描述多连接栏: package javax.persistence; @Target({TYPE, METHOD, FIELD}) public @interface PrimaryKeyJoinColumns { @PrimaryKeyJoinColumns[] value( ); } 让我们看一下这种策略的 XML 映射文件 : 一些持久化提供者要求一个鉴别栏为这种映射类型.大多数不需要。要确定你的持久 化提供者实现是否是必需的. 元素用来定义 Person 类别是 JOINED 继承策略的一部分.那么,对 于Employee,我们需要定义它的主键名不同于 Person 的主键栏名. 8.3.1.优点 比较这个映射和其他的映射策略来描述它的优点.虽然它不像 SINGLE_TABLE 策 略那样快,你可以定义 NOT NULL 约束在表中的任意栏上,并且模型正常化. 这种策略有两点好处优于 TABLE_PER_CLASS 策略.一是,关系数据库的模型规范 化.二是,如果 SQL UNION 不支持,它的执行要好于 TABLE_PER_CLASS 策略. 8.3.2.缺点 它的执行不如 SINGLE_TABLE 策略. 8.4.混合策略 现在的持久化规范中混合策略是可选的.这个策徊在未来版本的规东中将会被定 义. 8.5.非实体基类 继承映射我们描述到现在,在本章中考虑的实体组件类的层次.然而你需要从一 个非实体的超类继承.这个超类可能是已存在的类在你的域模型中,你又不想使 它成为一个实体.@javax.persistence.MappedSuperclass 注释允许你定义这 种类型的映射.让我们编辑一下例子类的层次并且改变 Person 到一个映射的超 类: @MappedSuperclass public class Person { @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = is; } public String getFirstName( ){ return firstName; } public void setFirstName(String first) { this.firstName = first; } public String getLastName( ){ return lastName; } public void setLastName(String last) { this.lastName = last; } } @Entity @Table(name="CUSTOMER") @Inheritance(strategy=InheritanceType.JOINED) @AttributeOverride(name="lastname", column=@Column(name="SURNAME")) public class Customer extends Person { ... } @Entity @Table(name="EMPLOYEE") @PrimaryKeyJoinColumn(name="EMP_PK") public class Employee extends Customer { ... } 因为它不是一个实体,映射的超类没有相关联的表.任何子类都可以继承基类的 持久化属性.你可以覆盖映射类的任何属性使用 @javax.persistence.AttributeOverride 注释(看第六章).这里是被修改了层 次的数据库 Schema: create table CUSTOMER ( id integer primary key not null, firstName varchar(255), SURNAME varchar(255), street varchar(255), city varchar(255), state varchar(255), zip varchar(255), ); create table EMPLOYEE ( EMP_PK integer primary key not null, employeeId integer ); 如你所看到的,Customer 实体继承了 id,firstName,和lastName 属性.因为 @AttributeOverride 注释指定了栏 lastName 名为姓.这种映射策略对于你想要 将基类变成实体是非常有用的并且你又不想强制它自身变成一个实体. 你可能有@MappedSuperclass 注释在两个存在@Entity 注释类的特定层次之间. 同样的,无注释类(例如,即不使用@Entity 也不使用 @MappedSuperclass 注释), 可以完全被持久化提供者忽略. 让我们看一下映射的 XML 配置文件: 元素是元素的直接子类,并且定义所有 非实体超类. 元素覆盖任意默认映射栏,定义在超类中 的. 第九章 查询和 EJB QL 查询是一个所有表示关系型数据库的基本功能.它允许你从持久化存储中取出复 杂的报表,计算,和杂乱的关系对象信息.查询在 Java 持久化规范中是用 EJB QL 查询语言和原生结构化查询语言(SQL). EJB QL是一个公共的查询主言类似于关系数据库中所使用的 SQL,但是它同 JAVA 对像一同工作胜于关系 Schema.为了执行查询,你参考你的实体属性和关系并非 在你映射的表和栏对象上. 当一个 EJQ QL 查询被运行的时候,实体管理信息使 用你提供的映射的元数据,在前两章中已讨论过, 而且自动地将它翻译成一 (或 一些)个内定的 SQL 查询.生成原生的SQL通过你的数据库上的JDBC驱动被直接 执行.因为 EJB QL是一个查询语言描述 JAVA 对象,所以使用便推携式的实现,实 体管理要为你处理 SQL 的转换. EJB QL语言是很容易让开发者学习并且能够精确的解释到本地数据库码的.当 执行快速的运行本地码时,这个富有弹性的查询语言被授予开发者.此外,因为 EJB QL是面向对象的,查询通常比同等的 SQL 更简洁和易读.EJB QL存在于 EJB2.1 规范中,并且它是在新版本中留下来的唯一功能.虽然,它已经形成,在 EJB 2.1 中它是不完善的.强制开发者脱离 JDBC 或写一些效率低下的代码.EJB QL已经有了非常大的改进而且扩大了对 SQL 的支持,现在可以符合你的大部分需 要.像GROUP BY,和HAVING 已经被添加,连同大量的更新和删除. 有时,EJB QL并不能满足我们的要求成过急.因为它是一个轻量级的查询语言, 它不能够拥有特定的数据库厂商提供的专有功能.EJB QL不支持存储过程,举例 来说.EJB 3.0 专家组预见了这种需要并且提供 API 映射到原生 SQL 调用你的实 体组件. EJB QL和原生 SQL 查询的执行通过 javax.persistence.Query 接口.这个查询接 口非常类似于java.sql.PreparedStatement接口.这个查询API为结果集页提代 方法,连同你传递的 JAVA 参数到你的查询中.查询通过注释或 XML 能够被解释, 或者动态的创建通过 EntityManager API 在运行期间. 9.1 查询 API 一个查询是 JAVA 持久化的一个开放的接口,你可以在运行期间通过实体管理器 来获取它: package javax.persistence; public interface Query { public List getResultList( ); public Object getSingleResult( ); public int executeUpdate( ); public Query setMaxResults(int maxResult); public Query setFirstResult(int startPosition); public Query setHint(String hintName, Object value); public Query setParameter(String name, Object value); public Query setParameter(String name, Date value, TemporalType temporalType); public Query setParameter(String name, Calendar value, TemporalType temporalType); public Query setParameter(int position, Object value); public Query setParameter(int position, Date value, TemporalType temporalType); public Query setParameter(int position, Calendar value, TemporalType temporalType); public Query setFlushMode(FlushModeType flushMode); } 查询的建立使用 EntityManger 中的方法: package javax.persistence; public interface EntityManager { public Query createQuery(String ejbqlString); public Query createNamedQuery(String name); public Query createNativeQuery(String sqlString); public Query createNativeQuery(String sqlString, Class resultClass); public Query createNativeQuery(String sqlString, String resultSetMapping); } 让我们看一下使用 EntityManager.createQuery( )在运行期间动态建立查询: try { Query query = entityManager.creatQuery( "from Customer c where c.firstName='Bill' and c.lastName='Burke'"); Customer cust = (Customer)query.getSingleResult( ); } catch (EntityNotFoundException notFound) { } catch (NonUniqueResultException nonUnique) { } 前面的查找返回一个结果,是唯一的 Customer 实体名字叫 Bill Burke.当 getSingleResult()个方法被调用时执行查询.这个方法认为调用时只返回一个 结果.如果没有结果返回,这个方法将会抛出 javax.persistence.EntityNotFoundException 运行异常.如果找到多于一个结 果, javax.persistence.NonUniqueResultException 异常将会在运行期间抛 出. 因为这两个异常都是 RuntimeException,例子中的代码不需要完整的 try/catch 块.有一种可能使这个例子抛出 NonUniqueResultException 异常.信不信由你, 在世界上有很多个 Bill Burkes(试着去搜寻他), 而且这一个名字似乎在美国 是像约翰史密斯一样,你可以改变查询使用 getresultList( )方法来获取一个 集合类型的结果集. Query query = entityManager.creatQuery( "from Customer c where c.firstName='Bill' and c.lastName='Burke'"); java.util.List bills = query.getResultList(); getresultList( )方法不会抛出异常不管是否存在 Bill Burkes.返回的列表可 以为空. 9.1.1.参数 像java.sql.PreparedStatement 在JDBC 中,EJB QL允许你指定参数在查徇定义 中,所以你可以重用和执行多次在询在叁数的不同组.提供了两种语法:命名参数 和位置参数.让我们修改前面的 Customer 查询问最后的姓和名改成命名参数: public List findByName(String first, String last) { Query query = entityManager.createQuery( "from Customer c where c.firstName=:first and c.lastName=:last"); query.setParameter("first", first); query.setParameter("last", last); return query.getResultList( ); } 使用:字符跟随的参数名是使用了 EJB QL声明的命名参数.在这个例子中 setParameter( )方法首先获取参数 first,然后取得真实值.EJB QL也支持位置 参数.让我们修改一下前面的例子: public List findByName(String first, String last) { Query query = entityManager.createQuery( "from Customer c where c.firstName=?1 and c.lastName=?2"); query.setParameter(1, first); query.setParameter(2, last); return query.getResultList( ); } 而非字符串类命名参数, setParameter( )也接受数值型的位置参数.?字符用来 代替使用名子参数的:字符. 使用名字参数相对于位置参数更易理解,被推荐.EJB QL码变成自我标识的 文档.当使用公共查询合作的时候,这成为重要. 9.1.2.日期参数 如果你想传递 java.util.Date or java.util.Calendar 类型作为参数到查询中, 你需要使用指定的 setParameter 方法: package javax.persistence; public enum TemporalType { DATE,//java.sql.Date TIME,//java.sql.Time TIMESTAMP //java.sql.Timestamp } public interface Query { Query setParameter(String name, java.util.Date value, TemporalType temporalType); Query setParameter(String name, Calendar value, TemporalType temporalType); Query setParameter(int position, Date value, TemporalType temporalType); Query setParameter(int position, Calendar value, TemporalType temporalType); } 一个Date或Calendar对象能够代表一个实际日期,一天中的一个时间,或者是数 值类型的日期.因为这些对像能够同时代表不同的事性,你需要告诉 Query 对像, 使用什么样的参数. javax.persistence.TemporalType 作为一个参数传递到 setParameter( )方法,告诉 Query 接口,转换 java.util.Date 或 java.util.Calendar 类型的参数到数据库中的类型,在原生查询的 SQL 类型中. 9.1.3.页面结果集 有时,在执行查询时返回非常多的结果集.举例来说,也许我们在一个网页上显示 客户的列表.网页仅能显示那么多客户,并且在数据库中可能有成千上万的客户. Query API有两个内置的函数来解决这种类型的情节: setMaxResults( ) 和 setFirstResult( ): public List getCustomers(int max, int index) { Query query = entityManager.createQuery("from Customer c"); return query.setMaxResults(max). setFirstResult(index). getResultList( ); } getCustomers( )方法执行一个但询来获取所有的客户集从数据库中.我们使用 setMaxResults( )方法限制返回的客户集散地,传递 max 参数到方法中.这个方 法也被设计,以便你能定义你想要返回的结果集在执行查询后. setFirstResult()方法告诉查询,在执行查询时返回结果集的位置.因此,如果你 设置返回的最大结果集为 3,并且第一个结果是 5,客户 5,6和7将被返回. 我们 在 getCustomers()和索引参数中设定这个值.让我们使用这个方法并写一段代 码来列出数据库中的的有客户: List results; int first = 0; int max = 10; do { results = getCustomers(max, first); Iterator it = results.iterator( ); while (it.hasNext( )){ Customer c = (Customer)it.next( ); System.out.println(c.getFirstName() + "" + c.getLastName( )); } entityManager.clear( ); first = first + results.getSize( ); } while (results.size( ) > 0); 在这个例子中,我们通过循环遍历数据库中的客户并且输出姓和名字段到系统输 出流中.如果数据库中有成千上万的客户,那么我们很快的用光存储器,如果 getCustomers( )方法的每次运行,返回的客户仍被实体管理器所管理.因此,在 我们输出完客户的一段范围之后,调用 EntityManager.clear( )方法来分离这 些客户.并且被 JVM 中的垃圾回收器所收集.在这种模式下当你需要处理多个实 体对像在相同的事务中. 9.1.4. 暗示 一些 JAVA 持久化厂商将会提供额外的扩展功能,当你执行一个查询时.举例来 说,JBOSS EJB3.0 实现允许你定义一个超时在查询定义中.这些扩展功能可以被 指定成提示.使用 setHint()方法在查询中.这里定义了一个使用暗示的 Jboss 查 询超时的一个例子. Query query = manager.createQuery("from Customer c"); query.setHint("org.hibernate.timeout", 1000); setHint( )方法使用一个字符串名和一个特定的对象参数. 9.1.5. FlushMode 在第五章中,我们讨论过刷新和刷新模式.有时,你想要在查询期间强迫使用一个 不同的模式.举例来说, 也许一个查询想要确定在查询被运行之前,实质体管理 没有刷新.(因为实体管理能够实现默认值).查询接口为了这个特别的目的提供 了setFlushMode( )方法: Query query = manager.createQuery("from Customer c"); query.setFlushMode(FlushModeType.COMMIT); 在这个例子中,我们告诉持久化提供者,在我们执行详细的查询之间,我们不想查 询做任何自动的刷新.如果有一些相互关联的肮实体在持久化上下文中,使用这 种模式,是很危险的.你要能返回一个错误的实体从查询中. 因此, 它被推荐你 使用 FlushModeType.AUTO. 9.2 EJB QL 现在你对查询对像是如何工作的有了一个个基本的了解,你可以学一些有用的新 特性,对你建立自己的EJB QL查义.EJB QL使用一组描像的持久化实体的Schema 来表示:抽像 schema 名,基本属性能超群,和关系属性.EJB QL使用抽像 Schema 名来标识组件, 基本的特性叙述数值, 而且关系特性在整个关系航行中. 为了讨论 EJB QL,我们将使用在第七章中定义的 Customer, Address, CreditCard, Cruise, Ship, Reservation, 和 Cabin 之间的实体关. 图 9-1 是这些组件中的方向和关系的一个类图. Figure 9-1 Titan Cruises class diagram 9.2.1. 抽像 Schema 名 抽像 Schema 名可以通过元数据来定义或者是使用默认值.它的默认值是无限制 的实体组件类,如果 name()属性没有指定当定义@Entity 注释时. 在下面的例子中,@Entity.name( )属性没有指定在 Customer 组件类型,因 此,Customer 被用到参考 EJB QL调用的实体. package com.titan.domain; @Entity public class Customer {...} entityManager.createQuery("SELECT c FROM Customer AS c"); 在下面的例子中, 因为@Entity.name( )被定义,你将引用 Customer 实体在 EJB QL如Cust: package com.titan.domain; @Entity(name="Cust") public class Customer {...} entityManager.createQuery("SELECT c FROM Cust AS c"); 9.2.2.简单的查询 简单的 EJB QL语句没有 WHERE 子句和仅有一个抽像 Schema 类型.例如,你可以定 义一个查询方法到查询所有的客户组件: SELECT OBJECT( c ) FROM Customer AS c FROM 子句确定那个实体类型将要被包含在 SELECT 语中(例如,它提供查询的范 围).在这种性况下,FROM子句定义的类型是Customer,它是Customer实体的抽像 Schema 名.允许一个识别符关联一个表.识别符可以是任意长度而且符合 JAVA 语 言程序规则的字段名.然而,识别入会 不能与已引子在的抽像 Schema 名子的值 相同.除此之外,标识符变量的名子是大小写无关的,因此一个客户的识别符会与 另一个客户的抽像 Schema 名产生冲突. 因为客户是客户 EJB 的抽像 schema 名 字,所以举例来说,下面的语句是违法的: SELECT OBJECT ( customer ) FROM Customer AS customer AS操作是可选的,但是在本书中它用来使 EJB QL语句更加清晰.下面的两个语句 是等价的: SELECT OBJECT(c) FROM Customer AS c SELECT c FROM Customer c SELECT 子句确定返回值的任意类型.在这个例子中,语句返回的是 Customer 实体, 如 c 识别符所指出的. OBJECT()操作是可选的并且它对于向后的EJB2.1规范是一个遗留. 识别符不可能是 EJB QL 保留字. 在 Java 语言持久化中, 下列的字组被保留: SELECT, FROM, WHERE, UPDATE, DELETE, JOIN, OUTER, INNER, GROUP,BY, HAVING, FETCH, DISTINCT, OBJECT, NULL, TRUE, FALSE, NOT, AND,OR, BETWEEN, LIKE,IN, AS, UNKNOWN, EMPTY, MEMBER,OF,IS, AVG, MAX, MIN, SUM COUNT, ORDER ASC, DESC, MOD, UPPER, LOWER, TRIM, POSITION, CHARACTER_LENGTH, CHAR_LENGTH, BIT_LENGTH, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP , 和 NEW. 因为你也不知道那一些将会被未来版本的EJB QL所使用,所以,避免使用所有的 SQL保留字是一个好的习惯.简而言之,你可以在SQL in a Nutshell (O'Reilly). 这本书的附录中找到较多的信息. 9.2.3.选择实体和关系属性 EJB QL 允许SELECT子句返回一些基本数值或关系属性.举例来说,我们可以定义 一个简单的SELECT语句来返回泰坦巡行中所有客户的名和姓: SELECT c.firstName, c.lastName FROM Customer AS c 当返回输入的时候,SELECT子句使用一个简单的路径来选择客户实体的姓和名属 性.持久化属性名的识别是通过访问的实体组件类的类型来标识,不管你是否使 用注释在get或set方法上,或者是在类的成员字段上. 如果你使用get或set方法指定你的持久化属性,那么属性名是从方法名中提取出 来的.方法名的get部分被移除,和剩下的字符串是小写字母: @Entity public class Customer { private ind id; private String first; private String last; @Id public int getId( ){ return id; } public String getFirstName(){ return first; } public String getLastName(){ return first; } 在这个例子中,我们使用get和set方法来定义持久化属性.SELECT子句看起来像 这样: SELECT c.firstName, c.lastName FROM Customer AS c 如果你直接映射实体在组件类的成员字段上,那么字段名在SELECT子句中被使 用: @Entity public class Customer { @Id private int id; private String first; private String last; } 在这个例子中,EJB QL语句需要重写改变了的属性名first和last: SELECT c.first, c.last FROM Customer AS c 当一个查询返回多于一条结果时,你需要使用 Query.getResultList( )方法.如 果SELECT 子句查询多于一个栏或实体,结果被 geTResultList( )方法聚集 (Object[])到java.util.List 类型的对象集合中.下面的代码显示如何存取被 返回的结果 : Query query = manager.createQuery( "SELECT c.firstName, c.lastName FROM Customer AS c"); List results = query.getResultList( ); Iterator it = results.iterator( ); while (it.hasNext( )){ Object[] result = (Object[])it.next( ); String first = (String)result[0]; String last = (String)result[1]; } 你可以使用单值关系的字段类型在简单的 select 语句中.下面 EJB QL语句查询 所有信用卡从关联的客户实体中: SELECT c.creditCard FROM Customer AS c 在这种情况,路径使用客户的抽像 schema 名,客户地址关系字段,和地址的城市 字段. 为了举例说明较多的复杂路径,我们将会需要扩展类图. 图 9-2 出示,信用卡被 关联到 CreditCompany ,它自己的位址. Figure 9-2.Expanded class diagram for CreditCard 使用这样的关系,我们们能指定一个更复杂的路径导航从用户到信用卡公司到地 址.这是一个 EJB QL语句,来查询泰坦上的所有客户的信用卡公司的地址. SELECT c.creditCard.creditCompany.address FROM Customer AS c EJB QL语句也可以导航所有方法到 Address 组件的字段.下面的 select 语句查 询在泰坦巡行中的客户所使用的信用卡公司的地址所在的城市. SELECT c.creditCard.creditCompany.address.city FROM Customer AS c 注意:这些 EJB QL语句仅返回在泰坦巡航上客户的信用卡公司的地址关系属性 或城市属性.那些不是泰坦巡航上的客户的信用卡公司的地址信息不被包含在结 果集中. 路径不能导航到远离持久化属性上.举例来说,假定地址使用了ZipCode类m同时 它的 zip 属性和这个属性是以字节流的形式存在于数据库中: //ZipCode 为非实体 public class ZipCode implements java.io.Serializable { public int mainCode; public int codeSuffix; ... } Customer omer CreditCar d CreditCompany Address omer @Entity public class Address { private ZipCode zip; } 你不能到一个 ZipCode 类的实体字段: // 这是不合法的 SELECT c.address.zip.mainCode FROM Customer AS c 当然,你可以在 ZipCode 类型的属性前加上@Embeddable 注释.如果你这样做,你 就可以获取 ZipCode 类的属性: public class ZipCode implements java.io.Serializable { public int mainCode; public int codeSuffix; ... } @Entity public class Address { @Embedded private ZipCode zip; } 下面的 EJB QL将是合法的; //@Embedded makes this legal now SELECT c.address.zip.mainCode FROM Customer AS c 在以集合关系为基础的关系导航是不合法的.下面的 EJB QL语句是不合法的,即 使路径结束在一个单一类型的关系字段上: // 这是不合法的 SELECT c.reservations.cruise FROM Customer AS c 如果你他细想一下,这个限制是有道理的 .你不能使用导航操作(.)在Java 中访 问一个 java.util.Collection 对象元素.例如,如果 getreservations( )返回一 个java.util.Collection 类型,这个语句是不合法的: //这是不合法的在 JAVA 语言程序中 customer.getReservations( ).getCruise( ); 参考以集合为基础关系字段的元素是可以的,但是它需要使用 IN或JOIN 操作和 一个标识分配在 FROM 子句中. 9.2.4. 构造语句 EJB QL 最重要的一个特性之一是可以指定一个构造在 SELECT子句中,它分配古 老的 JAVA对象(非实体)和传递栏在 select 查询中构造.例如,我们想要合并姓和名 从构造实体到古老的 JAVA 对象叫做名子: public class Name { private String first; private String last; public Name(String first, String last) { this.first = first; this.last = last; } public String getFirst( ){ return this.first; } public String getLast( ){ return this.last; } } 实际上我们查询返回一个名子类的列表代替简单的字符串列表.我们这样做通过 调用 Name 类的构造直接在我们的查询语句中: SELECT new com.titan.domain.Name(c.firstName, c.lastName) FROM Customer c 查询返回的每一行将会自动分配一个 Name 实例对象.因为产生键入报表和能替 你省下许多打字,这一个功能不能采信有用 9.2.5.IN 操作和 INNERJOIN 很多的实体组件间的关系是建立在集合基础上的 ,同时,能够存取和选择来自关 系中的组件是很重要的 .我们视直接查询在集合为基础的关系上是不合法的 .为 了克服这限制, EJB QL 介绍使用 IN操作,使用一个标识符在一个以集合为基础 的关系字段落上表现指定的元素. 下面的查询使用 IN操作到 select 元素从一个集合为基础的关系中,它返回所有 客户的预定信息: SELECT r FROM Customer AS c,IN( c.reservations ) r IN 操作分配指定的元素在预定的属性到 r标识符.因为我们有了一个标识符代表 集合中的指定元素,我们能直接引用它们甚至在 EJBQL语句是查询.我们也可以 使用元素标识符在路径中表达.举例来说,下面的语句查询泰坦上的客户预定的每 个巡航: SELECT r.cruise FROM Customer AS c, IN( c.reservations ) r 分配在 FROM 子句中的标识符是从左到右的求值.标识符一经定义,你可以在 FROM 子句中使用它在子查询中定义.c 标识符,首先被定义,在子查询中使用 IN操作定 义r标识符. 这个查询也要以表达成一个 INNERJOIN: SELECT r.cruise FROM Customer c INNER JOIN c.reservations r INNER JOIN 语法比 SQL 语言更好并且更直观对于开发者在关系世界中. 标识链可以变得非常长 .下面的语句使用了两个 IN操作到导航两个集合基础关 系和单个关系.当然不是必然有用,这个语句证明了一个查询中使用 IN操作跨过 多个关系. SELECT cbn.ship FROM Customer AS c,IN( c.reservations ) r, IN( r.cabins ) cbn 你也可以选择使用 INNER JOIN 语法: SELECT cbn.ship FROM Customer c INNER JOIN c.reservations r INNER JOIN r.cabins cbn 这些查询选择了所有用户预定的船 .INNER 关键字是可选择的 ,所以前面的查询 可以重写成: SELECT cbn.ship FROM Customer c JOIN c.reservations r JOIN r.cabins cbn 9.2.6.左连接 LEFT JOIN 语句能够取回一组实体 ,匹配值在连接语句中可能并不存在 .因为不 存在的数值, 一个空值可能放在一个结果集中. 举例来说, 我们想要生成一个用户名和所有用户的电话号码报表.一些用户可能 没有指定电话号码元 ,但是,我们仍想列出他们的名子 .我们需要使用一个 LEFT JOIN 来获取的有的信息,包括那些没有电话号码的客户信息: SELECT c.firstName, c.lastName, p.number From Customer c LEFT JOIN c.phoneNumbers p 如果在我们的系统中只有三个用户,并且 Bill Burke 没有提供任何的电话号码, 返回值会像下面这样: David Ortiz 617-555-0900 David Ortiz 617-555-9999 Trot Nixon 781-555-2323 Bill Burke null 前面的查询使用左外连接.这个语法是符合 SQL 92 标准的. SELECT c.firstName, c.lastName, p.number From Customer c LEFT OUTER JOIN c.phoneNumbers p 9.2.7.Fetch 连接 JOIN FETCH 语法允许你重新装载返回的实体关系,即使关系属性 FetchType 被设 置成 LAZY.举例来说,让我们定义客户一对多关系到电话如下所示: @OneToMany(fetch=FetchType.LAZY) public Collection getPhones( ){ return phones; } 如果我们想指出所有客户的信息 ,包括他们的电话号码,我们通常会查询所有的 客户,并且使用 getPhones()方法在循环中: 1 Query query = manager.createQuery("SELECT c FROM Customer c"); 2 List results = query.getResultList( ); 3 Iterator it = results.iterator( ); 4 while (it.hasNext( )){ 5 Customer c = (Customer)it.next( ); 6 System.out.print(c.getFirstName( ) + "" + c.getLastName( )); 7 for (Phone p : c.getPhoneNumbers( )){ 8 System.out.print(p.getNumber( ) + ""); 9 } 10 System.out.println(""); 11 } 在执行这些代码时会出现一些问题.因为Phone关系在Customer组件类中注释为 LAZY 装载,当我们做初始化查询的第一行时,Phone 集合还没有初始化.当 getPhonesNumbers( )在第七行执行时,持久化引挚必需做一个额外的查询来获 取与用户关联的 Phone 实体.这叫做 N+1 问题,因为我们必需做 N次额外的查询 超过我们的开始查询.当调整数据库应用程序列 ,减少来回查询的次数对于数据 库来说是非常重要的 .这就是 JOIN FETCH 语法要放置的地方 .让我们修改我们 的查询到重载的 Phone 关联: SELECT c FROM Customer c LEFT JOIN FETCH c.phones 使用 LEFT JOIN FETCH 将会预先加载 Phone 的关联.因为而非 N+1 查询,不过一 个查询在数据库中,所以这能对程序产生戏剧性的效果. 9.2.8.使用 DISTINCT 关键字 DISTINCT 关键字不返回查询中的重复行.例如,下面的查询查找所有用户的预定 信息.这个查询将返回重复数据: SELECT res FROM Reservation AS res,IN(res.customers) cust 如果一个用户预期定了多次,那么将会返回得复的引用在用户的强果中.使用 DISTINCT 关键字确定每个客户只在结果中出现一次: SELECT DISTINCT cust FROM Reservation AS res, IN(res.customers) cust 9.2.9.WHERE 子句和实字 你可使用实字来缩小查找元素的范围 .完成它需要使用 WHERE 字句,它的行为类 似于 SQL 语言中的 WHERE 子句. 例如,你可以定义一个 EJB QL语句来查找使用指定信用卡厂商的用户实体.实字 在这种情况下是一个字符串文字 .实字串被放在单引号内 .实字值包括一个单引 号,就像一个饭店名子叫 Wendy,使用两单引号来避开引用:Wendy''s. 下面的陈述将返回一个首都客户的信用卡 .(如果你不想被这些详细的信息所打 扰,你可以使用一个查询参数;工作将会由 Query API 来完成.) SELECT c FROM Customer AS c WHERE c.creditCard.creditCompany.name = 'Capital One' WHERE 子句中方法的表达类似于 SELECT 子句.当使用一个实字来比较时,这种方 式将会求出基本属性的值;你不能够把一个关系栏与一个实字进行比较. 除了字符串实字外,实字可能是精确的数值(long型)和近似值(doulbe型).精确 的数字实字值使用JAVA整型数值语法(321,-8932,+22),近似的数值实字值使用 JAVA的浮点语法来表达(5E3,-8.932E5)或者是十进投影的decimal (5.234, 38282.2)符号.举例来说, 下列的 EJB QL 查询 100,000 吨的所有的船: SELECT s FROM Ship AS s WHERE s.tonnage = 100000.00 布尔型的实字有 true 和false.这是一个 EJB QL语句,查询所有有 good credit 的用户: SELECT c FROM Customer AS c WHERE c.hasGoodCredit = TRUE 9.2.10.WHERE 子句和操作优先级 WHERE 子句由条件表达式组成 ,用来减少查询的范围和限制查询条目的数量 .一 些条件和逻辑操作可以用在表达式中;他们的优先级被列出: ◆导航操作符(.) ◆算术支符符:+,-(一元的);*,/(乘和除);+,-(加和减) ◆比较运算符:=,>,>=,<,<=,<>( 不等),LIKE, BETWEEN, IN, IS NULL, IS EMPTY, MEMBER OF ◆逻辑操作符:NOT, AND,OR 9.2.11.WHERE 子句和算术运算 算述运算允许一个查询执行算术在处理一个比较过程中.算术运算仅能用在 WHERE 子句中,不能用在 SELECT 子句中;下面的 EJB QL语句返回将会被收费超过 $300 的埠科的所有预定 EJBs 的参考: SELECT r FROM Reservation AS r WHERE (r.amountPaid *.01) > 300.00 应用算术运算的规则类似于在 Java 程序语言,在执行一个计算处理的数字有了 很大的扩展和提升.例如,乘以一个double和一个int型数值,首先要求把int转换 成一个double值.(计算的结果总是范围最宽的数值类型,所以一个int和一个 duble相乘的结果是doulbe类型的值). String,boolean,和实体对象类型不能够用在算术运算中,例如,使用加法运算在 两个String值上是违法的运算.有一个特殊的函数来连接String类型值,稍后在 "Functional expressions in the WHERE clause."节中会学到. 9.2.12.Where子句和逻辑运算 逻辑运算如AND,OR,和NOT运算,EJB QL同SQL 在逻辑运算上相同. 逻辑运算求得的仅是布尔表达式,所以,每次操作求得的值是true,false,或NULL. 逻辑运算的优先级低,所以所有的表式求值过后才被应用. AND和OR运算不同于JAVA语言,&&和||.EJB QL不指定右边的操作条件是否被求值. 举例来说,&&操作符在JAVA中求值,如果左边的值为超值和会求右边的值.类似 的,||逻辑运算,只有当左边的运算值为假,才会执行右边的运算.我们不能为EJB QL的AND 和OR运算符制造相同的假设.是否这些运算被求值,右边的操作数依赖 原生的但询语言被转换成内定的查询语言.最好的是假设两边的运算都被求值在 逻辑运算中. NOT运算符颠倒布尔值的结果;如果布尔值为真,经NOT运算后的结果变为假,反之 亦然. 9.2.13.WHERE子句和比较符号 比较运算符,使用符号:=, >, >=, <, <=, 和 <>, 对你来说,应该是很熟悉的. 下面的查询返回所有 tonnage 字段,大于等于 8,000 吨并且小于等于 130,000 吨 的航: SELECT s FROM Ship AS s WHERE s.tonnage >= 80000.00 AND s.tonnage <= 130000.00 只有 = 和 <>(等于和不等于)运算用于 boolean 和实体对象标识.大于和小标 识符(>, >=, <, <=)数值型和字符串型.然而,这些操作的语法没有在JAVA持久 化规范中定义.字符(大小或小写)很重要吗?与开头和结尾的空格有关系吗?结 果是这些问题目会影响字符串值的排序.因为内定的查询语言可能有非常不同的 排序规则,所使 EJB QL 维持如内定查询语言的抽象化的它的状态,它不能够命 令字串排序.事实上,甚到不同的关系数据库厂商在字符串排序上有不能的变化, 使它全部但是不可能的为 SQL" 适合的 " 数据库甚至标准化排序. 当然,如果你将来还使用相同的数据库的话,这是所用的理论.在这样的情况下, 最好是去查询数据库的文档来对照排序字符串.它将准确的告诉你EJB QL怎能样 进行比较工作的. 9.2.14.WHERE子句和等价语句 当比较一个精确的数值(short,int,long型)和一个近似数值(double,float)时, 所有的其它等价比较必需使用相同的类型.例如,比较字串数值 123 到整数实字 123. 然而,你能为平等比较二字串类型. 你可以在比较数值的过程式中应用数据类型的转换.例如,一个short类型与int 类型,一个int到long类型,等等.JAVA持久化也规定在原始类型与包装类进行比 较时可以进行类型转换. 在旧的规范中,字符串类型的比较必需完全的匹配,字符到字符.EJB2.1中抛弃了 这种要求,使用等价的求值在字符串类型间变得更模糊.这在 Java 语言持久化 规范中得到延续.这个不明确存在于不同的关系型数据库之间.参考提供商学院 的文档来确定字符串的比较求值. 你也可以比较实体对像是否等价,但是也必需是类型相同.更详细的,他们必需都 是来自相同配置的实体组件的引用.如例,下面将查询指定客户的所有预定.它将 使用一个Customer实体作为参数: SELECT r FROM Reservation r,IN( r.customers )AS cust WHERE cust = :specificCustomer 实体组件的类型一经确定,在比较过程中主要是主键.如果他们有相同的主键,认 为他们相等. 你也可以使用java.util.Date对象进行等价的比较.见前面的名为"Date Parameters."节. 9.2.15.WHERE子句和BETWEEN BETWEEN子句包含一个指定的运算值的范围.在这个例子中,我们使用它来选项择 所有重量在80,000和130,000吨之间的船: SELECT s FROM Ship AS s WHERE s.tonnage BETWEEN 80000.00 AND 130000.00 BETWEEN子句公能用在原始的数值类型(byte, short, int, long, double, float) 和与它们相关的java.lang.Number 类型 (Byte, Short, Integer, 等 等.).它不能用在String,Boolean,或实体引用上. 使用NOT逻辑运算连接BETWEEN包含的指定范围.例如,下面的EJB QL语句查询所 有的重量小于80,000或大于130,000吨,但不包含其中的任何船: SELECT s FROM Ship AS s WHERE s.tonnage NOT BETWEEN 80000.00 AND 130000.00 查询的效果是它与比较运算符一起运行: SELECT s FROM Ship AS s WHERE s.tonnage < 80000.00 OR s.tonnage > 130000.00 9.2.16.WHERE子句和IN IN条件操作在WHERE子句中使用不同于FROM子句中的IN运算符(这也是为什么 JOIN关键字在FROM子句中首先被引用超过IN关键字在集合中的导航).在WHERE子 句中,IN用来测试实字成员列表的值.例如,下面的EJB QL语句使用IN运算符来选 择所用的用户,这些用户是居住在指定的州的集合: SELECT c FROM Customer AS c WHERE c.address.state IN('FL','TX','MI','WI','MN') 应用NOT运算符在表达式中进行返向选择,除了所居住在州列表中的客户: SELECT c FROM Customer AS c WHERE c.address.state NOT IN('FL','TX','MI','WI','MN') 如果字段测试为空,表大兴安岭式的值为"unknown",它意味着无法预知. IN操作符可以与字符串或数值型的值进行操作求值.例如,下面的EJB QL语句使 用IN运算符符来选择所有甲板层次在1,3,5或7层的小屋: SELECT cab FROM Cabin AS cab WHERE cab.deckLevel IN(1,3,5,7) IN操作符也可以使用输入参数.例如,下面的查询所有的居住在指定州的用户: SELECT c FROM Customer AS c WHERE c.address.state IN(?1,?2,?3,'WI','MN') 在这个例子中,输入参数(?1,?2, 和 ?3)组合同字符串实字('WI' 和 'MN')来 显示,混合实字和输入叁数被允许, 可以为他们提供"like"类型. 9.2.17.WHERE子句和IS NULL IS NULL 比较运算符允许你测试是否一个表达式的值为空.例如,下面的EJB QL 查询所用没用地址的客户; SELECT c FROM Customer AS c WHERE c.address IS NULL 使用NOT逻辑运算符,我们可以颠倒查询的结果,查询所用地址不为空的用户: SELECT c FROM Customer AS c WHERE c.address IS NOT NULL 表达方法的组成使用"内联"语义.如果实体存在一个空的关系字段,使用的任何 查询用到这个字段的表达式的一部分,将会从实体条件中排除.例如,如果“John Smith”的客户实体的地址关系字段落为空值,那么在下面的查询结果集中"John Smith"用户实体将不会被包含进来: SELECT c FROM Customer AS c WHERE c.address.state = 'TX' AND c.lastName = 'Smith' AND c.firstName = 'John' 这似乎很明显,但是州明确的帮且我们排除了那些不明确关联的空值关系字段. NULL比较运算也可以使用输入参数.在这个例子中,NULL常常同NOT操运算一同组 合来确定一个不为空值的输入参数,例如,这个查询可以使用null输入参数来测 试.EJB QL语句首先检查city和state输入参数是否为空并且使用他们在比较运 算中: SELECT c FROM Customer AS c WHERE :city IS NOT NULL AND :state IS NOT NULL AND c.address.state = :state AND c.address.city = :city 在这情况,如果输入叁数之中任何一个为空值,查询返回一个空的列表 避免可能 出现的UNKNOWN结果从空值参数.你的JAVA代码必需要做空值的检查来避免不必 要的数据库循环. 如果查询结果包含一个空关系或基本字段,结果必需包含空值.例如,下面的查询 姓是smith的用户地址: SELECT c.address FROM Customer AS c WHERE c.lastName = 'Smith' 如果"John Smith"用户实体有一个地址关系字段的值为空,前面的查询结果返回一 个包含空值的列表,空值代表“John Smith”地址关系字段另外的一串地址实体的 引用.你可以排除空值通过包含 NOTNULL操作,如这里所看到的: SELECT c.address.city FROM Customer AS c WHERE c.address.city NOT NULL AND c.address.state = 'FL' 9.2.18.WHERE 子句和 ISEMPTY IS EMPTY 运算允许检查以集合为基础关系的结果是否为空 .记住前面第七章中 的集合为基本关系的从来没用过空值.如果集合为基本关系的字段没用元素,它将 返回一个空的 Collection or Set. 测试是否以集合物件为基础的关系是空的有如同测试一般的目的,是否一个关系 栏位或者基本栏位是null: 它能被用限制查询的范围和查询项目. 举例来说, 下列的查询选择没有订购任何的预定的所有的巡航: SELECT crs FROM Cruise AS crs WHERE crs.reservations IS EMPTY NOT 运算颠倒结果是 IS EMPTY.下面的查询所有到少有过一次预定的巡行: SELECT crs FROM Cruise AS crs WHERE crs.reservations IS NOT EMPTY 它是违法的 IS EMPTY,相反的,以集合为基本关系的,已经被分配了一个标识在 FROM 子句中: // illegal query SELECT r FROM Reservation AS r INNER JOIN r.customers AS c WHERE r.customers IS NOT EMPTY AND c.address.city = 'Boston' 这一个查询似乎是对未知的结果很保险,不过它不是.它是违法的因为 IS EMPTY 运算不能使用在一个以集合为基本关系的标识同一个 INNER JOIN 上被使用.因 为关系被指定在 INNER JOIN 子句中,只用那些预定实体有一个非空的用户字段 将会被包含在查询中;因为它的用户元素不能指定 c标识符,所以存在空关系的 任何预定实体将会被排除. 9.2.19.WHERE 子句和 MEMBER OF MEMBER OF 是一个非常重要的工具来确定是否一个实体成员被指定成为一个以 集合为基本关系的字段.下面的查询将确定是否一个指定的用户实体是 Reservation/Customer 关系中的一个成员: SELECT crs FROM Cruise AS crs,IN(crs.reservations) AS res, Customer AS cust WHERE cust = :myCustomer AND cust MEMBER OF res.customers 使用 NOT 运算到 MEMBER OF可以颠倒结果,选择所有的巡行过程中没用预定的客 户: SELECT crs FROM Cruise AS crs,IN(crs.reservations) AS res, Customer AS cust WHERE cust = :myCustomer AND cust NOT MEMBER OF res.customers 检查是否一个实体是空集合的成员总是反回 false. 9.2.20.WHERE 子句和 LIKE LIKE 比较运算允许查询选择String类型的字段匹配指定的模式.例如,下面的 EJB QL语句选择所有带连字符的用户名,像"Monson-Haefel" 和"Berners-Lee": SELECT OBJECT( c ) FROM Customer AS c WHERE c.lastName LIKE '%-%' 你也可以使用两个专用字符来建立一个比较模式:%(百分号)代表字符的任何顺 序和 _(底线) 代表任何单个字符.你能在一个字符串里面在任何的位置使用这 些字符.如果一 % 或 _ 在实际中出现, 你也可以使用\字符来代替它.NOT逻辑 运算排除反转评价所匹配的模式.下面的例子中将虾米示范区LIKE子句求字符串 类型字段: phone.number LIKE '617%' True for "617-322-4151" False for "415-222-3523" cabin.name LIKE 'Suite _100' True for "Suite A100" False for "Suite A233" phone.number NOT LIKE '608%' True for "415-222-3523" False for "608-233-8484" someField.underscored LIKE '\_%' True for "_xyz" False for "abc" someField.percentage LIKE '\%%' True for "% XYZ" False for "ABC" LIKE 运算也可以同输入参数一同使用: SELECT c FROM Customer AS c WHERE c.lastName LIKE :param 9.2.21.函数表达式 EJB QL中有非常多的函数用来处理字符串和数值. 9.2.21.1.WHERE 子句中的函数表达式 EJB QL 中有七种函数表达式允许对 String 进行简单的操作和三种对基本数值 操作的函数表达式.字符串函数是: LOWER(String) 将字符串转化为小写. UPPER(String) 将字符串转化为大写. TRIM([[LEADING | TRAILING | BOTH] [trim_char] FROM] String) 允许你去掉指定字符从开头,结尾,或两者.如果你没有指定一个 trim 字符,那么 字符中的空格将不会被去掉. CONCAT(String1, String2) 返回字符串 1和字符串 2的连接. LENGTH(String) 返回整型的字符串长度值. LOCATE(String1, String2 [, start]) 返回字符串 1在字符串 2中的位置值.如前面所示,在String2 中指出开始查找的 字符位置.支持地开始参数选项 ;一些容器支持它,也有一些不支持.如果你想使 查询变得简洁,不要使用它. SUBSTRING(String1, start, length) 返回 String1 中指定长度的字符串,在start 处开始取. start 和length 参数指定一个字符串中的整型的位置值.你可以使用这些表达式 在WHERE 子句中来使查询项目更加精炼.这是 LOCATE 和 LENGTH 函数的使用: SELECT c FROM Customer AS c WHERE LENGTH(c.lastName) > 6 AND LOCATE( c.lastName,'Monson') > -1 这个语句查询所有的姓包含'Monson'的客户,但是指定名字长度必需大于 6个字 符.因此,"Monson-Haefel" 和 "Monson-Ares"为真,但是"Monson"返回假,因为 它公有 6个字符. 在EJB QL的算术运算函数可以应用原始数据类型和原始数据类型相对应的包装 类型: ABS(number) 返回 number 的绝对值(number 可以是 int,float,或double 类型) SQRT(double) 返回平方根 MOD(int, int) 返回第一个参数被第二个参数求余后的结果.(例如,MOD(7,5) 等于 2) 9.2.21.2.返回日期和时间的函数 EJB QL中有三种函数用于返回当前日期,时间,和时间戳:CURRENT_DATE, CURRENT_TIME, 和 CURRENT_TIMESTAMP.下面是一个搜索当天预定的例子: SELECT res FROM Reservation res WHERE res.date = CURRENT_DATE 9.2.21.3.SELECT 子句中的聚合函数 聚合函数与返回集合值的查询一同使用.他们都非常简单而且很方便利, 尤其是 COUNT()函数. 9.2.21.3.1.COUNT(标识符或方法表达式) 这个函数返回最终查旬的结果集中的项目数 .返回类型是 java.lang.Long,依赖 于查询方法返回的类型 .例如,下面的查询提供一个居住在 Wisconsin 的所用客 户的人数: SELECTCOUNT( c ) FROM Customers AS c WHERE c.address.state = 'WI' COUNT()函数中也可以使用标识符,在这种情况下计算实体数目,或表达方法,计 算CMR 或CMP 字段.例如,下面的语句提供邮编以 554 开头的数量: SELECT COUNT(c.address.zip) FROM Customers AS c WHERE c.address.zip LIKE '554%' 在有些情况下,查询一个相关联的表达方法可以使用计算一个标识.例如,下面的 查询返回的结果,计算 Customer 代替 Zip 字段,与前面的查询等价: SELECT COUNT( c ) FROM Customers AS c WHERE c.address.zip LIKE '554%' 9.2.21.3.2.MAX,MIN 函数 这些函数用来查询最大值或最小值从集合类型的字段中.它们不与标识符或一个 关系字段中结束的路径一起使用.结果类型是求得的字段类型.例如,下面的查询 返回预定的最高价格: SELECT MAX( r.amountPaid ) FROM Reservation AS r MAX( ) 和 MIN( )函数可以应用在任何有效值上,包括原始类型,String,甚至是 序列化的对象.应用 MAX( )和MIN( )函数在序列化对象上的结果没有指定,因为 没有标准的方法来确定那个序列化对象更多或更少于其它对象. 应用在 String 字段落上的 MAX( ) 和MIN( ) 函数的结果,依赖于底层的数据存 储.这样做会同相关联的 String 类型比较成固定问题. 9.2.21.3.3. AVG(数值型), SUM(数值型)函数 AVG( ) 和SUM( )函数可以被应用在一个原始数值型字段 (byte, long, float, 等.) 或它们对应的包装类型(Byte, Long, Float, 等.). 查询的结果正是使用 SUM()函数所用的数值类型.AVG()函数的结果是 java.lang.Double 类型,依赖于 它是否使用了 SELECT 方法的返回类型. 例如,下面的查询使用SUM( )方法来获取,客户为指定的巡航支持的总数量::(被 输入叁数指定) SELECT SUM( r.amountPaid) FROM Cruise c join c.reservations r WHERE c = :cr 9.2.21.3.4.DISTINCT, nulls, 和 无参 DISTINCT 操作可以用在任意的聚合函数中用于去掉重复值.下面的查询使用 DISTINCT 操作来计算与指定的 Zip 码相匹配的数目: SELECT DISTINCT COUNT(c.address.zip) FROM Customers AS c WHERE c.address.zip LIKE '554%' DISTINCT 操作首先排除重复的 Zip 码;如果 100 个用户住在相同的地区使用相同 的邮编,结果返回的邮编数目仅为一 .当重复行被排除后,COUNT()函数将计逄剩 下的项目数. 任何null值的字段将会自动的被排除从操作的聚合函数的结果集中.COUNT()函 数也将忽略null值.聚合函数AVG( ), SUM( ), MAX( ), 和 MIN( ) 返回null当 算一个空集合.例如,下面的查询尝试获得对于一次特定的巡航客户支付的平均 的价格: SELECT AVG( r.amountPaid) FROM Cruise As c JOIN c.reservations r WHERE c = :myCruise 如果被输入叁数指定的巡航没有预定, 操作在集合上的AVG()函数为空.(没有预 定因此没有数量支付) 当求值的参数为空集合时COUNT()函数返回0(零).如果下面的查询求值在一个没 有预定的巡行上,因为参数为空集,所以结果为0(零). SELECT COUNT( r ) FROM Cruise AS c, IN( c.reservations )AS r WHERE c = ?1 9.2.22. The ORDER BY 子句 ORDER BY子句允许你指定一个查询中返回的实体集合的顺序 .ORDER BY子句的 语义与 SQL 中的类似.例如,我们可以构造一个简单的查询使用 ORDER 其所长 BY 子句来返回一个泰坦上巡行的客户的字母顺序列表: SELECT c FROM Customers AS c ORDER BY c.lastName 这能返回一个客户实体的集合在下面的定单中(假定输出姓和名): Aares, John Astro, Linda Brooks, Hank . . Xerces, Karen Zastro, William 你可以在 WHERE 子句中使用 ORDER BY子句也可以不使用.例如,我们使前面的查 询更精炼,仅列出居住在美国,Boston 的客户: SELECT c FROM Customers AS c WHERE c.address.city = 'Boston' AND c.address.state = 'MA' ORDERBY c.lastName ORDER BY子句默认的列表排序方式是升序的,意味着较小的值首先被列出,较大 的值在最后列出.你可以通过使用关键字 ASC 和 DESC 来确的指定排序方式为升 序或降序.默认的是 ASC.NULL 元素将会被放置在顶部或者依赖下面的数据库放 在底部.下面的语句以相反(降序)的方式列出客户: SELECT c FROM Customers AS c ORDERBY c.lastName DESC 查询结果是: Zastro, William Xerces, Karen . . Brooks, Hank Astro, Linda Aares, John 你可以指定多个排序列.例如,你可以通过 lastName 的升序和 firstName 的降序 进行排序: SELECT c FROM Customers AS c ORDER BY c.lastName ASC, c.firstName DESC 如果有五个客户实体的 lastName 都是 Brooks,这个查询结果如下: Brooks, William Brooks, Henry Brooks, Hank Brooks, Ben Brooks, Andy 尽管 ORDER BY子句必需使用基本字段,值的选择可以是一个实体标识符,一个关 系字段,或者是一个基本字段.例如,下面的查询返回一个邮编代码列表: SELECT addr.zip FROM Address AS addr ORDER BY addr.zip 下面的查询将返回名叫 Smith 的所有地址实体,使用他们的 Zip 代码进行排序: SELECT c.address FOR Customer AS c WHERE c.lastName = 'Smith' ORDER BY c.address.zip 在ORDER BY子句子使用基本字段要注意.如果查询一个实体的集合,Order by子 句能与被选择的实体基本字段一起使用.因为ORDER BY子句所使用的基本字段不 选择实体类型的栏位,所以下列的查询是违法的: // Illegal EJB QL SELECT c FROM Customer AS c ORDER BY c.address.city 因为city字段不是Customer实体的直接字段,你不能够在ORDER BY子句中使用 它. 类似的限制同样适用于结果 .在ORDER BY子句中使用的字段也同样适用于 SELECT子句.下面的查询是不合法的,因为在SELECT子句中的标识符与ORDER BY 中所使用的不同: SELECT c.address.city FROM Customer AS c ORDER BY c.address.state 在前面的查询中,我们想要列出所用的城市通过它们所在的州进行排序.不幸地, 这是违法的. 如果你没有选择 state 字段你不能通过它进行排序. 9.2.23. GROUP BY and HAVING 子句 GROUP BY and HAVING 子句普遍的用于比较严格的组织查询和缩小聚合函数的 结果.因为它允许聚集各种类型的数据,所以 GROUP BY子句通常在组合同聚合函 数时被使用.想要做一个报表用来查询特定巡航的预定 .你会想使用 COUNT 函数 来计算每次预定,但是,怎样分组计算每次巡行 ?这就是为什么 GROUP BY语法允 许你这么做.这是一个查询返回巡航名和每次巡航预定数目: SELECT cr.name, COUNT (res) FROM Cruise cr LEFT JOIN cr.reservations res GROUP BY cr.name GROUP BY子句必需指定一个栏位来返回查询结果 .因为我们使用了 LEFT JOIN. 巡行没有任何预定将返回 0,如果你想要排除巡航 ,报表中不存在的任何预定的 巡航,你可以使用 INNER JOIN 来代替. 如果我们组合它的构造,GROUP BY语法会 变得更有趣.让我们组装 ReservationSummary 实例列表.ReservationSummary 类 可以帮助组成巡航名,预定的数目 , 和钱被收集: public class ReservationSummary { public String cruise; public int numReservations; public double cashflow; public ReservationSummary(String c, int num, double cash) { this.cruise = c; this.numReservations = num; this.cashflow = cash; } } 我们将在查询中直接调用构造: SELECT new ReservationSummary(cr.name, COUNT(res), SUM(res.amountPaid)) FROM Cruise cr LEFT JOIN cr.reservations res GROUP BY cr.name HAVING子句同GROUP BY子句一同使用如同一个过滤器,,限制最后的输出.HAVING 子句使用聚合函数表达使用的唯一标识符,在SELECT子句中.你可以限制GROUP BY的结果使用HAVING子句.让我们限制报表只表示和超过 10个 预定的那些巡 航: SELECT cr.name, COUNT (res) FROM Cruise cr JOIN cr.reservations res GROUP BY cr.name HAVING count(res) > 10 管理SELECT和HAVING子句有相同的规则.只有分组属性函数的外面使用. 9.2.24. 子查询 子查询是SELECT语句的嵌套在另一个查询中.EJB QL支持子查询在WHERE和 HAVING子句中.当使用正常机制来缩小查询不能获取你想要的数据结果时,子查 询非常有用.这里有一个查询预定数量的例子,查询预会款高于所用预定平均值 的预定: SELECT COUNT(res) FROM Reservation res WHERE res.amountPaid > (SELECT avg(r.amountPaid) FROM Reservation r) 如果你看的足够认真,你将会看到这个例子中实际上可以分成两个查询.你可以 执行第一个查询找到平均值,将这个值传递组第二个查询,来找到预付款高于平 均值的预定.出于性能的原因,最好提交一个查询,因为它能免勉额外的数据库调 用.有可能数据库优化这个大的查询.你也可以引用标识符在FROM子句的子查询 外.我们想要查找所用巡行,这些巡行多于$100,000.查询会是这样. FROM Cruise cr WHERE 100000 < ( SELECT SUM(res.amountPaid) FROM cr.reservations res) ) 在这个例子中的子查询参照预定关联的巡航,指定在外部的FROM子句查询中. 9.2.24.1. ALL, ANY, SOME关键字 当子查询返回多行时,它可能确定结果使用ALL,ANY,和SOME表示. 所用的事情在子查询中配匹条件,ALL运算返回TRUE.例如,我们想要列出巡航中 每次没有付款的预定: FROM Cruise cr WHERE 0 < ALL ( SELECT res.amountPaid from cr.reservations res ) 如果子查询中的条件匹配ANY运算返回TRUE.例如,我们想要查找所有巡航中到少 有一个没有付款的预定. FROM Cruise cr WHERE 0 = ANY (SELECT res.amountPaid from cr.reservations res); SOME与ANY同意,并且两者的语法相同,你可以使用NOT表达式来获取相同的查询: FROM Cruise cr WHERE 0 < NOT ALL (SELECT res.amountPaid from cr.reservations res) 9.2.24.2. EXISTS关键字 如果子查询的结果由一个或多个值组成EXISTS运算返回true.如果子查询没有值 返回,那么将返回false.我们重写查询来查找一些过期付款的用户的巡航: FROM Cruise cr WHERE EXISTS (SELECT res FROM cr.reservations WHERE res.amountPaid = 0) 9.2.25. 批量 UPDATE and DELETE JAVA持久化的一个能力是执行批量的更新和删除操作.这样可以节省很多的输入. 例如,我们想要给名为Bill Burke的信用卡增加$10. UPDATE Reservation res SET res.amountPaid = (res.amountPaid + 10) WHERE EXISTS ( SELECT c FROM res.customers c WHERE c.firstName = 'Bill' AND c.lastName='Burke' ) 一个DELETE的例子,我们想要移除所有Bill Burke的预定. DELETE FROM Reservation res WHERE EXISTS ( SELECT c FROM res.customers c WHERE c.firstName = 'Bill' AND c.lastName='Burke' ) 在使用批量的更新和删除是你要非常的小心.它有可能是依赖厂商实现的,产生 的数据库和已经被持久化上下文所管理的实体间的不一致.厂商的执行,只需要 在数据库中直接执行更新或删除.他们不需要修改当前管理的实体状态.处于这 个原因,一般推荐,你拥用事务时进行操作或在一个事务开始时(在任何实体间访 问都会影响批量操作).你要以有选择的执行EntityManager.flush( ) 和 EntityManager.clear( ) 在执行批量操作之间,这将会保证安全. 9.3.原生查询 EJB QL中富有大量的查询语句并且基本上能符合你的绝大多数的查询需求.有时, 你想要使用特定厂商提供的数据库上的专有能力. 实体管理服务提供了一个方法来建立原生的SQL查询并且映射他们到你的对象上. 原生查询能反回实体,栏位值,或者两者的组合.EntityManager接口有三种方法 来建立原生查询:一种返回标量值,一种是返回实体类型,最后一种是定义一个复 杂的结果集,它能映射到多个实体的混合和标量值. 你可以进行JDBC的连接通过javax.sql.DataSource,使用@Resource注入和执行 你的SQL语句.要意识到你所做的改变不会被当前的持久化上下文所反映. 9.3.1. 标量原生查询 Query createNativeQuery(String sql) 这将建立一个原生查询返回一个标量结果.它需要一个参数:你的原生SQL.它执 行并且返回结果集同EJB QL相同的形式,返回标量值. 9.3.2.简单的实体原生查询 Query createNativeQuery(String sql, Class entityClass) 一个简单的原生查询通过一个SQL语句和隐式的映像到一个实体,映射元数据为 基础的一个实体.它认为原生查询的结果集中的栏将完全匹配实体的O/R映射.原 生SQL查询的映射实体的确定通过entityClass 参数: Query query = manager.createNativeQuery( "SELECT p.phone_PK, p.phone_number, p.type FROM PHONE AS p", Phone.class ); 实体的所有属性被列出: 9.3.3.复杂的原生查询 这个实体管理方法允许你有一个复杂的映射为原生SQL.你可以同时返回多个实 体和标量栏.mappingName 参数参考@javax.persistence.SqlResultSetMapping 定义.这个批注用来定义一个怎能样查询原生结果的钓子到O/R模型.如果返回的 栏位名与批注映射的属性不匹配,你可以提代一个字段到栏位的映射为他们,使 用@javax.persistence.FieldResult : package javax.persistence; public @interface SqlResultSetMapping { String name( ); EntityResult[] entities( ) default {}; ColumnResult[] columns( ) default {}; } public @interface EntityResult { Class entityClass( ); FieldResult[] fields( ) default {}; String discriminatorColumn( ) default ""; } public @interface FieldResult { String name( ); String column( ); } public @interface ColumnResult { String name( ); } 让我们做一系列的例子表示这会如何工作. 9.3.3.1. 使用多个实体的原生查询 @Entity @SqlResultSetMapping(name="customerAndCreditCardMapping", entities={@EntityResult(entityClass=Customer.class), @EntityResult(entityClass=CreditCard.class, fields={@FieldResult(name="id", column="CC_ID"), @FieldResult(name="number", column="number")} )}) public class Customer {...} // execution code { Query query = manager.createNativeQuery( "SELECT c.id, c.firstName, cc.id As CC_ID, cc.number" + "FROM CUST_TABLE c, CREDIT_CARD_TABLE cc" + "WHERE c.credit_card_id = cc.id", "customerAndCreditCardMapping"); } 因为结果集返回多个实体类型,我们必需使用一个@SqlResultSetMapping.这个 批注可以被放在一个实体类或方法上.entities( )属性用来设置@EntityResult 批注组成的队列.每一个@EntityResult注释指定将要通过原生SQL查询返回的实 体. @javax.persistence.FieldResult注释用来明确查询中与实体属性对应的映射 栏位.@FieldResult批注的name()属性标识实体组件的属性, column( ) 属性标 识通过原生查询返回的结果集栏位. 在这个例子中,我们需要指定@FieldResults为客户.原生查询为实体引用的每一 个栏位.因为我们只查询CreditCard 实体的ID和number栏,@FieldResult批注需 要被指定.在 CreditCard的@EntityResult批注中,fields()属性定义 CreditCard 属性每次查询的映射.因为Customer和CreditCard主键栏有相同的 名子,SQL查询需要辨别出他们的不同.cc.id As CC_ID这段SQL代码演示出这种 标识. 我们也可以使用XML来表达: 9.3.3.2.混合标量和实体结果 在我们的最终例子,显示一个实体和一个标量值的混合.我们写一个原生查询,来 返回一个每次巡行由多少预定组成的巡行列表. @SqlResultSetMapping(name="reservationCount", entities=@EntityResult(name="com.titan.domain.Cruise", fields=@FieldResult(name="id", column="id")), columns=@ColumnResult(name="resCount")) @Entity public class Cruise {...} { Query query = manager.createNativeQuery( "SELECT c.id, count(Reservation.id) as resCount FROM Cruise c LEFT JOIN Reservation ON c.id = Reservation.CRUISE_ID GROUP BY c.id", "reservationCount"); } reservationCount映射的定义,原生查询表现对一个巡航实体和一个所有巡航预 定的数目的请求.@FieldResult批注标识c.id栏同Cruise实体相关 联.@ColumnResult批注标识resCount栏同一个标量值. 等价的XML文件: 9.4命名查询 JAVA持久化提供了一种机制,所以在建立一个查询时,你可以预先定义EJB QL或 原SQL查询,并且引用它们通过名字.你可以先建立查询,然后建立JAVA语言中的 String类型的常量:在多种不同的情形中重复使用他们.你预先定义一个查询,当 在后面用到的时候,可以很容易的进行调整.@javax.persistence.NamedQuery 批注用在预定义EJB QL中: package javax.persistence; public @interface NamedQuery { String name( ); String query( ); QueryHint[] hints( ) default {}; } public @interface QueryHint { String name( ); String value( ); } public @interface NamedQueries { NamedQuery[] value( ); } 当你定义一个或多个查询在类或包中,你可以使用 @javax.persistence.NamedQueries 批注.@javax.persistence.QueryHint批注 定义厂商提供的暗示.这些暗示工作方式与Query.setHint( )方法类似,它的描 述在本单的前面.这是一个例: package com.titan.domain; import javax.persistence.*; @NamedQueries({ @NamedQuery (name="getAverageReservation", query= "SELECT AVG( r.amountPaid) FROM Cruise As c, JOIN c.reservations r WHERE c = :cruise"), @NamedQuery(name="findFullyPaidCruises", query= "FROM Cruise cr WHERE 0 < ALL ( SELECT res.amountPaid from cr.reservations res )") }) @Entity public class Cruise {...} 在这个例子中定义了两个EJB QL查询在Cruise实体组件类.你可以引用这些定义 在EntityManager.createNamedQuery( )方法中: Query query = em.createNamedQuery("findFullyPaidCruises"); Query.setParameter("cruise", cruise); 等价于@NamedQuery的XML文件: SELECT AVG( r.amountPaid) FROM Cruise As c JOIN c.reservations r WHERE c = :cruise 9.4.1.命名原生查询 @javax.persistence.NamedNativeQuery 批注用于预处理原生SQL查询: package javax.persistence; public @interface NamedNativeQuery { String name( ); String query( ); Class resultClass( ) default void.class; String resultSetMapping( ) default ""; } public @interface NamedNativeQueries { NamedNativeQuery[] value( ); } resultClass() 属性是为当你有一个原生查询时,只返回一个实体类型.(看这章 的前面"Native Queries" 节).resultSetMapping( ) 属性解决一个预定 @SqlResultSetMapping.这两个属性是可选的,但是你必需至少定义它们中的一 个.这是@NamedNativeQuery批注的一个例子: @NamedNativeQuery( name="findCustAndCCNum", query="SELECT c.id, c.firstName, c.lastName, cc.number AS CC_NUM FROM CUST_TABLE c, CREDIT_CARD_TABLE cc WHERE c.credit_card_id = cc.id", resultSetMapping="customerAndCCNumMapping") @SqlResultSetMapping(name="customerAndCCNumMapping", entities={@EntityResult(entityClass=Customer.class)}, columns={@ColumnResult(name="CC_NUM")} ) @Entity public class Customer {...} 你可以参考EntityManager.createNamedQuery( ) 的定义: Query query = em.createNamedQuery("findCustAndCCNum"); 等价的XML文件: SELECT c.id, c.firstName, c.lastName, cc.number AS CC_NUM FROM CUST_TABLE c, CREDIT_CARD_TABLE cc WHERE c.credit_card_id = cc.id 第十章 实体回调和监听 当你执行EntityManager的persist( ), merge( ), remove( ), 和 find( )方 法,或执行EJB QL查询,一个预定义的生命周期事件被触发.举例来说,persist() 方法触发数据库的插入.合并触发数据库的更新.remove()方法触发数据库的删 除.查询触发数据从数据库中的装载.有时有是非常有用的当这些事件发生的时 候,你的实质组件被通知. 举例来说,也许你想要在数据库中建立一个在每栏上 的交互日志的一个审计.JAVA持久化规范允许你设置回调方法在实体继而上,所 以当事件发生时你的实体类的实例会被通知.你也可以注删监听类来拦截这些事 件.这叫做实体的监听.这一章讨论怎样注册你的实体组件类,为生命周期的回调 以用怎样写实体监听来拦截实体事件的生命周期. 10.1.回调事件 一个特定的注解代表每个实体生命周期的各个阶段: @javax.persistence.PrePersist @javax.persistence.PostPersist @javax.persistence.PostLoad @javax.persistence.PreUpdate @javax.persistence.PostUpdate @javax.persistence.PreRemove @javax.persistence.PostRemove @PrePersist 和 @PostPersist事件做一个实体实例的插入操作到数据库中.当 EntityManager.persist()被调用时@PrePersist 事件立刻发生,或者一个实体 实例将要插入到数据库中(如同一个层叠的合并).@PostPersist 事件不会被触 发直到实际数据库的插入. @PreUpdate 事件的触发是在实体状态与数据库同步之前.@PostUpdate事件发生 在之后.同步可以发生在事务提交时,当EntityManager.flush( )执行时,或当持 久化上下文认为必需更新到数据库. @PreRemove 和 @PostRemove 事件做实体的移动从数据库中.每当 EntityManager.remove( )被调用在实体组件上,@PreRemove 事件被触发.直接 的或因为层叠.The @PostRemove事件立刻发生在实际数据库中删除发生之后. @PostLoad 事件的触发是在一个实体实例通过一个find()或getreference()方 法调用在EntityManager接口上被装载.或者是EJB QL查询的执行.也可以在 refresh( )方法的调用之后. 10.2.实体类上的回调 你能为一个实体组件的实例注册一个回调在生命周期的任意一个,通过注释一个 public ,private,protected,或同一个包内方法在组件类上,这个方法的返回类 型必需是void,并且抛出无检查的异常,同时没用参数. @Entity public class Cabin { ... @PostPersist void afterInsert( ){ ... } @PostLoad void afterLoading( ){ ... } } 当事件被触发在一个特殊的实体实例上,实体管理器将调用适当的方法在组件类 上. 如果你愿使用注释,你可以使用这些事件通过使用, , , , , ,和 元素的子元素在ORM映射的布署描述符上. 这些元素有一个叫name的属性,这是当回调发生时你调用的方法名. 10.3.实体监听 实体监听是一般的能截取实体回调事件的类.它并不是实体本身,但是他们能被 绑定到一个实体类通过批注或XML 你可以分配一个方法到实体的监听类上用来 截取一个特定生命周期的事件.这些方法返回void并且使用一个Object参数.实 体实例在那一个事件上被触发.方法与它感兴趣的回调一起被注解. public class TitanAuditLogger { @PostPersist void postInsert(Object entity) { System.out.println("Inserted entity: " + entity.getClass().getName( )); } @PostLoad void postLoad(Object entity) { System.out.println("Loaded entity: " + entity.getClass().getName( )); } } 实体监听类必需用一个公共的无参的构造函数.它也可以应用在一个实体类中通 过使用@javax.persistence.EntityListeners批注: package javax.persistence; @Target(TYPE) @Retention(RUNTIME) public @interface EntityListeners { Class[] value( ); } 你可以指定一个或多个实体监听来截取实体类的回调事件: @Entity @EntityListeners ({TitanAuditLogger.class, EntityJmxNotifier.class}) public class Cabin { ... @PostPersist void afterInsert( ){ ... } @PostLoad void afterLoading( ){ ... } } 使用@EntityListeners批注在Cabin实体类上,当Cabin实体的实例与持久化上下 文相互作用时,实体监听类的任意回调方法将会被调用.实体监听也可以应用扩 展标示语言: 元素可以在实体定义的里面被使用.这里面列出的监听类 将要被执行.任意实体监听类都不能使用批注来标识回调,可以使用 元素或其它的回调元素来指定这些方法. 定义的 效果可以覆盖任意@EntityListeners批注应用在实体类上. 实体监听的执行次序是他们的定义在@EntityListeners 注释或ORM XML映射文 件中的顺序.实体组件类上的任意回调总是在自身调用后,最后被调用.让我们看 一下下面的调用操作会发生什么: 1 EntityManager em = factory.createEntityManager( ); 2 em.getTransaction().begin( ); 3 4 Cabin cabin = new Cabin( ); 5 em.persist(cabin); 6 7 Cabin anotherCabin = em.find(Cabin.class, 5); 8 9 em.getTransaction().commit( ); 假定这个码在我们这一章节显示的例子上运行.EntityJmxNotifier类对定义的回调感兴趣.当第5行的EntityManager.persist( )方法被调用 时EntityJmxNotifier.beforeInsert( )也会执行. 在第7行,TitanAuditLogger.postLoad( ), EntityJmxNotifier.afterLoading( ), 和Cabin.afterLoading( ) 方法被调用, 以这样的顺序,同@PostLoad 事件被 EntityManager.find( ) 调用触发. 持久化提供者决定延迟新的持久化Cabin插入到数据库中直到事务提交.在第9行 中,TitanAuditLogger.postPersist( ) 和Cabin.afterInsert( )方法会被除调 用.以那个顺序,@PostLoad 事件会被EntityManager.find( ) 调用触发. Cabin.afterLoading( )方法被相同的实例所调用.我们在第5行的持久化. 10.3.1.默认的实体监听器 你可以指定一个默认的实体监听器集合,应用在每一个实体类上,在持久化单元 中通过使用 元素的子元素 在ORM映射 文件中.举例来说,如果你想应用TitanAuditLogger到每个实体类所在的持久化 单元,你应该像下面这样做: 如果你想关掉默认的实体监听器在指定的实体类上,你可以使用 @javax.persistence.ExcludeDefaultListeners批注: @Entity @ExcludeDefaultListeners public class Cabin { ... } 这是等价于批注的XML文件: 如果@ExcludeDefaultListeners 批注或其等价的XML应用到Cabin实体 上,TitanAuditLogger将会关闭在Cabin实体上. 10.3.2.继承和监听 如果一个继承实体的基类存在监听器,那么也将应用在继承的实体上,任意子类 将会继承这些实体监听器.如果子类也应用了实体监听器,那么基类的和子类的 实体监听器将被绑定. @Entity @EntityListeners(TitanAuditLogger.class) public class Person { @PostPersist void anotherCallback( ){ ... } } @Entity @EntityListeners(EntityJmxNotifier.class) public class Customer extends Person { ... } 在这个例子中,TitanAuditLogger和EntityJmxNotifier 实体监听将会被绑定到 Customer实体.如果这些监听有一个@PostPersist回调,回调的执行顺序如下: ◆TitanAuditLogger's @PostPersist 方法 ◆EntityJmxNotifier's @PostPersist 方法 ◆Person's @PostPersist anotherCallback( ) 方法 应用在基类的实体监听的发生在任意绑定的子类之前.回调方法的定义直接在实 体类发生后: 你可以关闭实体监听的继承使用 @javax.persistence.ExcludeSuperclassListeners : @Entity @EntityListeners(TitanAuditLogger.class) public class Person { } @Entity @EntityListeners(EntityJmxNotifier.class) @ExcludeSuperclassListeners public class Customer extends Person { ... } 在这个例子中,仅有EntityJmxNotifier监听器会被执行在Customer实体的实例 上.@ExcludeSuperclassListeners 有一个等价的XML映射在元素内. 第十一章 会话Bean 实体组件提供了一个面向对象的模型,使开发者很容易创建,修乞讨,和删除从数 据库中.它们鼓励开发者生产的重用,来减少开发成本.例如,你已经定义了一个 组件来代表船的概念,那么这个组件可以被重用到整个逻辑系统而不用重定义, 重编码,或者是重测试业务逻辑和数据访问. 然而, 实质豆子不是整个的故事.我们还有另外的一种企业Bean:会话Bean.会话 Bean弥补实体组件所留下来的缺口.他们非常有用对描述其它组件间的交互和实 现特定的任务.不同于实体组件的是,会话组件不代表数据在数据库中,但是他们 可以访问数据.这就意味着我们可以使用会话组件来读,更新,和插入数据在业务 逻辑处理中.例如,我们可以使用一个会话组件来提供信息列表,如一个所有可用 的小屋的列表.有时我们生成的列表会与实体组件进行交互,像小屋列表,我们在 第四章中开发的TravelAgent EJB. 什么时候使用实体组件?什么时候使用会话组件?如一个经验法则,一个实体组 件应该提供一个安全的和一致的界面的概念给共享的数据的集合.这个数据也许 会频繁的更新.会话组件的存取跨趣这个观念,没有共享的数据,而且通常是只读. 会话组件包含业务逻辑和实体模型的持久数据. 除了直接存取数据外,会话组件可以代表任务流程.任务的充程提及所有的步骤 必需完成一个特别的任务,像是在一艘船上订购或租用录象机.会话组件经常管 理交互在实体组件间,描述怎能样的联合工作来完成一个特定的任务.会话组件 和实体组件间的关系就像玩和演员的脚本之间的关系.演员没必要不使用脚本; 他们代表一些事情,但是他们不能告诉你一个故事.除非你能在实体之间产生相 互作用,否则同样地,在一个数据库被表现的实体不是意义深长的.没道理在数据 库中存在完整的cabins.ships,customers,如果我们不在它们之间建立交互,像 是为一次巡航订购一个客户. 会话组件分为两种基本类型:无状态和有状态的.一个无状态会话组件是一个关 系服务的集合,通过方法来代表;组件没有维持状态从一个方法到下一个.当你调 用无状态会话组件的方法时,它的执行方法和返回的结果不需要带走,其它的请 求到来之间或跟随.会话Bean如同一系列程序或处理集,执行一个多个参数组成 一个请求和返回一个结果. 有状态会话Bean是一个扩展的客户应用程序.它的执行任务是维护用户的状态和 利益.因为它表现一个持续性的会话在会话Bean和客户之间所以这种状态被称做 会话状态.会话Bean的方法调用可以读写数据从会话状态,它共享在组件的所有 方法之间.有状态会话Bean对于一个细节是特定的.他能代表逻辑那一个可能在 两点间系统的用户端程序被捕获.依赖于厂商,有状态会话Bean有超时限制.如果 客户失败在使用有状态Bean之间超阶级时, Bean实例将被销毁并且EJB 对象的 引用是无效的.这样防止了有状态会话Bean从用户已经关闭很久的延迟或其它的 已经结束了的使用.毕竟,用户端能当机, 和使用者离开桌子而忘不了记他们正 在做的事情.我们不想有状态会话Bean关联一个不存在的客户或健忘的用户使我 们的服务器变得凌乱.一个客户也可以移除一个有状态会话Bean通过调用它的 remove方法. 无状态会话Bean是以在服务器上存在很长时间,因为他们不保留任何会话状态和 不是客户专用的.一旦一个无状态会话Bean完成方法调用,它可以服务于一个新 用户.无状态会话bean也可能超阶级时并且被用户移除,但是超时或移除对组件 的影响不同于有状态会话Bean.一个超时或移除操作对客户的EJB对象引用来说 是简单的使它无效;bean的实例没有被移除并且空闲着服务其它用户的请求. 是否他们是有状态的或者是无状态的,会话bean是不会持久化的,换句话来说,会 话组件不代表持久数据不能保存到数据库中. 11.1.无状态会话Bean 一个无状态会话Bean是非常高效和相对容易开发的.一个会话Bean可以自由的交 换在EJB 对象之间,因为它不是用户专用的并且不维持任何会话状态.服务一以 完成方法调用就可以与其它的EJB 对象交换.因为它不维护会话状态,一个无状 态会话Bean不是密需要求钝化或激活,减少交换的开支,简而言之,无状态会话 Bean是轻量级并且快速的. 无状态会话Bean不维持任何的会话状态,意味着每个方法调用都独立于前面的调 用,并且方法需要知道传递的方法参数.因为无状态会话Bean不会记住任何事情 从一个方法调用到另一个,它们必需用处理整个任务在一个方法调用中.公有这 规定例外的是信怎能的获取是从SessionContext和JNDI ENC中,或者环境的引用 直接注入到组件类中(我们将会在后面谈到依赖注入).无状态会话Bean是EJB的 传统交易征理应用的版式本,它的执行使用过程调用.过程从头到尾执行返回结 果.过程一旦结束,关于被操纵的数据或请求细节都不会被记录. 这些限制并不意味着无状态会话Bean不能有实例变量或维持任何的内部状态.没 有什么会阻止你使用一个变量来跟踪组件的调用时间或保存调试数据.一个实例 变量可以保有存对一个资源的引用,如一个URL连接记录,验证信用卡通过不同的 EJB,或者是其它有用的资源从JNDI ENC上获取的或者是直接使用EJB的注入特性 的依赖注入字段.然而,重要的是这些状态客户端是无法看到的.一个客户不能使 用相同的组件来维护它的全部请求.实例变量可能有不同的值在不同的实例中, 所以,实体组件从一个客户端到另外的被交换汤不换药的时候,它们的值是不确 定的.因此实例变量的引用是类属的.例如,每一个组件实例适当的记录除错信息, 可能是唯一的方法理解在一个大的肥务器上很多组件实例发什么.用户不知道或 者不关心调试信息在那输出.然而,它会让实体清楚的记住处理的预定信息在下 次被调用时,它可能完全的服务于另一个客户. 无状态会话Bean可以生成报表,批处理,或者一些无状态服务,如信用卡的有效性 验证.另一个好的应用程序可能是StockQuote返回存储的当前价格.任意的行动 可以在一个方法的调用中完成,通过一个高效的无状态会话Bean. 11.1.1.ProcessPayment EJB 在第二章和第三章章中讨论过的TravelAgent 组件 ,它的业务方法调用 bookPassage()使用ProcessPayment组件.下一节中开发一个完整定义的 TravelAgent组件,包括业务逻辑的bookPassage()方法.此时,然而,我们主要地 对 ProcessPayment EJB 感兴趣,无状态会话Bean TravelAgent使用charge代表 巡航客户的价钱.收费用户被激活在泰坦的业务系统中.不仅是预定系统需要用 户收费,同样的礼品店,时装商店,和其它的关联业务.因为很多不同的收费系统 服务于用户,我们概括收费用户的业务逻辑在它们自身的组件中. Payments记录在数据库中的指定表PAYMENT.PAYMENT数据批量处理结帐,和一般 不在外面结算.换句话来说,数据的插入只能通过泰坦系统;它不能读,更新,或删 除.因为处理付费可以在一个方法中完成,并且因为数据不能频繁的更新和共享, 我们将使用无状态会话Bean来处理付款.可以使用不同的付款方式:信用卡,帐单, 现金.我们将模拟付款在无状态会话Bean ProcessPayment中. 11.1.1.1.数据表:PAYMENT ProcessPayment组件访问泰坦系统中存在的表PAYMENT,在你的数据库中建立表 PAYMENT,表定义如下: CREATE TABLE PAYMENT ( customer_id INTEGER, amount DECIMAL(8,2), type CHAR(10), check_bar_code CHAR(50), check_number INTEGER, credit_number CHAR(20), credit_exp_date DATE ) 11.1.1.2.业务接口:ProcessPayment 一个无状态会话Bean有一个或多个业务接口.显然ProcessPayment业务接口需要 一个byCredit()方法因为TravelAgent组件要使用它.我们也可以识别另两种方 法,我们需用要:byCash()为用户付现金和byCheck()为用户个人帐单. 一个业务接口可以是远程或本地的,但是不能两者谐是.远程业务接口可以接收 网络上用户的方法调用.当一个用户调用一个方法在会话Bean的远程接口上,参 数值和返回值是被拷贝的.这确定是否与客户运行在同一个虚拟机上,或者是网 络上的另一台机器.从语义学上叫做值的呼叫call-by-value. 本地接是仅在同一个JVM上有效的,如会话Bean.在一个Local接口上的调用不会 拷贝参数或反回值.因为这,本地接口被称为call-by-reference 因为TravelAgent组件将要有相同的部署,ProcessPayment提供一个本地接口,所 以调用ProcessPayment的效率更高.ProcessPayment组件也会有远程式的用户, 所以我们同样要提供一个远程的业务接口. 本地和远程接口将发布相同的API.为了使我们的设计清晰一些,我们将继承基接 口com.titan.processpayment.ProcessPayment: package com.titan.processpayment; import com.titan.domain.*; public interface ProcessPayment { public boolean byCheck(Customer customer, CheckDO check, double amount) throws PaymentException; public boolean byCash(Customer customer, double amount) throws PaymentException; public boolean byCredit(Customer customer, CreditCardDO card, double amount) throws PaymentException; } EJB规范允许定义一个普通的基类为远程和本地接口,如果他们共享相同的方法. 三种业务方法被定义:byCheck( ), byCash( ), 和 byCredit( ), 每个使用付 款形式相关的信息,使用一个布尔值来指明付款是否成功.这些方法可以抛出应 用程序所指定的异常,如PaymentException. 如果处理付款时有事情发生将会抛 出PaymentException is ,例如,少于帐单的或是过期的信用卡.然而,注意, 对 预定制度,关于 ProcessPayment 指定到预定系统.它可以使用在泰坦系统的任 意地方.除此之外,每个方法的定义是在基类业务接口的基础上完成不依赖其它 的.处理付款需要的所有数据是通过方法的参数来获取的.紧接着,指定本地和远 程接口; package com.titan.processpayment; import javax.ejb.Remote; @Remote public interface ProcessPaymentRemote extends ProcessPayment { } package com.titan.processpayment; import javax.ejb.Local; @Local public interface ProcessPaymentLocal extends ProcessPayment{ } ProcessPaymentRemote 和 ProcessPaymentLocal 接品继承基础接口 ProcessPayment所以不需要重复定义方法.ProcessPaymentRemote 定义成远程 接口通过@javax.ejb.Remote接口,ProcessPaymentLocal通过@javax.ejb.Local 注释. 11.1.1.3.实体做参数 ProcessPayment组件的业务接口使用Customer实体组件作为方法的一个参数.因 为实体组件是古老的JAVA对象,它们可以被序列化来跨过网络如同古老的Java对 象,只要他们实现java.io.Serializable 或 Externalizable.这是很重要的因 为ProcessPayment组件访问Customer实体的内部状态.如果每次调用Customer的 get方法通过一个远程接口(EJB2.1规范中要求),ProcessPayment组件的效率将 会是低下,因为网络调用的成本非常昂贵.这是 EJB 3.0 规范简化的另一个例子. 在EJB2.1中,因为实体是最好的元件,如果他想要传递一个实体状态代替访问它 通过远程接口,你需要写一个类似的值对象. 11.1.1.4.域对象:CreditCardDO和CheckDO类 ProcessPayment业务接口使用两个特别的有趣的类,CreditCardDO 和 CheckDO: /* CreditCardDO.java */ package com.titan.processpayment; import java.util.Date; public class CreditCardDO implements java.io.Serializable { final static public String MASTER_CARD = "MASTER_CARD"; final static public String VISA = "VISA"; final static public String AMERICAN_EXPRESS = "AMERICAN_EXPRESS"; final static public String DISCOVER = "DISCOVER"; final static public String DINERS_CARD = "DINERS_CLUB_CARD"; public String number; public Date expiration; public String type; public CreditCardDO (String nmbr, Date exp, String typ) { number = nmbr; expiration = exp; type = typ; } } /* CheckDO.java */ package com.titan.processpayment; public class CheckDO implements java.io.Serializable { public String checkBarCode; public int checkNumber; public CheckDO(String barCode, int number) { checkBarCode = barCode; checkNumber = number; } } CreditCardDO 和 CheckDO 是域对象.他们是序列化的Java类,非企业级组件.他 们提供一个方便的机制给传送相关的数据.CreditCardDO,例如,集合所有的信用 卡数据在一个类中.使它容易传递信息跨过网络,使接口清晰一些. 11.1.1.5一个应用异常:PaymentException 任何远程或本地接口都抛出应用异常.在这个例子中,应用异常被描述成一个业 务逻辑问题,一个付款问题.应有异常将意味着用户,提供了一个短暂和相关标识 的错误. 读懂你使用的异常是很重要的,当你使用他们时.异常如 javax.naming.NamingException 和 java.sql.SQLException被其它的Java子系 统抛出.业务处理EJB模拟什么也不做.JAVA 语言子系统异常外的这些类型由会 话Bean实现.另外异常(非RuntimeException 的检查,在默认情况下,不会引起事 务的回滚.而非直接抛出这些类型的异常,你可以捕获他们在try/catch块和抛出 适当的异常.通常的做法是包装检查异常在javax.ejb.EJBException ,同样的这 些子系统的错误类型是无法恢复的. EJBException指示容器运行存在问题,在处理一个业务接口的调 用.EJBException没有检查(它继承了java.lang.RuntimeException ),所以,如 果你没有捕获它,在编译时不会发现错误.然而,在某种情况下,捕获 EJBException是一个好的想法,并且在其它环境中,它被传播. 一个PaymentException描述一个指定的业务问题,那可能是可以获得的.这使它 成为一个应用异常.EJB容器对待任何没用继承RuntimeException异常的异常视 为应用异常.下面是PaymentException异常的定义: package com.titan.processpayment; public class PaymentException extends java.lang.Exception { public PaymentException( ){ super( ); } public PaymentException(String msg) { super(msg); } } 一个应用异常被传播到客户端调用.任意实例变量包含这些异常应该是序列化的. 非应用异常总是包装在EJBException异常中.这意味着你抛出的任意异常或者继 承RuntimeException被EJB容器捕获和包装在一个EJBException.这对会话Bean 来说是非常重要的,在同实体Bean交互时.所有被Java持久化抛出的异常是 RuntimeException接口.如果它需要在指定的持久化异常上捕获,客户端代码需 要知道.异常的行为可以被明确的定义使用 @javax.ejb.ApplicationException 批注和XML布署描述符的元素.那些构造将会在第十六 章中详细讨论,因为他们对事务有很大的影响. 11.1.1.6.组件类:ProcessPaymentBean ProcessPayment组件模拟特定的业务处理,所以它是无状态会话Bean的优秀候选. 这个组件真正代表一个独立操作的集合,指示它是一个无状态会话Bean的一个好 的代表.这里有ProcessPaymentBean类的定义: package com.titan.processpayment; import com.titan.domain.*; import java.sql.*; import javax.ejb.*; import javax.annotation.Resource; import javax.sql.DataSource; import javax.ejb.EJBException; @Stateless public class ProcessPaymentBean implements ProcessPaymentRemote, ProcessPaymentLocal { final public static String CASH = "CASH"; final public static String CREDIT = "CREDIT"; final public static String CHECK = "CHECK"; @Resource(mappedName="titanDB") DataSource dataSource; @Resource(name="min") int minCheckNumber; public boolean byCash(Customer customer, double amount) throws PaymentException { return process(customer.getId( ), amount, CASH, null, -1, null, null); } public boolean byCheck(Customer customer, CheckDO check, double amount) throws PaymentException { if (check.checkNumber > minCheckNumber) { return process(customer.getId( ), amount, CHECK, check.checkBarCode, check.checkNumber, null, null); } else { throw new PaymentException("Check number is too low. Must be at least "+minCheckNumber); } } public boolean byCredit(Customer customer, CreditCardDO card, double amount) throws PaymentException { if (card.expiration.before(new java.util.Date( ))) { throw new PaymentException("Expiration date has passed"); } else { return process(customer.getId( ), amount, CREDIT, null, -1, card.number, new java.sql.Date(card.expiration.getTime( ))); } } private boolean process(int customerID, double amount, String type, String checkBarCode, int checkNumber, String creditNumber, java.sql.Date creditExpDate) throws PaymentException { Connection con = null; PreparedStatement ps = null; try { con = dataSource.getConnection( ); ps = con.prepareStatement ("INSERT INTO payment (customer_id, amount, type,"+ "check_bar_code,check_number,credit_number,"+ "credit_exp_date) VALUES (?,?,?,?,?,?,?)"); ps.setInt(1,customerID); ps.setDouble(2,amount); ps.setString(3,type); ps.setString(4,checkBarCode); ps.setInt(5,checkNumber); ps.setString(6,creditNumber); ps.setDate(7,creditExpDate); int retVal = ps.executeUpdate( ); if (retVal!=1) { throw new EJBException("Payment insert failed"); } return true; } catch(SQLException sql) { throw new EJBException(sql); } finally { try { if (ps != null) ps.close( ); if (con!= null) con.close( ); } catch(SQLException se) { se.printStackTrace( ); } } } } 这个组件类使用批注@javax.ejb.Stateless来标识它是一个无状态会话Bean: Package javax.ejb; @Target(TYPE) @Retention(RUNTIME) public @interface Stateless { String name( ) default ""; } name()属性标识会话Bean的名字.组件名字黑认的是无限制的类型,如果你初始 化这个属性.对于ProcessPayment组件,组件默认名为ProcessPaymentBean.在大 部份的情形下,你不一定要知道 EJB 名字的观念.当你想要覆盖或在XML部署描 述符中增加元素时,它是有用的.组件类鉴别它的远程和本地接口通过实现 ProcessPaymentRemote 和 ProcessPaymentLocal接口.当组件被部署后,容器查 找组件的接口看他们是否有批注@javax.ejb.Local 或 @javax.ejb.Remote.这 样的自省确定组件类的远程和本地接口.做为选择,组件类可以不实现任何接 口,@Local 和 @Remote批注可以直接应用在组件类上. @Stateless @Local(ProcessPaymentLocal.class) @Remote(ProcessPaymentRemote.class) public class ProcessPaymentBean { final public static String CASH = "CASH"; final public static String CREDIT = "CREDIT"; final public static String CHECK = "CHECK"; 当你使用组件类时,@Local和@Remote批注组成一个接口类的一个队列.这种方式 不被推荐除非你必需这样做,因为实现业务接口直接被强制连接在组件类和这些 接口之间. 三种付款方式使用私用帮且方法process(),它们的工作是增加付款到数据加中. 这种策略减少程序出错的可能性和使组件易于维护.Process()方法没有使用实 体组件但是简单的插入付款信息直接到PAYMENT表使用JDBC.JDBC连接的获取是 从组件类的datasource字段. byCheck( ) 和 byCredit( )方法包含一些逻辑验证数据在处理之前.byCredit() 验证信用卡过期日期是否是在当前处理的日期之前.如果是过期的半会抛出 PaymentException异常.byCheck() 确认支票的序号比一个特定的最小量高.这 个确定通过minCheckNumber字段.如果检查低于这个值,一个PaymentException 异常会抛出. 11.1.1.7.访问环境属性(注入) datasource 和 minCheckNumber 字段是会话Bean字段的一个例子,通过EJB环境 来初始化.每个EJB窗口有它自已的内部注册,存储配置值和引用定义外部资源与 服务.这个注册被称为企业命名上下文(ENC).如果你看成员函数的定义,你将会 看到他们使用@javax.annotation.Resource批注.告诉EJB容器,当一个例证被实 例时,这些字段的初始化值在容器的ENC. 当 EJB 容器被部署的时候,ENC和嵌入批注如@Resource,同信息存储在EJB XML 布置描述文件中.例如,@Resource批注标记datasource字段包括一个 mappedName()属性,标识外部JDBC数据源将要被映射到ENC中.对于 minCheckNumber,@Resource批注标识一个name值到ENC中,那将使用在初始化的 外部字段上.name值的配置可以使用EJB的XML部署描述文件: ProcessPaymentBean min java.lang.Integer 250 XML配置下的EJB ENC的name最小值是250.这是一个很好的主意在实体环境中取 得取得临限和它的限度,取代了他们的硬编码:它给你更好的弹性.举例来说,泰 坦神决改变起最小的检查编号,你只需要改变组件的XML部署描述符,不用修改类 的定义. 和@Resource的准确定义及细节将会在第14章进行讨论. 11.1.2. XML部署描述符 EJB有一个可选的XML部署描述定义在EJB JAR文件的META-INF/ejb-jar.xml中. 你可以使用部署描述和批注中的一个,增加元数据就不用定义批注,或者覆盖批 注.选择是由你决定.这是部署描述提供一个完整的可替代批注的定义 ProcessPayment组件: ProcessPaymentBean com.titan.processpayment.ProcessPaymentRemote com.titan.processpayment.ProcessPaymentLocal com.titan.processpayment.ProcessPaymentBean Stateless theDatasource javax.sql.DataSource Container titandb com.titan.processpayment.ProcessPaymentBean dataSource min java.lang.Integer 250 com.titan.processpayment.ProcessPaymentBean minCheckNumber 关于唯一的XML配置部署非常的有趣,如果你用了 RuntimeException 并非组件 类的EJBException.你的Java代码将不会引用到EJB规范 API.如果你看一下Java 代码,你甚至不会知道它是 EJB. 元素在中定义,设置部署的EJB集合.元 素表示你部署一个会话Bean.给会话Bean一个标识,这个你可以引 用.无素标识组件的业务接口,并且定义组件 类.无素标识会话Bean是一个无状态会话Bean.resource-ref> 和 初始化组件类的 datasource和minCheckNumber字段(细节见14 章). XML部署描述符也支持部分描述.例如,你想要在XML中配置minCheckNumber字段, 你不需要定义全部的元数据: ProcessPaymentBean min java.lang.Integer 250 11.2.SessionContext (会话的上下文) javax.ejb.SessionContext接口提供了一个到EJB容器环境的视 图.SessionContext对象可以像组件实例的接口到EJB容器一样获取关于方法调 用的上下文信息,和提供快速的访问EJB服务.一个会话Bean可以获得一个会话上 下文的引用通过@Resource 批注: @Stateless public class ProcessPaymentBean implements ProcessPaymentLocal { @Resource SessionContext ctx; ... } SessionContext允许你获取如同当前用户调用EJB,或查找实体在EJB ENC中.让 我们看一下这个接口的定义: public interface javax.ejb.SessionContext extends javax.ejb.EJBContext { EJBLocalObject getEJBLocalObject( ) throws IllegalStateException EJBObject getEJBObject( ) throws IllegalStateException; MessageContext getMessageContext( ) throws IllegalStateException; getBusinessObject(Class businessInterface) throws IllegalStateException; Class getInvokedBusinessInterface( ); } getEJBObject( ) 和 getEJBLocalObject( )方法是陈旧的并且调用它们时会抛 出一个异常.这些对像是在EJB的2.1规范中的风格. SessionContext.getBusinessObject( ) 方法返回一个引用到当前EJB,它可以 被其它的用户调用.这个EJB的引用等价于Java中的this指 针.businessInterface 参数必需是EJB的本地或远程接口中的一个,这样容器才 会知道当前的组件是否创建一个远程或本地引用.getBusinessObject( )方法允 许组件实例获取对自身对象的引用,它可以传递到其它组件,下面是一个例子: @Stateless public class A_Bean implements A_BeanRemote { @Resource private SessionContext context; public void someMethod( ){ B_BeanRemote b = ... // Get a remote reference to B_Bean. A_BeanRemote mySelf = getBusinessObject(A_BeanRemote.class); b.aMethod( mySelf ); } ... } 一个组件实例传递一个this引用到另一个组件是不合法的;取而代之的是,它可 以传递远程或本地EJB对象引用,组件实例的获取从它的SessionContext. SessionContext.getInvokedBusinessInterface( )方法允许你确定是否EJB调 用通过它的远程,本地,或Web Service接口.它返回调用业务接口的类. 11.2.1. EJBContext接口 SessionContext 继承javax.ejb.EJBContext类.EJBContext定义了一些为组件 运行期间提供非常有用的信息的一些方法.下面是EJBContext接口的定义: package javax.ejb; public interface EJBContext { public Object lookup(String name); // EJB 2.1 only: TimerService public TimerService getTimerService( ) throws java.lang.IllegalStateException; // security methods public java.security.Principal getCallerPrincipal( ); public boolean isCallerInRole(java.lang.String roleName); // transaction methods public javax.transaction.UserTransaction getUserTransaction( ) throws java.lang.IllegalStateException; public boolean getRollbackOnly( ) throws java.lang.IllegalStateException; public void setRollbackOnly( ) throws java.lang.IllegalStateException; // deprecated and obsolete methods public java.security.Identity getCallerIdentity( ); public boolean isCallerInRole(java.security.Identity role); public java.util.Properties getEnvironment( ); public EJBHome getEJBHome( ) java.lang.IllegalStateException; public EJBLocalHome getEJBLocalHome( ) java.lang.IllegalStateException; } EJBContext.lookup( )方法允许你查找实体在EJB的 ENC中并且使其更容易. EJBContext.getTimerService( )方法返回一个到容器的时间服务的引用, 允许无状态会话Bean为自己建立时间事件的通知.换句话说,一个会话Bean可以 设置一个警报,以便容器调用它在一个指定的日期或间隔性的传递.一个时间服 务也可以通过@Resource 批注来注入.时间服务的细节将会在第13章中进行介 绍. EJBContext.getCallerPrincipal( )方法用来获取客户端当前访问的组件的 java.security.Principal对象的引用.主要的对象罐,举例来说, 通过用EJB来 跟踪标识用户的更新. @Stateless public class BankBean implements Bank { @Resource SessionContext context; ... public void withdraw(int acctid, double amount) throws AccessDeniedException { String modifiedBy = principal.getName( ); ... } ... } EJBContext.isCallerInRole( )方法告诉你,是否客户端的访问组件是以一个指 定的成员角色,标识通过角色名.这个方法非常有用当多个访问控制,需要方法为 基础的简单访问控制提从.在一个银行系统中,例如,你可能允许出纳员这个角色 来做大多数的退回操作,但是,只有经理角色可以处理超过$10,000的退回.因为 它包括一个业务逻辑问题,所以这种细致的近代制不能够使用EJB的安全属性来 从事.因此,我们使用isCallerInRole()方法提供给EJB,增 加自动访问控制.首 先,我们假定所有的经理也都是出纳员.withdraw( ) 方法中的业务逻辑使 用 isCallerInRole( )来确定仅有经理角色的可以撤回总数超过$10,000: @Stateless public class BankBean implements Bank { @Resource SessionContext context; public void withdraw(int acctid, double amount) throws AccessDeniedException { if (amount > 10000) { boolean isManager = context.isCallerInRole("Manager"); if (!isManager) { // Only Managers can withdraw more than 10k. throw new AccessDeniedException( ); } } } ... } 事务方法getUserTransaction( ), setRollbackOnly( ), 和 geTRollbackOnly( )的细节在第16章中会进行描述. EJBContext包含的一些方法被用在旧的EJB规,但是已经被EJB3.0放弃了.安全方 法的交互同Identity类,像getEnvironment( ), EJBContext.getEJBHome( ), 和 EJBContext.getEJBLocalHome( ) 一样好的方法.已经被废弃.如果执行这些 方法会抛出RuntimeException异常. 具体的EJBContext在本章中应用的与消息驱动Bean相同.然而,有一些异常,而这 些会在12章中讲到. 11.3.无状态会话Bean的生命周期 无状态会话Bean的生命周期非常的简单.它有两种状态:不存在和方法-就绪池. 方法-就绪池是一个由没有使用的无状态会话Bean对象组成的实例池.因为所有 的注入都可能发生,如果你不使用它可能会更有效的保存无状态会话Bean的实例. 这是无状态会话Bean和有状态会话Bean之间最大的不同;无状态会话Bean定义实 例在池中,在他们的生命周期中,有状态会话Bean不会这样.图11-1所示的状态和 无状态会话Bean实例的生命周期的转变. [*] 一些厂商可能没有无状态实例池,但是,会使用方法调用来代替创建和销毁 实例.这是由实现的规范决定,并不会影响无状态会话Bean实例的生命周期. Figure 11-1. Stateless session bean life cycle 11.3.1. 不存在状态 当一个Bean在不存在状态时,它不是一个实例在系统的内存中,换句话来说,它还 没有被实例化. 11.3.2.方法-就绪池 在容器需要它们的时候,无状态会话Bean的实例进入方法-就绪池中.当EJB服务 器首次启动时,它会创建一定数量的无状态会话Bean和使它们进入到方法-就绪 池中(服务器的行为依赖执行).当创建的无状态会话Bean的数量不够时,会创建 更多的实例添加到池中. 11.3.2.1. 转换到方法-就绪池 当一个实例从不存在转换到方法-就绪池状态,会执行三种操作.首先,无状态会 话Bean通过Class.newInstance( )方法调用进行初始化.第二,容器注入任意资 源,组件无数据请求通过注入或XML部署描述符. 注意:你需要提供一个默认的构造.一个默认的构造是public类型的无参构造. 容器通过Class.newInstance( )来实例化组件类,它要求一个无参的构造函数. 如果你没有定义无参的构造,这个无参构造是暗含的. 最后,EJB容器将宣布一个后建造的结果.组件类可以注册这个事件通过批注一个 方法使用with @javax.annotation.PostConstruct.在这个实体被实例化后,这 个批注方法会被容器调用.这个回调方法可以是任意名,但是返回类型必需是 void,没有参数,并且抛出没有检查的异常.组件类可仅能定义一个使用 @PostConstruct批注的方法(但它不是必需的). @Stateless public class MyBean implements MyLocal { @PostConstruct public void myInit( ){} 另一种方法是,你可以定义@PostConstruct方法在EJB的XML部署描述符中: MyBean myInit 无状态会话Bean不受激活的影响,所以他们可以保持连接到资源为实体的生命周 期.[*]@PreDestroy方法可以关闭任何打开的之源在无状态会话Bean结束生命周 期被从内存赶出之前.关于 @PreDestroy 你可以在后面段中学到. 假定无状态会话Bean的生命周期持续的非常长.然而一些EJB服务实际上创建和 删除实例使用方法调用,使这种策略很少被使用.关于你的EJB服务器如何处理无 状态会话Bean的实例,你需要参考厂商的文档. 11.3.2.2. 在方法-就绪池中的生命 一旦一个实例在方法-就绪池中,它将准备服务于客户的请求.当一个客户调用 一个业务方法在EJB对象上,方法会调用代表池中任意有效实例,在方法-就绪池 中.当实例执行一个请求时,它对于其它的EJB对象是不可用的.无状态会话Bean 的实例公服务于一个EJB对象在单个方法调用期间. 当一个实例被交换的时候, 它的 SessionContext 改变会反映到用户端和调用 它的方法上.组件实例可能会包含客户请求的事务范围内并且它访问的 SessionContext信息指向特定的客户请求,它不再关联EJB对象并且反回到方法 -就绪池中. 客户端需要一个远程或本地引用到无状态会话Bean,开始引用注入(servlet支持 注入,例如)或者通过查找无状态会话Bean的JNDI.这个引用的返回并不是因为会 话Bean实例的创建或撤出从池中直到方法调用它.. 一个实例的生命周期中调用PostConstruct只能有一次:当它从不存在状态到方 法-就绪池中.它在每次不能再回调,一个客户请求一个远程引用到组件. 11.3.2.3.从方法-就绪池中转出:无状态Bean实例的死亡 当服务器不再需要它们时,Bean实例将会从方法-就绪池中离开,转到不存在状 态,当服务器决定减少方法-就绪池中的总数量,会逐出一个或多个实例从内存 中.当组件的PreDestroy事件被触发时,处理开始.组件类可以注册这个事件通过 使用@javax.annotation.PreDestroy批注一个方汉.当这个事件被激活时,容器 会调用这经注方法.这个回调方法可以是任意名,但是它必需返回void,没有参数, 并且抛出没有检查的异常.组件类中仅能定义一个@preDestroy方法(但是它不是 要求必需有的).一个@PreDestroy 回调方法可以执行任意清除操作,像关闭打开 的资源. @Stateless public class MyBean implements MyLocal { @PreDestroy public void cleanup( ){ ... } 另一种选择是,你可以定义@PreDestroy方法在EJB的XML部署描述符中: MyBean cleanup 像@PostConstruct, @PreDestroy 方法的调用仅能一次:当组件转换到不存在状 态时.在这个回调期间,SessionContext和访问的JNDI ENC对于组件的实例仍然 是可以的.在执行@PreDestroy 方法之后,组件解除引用并且最终被回收. 11.4.有状态会话Bean 每一个有状态会话Bean的实例在其生命内服务于一个客户;它充当客户的代理. 有状态会话Bean不能在EJB对象之间交换,他们保持一个实例在池中.像无状态会 话Bean.一旦无状态会话Bean被初始化,并且分配到一个EJB对象,它就贡南整个 生命周期给那个EJB对象. 注意:这是概念模型.一些EJB容器可能实际上使用实例交换同有状态会话Bean但 是,使用相同的实例来服务于所有的请求.然而,相同的有状态会话Bean实例服务 于所有的请求. 有状态会话Bean维持一个会话状态,意味着组件的变量可以保持在方法调用这间 的客户指定的数据.这使得方法相互依赖成为可能,所以,在一个方法调用中的组 件状态的改变,会影响并发调用方法的结果.因此,客户的每一个方法调用必需通 过相同的实例(至少概念),所以组件实例的状态可以预测从一个方法调用到下一 个.比较而言,无状态会话Bean不保持客户的特定数据从一个方法调用到下一个, 所以任意实例可以服务于客户的任何方法调用. 虽然,有状态会话Bean维持会话状态,但是他们本身并不会像实体Bean一样持久, 实体Bean代表数据库中的数据.他们的持久化字段会直接写入到数据库中.有状 态会话Bean可以访问数据库,但是,并不代表数据在数据库中. 有状态会话Bean常常考虑客户端的扩展.如果你把它想像成由运算和状态组成的 一个客户端,这很有意义.第一个任务也能有依赖一些信息的集合或者是前面操 作的改变.一个GUI客户端是一个理想的例子:当你填充字段在一个图形化的客户 端,你将建立会话状态.按下一个按钮会执一个操作,会真充更多的字段,基于你 先前进入了的信息.在栏位的信息是会话的状态. 有状态会话Bean允许你放入一些业务逻辑和客户的会话状态并且移动它到服务 器上.移动这些逻辑到服务器使客户应用程序变得瘦小,并且系统变成一个整体 易于管理.有状态会话Bean做为客户端的代理,管理程序或完成一组任务;它管理 与其它组件间的交互在一些数据访问的操作上,完成一组复杂的任务.通过装入 和管理任务在客户端的行为上,有状态Bean呈现一个科单的接口,隐藏在数据库 上的交互操作的细节和客户的其它组件. 11.4.1.提高TravelAgent组件能力 TravelAgent组件使用Cabin, Cruise, Reservation, 和 Customer实体组件开 发在第6和第7章中.它将会协调这些实体组件之间的交互,作用在一次巡航上订 购的一个位乘客.我们将修改在第7章中使用的实体Reservation,它能立刻与它 的全部关系一起建立识别.换句话说,我们除了定义一个默认的构造函数外,还将 定义另一个构造函数. public class Reservation { public Reservation( ){} public Reservation(Customer customer, Cruise cruise, Cabin cabin, double price, Date dateBooked) { setAmountPaid(price); setDate(dateBooked); setCruise(cruise); Set cabins = new HashSet( ); cabins.add(cabin); this.setCabins(cabins); Set customers = new HashSet( ); customers.add(customer); this.setCustomers(customers); } 建立这个构造将使我们避免调用那些setter方法在TravelAgent 组件代码中,并 且减少混乱. 11.4.1.TravelAgent组件 在前面我们已经看过,一个有状态会话Bean包装处理在一次巡航中的预定.我们 将进一步完善这个组件来示范有状态会话Bean可以使用的业务对象.我们不能为 了扩展它而使用本地接口,部分因为它是被设计来供远程调用的(因此,不需要本 地组件接口),而且部分因为开发有状态会话Bean的本地接口类同于开发无状态 会话Bean. 11.4.2.1.远程接口:TravelAgent 如同一个无状态会话Bean的模型任务,当维持会话的状态时候, TravelAgent EJB 处理一些其它组件之间的交互.下面是编辑的travelAgentRemote接口: package com.titan.travelagent; import com.titan.processpayment.CreditCardDO; import javax.ejb.Remote; import com.titan.domain.Customer; @Remote public interface TravelAgentRemote { public Customer findOrCreateCustomer(String first, String last); public void updateAddress(Address addr); public void setCruiseID(int cruise); public void setCabinID(int cabin); public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState; } TravelAgent EJB 的目的是航预定.为了完成这个任务,组件需要知道cruise, cabin, 和 customer 组成的预定.因此,客户使用TravelAgent EJB 来收集各种 预定信息.TRavelAgentRemote接口提供方法设置用户要预定的巡航和小屋的ID. 我们假定小屋ID从一个列表并且巡航ID从其它的源获得.客户端将传递用户名到 findOrCreateCustomer( )方法.如果数据库中存在此用户,TravelAgent EJB 将 使用这个用户;否则,将创建它. customer, cruise, 和 cabin 一经选择,TravelAgent EJB将会准备预定的处理. 这个操作的执行通过bookPassage( )方法,它需要用户的信用卡信息和巡航所需 的价钱.bookPassage( )依赖于装入的用户帐号,预定选择的船舱,正确的船,正 确的巡航,和生成一张用户的票.这是如何完成的,此时对我们已经不重要了;当 我们正在开发一个远程接口时,我们会考虑到组件业务的定义.当我们谈论组件 类时,我们会讨论它的实现. 注意:bookPassage( )方法抛出一个应用指定的异 常,IncompleteConversationalState.当一个用户预定一次巡航的时候,连接业 务问题时会遇到这个问题.IncompleteConversationalState异常指示 TravelAgent EJB没有足够的信息来处理预定.下面是 IncompleteConversationalState 类: package com.titan.travelagent; public class IncompleteConversationalState extends java.lang.Exception { public IncompleteConversationalState( ){super( );} public IncompleteConversationalState(String msg){super(msg);} } 11.4.2.2.域对象:TicketDO类 像用于付款组件的CreditCardDO 和 CheckDO类,TicketDo类定义一个值传递对 象.一张票应该是一个预定的实体,因为它是一个古老的Java对象并且被序列化 的可以从客户端传递回来.然而,确定一个业务对象的使用也可以指定它是一个 组件或是一个简单的类.因为Rservervation实体参照很多其它的实体,这些序列 化对象从远程返回会变得非常大,如此,变得效率低下.用TicketDO对象,你可以 整理精确的信息,送回到客户端. TicketDO的构造使用其它的实体数据: package com.titan.travelagent; import com.titan.domain.Cruise; import com.titan.domain.Cabin; import com.titan.domain.Customer; public class TicketDO implements java.io.Serializable { public int customerID; public int cruiseID; public int cabinID; public double price; public String description; public TicketDO(Customer customer, Cruise cruise, Cabin cabin, double price) { description = customer.getFirstName( )+ "" + customer.getLastName( ) + " has been booked for the " + cruise.getName( ) + " cruise on ship " + cruise.getShip( ).getName( ) + ".\n" + " Your accommodations include " + cabin.getName( ) + " a " + cabin.getBedCount( ) + " bed cabin on deck level " + cabin.getDeckLevel( ) + ".\n Total charge = " + price; customerID = customer.getId( ); cruiseID = cruise.getId( ); cabinID = cabin.getId( ); this.price = price; } public String toString( ){ return description; } } 11.4.2.3.使用客户视图 在定义你的组件接口之前解决,是一个很好的主意,指出客户端怎样使用组件.假 定TravelAgent EJB 的使用是通过一个GUI的Java应用程序字段.这些字段捕获 用户引用的巡航和般舱的类型.我们检查你码使用开始预定的处理: Context jndi = getInitialContext( ); Object ref = jndi.lookup("TravelAgentBean/remote"); TravelAgentRemote agent = (TravelAgentRemote) PortableRemoteObject.narrow(ref, TravelAgentRemote.class); 这个代码会简单的查找TravelAgent EJB 在JNDI中.这样做实际上是创建一个会 话来服务于客户端和描述通过代理变量. 注意:每次在你查找一个有状态会话Bean在JNDI中,一个新的会话被建立. Customer cust = agent.findOrCreateCustomer(textField_firstName.getText( ), textField_lastName.getText( )); 这个代码定位一个已经存在的用户或者是通过电话聚集息信创建一个新的客户. 调用findOrCreateCustomer( )事实上会存储用户被引用存在TravelAgent EJB 的内部状态.现实中的旅行代理也会收集任何地址的改变到客户端,并且使这使 些改变到服务器上. Address updatedAddress = new Address(textField_street.getText( ), ...); agent.updateAddress(updatedAddress); 然后,我们收集cruise和cabin选择从另一部分客户端应用程序: Integer cruise_id = new Integer(textField_cruiseNumber.getText( )); Integer cabin_id = new Integer( textField_cabinNumber.getText( )); agent.setCruiseID(cruise_id); agent.setCabinID(cabin_id); 旅行代理程式选择巡航和小屋客户希望保留. 这些ID被设置在 TravelAgent EJB,这为整个的程序维持会话的状态. 在程序结束的时候,旅行代理程式藉由处理那个订购而且产生一张票完成预定. 因为 TravelAgent EJB 已经维护会话的状态,存储客户,小屋和巡航信息,仅信 用卡和价钱需要完成处理. String cardNumber = textField_cardNumber.getText( ); Date date = dateFormatter.parse(textField_cardExpiration.getText( )); String cardBrand = textField_cardBrand.getText( ); CreditCardDO card = new CreditCardDO(cardNumber,date,cardBrand); double price = double.valueOf(textField_cruisePrice.getText( )).doubleValue( ); TicketDO ticket = agent.bookPassage(card,price); PrintingService.print(ticket); 这一个摘要用户端将会如何使用 TravelAgent EJB 确定我们的远程接口是能工 作的.现在我们可以向前发展的开发. 11.4.2.4. 组件类: TravelAgentBean 我们现在实现了远程接口为TravelAgent EJB提供的行为的全部.这是一个新的 TravelAgentBean 类的完整定义: package com.titan.travelagent; import com.titan.processpayment.*; import com.titan.domain.*; import javax.ejb.*; import javax.persistence.*; import javax.annotation.EJB; import java.util.Date; @Stateful public class TravelAgentBean implements TravelAgentRemote { @PersistenceContext(unitName="titan") private EntityManager entityManager; @EJB private ProcessPaymentLocal processPayment; private Customer customer; private Cruise cruise; private Cabin cabin; public Customer findOrCreateCustomer(String first, String last) { try { Query q = entityManager.createQuery("from Customer c where c.firstName = :first and c.lastName = :last"); q.setParameter("first", first); q.setParameter("last", last); this.customer = (Customer)q.getSingleResult( ); } catch (NoResultException notFound) { this.customer = new Customer( ); this.customer.setFirstName(first); this.customer.setLastName(last); entityManager.persist(this.customer); } return this.customer; } public void updateAddress(Address addr) { this.customer.setAddress(addr); this.customer = entityManager.merge(customer); } public void setCabinID(int cabinID) { this.cabin = entityManager.find(Cabin.class, cabinID); if (cabin == null) throw new NoResultException("Cabin not found"); } public void setCruiseID(int cruiseID) { this.cruise = entityManager.find(Cruise.class, cruiseID); if (cruise == null) throw new NoResultException("Cruise not found"); } @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); processPayment.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); return ticket; } catch(Exception e) { throw new EJBException(e); } } } 这里有我们需要消化的很多代码,所以,我们会将它划分成小块来学习它. @Stateful public class TravelAgentBean implements TravelAgentRemote { @PersistenceContext(unitName="titan") private EntityManager entityManager; @EJB private ProcessPaymentLocal processPayment; TravelAgent EJB需要一个引用到泰坦实体管理服,所以它才可以查找,更新,和 创建需要的实体组件.@javax.persistence.PersistenceContext批注的目的是 引起EJB容器初始化entityManager字段.ProcessPayment EJB 同样也需要能够 设置一个信用卡的服款.@javax.ejb.EJB 批用于向一个无状态会话Bean的引用, 有很多的方法,如ProcessPayment EJB 组件类中的datasource字段使用 @Resource批注来初始化.第14章中讨论这些批注的详细语法. 下一步,让我们检查一下findOrCreateCustomer( )方法: public Customer findOrCreateCustomer(String first, String last) { try { Query q = entityManager.createQuery("select c " + + "from Customer c " + "where c.firstName = :first and c.lastName = :last"); q.setParameter("first", first); q.setParameter("last", last); this.customer = (Customer)q.getSingleResult( ); } catch (NoResultException notFound) { this.customer = new Customer( ); this.customer.setFirstName(first); this.customer.setLastName(last); entityManager.persist(this.customer); } return this.customer; } findOrCreateCustomer( )方法在传递的两个参数的基础之上动态的创建一个查 询语句,来查找存在的用户.Query.getSingleResult( )方法会抛出一个 javax.persistence.NoResultException异常.如果被抛出,告诉我们一个新的用 户实体必需被创建. public void updateAddress(Address addr) { this.customer.setAddress(addr); this.customer = entityManager.merge(customer); } updateAddress( )方法简单的将客户地址的改变同步到数据库中.customer字段 不再被持久化上下文所管理,它的初始化与findOrCreateCustomer( )方法分离, 并且,当方法结束时分离.因为Customer实例被分离,EntityManager.merge( )方 法用来更新改变的用户地址. TravelAgent EJB 为想得到的cruise和cabin设置了方法.这些方法使用int类型 的ID作为参数并且重新找会适当的Cruise或Cabin实体从注入的EntityManager 中.这些引用也是TravelAgent EJB会话状态的一部分.setCabinID() 和 getCabinID() 如何被定义在这里: public void setCabinID(int cabinID) { this.cabin = entityManager.find(Cabin.class, cabinID); if (cabin == null) throw new NoResultException("Cabin not found"); } public void setCruiseID(int cruiseID) { this.cruise = entityManager.find(Cruise.class, cruiseID); if (cruise == null) throw new NoResultException("Cruise not found"); } 我们通过 EntityManager.find( )方法来查找Cabin和Cruise实体.如果这个方 法返回null,我们将抛出一个NoResultException到客户端,通知它Cabin 或 Cruise 是无效的. 你可能会很奇怪我人产设置这些使用int类型的ID,但是我们要保持实体的引用 在会话状态.在客户端的代码中,我们获取cabin和cruise的ID是从文本字段中. 当一个ID很简单的时候,为什么使用客户端获取一个组件的引用到Cruise和 Cabin实体?同时,我们可以等待,直到bookPassage()方法在重建远程引用前被调 用,但是这个策略保持bookPassage()方法简单. 11.4.2.5.bookPassage( ) 方法 最后有趣的一点是我们定义的bookPassage( )方法.这一个方法使用被 findOrCreateCustomer() , setCabinID() 和 setCruiseID() 累积方法为一个 客户处理预定的会话的状态.在这里是定义的 bookPassage(): @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); return ticket; } catch(Exception e) { throw new EJBException(e); } } 这一个方法示范 taskflow 观念. 它使用一些Bean.包括ProcessPayment EJB 和Reservation, Customer, Cabin, 和Cruise实体,完成一个任务:一个客户在 一次巡航上的预定.看似简单,这个方法装入一些些平常被执行在客户段上的交 互. 为了在客户段调用bookPassage( )方法,TravelAgent EJB 执行下面的和, 在这个顺序中: 1.创建一个Reservation 对象 2.在实体管理服务器中持久化这个新建的Reservation 对象 3.使用ProcessPayment EJB来管理用户的信用卡 4.用描述客户的购买的所有的相关信息产生新的 TicketDO. 注意:bookPassage( )个方法使用了@javax.ejb.Remove annotation批 注.@Remove批注告诉EJB容器,当方法完成时,客户不再需要会话.在 bookPassage()方法完成后,EJB容器将会移除会话从EJB容器中. TravelAgent EJB 现在已经完成. 我们从设计的观点来看,一个无状态会话Bean 把一个任务封装到内部,为客户和更多的灵活性,这意味着一个不那么复杂的界 面.我们可以很容易的改变bookPassage()检查出重复订票(当一个用户预定两次 巡在重复的时间里).这种类型的增强不必变远程接口,所以客户端的应用程序不 需要修改.有状态会话Bean会所任务封装到其内部,允许系统在不影响客户的情 况下改变. 此外,客户端的类型可以改变.在可测量性和交易处理控制中最大的问题是两点 间的业务逻辑纠缠不清同客户端的逻辑.因此在使 用不同客户的不同业务逻辑 是很困难的.如果使用无状态会话Bean,这就不会是问题,因为无状态会话Bean是 一个客户的扩展,而且不与客户段的逻辑绑定.比如,我们实现预定系统使用Java 的小应用程序同GUI容器.TravelAgent EJB将会管理会话状态和执行所有的业务 逻辑当小应用程序关注GUI表达时.如果过期,我们决定使用一个瘦客户端(例如, 通过Java的Servlet生成HTML),我们将简单的重用TravelAgent EJB 在Servlet 中.因为所有的业务逻辑在有状态会话Bean中,改变(Java小应用程序或Servlet 或其它的事情)变得更容易. TravelAgent EJB 也提处理用户预定的事务集成处理.如果bookPassage( )的主 要操作失败,所有的必变不会被应用并且回滚.如果信用卡没有被 ProcessPayment EJB改变,新创建的Reservation组件和与它关联的记录不会被 建立.在第16章中将会详细的节绍TravelAgent EJB的事务方面. 远程接口和本地接口可以在同样的任务中使用.例如,bookPassage()方法使用本 地引用当访部ProcessPayment EJB.这样使用是完体可以的.EJB容器确认事务是 自动提交的,远程或本地EJB的失败将会影响整个事务. 11.4.2.6. XML部署描述符 提供 TravelAgent EJB 的一个完整的可替代批注的,选择定义的一个配置记述 文件在这里: TravelAgentBean com.titan.travelagent.TravelAgentRemote com.titan.travelagent.TravelAgentBean Stateful ejb/PaymentProcessor Session com.titan.processpayment.ProcessPaymentLocal com.titan.travelagent.TravelAgentBean processPayment persistence/titan titan com.titan.travelagent.TravelAgentBean entityManager 有状态会话Bean和无状态会话Bean在语法上有一点微小的差异. 无素设置成Stateful而不是Stateless.你也会注意到我们使用 来初始化组件类的processPayment和 entityManager 字段.这些元素在第 14 章中被更详细地解释. 11.5.有状态会话Bean的生命期 有状态会话Bean和其它的组件类型,如无状态会话Bean,的最大不同是它不使用 实例池.有状态会话Bean在整个生命周期内只服务于一个客户,所以,交换或实例 池是不可能的.当它们空闲下来的时候,有状态会话Bean的实例会被回收从内存 中.EJB对象仍连接到客户端.但是,组件的实例已经被废弃和在不活动时会被垃 圾回收器收回.这意味着,每一个有状态Bean必需在被收回之前被钝化为了保存 信实例的会话状态,并且当EJB对象被激活时,它要恢复状态. 注意:一些提供商会有状态会话Bean同池一起使用,但这个专让的实施并不会影 响有状态会话Bean指定的生命周期. 组件的生命周期依赖于它是否实现了指定的接口 javax.ejb.SessionSynchronization .这个接口定义了额外的回调方法集,可以 通知组件参与事务.一个实现SessionSynchronization的组件能够在更新前存储 数据库中的数据跨过多个方法的调用.本章中不讨论事务的细节;我们将会在第 16 章中考虑组件的寿命周期的一部分.这一节中我们将描述不实现 SessionSynchronization 接口的有状态会话Bean的生命周期. 有状态会话Bean的生命周期中存在三种状态:不存在,方法-就绪,和钝化.这听 起来与无状态会话Bean很相像,但是方法-就绪状态不同于无状态会话Bean的方 法就绪池.图11-2指出有状态会话Bean的状态图. 11.5.1.不存在状态 一个有状态组件的实例在不存在状态是它还没有被初始化.它还不存在在系统的 内存中. 11.5.2.方法-就绪状态 方法-就绪状态是组件的实例可以服务于来自客户端的请求.这节将讨论是否存 在事务的方法-就绪状态. 11.5.2.1.转换到方法-就绪状态 当实各户端首次调用方法在有状态会话Bean上的引用时,组件的生命开始.容器 调用newInstance()方法在组件类上,创建一个组件的实例.下一步,容器注入一 些依赖到组件的实例上.此时,组件的实例被指定到客户端引用.最后,像无状态 会话Bean,容器中如果有一个方法使用@PostConstruct 批注将会调用这个回调 方法.@PostConstruct完成后,容器继续它的实际方法调用. 11.5.2.3.方法-就绪状态的生命 当进入方法-就绪状态,Bean的实例平等的接收客户端的方法调用,可能会包括 控制其它的组件或直接地存取数据库.在这期间,组件会保持会话状态并且打开 资源,在它的实例变量上. 11.5.2.3. 转换出方法-就绪状态 实例将离开方法-就绪状态进入到钝化状态或不存在状态.依赖用户端如何使用 有状态的Bean,EJB容器的装载,和商厂的钝化运算,一个Bean实体可以被钝化(和 激活)很多次在它的生命里,或从来没有被钝化过.如果Bean被移除,它会进入不 存在状态.一个客户端应用程序可以移除一个Bean通过使用了@Remove批注的业 务接口的调用. 如果超时容器也可以移除Bean的实例从方法-就绪状态到不存在状态.超时的定 义是由厂商指定管理.当超时发生在方法-就绪状态,容器也许,但并不是必需的, 去调用@PreDestroy回调方法.当在处理过程中,有状态会话Bean是不能超时的. 11.5.3.钝化状态 在会话Bean的生命里,可能有数个不活动的实例没有用于维护客户端的方法调用. 为了保存资源,容器会钝化Bean的实例通过保存它们的会话状态和驱逐Bean的实 例从内存中.一个组件的会话状态可能包含原始值,序列化对象,和下面指定的特 殊类型: ◆javax.ejb.SessionContext ◆javax.jta.UserTransaction (bean transaction interface) ◆javax.naming.Context (only when it references the JNDI ENC) ◆javax.persistence.EntityManager ◆javax.persistence.EntityManagerFactory ◆References to managed resource factories (e.g., javax.sql.DataSource ) ◆References to other EJBs 在这一个列表 (和他们的子类型) 的类型特别地被钝化机制处理.它们不需要序 列化;当Bean的实例被激活时,他们将自动的通过钝化和恢复来保存. 当一个Bean要钝化,它的方法将会使用@PrePassivate 批注,接收一个回调在这 个事件上.这可以用来提醒Bean的实例,它正要进入钝化状态.在这时,实例将会 关闭打开的任何资源和设置所有的l暂时的,非序列化的字段为空.当Bean是序列 化的,是为了防上问题的发生.非持久化的字段会被忽略. 容器是怎样存储Bean的会话状态的呢?它主要是由容器决定.容器可以使用标准 的序列化的Java对象来保存Bean的实例,或达成相同的结果的一些其他的机制. 一些厂商, 举例来说, 只是在一个高速缓冲存储器中读栏位的数值而且储存他 们.容器要求保存远程引引到其它Bean同会话状态.当Bean被激活时,容器必需自 动恢复任何Bean的引用.容器也必需恢复任何前面列表中列出的指定类型的引 用. 当客户端请求一个被钝化的EJB对象时,容器会激活它的实例.这会包括非序列化 Bean的实例和重建SessionContext的引用,组件的引用,和管理资源的工厂,被实 例处理,在它被钝化之前.当一个Bean的会话状态被成功恢复后,如果组件类定义 了@PostActivate 回调,那么组件类的实例会调用它.组件类应该打开任意资源, 除了那些被the @PostActivate批注的暂时性的字段值和不能被钝化的资源.当 @PostActivate 完成后,组件返回到方法-就绪状态并且作为可以服务于客户端 请求的EJB对象的代表. 组件实例的激活遵循JAVA的序列化规则,不管组件的状态实际上是如何被存储的. 异常到非持久化字段.在Java序列化过程中,当对象是非序列化时,非持久化字段 被设置成默认值.原始数值类型变成0,布尔型变成假,对象类型引用为null.在 EJB中,当组件被激活时,临时字段可以包含任意的数值.在激活之后,临时字段的 值是不被厂商所预知的,因此不能信赖产初始设定的值.相反的,应该使用 @PstActivate 回调方法来重设他们的值. 容器也可以移动组件实例从钝化状态到不存在状态,如果组件超时.当超时发生 在钝化状态,任意@PreDestroy回调方法都不能调用. 11.5.3.系统异常 第当系统异常被组件的方法抛出时,容器会使EJB对像无效,并且会销毁Bean的实 例.组件的实例直接移到不存在状态,并且任意的@PreDestroy 回调方法都不能 被调用. 注意:这是规范中的一个漏洞. 一个系统异常 是任意没有检查的异常,没有批注的如一个 @ApplicationException ,包括EJBException.系统异常和应用异常的细节将会 在第十六章中详细介绍. 11.6.有状态会话Bean和持久化上下文的继承 在第五章中,我们讨论过事务范围持久化上下文和一个扩展的上下文的不 同.EntityManager注入到TRavelAgentBean 类同@PersistenceContext批注是一 个事务范围持久化上下文.在第 16 章中,我们将会看到travelAgentBean里的每 一个方法都有一个事务开始化结束的范围方法.这意味着你保持或取来的任何实 体的实例,在方法调用后的持久化上下文结束时被分离.任何本地存储的 EntityManager 将会丢失.这是为什么我们为什么必须在 travelAgentBean.updateAddress() 方法中调用 EntityManager.merge() 而且 重新设定用户的成员变量.我们使用findOrCreateCustomer( ) 取来的Customer 实体在方法调用结束后就变成无人管理的了,并且EntityManager不再跟踪实例 状态的改变.果如装载了全部的实体在TRavelAgentBean方法调用之后仍管理它 们,这样会很好么?这就是EXTENDED持久化上下文的用处: 有状态会话Bean仅仅是EJB组件,它允许注入一个EXTENDED持久化上下文通过 @PersistenceContext 批注.在@PostConstruct之前,扩展的持久化上下文创建 并且绑定到有状态Bean的实例上.当有状态Bean的实例被移除时,扩展的持久化 上下文也被清除.因为有状态Bean要表现出同客户端的会话,它应该有存储和管 理实体实例跨跃方法调用.无状态会话Bean和消息驱动Bean使用池管理,并且管 理实体的实例很容易变得陈旧和无法使用.让我们看一下改变的 travelAgentBean 类,使用一个扩展持久化上下文: import static javax.persistence.PersistenceContextType.EXTENDED; @Stateful public class TravelAgentBean implements TravelAgentRemote { @PersistenceContext(unitName="titan", type=EXTENDED) private EntityManager entityManager; public void updateAddress(Address addr) { customer.setAddress(addr); } ... } 当持久化上下文的类型标记成EXTENDED,查询将保持管理和绑定到有状态会话 Bean的EntityManager,updateAddress( ) 方法变得简单了一些,如 EntityManager.merge()方法不再需要.当任何事务方法被调用时,使用了扩展的 持久化上下文的会话Bean将会被自动注册到一个事务中.当updateAddress( )方 法完成后,EJB 容器将会自动提交改变的状态,使客户不再需要手工调用merge() 或flush()方法. 11.7.有状态会话Bean的嵌套 当你注入其他的有状态会话Bean到他们里,它们的行为会变得很有趣.当你注入 一个有状态会话Bean到EJB中使用@EJB批注,一个会话创建注入实例: @Stateful public class ShoppingCartBean implements ShoppingCart{ @EJB AnotherStatefulLocal another; @Remove void checkout {} } 当有状态会话Bean引用注入的其它有状态会话Bean,包含的Bean有自己的注入会 话.这意味着,当包含的Bean被建立的时候,一个唯一的会话被建立用作注入参考. 当包含的Bean被移除后,包含的有状态Bean也会被移除.在ShoppingCartBean 例 子中,当它的一个实例被创建时,一个AnotherStatefulLocal会话的创建是为了 another成员变量.当checkout( ) 方法被调用,another的有状态会话也被移除. 这一特性的指定允许你清除聚集的会话业务征时,不用担心包含的有状态会话的 生命周期. 嵌套有状态会话Bean有一个有趣的效果那就是如果有一个已经存在的嵌套有状 态Bean注入一个EXTENDED持久化上下文,当发生时,包含与被包含的有状态会话 Bean共享相同的EXTENDED持久化上下文: @Stateful public class ShoppingCartBean implements ShoppingCart{ @EJB AnotherStatefulLocal another; @PersistenceContext(unitName="titan", tyxspe=EXTENDED) private EntityManager entityManager; @Remove void checkout {} } @Stateful public class AnotherStatefulBean implements AnotherStatefulLocal { @PersistenceContext(unitName="titan", type=EXTENDED) private EntityManager entityManager; } 实际上AnotherStatefulBean.entityManager 和 ShoppingCartBean.entityManager字段参考相同的持久化上下文.这意味着他们 也共享相同管理实体实例.需要注意的是,持久化上下文嵌套工作仅当引用是本 地的有状态会话接口的注入.如果嵌套的是远程接口,组件不会共享相同的 EXTENDED持久化上下文. 第十二章 消息驱动Bean 消息驱动Bean从EJB2.0开始就支持处理异步消息从一个JMS提供者.EJB2.1扩展 定义了消息驱动Bean,所以它可以支持任意消息系统,不仅是JMS通过JCA.EJB3.0 并没有真正扩展它的新特性在以前的版本上,但是它使用批注使配置更简单.这 一个章节调查以 JMS 为基础的信息驱动Bean和被扩大的信息驱动的 EJB 3.0 开发者能得到的组件模型. 12.1.JMS和消息驱动Bean 所有的EJB3.0提供商都支持一个JMS(Java Message Service)服务.大多数厂商 有一个JMS提供者内置并且支持其它的JMS提供者通过JCA(Java Connector Architecture Java连接体系).然而,除非你的提供商有它自己的JMS提供或者允 许你集成一些其它的提供者,一个JMS提供者完全的支持消息驱动Bean.通过强迫 JMS采用,Sun公司已保证EJB开发者能期待JMS的提供者将消息发送和接收到两 端. 12.1.1.JMS如同一个资源 JMS 是一个厂商-能被用存取信息系统的企业的中间的 API. 企业信息系统 (a.k.a. 信息定向的中介软件) 在一个网络上的软件应用程序之间促进信息的 交换.JMS 的角色不像 JDBC 的角色: 正如 JDBC 提供一个通常的 API 给存取 许多不同的表示关系的数据库, JMS 提供厂商-对信息系统的企业独立的存取. 虽然信息产品不是像数据库产品那样常见,但是并不缺乏支持JMS的消息系统,包 括包括 JBossMQ , IBM 的 MQSeries, BEA 的 WebLogic JMS服务, Sun 公司 微系统的 Sun ONE消息队列, 和索呢的SonicMQ.用JMS API 送的软件应用程序 或者接收信息从一个JMS厂商到另外的是便携模式. 应用程序使用JMS被叫做JMS客户端,而且处理工作路线排定和信息的递送的信息 系统叫做 JMS 提供者.一个JMS应用程序是由许多JMS 用户端组成的一个业务系 统, 通常, 一个JMS 提供者.传达一个信息的一个JMS 用户端叫做一位生产者 , 和接收一个信息的一个JMS用户端叫做一个用户.一个JMS 用户端可能是一位生 产者和一个用户. 在 EJB中 ,所有类型的Bean都可以使用 JMS 传达信息.信息被其它的Java应用 程序或通过消息驱动Bean被销毁.JMS使企业Bean使用消息服务发送消息变得很 容易,有时被称作消息的经济人或路由器.消息的经济人对一些十看之久的古老 的和最稳定的IBM MQSeriesbut JMS非常新,并且它明确的设计来传递一个消息 类型变量从一个Java应用程序到其它. 12.1.1.实现the TravelAgent EJB使用JMS 我们何以修改第十一章中的TravelAgent EJB,使用JMS来警告一些Java应用程序 预定已经完成.下面代码显示如何修改bookPassage()方法,TravelAgent EJB发 送一个简单的消息,描述获取从TicketDo对象: @Resource(mappedName="ConnectionFactoryNameGoesHere") private ConnectionFactory connectionFactory; @Resource(mappedName="TicketTopic") private Topic topic; @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); Connection connect = factory.createConnection( ); Session session = connect.createSession(true,0); MessageProducer producer = session.createProducer(topic); TextMessage textMsg = session.createTextMessage( ); textMsg.setText(ticketDescription); producer.send(textMsg); connect.close( ); return ticket; } catch(Exception e) { throw new EJBException(e); } } 在我们添加完所有代码看来有一点无法抗拒的同时,JMS的基本要求并非都那样 复杂. 12.1.1.2. ConnectionFactory 和 Topic 为了发送一个JMS消息,我们需要一个到JMS提供者和目的地的连接.一个JMS连接 工厂建立到提供者的连接;到目的地的连接通过一个Topic对象.二者的连接工厂 通过@javax.annotation.Resource注入到TravelAgent EJB的字段中. @Resource(mappedName="ConnectionFactoryNameGoesHere") private ConnectionFactory connectionFactory; @Resource(mappedName="TicketTopic") private Topic topic; ConnectionFactory类似于JDBC中的DataSource.如同 DataSource 提供 JDBC 连接到一个数据库,ConnectionFactory 提供 JMS 和一个信息路由器的关 联.[*] 这样比喻并不是很完美的.也可能说Session就像是 DataSource,因为两者表现 处理-资源的连接. Topic对象本身代表一个独立的网络目的地到消息的终点.在JMS中,消息并不是 直扫发送到应用程序的;他们选发送主题或队列.一个主题就像是一个电子邮件 列表或新闻群组;任何信任的应用程序都可以接收消息从而传达给一个主题的讯 息.当一个 JMS 用户端接收来自一个主题的信息的时候,用户端被说订购那一个 主题.JMS 由让他们经过目的地传达给彼此的讯息分离应用程序,这视为一个虚 拟的通道.一个储列是我们将会稍后详细地讨论的另外类型的目的地. 12.1.1.3. Connection and Session ConnectionFactory 用来创建一个连接,它实际上是连接到JMS的提供者: Connection connect = connectionFactory.createConnection( ); Session session = connect.createSession(true,0); 当你有了一个连接,你可以使用它来创建一个session.一个Session允许你成批 的发送和接收消息.在这情况,你只需单个Session.如果你希望产生和销毁消息 在不同的线程中,使用多个Session是有帮助的.Session 对象使用单线程模式, 它禁止多个线程同时访问一个Session.产生Session的线程,通常就是Session的 生产者和消费者.(也就是, MessageProducer 和 MessageConsumer 物件).如果 你希望产生和销毁消息使用多线程,你必需创建一个不同的Session为每个线程. createSession( )方法有两个参数: createSession(boolean transacted, int acknowledgeMode) 所以依照 EJB 规格,这些函数中的独立参数在运行时间被忽略.因为EJB容器管 理事务和JMS资源的承认模态从JNDI ENC中获取.规范推荐,开发者使用参数为 True对于transactedt和acknowledgeMode为0,但是因为他们之后被忽略,所以使 用那一个都没有关系.不幸的是,并不是所有的厂商都支持这部分规范,一些厂商 不理睬这些参数;其它的不是这样. 最好的程序实践是在使用它时,有一个好的连接习惯. Connection connect = factory.createConnection( ); ... connect.close( ); 12.1.1.4. MessageProducer Session是用来创建一个MessageProducer,它从TravelAgent EJB中发送消息到 Topic对象指定的目的地.任意订阅主题的JMS客户端将会接收一个消息的副本. MessageProducer producer = session.createProducer(topic); TextMessage textMsg = session.createTextMessage( ); textMsg.setText(ticketDescription); producer.send(textMsg); 12.1.1.5. 消息类型 在JMS中,消息是一个Java对象,它由两部分组成:消息头和消息主体.消息头由递 送信息和元数据组成,而且信息主体携带应用程序数据,可以有很多种形式:文本, 序列化对象,字节流,等等.JMS API 定义一些信息类型 (TextMessage , MapMessage , ObjectMessage, 和其它) 而且提供方法给递送的讯息而且接收 来自其他的应用程序的信息. 举例来说,我们能改变 TravelAgent EJB ,以便它传达 MapMessage 并非 TextMessage: TicketDO ticket = new TicketDO(customer,cruise,cabin,price); ... MessageProducer producer = session.createProducer(topic); MapMessage mapMsg = session.createMapMessage( ); mapMsg.setInt("CustomerID", ticket.customerID.intValue( )); mapMsg.setInt("CruiseID", ticket.cruiseID.intValue( )); mapMsg.setInt("CabinID", ticket.cabinID.intValue( )); mapMsg.setDouble("Price", ticket.price); producer.send(mapMsg); JMS的客户端可以通过名子来访问MapMessage (CustomerID, CruiseID, CabinID, and Price)的属性.作为另一种选择,TravelAgent EJB可以修改使用 ObjectMessage类型,将允许我们发送完整的TicketDO对象如同消息中使用序列 化对象: TicketDO ticket = new TicketDO(customer,cruise,cabin,price); ... MessageProducer producer = session.createProducer(topic); ObjectMessage objectMsg = session.createObjectMessage( ); ObjectMsg.setObject(ticket); producer.send(mapMsg); 除了TextMessage ,MapMessage 和 ObjectMessage 之外, JMS 提供其他二种类 型: StreamMessage 和 BytesMessage.StreamMessage 能够包含I/O流作为负 载.BytesMessage 可以使用字节数组,用于不透明的数据. 12.1.2. JMS 客户端的应用程序 为了得知道JMS是如何被使用的,较好的方法是,我们创建一个Java应用程序,来 接收和处理预定信息.当它接收信息的时候,这一个应用程序是打印每张票的描 述的一个简单的 JMS 用户端.我们将假定TravelAgent EJB使用TextMessage类 型发送票的描述信息到JMS用户端.这里是JMS应用客户端程序: import javax.jms.Message; import javax.jms.TextMessage; import javax.jms.ConnectionFactory; import javax.jms.Connection; import javax.jms.Session; import javax.jms.Topic; import javax.jms.JMSException; import javax.naming.InitialContext; public class JmsClient_1 implements javax.jms.MessageListener { public static void main(String [] args) throws Exception { if(args.length != 2) throw new Exception("Wrong number of arguments"); //factoryName,topicName发送端和接收端 new JmsClient_1(args[0], args[1]); while(true){Thread.sleep(10000);} } public JmsClient_1(String factoryName, String topicName) throws Exception { InitialContext jndiContext = getInitialContext( ); ConnectionFactory factory = (ConnectionFactory) jndiContext.lookup("ConnectionFactoryNameGoesHere"); Topic topic = (Topic)jndiContext.lookup("TopicNameGoesHere"); Connection connect = factory.createConnection( ); Session session = connect.createSession(false,Session.AUTO_ACKNOWLEDGE); MessageConsumer consumer = session.createConsumer(topic); consumer.setMessageListener(this); connect.start( ); } public void onMessage(Message message) { try { TextMessage textMsg = (TextMessage)message; String text = textMsg.getText( ); System.out.println("\n RESERVATION RECEIVED\n"+text); } catch(JMSException jmsE) { jmsE.printStackTrace( ); } } public static InitialContext getInitialContext( ){ // create vendor-specific JNDI context here } } JmsClient_1 构造获取ConnectionFactory 和 Topic 从JNDI InitialContext. 这个上下文与厂商指定的属性一起建立,以便用户端连接到那个相同的JMS提供 者当作TravelAgent EJB.举例来说, 在这里是 getInitialContext()如何给 JBoss 应用程序服务器的方法编码:[*] JNDI允许你在jndi.properties 文件中设置属性,包含InitialContext属性值可 以在运行期间,动态的被发现.在本书中,我们做了明确的设置. public static InitialContext getInitialContext( ){ Properties env = new Properties( ); env.put(Context.SECURITY_PRINCIPAL,"guest"); env.put(Context.SECURITY_CREDENTIALS,"guest"); env.put(Context.INITIAL_CONTEXT_FACTORY, " org.jboss.security.jndi.JndiLoginInitialContextFactory"); env.put(Context.PROVIDER_URL," jnp://hostname:1099"); return new InitialContext(env); } 一旦客户端获得了 ConnectionFactory 和 Topic, 它建创建一个Connection和 一个Session用相同的方法使用TravelAgent EJB.主要的不同是Session对象用 来创建一个MessageConsumer而不是MessageProducer.MessageConsumer处理输 入的信息,发布到它的主题. Session session = connect.createSession(false,Session.AUTO_ACKNOWLEDGE); MessageConsumer consumer = session.createConsumer(topic); consumer.setMessageListener(this); connect.start( ); MessageConsumer能直接地接收信息或代表信息对javax.jms.MessageListener 处理.我们可以选择JmsClient_1 来实现MessageListener接口,所以它自射可以 处理消息.MessageListener对象实现了一个方法.onMessage(),每次调用一个新 消息发送到签署者的主题.在这种情况下TravelAgent EJB 每次发送一个预定消 息到主题,JMS客户端的onMessage()方法被调用,接收和征理消息的一个拷贝. public void onMessage(Message message) { try { TextMessage textMsg = (TextMessage)message; String text = textMsg.getText( ); System.out.println("\n RESERVATION RECEIVED:\n"+text); } catch(JMSException jmsE) { jmsE.printStackTrace( ); } } 12.1.3. JMS是异步的 JMS消息的主要优点之一就是它是异眇的.换句话说,一个JMS的客户端发送一个 消息不用等待一个应答消息.相比之下,与JAVA RMI的同步消息或JAX-RPC更有弹 性.第次,客户端调用组件的方法,它会阻塞当前的线程,直到方法完成运行.这样 紧密的处理使客户端依赖于EJB服务的可用性,结果使得客户和企业Bean的结合 更加紧密.JMS客户端发送消息异步(主题或队列)到目标从其它的JMS客户端也 可以接收消息.当一个JMS客户端发送一个消息时,它不需要等待应答;它发送消 息到一个路由,由它负责转寄给其它的客户.如果有一个或多个不可用的收件人, 这也不会影响客户;它仍然保持工作.它是路由器的职责确定信息最后到达它的 目的地.客户发送的消息与接收消息的客户分离;发送消息并不依赖于接收者的 可用性. RMI的限制使得JMS成为一个吸引人的选择,连接到其它的应用程序.使用标准的 JNDI 命名环境的上下文,一个企业Bean可以获取一个JMS连接到JMS提供者并且 使用它发表异消息到其它的Java应用程序.例如,一个TravelAgent会话Bean可以 使用JMS来通知其它的应用程序一个预定已经被处理,如图12-1所示. Figure 12-1. Using JMS with the TravelAgent EJB 在这种情况下,应用程序接收JMS消息从TravelAgent EJB也可能是消息驱动Bean, 企业的其他的 Java 语言应用程序 , 或受益于被通知预定已经被处理的其他组 织的应用程序.例子中可能包含一些业务部分,共享用户信息或者一个内部标记 应用程序,增加用户到一个邮件列表.因为消息本身是分离的并且不同步,发送者 的事务和安全上下文并不会传递到接收者.举例来说,TravelAgent EJB 发送票 的信息的时候,JMS提供者能够鉴别它,但是消息安全上下文将不被传递到消息的 接收者.当一个JMS客户端接收消息从TravelAgent EJB,当消息被发送,客户端没 有关于安全的上下文.因为发送者和接收者时常在和不同的安全环境中操作,所 以这是它应该做的. 同样的,事物也不会在发送者到接收者之间被传递.对于一件事情,发送者并不会 意识到那一个接收者会收到消息.如果消息以主题的形式发出,那么消息的接收 者可能是一个也可能成千上万;管理一个分布式的事务在这样不明确的环境下是 不可取的.另外,客户端接收的消息不会在消息发送很长时间才接收.也许会发生 网络问题,客户端下线,或者是其它的问题.因为他们在资源上面锁,而且应用程 序不可预知的结束,因而,不允许长时间的处理,所以,事务被设计很快地被执行. 一个JMS客户端可以,然而,一个分布式事务管理的JMS提供者管理事上下文的发 送和接收操作.举例来说,如果TravelAgent EJB操作失败,JMS提供者丢弃票的信 息通过TravelAgent EJB.事务和JMS的细节将会在第十六章中详细介绍. 12.1.4. JMS 消息模型 JMS提供了两种类型的消息模型:发布-订阅和点对点.JMS 规格当做消息域提及 这些.在JMS术语中,发布-订阅和点对点常常被缩小到pub/sub和p2p(或FTP),各 自的. 简单的认识是,发布-订阅形是一对多广播消息,而点对点是一对一的传递消息 见下图 Figure 12-2. JMS messaging domains 每一个消息域(例如,pub/sub和p2p)有他自己的接口和类集为发送和接收消息. 这样的结果是有两种不同的API.共享相同的共用类型,JMS1.1介绍一个统一的 API,允许开发者使用一个接口和类集为消息域.本章中仅使用统一的API. 12.1.4.1. 发布-订阅 在发布-订阅消息中,一个消息生产者可以发送一个消息到多个消费者,通过一 个叫做topic的虚拟通道.消费者可以选择订阅一个主题.被定址到一个主题的任 何的信息被递送给所有的主题的用户.pub/sub 消息模型是一个主要以发布为基 础的模型,消息被自动广播到消费者不需要消费者的请求或选取主题消息. 在pub/sub 消息模式中,消息的生产者发送消息不依赖于消息的接收者.JMS客户 端使用pub/sub可以建立一个长期的订阅,允许消费者断开或重连并且收集发布 的消息直到他们断开.本章中的TravelAgent EJB使用pub/sub程序模型同一个主 题对象做为目的地. 12.1.4.2. 点对点 点对点的消息模式允许JMS客户端发送和接收消息使用同步和异步传输通过 queues虚拟通道.P2P消息模式是传统的拉或投票为基础的模式,消息请求从队列 代替客户端出栈.一个队列可以有多个收件人,但是,仅有一个收件人可以得到每 次的消息.如图12-2所示,JMS提供者负责消息的输出在JMS客户端之间,确保每个 消息仅被一个JMS客户端消耗.JMS规范没有指定多个接收者之间的消息分发规 则. JMS规范并没有指定什么状态现使用p2p和pub/sub模型来实现.任意型可以使用 推进或拉的概仿,pub/sub是推,p2p是拉. 12.1.4.3.你应该使用哪一种消息模型? 这两种模型的基本原理在于JMS的规范.JMS开始作为一种方法提供了一公有的 API为访问已存在的消息系统,在时间概念上一些消息提供商使用p2p模型,也有 一些使用pub/sub模型.因此,JMS需要提供一个API为得到广泛的工业支持. 几乎pub/sub模型可以做的,point-to-point也可以做,反之亦然.类似于程序语 言的选择.理论上,任意的应用程序可以用Pascal来写,也可以使用C++,或是Java 语言.在一些情况下,它降低了选择,或者那一个模型你已经熟悉了.在大部份的 情形下,模型的选择信赖于那一个模型更适合你的应用程序.同pub/sub模式,任 意数量的订阅可以被一个主题上列出,并且所有的接收者会收到相同的信息拷贝. 发布者并不关心是否所有的人在听,或者没有人在听.例如,考虑到广播的发布者. 是否指定的定阅者当前没有连接或者是失去全部的引用,发布者并不关心.相比 之下,一个点对点的会话可能会有意的为一对一会话指定应用程序,直到其它的 结束.在这一个情节中,每个信息真的有关系. 范围和数据的多样性信息表现可 能是一个因数也.使用pub/sub模式,信息被分派到提供特定主题的使用的过滤为 基础的用户.即使消息正在被使用并且建立了一个一对一会话同其它的已知应用 程序,它会很有用,使用pub/sub来分隔不同类型的信息.信息的每个类型能经过 它自己的独特用户和 onMessage() 收听者被分开地处理. 当你想要指定一个接收者来处理给定的消息,Point-to-point 方式非常方便.这 也许是两种类型最大的不同:p2p保证仅有一个消费者处理每个消息.当消息需要 分离的处理时这种能力非常的重要,但是一前一后. 12.1.5. 会话Bean不能接收消息 JmsClient_1的设计用来消毁消息通过TravelAgent EJB.其它的会话Bean也可以 接收这些消息吗?答案是:是,但这是一个不好的想法. 会话Bean用来响应EJB客户端的调用,并且他们不能够用来回应JMS消息,如同消 息驱动Bean.一个会话或一实体Bean不可能被消息所驱动.可能会开发一个会话 Bean可以消灭JMS消息从业务方法,但是EJB客户端必需首先调用这个方法.例如, 当业务方法在一个假设的EJB上被调用的时候,它将设置JMS会话和尝试读取一个 消息从队列中: @Stateless public class HypotheticalBean implements HypotheticalRemote { @Resource(mappedName="ConnectionFactory"); private ConnectionFactory factory; @Resource(mappedName="MyQueue") private Queue queue; public String businessMethod( ){ try{ Connection connect = factory.createConnection( ); Session session = connect.createSession(true,0); MessageConsumer receiver = session.createConsumer(queue); TextMessage textMsg = (TextMessage)receiver.receive( ); connect.close( ); return textMsg.getText( ); } catch(Exception e) { throws new EJBException(e); } } ... } 消息的消费者用来获取消息从队列中.当这个操作被正确的编码,它将会是很危 险的,因为MessageConsumer.receive( ) 方法调用会阻塞线程直到信息变为可 用的.如果消息永远不到来,线程被阻塞!如果没有人发送信息到队 列,MessageConsumer将会永远的等下去.为了更好,使用另一个receive()方法减 少危险.例如,receive(long timeout) 允许你设定一个时间,在这之后 MessageConsumer 将停止阻塞的线程并且放弃等待的消息.也有 receiveNoWait(),检查一个消息并且返回为null如果等待的是none,避免长时间 的线程阻塞.然而,这一个运算仍然是危险的. 不能够保证receive()方法预期执 行的风险更小一些,并且程序错误(例如,使用错误的receive()方法)的风险会更 大. 故事的道理很简单:不要尝试在会话Bean中定代码,强制它去接收消息.如果你需 要接收消息,使用一个消息驱动Bean;MDB特别的设计来消毁JMS消息. 12.1.6. 更多的学习关于JMS JMS(和企业信息概要)代表一个强大的范例在分布式计算机中.本章提供了简短 的JMS概述,它将为你在下一节的消息驱动Bean讨论中做充足的准备.为了要了解 JMS 和它如何被使用,你将会需要独立地学习它.[*] 花时间了解 JMS 很好值得 做的. 12.2.基于JMS的消息驱动Bean 消息驱动Bean(MDBs)是无状态的,服务器端,事务组件,为处理异步消息通过JMS. 当消息驱动Bean负责消息处理,它的容器管理组件环境,包括事务,安全,资源,并 发,和消息承认.注意:特别地重要的是容器处理并发.容器提供给MDB的线程安安 全优于传统的JMS客户端.必需管理资源,事务,和安全在多线程环境中.因为无数 个MDB的实例可以并发的执行在容器中,所以一个MDB可以处理数百个消息的并 发. 消息驱动Bean完全是一个企业Bean,同会话Bean或实体Bean,但是也有很多重要 的不同之处.一个消息驱动Bean有一个Bean类,它没有远程或本地业务接口.这些 接口是不需要的,因为消息驱动Bean仅用来响应异步消息. 12.2.1.ReservationProcessor EJB ReservationProcessor EJB 是一个消息驱动Bean用于接收JMS消息,通知一个新 的预定.ReservationProcessor EJB是通过JMS处理预定的一个TravelAgent EJB 的自动化版本.这些信息可能来自企业的另外的一个应用程序或从一个应用程序 在一些其他的团体的另外的一个旅行代理程序中.当ReservationProcessor EJB 接收一个消息,它会创建一个新的Reservation EJB(增加到数据库中),处理付款 使用ProcessPayment EJB,并且发送出一张票,处理演示如图12-3. Figure 12-3. The ReservationProcessor EJB processing reservations 12.2.2. ReservationProcessorBean 类 下面是ReservationProcessorBean类的部分定义.一些方法是空的;他们将会在 后面被填充,注意:onMessage()方法包含业务逻辑;它的业务逻辑类似于十一章 中的TravelAgent EJB的bookPassage()方法.下面是代码: package com.titan.reservationprocessor; import javax.jms.*; import com.titan.domain.*; import com.titan.processpayment.*; import com.titan.travelagent.*; import java.util.Date; import javax.ejb.*; import javax.annotation.*; import javax.persistence.*; @MessageDriven(activationConfig={ @ActivationConfigProperty( propertyName="destinationType", propertyValue="javax.jms.Queue"), @ActivationConfigProperty( propertyName="messageSelector", propertyValue="MessageFormat = 'Version 3.4'"), @ActivationConfigProperty( propertyName="acknowledgeMode", propertyValue="Auto-acknowledge")}) public class ReservationProcessorBean implements javax.jms.MessageListener { @PersistenceContext(unitName="titanDB") private EntityManager em; @EJB private ProcessPaymentLocal process; @Resource(mappedName="ConnectionFactory") private ConnectionFactory connectionFactory; public void onMessage(Message message) { try { MapMessage reservationMsg = (MapMessage)message; int customerPk = reservationMsg.getInt("CustomerID"); int cruisePk = reservationMsg.getInt("CruiseID"); int cabinPk = reservationMsg.getInt("CabinID"); double price = reservationMsg.getDouble("Price"); // get the credit card Date expirationDate = new Date(reservationMsg.getLong("CreditCardExpDate")); String cardNumber = reservationMsg.getString("CreditCardNum"); String cardType = reservationMsg.getString("CreditCardType"); CreditCardDO card = new CreditCardDO(cardNumber, expirationDate, cardType); Customer customer = em.find(Customer.class, customerPk); Cruise cruise = em.find(Cruise.class, cruisePk); Cabin cabin = em.find(Cabin.class, cabinPk); Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); em.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); deliverTicket(reservationMsg, ticket); } catch(Exception e) { throw new EJBException(e); } } public void deliverTicket(MapMessage reservationMsg, TicketDO ticket) { // send it to the proper destination } } 12.2.2.1.消息驱动Bean的上下文 消息驱动Bean也有上下文对象,类似于第十一章中的javax.ejb.SessionContext. 这个对象可能使用@javax.annotation.Resource 批注来注入: @Resource MessageDrivenContext context; MessageDrivenContext简单的继承了EJBContext;它并没有增加任何的新方 法.EJBContext的定义如下: package javax.ejb; public interface EJBContext { // transaction methods public javax.transaction.UserTransaction getUserTransaction( ) throws java.lang.IllegalStateException; public boolean getRollbackOnly( ) throws java.lang.IllegalStateException; public void setRollbackOnly( ) throws java.lang.IllegalStateException; // EJB home methods public EJBHome getEJBHome( ); public EJBLocalHome getEJBLocalHome( ); // security methods public java.security.Principal getCallerPrincipal( ); public boolean isCallerInRole(java.lang.String roleName); // deprecated methods public java.security.Identity getCallerIdentity( ); public boolean isCallerInRole(java.security.Identity role); public java.util.Properties getEnvironment( ); } 仅事务的方法MessageDrivenContext 继承从EJBContext可用于消息驱动 Bean.home方法getEJBHome( ) 和getEJBLocalHome( )抛出 RuntimeException 异常.因为消息驱动Bean没有home接口或EJB home对象.如果你调用 MessageDrivenContext上的安全方法methodsgetCallerPrincipal( ) 和 isCallerInRole( )也会抛出RuntimeException异常.当一个MDB服务一个JMS消 息,没有"调用者",所以没有安全上下文可以获得从调用者.记信JMS是异部的并 且不能够传递发送者的安全上下文到接收者,不要有这种意识,因为发送者和接 收者往往操作在不同的环境中. MDB常常执行一个容器初始化或组件初始化处理,所以事务方法允许MDB管理它的 上下文.事务的上下文不能够从JMS发送者传递过来;它的初始化通过容器或明确 的使用javax.jta.UserTransaction批注.EJBContext 中的事务方法详细介绍将 会在第十六章中进行. 消息驱动Bean也可以访问它自己的JNDI ENCs,提供MDB实例访问环境,其它的企 业组件,和资源.例如,ReservationProcessor EJB 利用ENC来获取对泰坦的 EntityManager引用,ProcessPayment EJB和一个JMS ConnectionFactory和 Queue为发送票. 12.2.2.2. MessageListener 接口 MDB常常实现javax.jms.MessageListener 接口,它定义了onMessage()方法.这 个方法处理被组件接收的JMS消息. package javax.jms; public interface MessageListener { public void onMessage(Message message); } 虽然 MDBs 通常实现这一接口,我们将会在后面看到MDB可以结合其它消息系统, 定义一个不同的接口合同. 12.2.2.3. Taskflow and integration for B2B: onMessage( ) onMessage( )方法中存在所有的业务逻辑.当一个消息到达时,容器传递它们到 MDB通过onMessage()方法.当方法返回时,MDB准备好处理新的消息.在 ReservationProcessor EJB中,onMessage()方法提取MapMessage中关于一个预 定的信息并且使用这些信息建立一个预定在系统中: public void onMessage(Message message) { try { MapMessage reservationMsg = (MapMessage)message; int customerPk = reservationMsg.getInt("CustomerID"); int cruisePk = reservationMsg.getInt("CruiseID"); int cabinPk = reservationMsg.getInt("CabinID"); double price = reservationMsg.getDouble("Price"); // get the credit card Date expirationDate = new Date(reservationMsg.getLong("CreditCardExpDate")); String cardNumber = reservationMsg.getString("CreditCardNum"); String cardType = reservationMsg.setString("CreditCardType"); CreditCardDO card = new CreditCardDO(cardNumber, expirationDate, cardType); JMS常常被用作一个集成点对业务到业务(B2B)的应用程序,所以,很容易想像预 定消息来自泰坦的一部分(也许是第三方的处理或部分旅行代理). ReservationProcessor EJB需要访问Customer, Cruise, 和Cabin实体为了处理 预定.MapMessage中包含这些实体的主键;ReservationProcessor EJB 使用注入 EntityManager 查找实体组件: public void onMessage(Message message) { ... Customer customer = em.find(Customer.class, customerPk); Cruise cruise = em.find(Cruise.class, cruisePk); Cabin cabin = em.find(Cabin.class, cabinPk); ... } 信息一从MapMessage中提出,它将用于创建预定和处理付款.这基本上是在第 11 章中被 TravelAgent EJB 用了的相同的任务.一个Reservation实体创建代表它 自身的预定,ProcessPayment EJB 创建用于处理信用卡的付款: Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); em.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); deliverTicket(reservationMsg, ticket); 类似于会话Bean,MDB可以访问任何会话Bean和使用组件来完成一个任务.MDB 能 处理程序而且与其他的豆子和资源互动.举例来说, MDB 使用 JDBC 存取以它正 在处理的信息的内容为基础的一个数据库是很常见的. 12.2.2.4. 从消息驱动Bean发送消息 一个MDB也可以使用JMS来发送消息.deliverTicket( ) 方法发送票的信息到目 标,它的定义通过发送JMS客户端: public void deliverTicket(MapMessage reservationMsg, TicketDO ticket) throws JMSException{ Queue queue = (Queue)reservationMsg.getJMSReplyTo( ); Connection connect = connectionFactory.createConnection( ); Session session = connect.createSession(true,0); MessageProducer sender = session.createProducer(queue); ObjectMessage message = session.createObjectMessage( ); message.setObject(ticket); sender.send(message); connect.close( ); } 消息被分成两部分:消息头和消息主体.消息头包含路由信息,以及可能为信息过 滤和其他的属性有特性.这些属性之一可能是 JMSReplyTo.消息的发送者可能将 JMSReplyTo属性设定为任何的目的地,访问它的JMS提供者.在预定信息的情况, 消息发送者将 JMSReplyTo 属性设定为queue.结果是产生的票应该被送.另外的 一个应用程序能存取这一个queue读取票而且对客户分配他们或者在消息发送者 的数据库中储存信息. 你也可以使用JMSReplyTo地址来报告业务错误.例如,如果cabin已经被保 留,ReservationProcessor EJB 可能会发送一个错误消息到JMSReplyTo 队列, 那样预定将不会被处理.包括这类型的错误处理为读者被留下如一个练习. 12.2.3. @MessageDriven批注 MDB的定义使用 @javax.ejb.MessageDriven批注,或,作为选择的一种,使用EJB 的部署描述符.一个MDB可以单独部署.但是它时常与其他Bean一起部署作为它叁 考.举例来说, ReservationProcessor EJB 使用 ProcessPayment EJB 和泰坦 EntityManager ,因此,在相同的 J2EE 配置里面部署所有这些豆子是可行的. 12.2.3.1. @ActivationConfigProperty批注 我们在后面将会看到,因为MDB可以接收消息从任意的消息发送者,配置必需非常 的富有弹性,来描述所有属性,可能会有不同的提供者.以JCA(Java Connector Architecture Java连接体系)为基础的MDB不是必需使用JMS作为消息服务,因此, 这个需求是非常重要的.为了使它更容易,@MessageDriven.activationConfig( ) 属性是由@ActivationConfigProperty 批注组成的队列.这些批注使用简单的 name/value对来描述MDB的配置. @MessageDriven(activationConfig={ @ActivationConfigProperty( propertyName="destinationType", propertyValue="javax.jms.Queue"), @ActivationConfigProperty( propertyName="messageSelector", propertyValue="MessageFormat = 'Version 3.4'"), @ActivationConfigProperty( propertyName="acknowledgeMode", propertyValue="Auto-acknowledge")}) public class ReservationProcessorBean implements javax.jms.MessageListener { ... } 在activationConfig( )中用到的名字和值用来描述消息服务的变量,信赖于使 用的消息服务类型.但是EJB3.0中定义了固定的属性集为JMS基础的消息驱动 Bean.这些属性是:acknowledgeMode, messageSelector, destinationType, 和 subscriptionDurability . 12.2.3.2. 消息选择器 一个MDB可义一个消息选择器.消息选择器允许MDB有更多的选择,从指定的主题 或队列中的消息.消息选择器使用消息属性做为条件表达式的标准.这些条件表 达式使用布尔逻辑定义那些消息应该被传递.一个消息选择器的定义使用标准属 性名,messageSelector,在激活的配置元素中: [*] 信息选择器也以信息表头为基础,这在这一章节的范围之外. @ActivationConfigProperty( propertyName="messageSelector", propertyValue="MessageFormat = 'Version 3.4'"), 消息选择器是在消息属性的基础之上.消息属性是额外的头可以被指定到一个消 息;他们允许厂商和开发者绑定信息到消息,那不做为消息的主体部分.Message 接口提供了一些方法来读和写属性.属性可以是一个String类型值或一些原生始 类型值((boolean, byte, short, int, long, float, double).属性的名字,连 同他们的数值和变换规则一起, 严格地被 JMS 定义. ReservationProcessor EJB 使用一个消息选择器过滤选择指定格式的消息.在 这种情况下,格式是"Version 3.4";这是一个字符串,泰坦使用它来标识 MapMessage类型的消息,包括名字值CustomerID, CruiseID, CabinID, CreditCard, 和Price.换句话说,把 MessageFormat 加入每个预定信息允许我 们写MDB,来设计处理不同种类的预定信息.如果一个新的业务部分需要使用一个 不同类型的Message对象,泰坦需要使用一个消息版本和一个MDB来处理它. 这里是一个JMS 生产者设置一个MessageFormat 属性在消息上: Message message = session.createMapMessage( ); message.setStringProperty("MessageFormat","Version 3.4"); // set the reservation named values sender.send(message); 消息选择器是在SQL-92条件表达式语法的基础之上的子集,用在SQL语言中的 WHERE子句中.它们可以变得更复杂,包括文字数值,布尔表达式,一元操作等等. 12.2.3.3. Acknowledge模式 一个JMSacknowledgment意味着JMS的客户端通知JMS提供者当一个消息接收时. 在EJB中,MDB容器负责发送一个acknowledgment当接疏到消息时.承认一个消息 告诉JMS提供者,一个MDB容器已经接收并处理了消息.没有承认,JMS提供者并不 知道是否MDB容器已经收到消息,而且不必要的再递送能引起问题.例如,我们处 理一个预定消息使用ReservationProcessor EJB,我们并不想再一次接收相同的 消息. 应答模式的设置使用标准的acknowledgeMode激活配置属性,如下面的代码片断 所示: @ActivationConfigProperty( propertyName="acknowledgeMode", propertyValue="Auto-acknowledge ") 应答模式的取值有两种:Auto-acknowledge 和 Dups-ok-acknowledge.Auto- acknowledge告方容器,它将发送一个承认收到JMS提供者在消息已经被MDB实例 处理后.Dups-ok-acknowledge 告诉容器,并不是立即发送承诺书;在消息传递给 MDB实例后任意时间.同Dups-ok-acknowledge ,它可能为MDB容器延迟承诺书为 JMS提供者假定消息没有接收到并且发送一个消息副本.显然地, 由 Dups-ok-确 认, 你的 MDBs 一定能够正确地处理重复的信息. Auto-acknowledge避免重复消息因为承诺书会立即被发送.因此,JMS提供者不需 要发送一个副本.大多数MDB使用Auto-acknowledge来避免处理两次相同的消 息.Dups-ok-acknowledge 的存在因为它可以允许JMS提供者最优的使用网络.在 实践中,虽然应答 非常小,并且频繁的使用在MDB容器和JMS提供者之间,Dups- ok-acknowledge 并不会有太大的冲击. 说了这些,应答模式实际中大部分被忽略,除非MDB执行Bean管理事务,或容器管 理事务属性 NotSupported(见第16章).在所有的其它情况下,事务的客理是通过 容器,并且应答被放置在处理的上下文里面.如果事务成功,信息被承认. 如果处 理失败,信息没被承认.当使用容器管理事务必需要求一个事务属性,应答模式通 常不需要指定;然而,它为了讨论被包含在配置描述文件之中. 12.2.3.4. 经久的订阅 当一个以JMS为基础的MDB使用一个javax.jms.Topic,部署描述符必需定义是否 订阅是Durable 和 NonDurable.一个Durable订阅比一个MDB客器的连接到JMS提 供者更为长久,所以,如果EJB服务器的一部分遭遇失败,关闭,或其它的与JMS提 供者失去连接,消息将会被接收,并不会丢失.提供者存储任何消息直到容关闭, 消息被传递过去;当重器重连消息被传送到容器(从容器到MDB).这行为被普遍 称为储存-转向的信息. Durable MDBs能够支持断开连接,不管是有意的或是局 部的失败.如果订阅是NonDurable,任意消息将会被接收直到失去连接.开发者使 用NonDurable订阅当它不是重要的信息被处理.使用NonDurable 可以改善JMS的 执行效率,但是,会减少MDB的可靠性. @ActivateConfigProperty( propertyName="subscriptionDurability", propertyValue="Durable") 当目标类型是javax.jms.Queue时,如同在ReservationProcessor EJB中,因为是 以自然查询为基础的消息系统,经久性不是一个因素.如一个队列,消息可能被消 砂公有一次,并且保持队列直到它们被分配到一个队列的监听器. 12.2.4. XML 部署描述 这儿是一个部署描述提代了一个完整的批注可选择定义的 ReservationProcessor EJB: ReservationProcessorBean com.titan.reservationprocessor.ReservationProcessorBean javax.jms.MessageListener Container javax.jms.Queue destinationType javax.jms.Queue messageSelector MessageFormat = 'Version 3.4' acknowledgeMode Auto-acknowledge ejb/PaymentProcessor Session com.titan.processpayment.ProcessPaymentLocal com.titan.reservationprocessor.ReservationProcessorBean process persistence/titan titan com.titan.reservationprocessor.ReservationProcessorBean em jms/ConnectionFactory javax.jms.ConnectionFactory Container ConnectionFactory com.titan. reservationprocessor. ReservationProcessorBean datasource 元素描述消息属性,有元素.一个MDB定义在一个元素内,是 元素的子元素,旁边是Bean.类似于组 件类型,它定义一个和一个但是没有定义组件接 口(本地或远程).MDB没有本地或远程接口,所以不需要定义他们. 12.2.5. ReservationProcessor 客户端 为了测试ReservationProcessor EJB,我们需要开发一个客户端应用程序:一是 发送预定消息并且其它的消毁票消息处理通过ReservationProcessor EJB. 12.2.5.1. 预定消息生产者 JmsClient_ReservationProducer快速的发磅100条预定请求.由于速度的原因, 发送这些消息强制很多容器使用多个MDB实例来处理他 们.JmsClient_ReservationProducer的代码如下: import javax.jms.Message; import javax.jms.MapMessage; import javax.jms.ConnectionFactory; import javax.jms.Connection; import javax.jms.Session; import javax.jms.Queue; import javax.jms.MessageProducer; import javax.jms.JMSException; import javax.naming.InitialContext; import java.util.Date; import com.titan.processpayment.CreditCardDO; public class JmsClient_ReservationProducer { public static void main(String [] args) throws Exception { InitialContext jndiContext = getInitialContext( ); ConnectionFactory factory = (ConnectionFactory) jndiContext.lookup("ConnectionFactoryNameGoesHere"); Queue reservationQueue = (Queue) jndiContext.lookup("QueueNameGoesHere"); Connection connect = factory.createConnection( ); Session session = connect.createSession(false,Session.AUTO_ACKNOWLEDGE); MessageProducer sender = session.createProducer(reservationQueue); for(int i = 0; i < 100; i++){ MapMessage message = session.createMapMessage( ); message.setStringProperty("MessageFormat","Version 3.4"); message.setInt("CruiseID",1); message.setInt("CustomerID",i%10); message.setInt("CabinID",i); message.setDouble("Price",(double)1000+i); // the card expires in about 30 days Date expirationDate = new Date(System.currentTimeMillis( )+43200000); message.setString("CreditCardNum","923830283029"); message.setLong("CreditCardExpDate", expirationDate.getTime( )); message.setString("CreditCardType", CreditCardDO.MASTER_CARD); sender.send(message); } connect.close( ); } public static InitialContext getInitialContext( ) throws JMSException { // create vendor-specific JNDI context here } } 这个代码很简单的扩展了前面的TravelAgent EJB ,它获取一个 ConnectionFactory从JNDI和设置相关的JMS对象,这样它将用于发送消息.它将 创建100条预定消息并且发送他们到JMS队列进行异眇处理. 12.2.5.2. 与票相关的消息毁灭者 JmsClient_TicketConsumer的设计用于销毁票消息,通过 ReservationProcessor EJB传递消息到队列.它销毁消息并且打印出描述: import javax.jms.Message; import javax.jms.ObjectMessage; import javax.jms.ConnectionFactory; import javax.jms.Connection; import javax.jms.Session; import javax.jms.Queue; import javax.jms.MessageConsumer; import javax.jms.JMSException; import javax.naming.InitialContext; import com.titan.travelagent.TicketDO; public class JmsClient_TicketConsumer implements javax.jms.MessageListener { public static void main(String [] args) throws Exception { new JmsClient_TicketConsumer( ); while(true){Thread.sleep(10000);} } public JmsClient_TicketConsumer( ) throws Exception { InitialContext jndiContext = getInitialContext( ); ConnectionFactory factory = (ConnectionFactory) jndiContext.lookup("QueueFactoryNameGoesHere"); Queue ticketQueue = (Queue)jndiContext.lookup("QueueNameGoesHere"); Connection connect = factory.createConnection( ); Session session = connect.createSession(false,Session.AUTO_ACKNOWLEDGE); MessageConsumer receiver = session.createConsumer(ticketQueue); receiver.setMessageListener(this); connect.start( ); } public void onMessage(Message message) { try { ObjectMessage objMsg = (ObjectMessage)message; TicketDO ticket = (TicketDO)objMsg.getObject( ); System.out.println("********************************"); System.out.println(ticket); System.out.println("********************************"); } catch(JMSException jmsE) { jmsE.printStackTrace( ); } } public static InitialContext getInitialContext( ) throws JMSException { // create vendor-specific JNDI context here } } 为了使ReservationProcessor EJB 工作同两个客户端应用程 序,JmsClient_ReservationProducer 和 JmsClient_TicketConsumer ,你需要 配置你的EJB容器的JMS提供者,它有两种队列:一个为预定消息,另一个为票消 息. 12.3.消息驱动Bean的生命周期 同会话Bean有明确的生命周期,MDB也是.MDB实例生命周期有两种状态:不存在和 方法-就绪池.方法-就绪池类似用于无状态会话Bean的实例池.图12-4所示的 状态和MDB生命中实例状态的转换. [*]一些商可能没有池管理的MDB实例,但是,替代创建和销毁实例用每一个新消 息,这样的实现规范并不会影响无状态Bean实例的指定生命周期. 12.3.1.不存在状态 当一个MDB实例在不存在状态,它的实例不存在于系统内存中.换句话来说,它还 没有被初始化. 12.3.2.方法-就绪池 MDB实例进入方法-就绪池当容器需要他们时.当EJB服务首次启动时,它可能会 创建一定数量的MDB实例并且使他们进入方法-就绪池.(务器真实的行为依赖于 实现)当MDB实列的数量不足以处理传过来的消息时,更多的将会创建并增加到池 中. 12.3.2.1.转换到方法-就绪池 到一个实例从不存在状态转换到方法-就绪池,将会执行三种操作.首先,无状态 Bean类调用Class.newInstance()方法开始Bean的实例.第二,容器注入任意资源, 通过组件的元数据要求通过一个注入批注或XML部署描述符. [*]你必需提供一个默认的无参的构造.容器初始化实例使用 Class.newInstance( ), 它要求一个无参的构造.如果没有定义,无参的构造是 暗含的. 最后,EJB容器将会调用PostConstruct回调如果有一个.组件类或许有或没有使 用@javax.ejb.PostConstruct批注的方法.如果存在,这果存在这个批注方法,组 件示例后容器将会调用这个方法.@PostConstruct批注方法可以是任意名,但返 回类型必需是void,并且没有参数,抛出一个没有检查的异常.Bean类仅能定义一 个@PostConstruct 方法(但是它不是必需这样的). @MessageDriven public class MyBean implements MessageListener { @PostConstruct public void myInit( ){} MDB并不受激活的影响,所以他可以保持连接到资源在它的整个生命周期内.[*] @PreDestroy方法将会关闭打开的任何资源在无状态会话Bean被从内存中驱逐结 速它的生命周期之前.你将会读到更多的关于.@PreDestroy在后面的章节中. [*]假定无状态会话Bean实例的持续时间非常长.然而,一些EJB服务器可能实际 上销毁和创建实例通过每个方法的调用,使得这种策略很少被激活.参考你的提 供商文档的细范在EJB服务器上处理无状态实例. 12.3.2.2. 方法-就绪池中的生命 当一个消息传递到一个MDB,它会代表方法-就绪池中的任意有效的实例.当实例 执行请求时,就不能够处理其它的消息.MDB可以同时处理很多的消息,代表每一 个消息处理的不同MDB实例的责任.当一个消息被容器委派一个实例时,MDB实例 的MessageDrivenContext反映一个新的事务上下文.实例一结束,它立即可以处 理一个新的消息. 12.3.2.3.转换出方法-就绪池:MDB实例的销毁 服务器不再需要它们时,Bean的实例离开方法-就绪池到不存在状态,当服务器 决定减少方法-就绪池中实例的数量时,一个或多个实例将会被从内存中被驱逐. 处理通过@PreDestroy 回调方法在Bean的实例上.此外,关于@PostConstruct方 法,的实现是可选的,并且它的返回类型是void,没有参数,抛出一个没有检查的 异常.@PreDestroy 加调方法可以执行任何清除操作,如同关闭资源. @MessageDriven public class MyBean implements MessageListener { @PreDestroy public void cleanup( ){ ... } @PostConstruct, @PreDestroy只能被调用一次:当组件过渡到不存在状态.在这 一回调期间,MessageDrivenContext和方问的JNDI ENC仍然是可用的对于Bean的 实例.在@PreDestroy 方法执行之后,Bean被废除并且被垃圾回收. 12.4. 以连接器为基础的消息驱动Bean 虽然以 JMS 为基础的 MDB 已经证明非常有用, 它也有限制.也许最大的限制就 是EJB提供商支持仅一个或少数的JMS提供者(通常只有一个).在前面的EJB2.1 中,大多数厂商只支持他们自己的JMS提供者,并且没有其它的.显然,这样的限制 是你的选择:如果你的公司或公司的一部分使用一个JMS提供者,它不被EJB提供 商支持,你将不能够处理JMS提供者传递过来的消息. [*]绕过这个JMS通道,消息的路由从一个JMS提供者到另一个,这是一个EJB规范 外的一个习惯上的解决方法. 问题的本质是复杂的并且要求对事务管理有一个深入的理解.简言之,对于MDB, 消息被JMS提供者传递到MDB,并且所有的工作由MDB执行(例如,使用JDBC,调用其 他Bean上的方法,等等),必需是同一事务的一部分,被EJB容器初始化.这需要 EJB 容器事前知道信息递送即将来临以便在信息实际上被递送之前,它能开始处 理. 不幸地, JMS API 不支援这种功能.因此在早期的 EJB 中, JMS 提供者必 须执行和每个 EJB 厂商的订制整合.整合的花费是昂贵的(业务),所有旧的 EJB2.0厂商一般选择少许的集成JMS提供者. 另一个JMS为基础的MDB限制是JMS程序模型的限制;没有其它的消息系统支持. 虽然JMS是非常有用的,不过它不是唯一可用的消息系统.SOAP,Email,CORBA消息, 用于ERP系统中的所有消息系统(SAP,PeopleSoft,等等),遗留下来的消息系统, 如其它的非JMS消息系统. EJB 3.0(和 2.1) 支持扩展,更多公开定义的消息驱动Bean,允许他们服务任何 种类的消息系统从任何厂商.唯一的要求是新类型的消息驱动Bean上附着消息驱 动Bean的生命周期.EJB 厂商能建立代码来支持新的消息系统(一些其它的JMS), 但是,他们必需也一定支持任何以JCA 1.5为基础的消息驱动Bean. JCA提供一个标准的服务器提供接口(SPI)允许任何的IES插入到J2EE容器系统. 连接器结构的1.0版本仅仅应用在请求/响应资源,J2EE组件(EJB或servlet/Jsp) 初始化请求.当前版本为1.5,要求j2EE1.4或更高,更加全面并且可以开作在任意 的异步消息系统中. 在这倦的系统中,J2EE组件等候消息的到达,代替初始化交 互同EIS;EIS的初始化将互通过消息的分离. JCA1.5明确定义一个消息的契约同消息驱动Bean的连接.它定义一个EJB容器和 一个异步连接,所以消息驱动Bean可以自动处理来自EIS的信息.MDB以一个异步 连接为基础,实现一个指定的消息接口的定义通过连接器自身.代替实现 javax.jms.MessageListener接口.MDB实现一些其它类型的接口指定到EIS.例如, 第三章中介绍的一个假设的邮件连接,允许MDB处理类似的以JMS为基础的JMS消 息.电子邮件连接器从厂商 X 购买和传递一个JAR文件被叫做资源档案 (RAR).RAR 包含所有的连接代码和必需的部署描述符植入到 EJB 容器系统. 它也定义了开发者使用的用于创建一个电子邮件的MDB所需的接口.这是一个假 设的电子邮件消息接口,他必需实现一个电子邮件的MDB: package com.vendorx.email; public interface EmailListener { public void onMessage(javax.mail.Message message); } Bean类实现这个接口,负责处理电子邮件连接器递送的消息.下面的代码显示了 一个MDB实现EmailListener 接口和处理电子邮件: package com.titan.email; @MessageDriven(activationConfig={ @ActivationConfigProperty( propertyName="mailServer", propertyValue="mail.ispx.com"), @ActivationConfigProperty( propertyName="serverType", propertyValue="POP3 "), @ActivationConfigProperty( propertyName="messageFilter", propertyValue="to='submit@titan.com'")}) public class EmailBean implements com.vendorx.email.EmailListener { public void onMessage(javax.mail.Message message){ javax.mail.internet.MimeMessage msg = (javax.mail.internet.MimeMessage) message; Address [] addresses = msg.getFrom( ); // continue processing Email message } } 在这个例子中,容器调用onMessage()传递一个JavaMail Message对象,那个代表 一个email消息包含MIME附件.然而,消息接口通过使用连接器为基础的MDB没有 使用onMessage().方法名和方法签名可以是适当的EIS;它甚至可以有返回类型. 例如,一个连接器可以为SOAP开发处理请求/应答形式的消息.这个连接器可以使 用ReqRespListener定义通过XML消息的Java API.SOAP消息API由sun制定,它不 是J2EE平台的一部分: package javax.xml.messaging; import javax.xml.soap.SOAPMessage; public interface ReqRespListener { public SOAPMessage onMessage(SOAPMessage message); } 在这个接口中,onMessage()有一个SOAPMessage类型的返回值.这意谓 EJB 容器 和连接器负责把应答信息协调回到寄件人.(或者到一些部署描述符中定义的目 的地).除了支持不同的方法信号外,消息接可能存在多个方法为处理不同种类的 消息使用相同的MDB.新的消息驱动Bean无限制的被EJB容器系统支持.真正美好 的是以连接器为基础的MDB完成的跨跃多个EJB厂商,因为所有的提供商必需支持 他们.如果以使用连接器为基础的MDB同提供商A和过后你想改变提供商为B,你可 以继续使用相同的连接器为基础的MDB,不会出现问题. 活动配置属性使用非JMS为基础的MDB依赖连接器类型和它的请求.让我们看一个 例子: @MessageDriven(activationConfig={ @ActivationConfigProperty( propertyName="mailServer", propertyValue="mail.ispx.com"), @ActivationConfigProperty( propertyName="serverType", propertyValue="POP3"), @ActivationConfigProperty( propertyName="messageFilter", propertyValue="to='submit@titan.com'")}) 我们在前面谈论过的@ActivationConfigProperty.如同你看到的前面的例子,任 何name/value对被这个元数据支持,因此,它可以很容易的支持邮件指定的配置 为这种连接器类型. 12.5.消息连接 消息连接特性允许任意企业Bean发送消息到路由,到一个指定的消息驱动Bean在 相同的部署中.通过连接信息,你可以在应用程序中的组件之间传递消息.例如本 章开始,第11章的 TravelAgent EJB 被重新实现,所以它发送一个JMS消息同票 信息到一个Topic目的地.这是TravelAgent EJB的bookPassage( )方法的不同实 现,这次使用ObjectMessage 类型: @Resource(mappedName="ConnectionFactory") private ConnectionFactory connectionFactory; @Resource(mappedName="TicketTopic") private Topic topic; @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); Connection connect = topicFactory.createConnection( ); Session session = connect.createSession(true,0); MessageProducer publisher = session.createProducer(topic); ObjectMessage objectMsg = session.createObjectMessage( ); objectMsg.setObject(ticket); publisher.send(objectMsg); connect.close( ); return ticket; } catch(Exception e) { throw new EJBException(e); } } 我们在前面讨论的章节中,并没有真正提票的信息在那里被发送.它可以是预定 代理或一些其它的泰坦巡航系统部分.然而,消息连接可以确保消息直接发送到 我们部署的的消息驱动Bean. 例如,我们可能部署一个消息驱动Bean,TicketDistributor EJB,负责发票信息 到一些不同的目标,像在数据库中.部分组成,买卖,等等,如图12-5所示的 TicketDistributor EJB(一个消息驱动Bean)工作同TravelAgent EJB分发票信 息到不同的目标. TicketDistributor发送票信息到不同的目标,包括分布式关系数据库使用JDBC, 一个使用J2EE连接器的遗留系统(例如,IMS,CICS,等等),和使用JavaMail的邮件 系统.TravelAgent EJB 可能直接地处理这类型的分配, 但是,定义分离的MDB来 处理分发,会提供更好的弹性和较好的性能. TicketDistributor MDB 更富有弹性,因为信息路由的改变,不需要修改 TravelAgent EJB.TravelAgent EJB常常发送消息到相同的JMS主 题;TicketDistributor负责分发票信息到其它源.TicketDistributor也可以改 进执行效率,因为,TicketAgent并不用等待不同的目标(分开的数据库,遗留系统 和电子邮件)来接收和处理消息在完成预定之前.TicketAgent 仅仅有关它送票 信息和忘记它.它是TicketDistribution MDB的责任发送票的信息到适当的部分. 除此之外, TravelAgent EJB 不能跨过协调分配的处理的不同资源,这能产生重 要的瓶颈,而且会影响传输量. EJB规范并没有指批注用于消息连接,所以我们将要建立一个部署描述文件来开 启这一特性.为了连接输出信息通过TravelAgent EJB来发送同输入信息的销毁 和处理通过TicketDistribution MDB,我们需要定义 元素在部署描述文件中.元素的定义通过 TravelAgent EJB的元素.TicketDistributor EJB 同样定义元素.两个元素引用相同的逻辑目的地的 定义在集成描述中: com.titan.travelagent.TravelAgentBean jms/TicketTopic javax.jms.Topic Produces Distributor javax.jms.Topic topic TicketDistributorEJB Distributor Distributor 元素定义企业Bean发送和接收消息的目的地.当 包含一个 元素,意味 着消息的发送者的接收者将会共享一个逻辑目的地在集合描述中.在前面的例子 中,TravelAgent EJB的 定义了一个, 指向元素在 元素中名子是Distributor.定义通过 TicketDistributor MDB指向相同的 元素.这意味着 TravelAgent EJB 发送消息的目的地将会是TicketDistributor MDB. 是公有的XML片断,允许你注入引用到类中的字段或setter 方法.这里的描述将会注入Topic目的地的TravelAgent EJB的topic字段.我们将 会在第 14 章中更详细地介绍. 消息驱动Bean总是消费 元素的直接定义 中的消息目的地.然而,如果他们使用消息API来描述他们自 己的元素,他们也可以处理消息,发送合时的消息到 目的地.下面的列表显示TicketDistributor消费消息从Distributor目的地,但 是也可以使用JMS发送消息到一个完全不同的目的地,叫做Partner: TicketDistributorEJB Distributor jms/PartnerCompany javax.jms.Topic Produces Partner Distributor Partner 在开发时,每一个元素映射到一个真实的消息目的地在 目标环境中.在大多数情况下,将会是一个JMS的topic或queue,但是它也可以是 一些其它类型的消息系统. J2EE应用服务器不会路由消息通过一个实际的目的地.它可以异步发送它们从发 送者到接收者;在这种情况下,从TravelAgent EJB 到TicketDistributor MDB. 然而,如果应用服务器处理消息传递到它自射,代替消息的提供者,它可以是语义 上的消息系统.对于JMS,事务,持久化,而久性,安全和应答将会被正确的处理,是 否消息的发送直接从一个组件到另一个,或者是通过一个JMS提供者. 第十三章 时间服务 业务系统频繁的使用时序系统来运行程序在指定的时间.时序系统典型的应用程 序是生成报表,重格式化数据,或做一些统计工作在夜间.在其他的情况下,时序 系统会提供回调API可以警告事件的子系统在运行期间,截目日期,及其他.安排 计划系统时常运行成批工作 (a.k.a. 预定的工作 ),它的工作在指定的时间断. 在Unix世界的用户频繁的运行时序工作使用cron(克龙,时间单位,等于百万年), 一个简单且有用的时序系统运行程序列表在一个配置文件.另外,工作的时序系 统包括OMG的COS时间事件服务,时间事件的CORBA API,连同商业产品. 不管软件,时序系统在不同的情节中被使用. ◆在信用卡的处理系统中,信用卡的批量信息处理,的有的全天收费放在一起比 分开处理好.这种预定在夜间进行工作减少处理对系统的影响. ◆在一个医院或病房系统中,电子数据接口软件用于发送体检要求到不同的卫生 组织.每个卫生组织有自己的处理要求,但是这是所有的日常程序,因此工作预定 收集要求数据,把它放入适当的格式, 而且把它传递到保健组织. ◆在差不多任何的公司, 经理需指定报表以一般的方式运行.一个时序系统可以 配置成自动运行报表和传递他们到经理通过电子邮件. 时序系统也同样是普通任务的应用程序,系统管理文档典型的跨天或跨月包括很 多系统和一个人工干涉.在一个工作应用程序中,员工的统计任务的实际状态需 要周期性的点清在一个应用系统中,发票,销售单,等等.为了确定每件事情是有 时序性的.时序系统保持时间并且传递事件到有警报的应用程序和组件当一个指 定的日期或时间到达时,或者当一结时间过期.这是时序工作的一些任务: 在一个抵押系统中,有很多任务需要完成(例如,鉴定,利率,结束日期,等等) 在抵押完成前.定时期可以设置在抵押应用程序中执行定期统计,确定每件事情 处理的进度. 在一个医疗处理系统中,要求一定在90天内依照商议的期限,处理通过网络医师 和临床讲义.每个要求有一个时间设置在截止期限之前七天发出警报. 在一个股票经纪人系统, 买定单可以创建一个指定数量的共享,但是只有指定的 价格或比较低的价格.这些定单限制是一个典型的时间限制.如果股价在期限之 前跌落指定的价格, 定购的单被执行.如果股价在期限之前不跌落指定的价格, 定时器期满和买定单的限制被取消. 在EJB世界中,时序系统的一个非常有趣的事是可以直接同企业Bean一 同工作. 然而,在EJB2.1之前,没有标状的J2EE时序系统.EJB2.1介绍一个标准的,但是,限 制时序系统被叫做定时服务,它已经在EJB3.0中进行了扩充. J2SE中包括java.util.Timer类,它允许时序任务的线程作为后台线程执行.这对 于多样的应用程序来说是非常有用的,但是它也有很多的限制在企业处理过程中. 注意:虽然,时序java.util.Timer 的语义类似于EJB的时间服务. 定时器服务对EJB容器系统来说很容易,提供一个时间-事件API,用于定时器的 指定日期,周期,和间隔.一个定时器的设置与企业Bean的设置有关,并且会调用 ejbTimeout( ) 方法或者是 @javax.ejb.Timeout 批注的方法,当定时器结束时. 有其它的单节中描述EJB定时期服务API和它用于无状态会话Bean和消息驱动 Bean,除了提供一些批评,而且建议对于定时器的进一步维护. 13.1. Titan的定时维护 Titan巡航系统有一个执行方针有序的维护它的船.例如,发动机一整年需要广泛 的和不同程度的维护使用, 如同做航行术设备,通信,下水道和水系, 等等. 事实上,那里是照字面上数以千计维护动作一整年在一艘船上被执行.为了处理 所有这些项目,当一个项目需要被维修的时候,Titan使用 EJB 定时器服务提醒 适当的维护组员.在这一个章节中,我们修改Ship维护 EJB 处理它自己的维护时 间表.Titan的健康和安全部分可以使用业务方法在Ship维护EJB到时序和取消维 护项目,且当一个项目需要被维修的时候,船维护 EJB 将会照顾提醒正确的维护 组员. 13.2. 定时器维护的API 定时器服务允许一个企业Bean通知它当一个指定的日期到达时,当一些时间过去 时,或间隔的发生.使用定时器服务,一个企业Bean必需实现 javax.ejb.TimedObject接口,它定义单个加调方法,ejbTimeout(): package javax.ejb; public interface TimedObject { public void ejbTimeout(Timer timer) ; } 在EJB3.0中@javax.ejb.Timeout批注可以应用到方法上,它返回void并且有一个 javax.ejb.Timer类型参数.我们将看这两个的例子. 当预定的时间到达或指定间隔已经过去,容器系统会调用企业Bean的timeout回 调方法.企业Bean可以执行任何处理,当超时时它需要响应,如,执行一个报表,统 计记录,修改其它组件的状态,等等.例如,船维护EJB可以修改实现TimedObject 接口,如下所示: @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote implements javax.ejb.TimedObject { public void ejbTimeout(javax.ejb.Timer timer) { // business logic for timer goes here } } 作为另一种选择,也可以使用@javax.ejb.Timeout 批注: @Stateless public class ShipMaintenanceBean implement ShipMaintenanceRemote { @Timeout public void maintenance(javax.ejb.Timer timer) { // business logic for timer goes here } } 一个企业Bean进度本身引用TimerService参照当时间到来时进行通知,可以获取 从EJBContext或直接注入到你Bean中使用 @javax.annotation.Resource 批 注.TimerService允许组件注册自身来进行通知,当指定日期到达时,在一些时间 之后的周期,或间隔时间里.下面的代码显示一个组件注册通知,从现在起,的30 天. // Create a Calendar object that represents the time 30 days from now. Calendar time = Calendar.getInstance( );// the current time. time.add(Calendar.DATE, 30); // add 30 days to the current time. Date date = time.getTime( ); // Create a timer that will go off 30 days from now. TimerService timerService = // from EJBContext or injected timerService.createTimer( date, null); 例子中创建了一个Calendar 对象描述当前的时间,并且使这个对象增加30天所 以代表从现在起的30天.代码获取引用到容器的TimerService并且调用 TimerService.createTimer( )方法,传递Calendar 对象的java.util.Date类型 值,如此创建了一个在30 天之后将会引发定时器. 我们可以增加一个scheduleMaintenance( )方法,Ship维护的EJB允许一个客户 端定时维护指定的船.当这个方法被调用,客户端传递船的名子维护将会执行,一 个项目维掮 的描述,和执行的日期.例如,一个客户端可以从2006.4.2为巡航船 Valhalla 安排一个定时维护项目,如下代码所示: InitialContext jndiCntxt = new InitialContext( ); ShipMaintenanceRemote maintenance = (ShipMaintenanceRemote) jndiCntxt.lookup("ShipMaintenanceRemote "); ShipMaintenanceRemote Calendar april2nd = Calendar.getInstance( ); april2nd.set(2006, Calendar.APRIL, 2); String description = "Stress Test: Test Drive Shafts A&B..."; maintenance.scheduleMaintenance("Valhalla", description, april2nd.getTime( )); ShipMaintenanceBean实现scheduleMaintenance( )方法,照顾安排计划使用定 时器服务的事件, 如下所示: @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote{ @Resource javax.ejb.TimerService timerService; @PersistenceContext(unitName="titanDB") entityManager; public void scheduleMaintenance(String ship, String description, Date dateOfTest) { String item = ship + " is scheduling maintenance of " + description; timerService.createTimer(dateOf, msg); } @Timeout public void maintenance(javax.ejb.Timer timer) { // business logic for timer goes here } ... } 如你所看到的,Ship Maintenance EJB负责获取一个引用到定时器服务并且它的 时序安排有它自己的事件.当2006.4.2到来时,定时期服务调用使用批注的 maintenance( ) 方法在Ship Maintenance EJB描述Valhalla.当回调方法被调 用后,Ship Maintenance EJB发送一个JMS消息包括其它健康和安全部分的描述, 在泰坦巡航中,提醒它一个重要的测试要求.下面是maintenance的实现. @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote { @Resource javax.ejb.TimerService timerService; public void scheduleMaintenance(String ship, String description, Date dateOfTest) { String item = ship + " is scheduling maintenance of " + description; timerService.createTimer(dateOf, item); } @Resource(mappedName="ConnectionFactory") ConnectionFactory factory; @Resource(mappedName="MaintenanceTopic") topic; @Timeout public void maintenance(javax.ejb.Timer timer) { try{ String item = (String)timer.getInfo( ); Connection connect = factory.createConnection( ); Session session = connect.createSession(true,0); MessageProducer publisher = session.createProducer(topic); TextMessage msg = session.createTextMessage( ); msg.setText(item); publisher.send(msg); connect.close( ); }catch(Exception e){ throw new EJBException(e); } } } 13.2.1.TimerService接口 TimerService接口提供一个企业Bean,可以访问EJB容器的时间服务,所以,可以 新建一个定时器和列出已存在的定时器.TimerService 接口是javax.ejb包中的 一部分在EJB3.0中有如下定义; package javax.ejb; import java.util.Date; import java.io.Serializable; public interface TimerService { // Create a single-action timer that expires on a specified date. public Timer createTimer(Date expiration, Serializable info) throws IllegalArgumentException,IllegalStateException,EJBException; // Create a single-action timer that expires after a specified duration. public Timer createTimer(long duration, Serializable info) throws IllegalArgumentException,IllegalStateException,EJBException; // Create an interval timer that starts on a specified date. public Timer createTimer( Date initialExpiration, long intervalDuration, Serializable info) throws IllegalArgumentException,IllegalStateException,EJBException; // Create an interval timer that starts after a specified duration. public Timer createTimer( long initialDuration, long intervalDuration, Serializable info) throws IllegalArgumentException,IllegalStateException,EJBException; // Get all the active timers associated with this bean public java.util.Collection getTimers( ) throws IllegalStateException,EJBException; } 这四种类型的TimerService.createTimer( )方法,建立一个定时器使用不同的 配置类型.有两种必需的定时期类型:single-action 和interval. A single- action类型的定时期仅能过期一次,并且一个间隔的定时器可以过期多次,在指 定的周期内.当一个定时期过期,定时期服务调用组件的ejbTimeout( )方法,或 者是使用了@javax.ejb.Timeout的回调方法. 这里是这四种createTimer( ) 方法是怎样工作的.此时,我们只讨论过期和持续 时间参数和它们的使用.序列化的信息参数将会在后面的章节中进行讨论. createTimer(Date expiration, Serializable info) 创建一个single-action类型的定时期,它仅能过期一次.过期的设置使用 expiration 参数.这里是设置一个定时期,在2006.7.4日到期: Calendar july4th = Calendar.getInstance( ); july4th.set(2006, Calendar.JULY, 4); timerService.createTimer(july4th.getTime( ), null); createTimer(long duration, Serializable info) 创建一个single-action类型的定时期,它仅能过期一次.定时期在持续的时间 (使用毫秒来测量)后期满. 该如何设定在 90 天中期满的一个定时器在这里: long ninetyDays = 1000 * 60 * 60 * 24 * 90; // 90 days timerService.createTimer(ninetyDays, null); createTimer(Date initialExpiration, long intervalDuration, Serializable info) 创建一个间隔性的定时期可以过期多次.这个定时器首先终止在 initialExpiration 日期参数的设置.在第一次过期后,后来发生的,由 intervalDuration参数决定的间歇性的过期(在毫秒中).该如何设定 2006 年 七月 4 日期满而且继续在那日期之后每三天期满的一个定时器在这里: Calendar july4th = Calendar.getInstance( ); july4th.set(2006, Calendar.JULY, 4); long threeDaysInMillis = 1000 * 60 * 60 * 24 * 3; // 3 days timerService.createTimer(july4th.getTime( ), threeDaysInMillis, null); createTimer(long initialDuration, long intervalDuration, Serializable info) 创建一个间隔性的定时期可以过期多次.定时器首次过期是在给定的 initialDuration过去之后.在第一个过期之后,后来的过期在被 intervalDuration 叁数给予的时间间隔内发生.initialDuration 和 intervalDuration 在毫秒中. 该如何设定在 10 分钟中期满而且以后的每个小 时期满的一个定时器在这里: long tenMinutes = 1000 * 60 * 10; // 10 minutes long oneHour = 1000 * 60 * 60; // 1 hour timerService.createTimer(tenMinutes, oneHour, null); 当一个定时器被创建,定时器服务会使它持久化到一些类型的辅存中,所以,当系 失败后,它仍会存在.如果服务器关闭,当服务器再次启动时,定时期仍可以激活. 当规范不明确时,一般假定任何定时器的过期时当系统重启它再次被激活时.如 果是一个间隔性的定时器,可以过期多次,当服务器停止时,它也可能多次被关闭 当系统再次启动时.参考提供商文档来学习怎样处理当系统失败后定时期的过期 处理.TimerService.getTimers() 方法返回已经被指定的企业Bean设定的所有 定时器.例如,如果这个方法调用在EJB描述的巡航船Valhalla,它公返回为 Valahalla设置的定时器,不能设置其它船的定时器.getTimers( )方法返回一个 java.util.Collection.一个零个或多个javax.ejb.Timer对象的无序集合.每个 Timer对象负责不同的时间事件,已经被组件预定使用定时器服务的. getTimers( )方法常常用来管理已存在的定时器.一个Bean可以通过定时器对象 的集合和取消任何不再有效或重新制定的定时器.例如,Ship Maintenance EJB 定义clearSchedule( )方法,允许客户端取消所有的预定维护,下面是这个方法 的实现: @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote { @Resource javax.ejb.TimerService timerService; public void clearSchedule( ){ for (Object obj : timerService.getTimers( )){ javax.ejb.Timer timer = (javax.ejb.Timer)obj; timer.cancel( ); } } public void scheduleMaintenance(String name, String desc, Date date) { // business logic goes here public void ejbTimeout(javax.ejb.Timer timer) { // business logic for timer goes here } ... } 逻辑很简单.在获取引用到TimerService之后,我们获取了包含所有的定时器的 一个集合.我们通过集合对象的循环,取消船的 MaintenanceItem上的每个定时 器.Timer对象实现一个cancel()方法,可以移除时间事件从定时器服务,所以它 永远不会过期. 13.2.1.1. 异常 TimerService.getTimers( )方法能抛出一个IllegalStateException或一个 EJBException.所有的方法定义这两种异常,加上第三个异常 IllegalArgumentException .这里是为什么TimerService的方法会抛出这些异 常: java.lang.IllegalArgumentException 持续时间和过期时间参数必需是有效值.如果拒绝的数字用一个持续时间参数或 空值用于过期时间这个异常会抛出.它的类型是java.util.Date. java.lang.IllegalStateException 如果企业Bean尝试调用来自一个它没被允许的方法的 TimerService 方法之一, 这个异常会被抛出. 每个企业Bean(例如,实体,无状态会话Bean,和消息驱动 Bean)的类型定义有它自己的操作集合.然而,大体上, TimerService 方法能从 除了 EJBContext 方法以外的任何地方被调用.(例如,setEntityContext( ), setSessionContext( ), 和 setMessageDrivenContext( )) javax.ejb.EJBException 当一些系统级类型异常发生在定时器服务中会抛出睦异常. 13.2.2.Timer Timer是一个对象实现javax.ejb.Timer 接口.它代表一个定时事件被预定使用 在定时服务中.Timer对象的返回通过TimerService.createTimer( ) 和 TimerService.getTimers( )方法,并且Timer是TimedObject.ejbTimeout( )方 法的唯一参数或者使用@javax.ejb.Timeout批注的回调.Timer接口是: package javax.ejb; public interface Timer { // Cause the timer and all its associated expiration // notifications to be canceled public void cancel( )throws IllegalStateException,NoSuchObjectLocalException,EJBException; // Get the information associated with the timer at the time of creation. public java.io.Serializable getInfo( ) throws IllegalStateException,NoSuchObjectLocalException,EJBException; // Get the point in time at which the next timer // expiration is scheduled to occur. public java.util.Date getNextTimeout( ) throws IllegalStateException,NoSuchObjectLocalException,EJBException; // Get the number of milliseconds that will elapse // before the next scheduled timer expiration public long getTimeRemaining( ) throws IllegalStateException,NoSuchObjectLocalException,EJBException; //Get a serializable handle to the timer public TimerHandle getHandle( ) throws IllegalStateException,NoSuchObjectLocalException,EJBException; } 一个Timer实例完全代表一个时间事件和用于取消定时器,获取一个序列化处理, 获取应用程序数据关联的定时期,和查找下一个将要发生的预定过期的定时器. 13.2.2.1.取消定时器 在前面的章节中使用了Timer.cancel( )方法.它用于取消一个指定的定时器从 定时器服务,所以它永远不会到期.如果指定的定时器需要完全的移除或简单的 重新计划它是非常有用的.为了重新订制时间事件,取消定时器和创建一个新的. 例如,当一个船组件失败和被替换,组件必需有一个它维护的重新计划;如果它在 五月被替代,它一点道理也没有在六月执行发动机的维修. scheduleMaintenance( )方法可以修改,所以它可以增加一个新的维修项目或替 换已经存在的通过取消和增加一个新的: package com.titan.maintenance; import javax.ejb.*; import java.util.Date; import javax.annotation.Resource; @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote { @Resource javax.ejb.TimerService timerService; public void scheduleMaintenance(String ship, String description, Date dateOf) { String item = ship + " is scheduling maintenance of " + description; for (Object obj : timerService.getTimers( )){ javax.ejb.Timer timer = (javax.ejb.Timer)obj; String scheduled = (String)timer.getInfo( ); if (scheduled.equals(item)) { timer.cancel( ); } } timerService.createTimer(dateOf, item); } @Timeout public void maintenance(javax.ejb.Timer timer) { // business logic for timer goes here } } scheduleMaintenance( ) 方法首先获取一个船的定时器的所有集合.它比较传 递到方法中的每个定时器的描述.如果匹配,意味着一个维护项目的定时器已经 存在并且将要被取消.在循环之后,一个新的定时器被添加到定时器服务中. 13.2.2.2.鉴别定时器 当然,比较描述符来鉴别定时器是不可靠的, 因为描述容易随着时间的过去改变. 真的被需要的是能包含描述和一个精确的识别符的一个比较强健的信息标识. 所有的TimeService.createTimer( )方法定义一个info对象,同他们的最后一个 参数.info对象被定时器服务存储的应用程序数据而且被传递到企业Bean,当超 时时回调被调用.serializable 对象用于info参数可以是任何事情,只要它实现 java.io.Serializable接口和下面的序列化规则.info对象可以放到很多地方使 用,但是一种很明显的使用将联合定时器和一些鉴别符. []在一些基本的情况下,所有的对象的序列化需要实现java.io.Serializable接 口和确定任何非序列化(例如,JDBC连接处理)字段标记上暂时的. 要想从定时器中获取一个info对象,你可以调用定时器的getInfo()方法.这个方 法返回一个序列化对象,你需要转换成一个适当的类型.所以,我们将使用字符串 做为info对象,但是有很多精细(和可靠)的可能性.例如,比较取代重复的定时 器维护描述符,Titan决定使用唯一的项目维护编号(MINs). MINs和维护描述可 以组合到一个新的MaintenanceItem对象: public class MaintenanceItem implements java.io.Serializable { private long maintenanceItemNumber; private String shipName; private String description; public MaintenanceItem(long min, String ship, String desc) { maintenanceItemNumber = min; shipName = ship; description = desc; } public long getMIN( ){ return maintenanceItemNumber; } public String getShipName( ){ return shipName; } public String getDescription( ){ return description; } } 使用MaintenanceItem类型,我们可以修改scheduleMaintenance( )方法,使其更 精确,如下所示(黑体部分): @Stateless public class ShipMaintenanceBean implements ShipMaintenanceRemote { @Resource javax.ejb.TimerService timerService; public void scheduleMaintenance( MaintenanceItem maintenanceItem, Date dateOfTest) { for (Object obj : timerService.getTimers( )){ MaintenanceItem scheduled = (MaintenanceItem)timer.getInfo( ); if (scheduled.getMIN() == maintenanceItem.getMIN( )){ Timer.cancel( ); } } } @Timeout public void maintenance(javax.ejb.Timer timer) { // business logic for timer goes here } } MaintenanceInfo类包含将要维护的工作和发送到维护系统使用JMS.当有一个定 时器到期时,定时器服务调用Ship Maintenance EJB上的maintenance( )方法. 当此方法被调用,info对象获取从Timer对象,并且确定将要执行的定时器业务逻 辑. 13.2.2.3. 取回来自定时器的其他的信息 Timer.getNextTimeout( ) 方法简单的返回日期戳通过java.util.Date实例,下 一个将要过期的时间.如果是一个single-action 类型的定时器,Date返回的时 间是定时器将要终止的时间.然而,如果是一个间隔性定时器,Date返回剩余的时 间直到下次终止.奇妙地,当一个间隔性定时器被设置,没有方法确定后来终目日 期或间隔.处理这的最好方式是把信息放入你的info对象中. Timer.getTimeRemaining( )方法返回数值是毫秒为单为,在定时器的下个终目 时间.同样的,getNextTimeout( )方法只提供关于下次终目时的信息. 13.2.2.4. TimerHandle 对象 Timer.getHandle( ) 方法返回一个TimerHandle对象.TimerHandle对象类似于 我们在第五单中讨论过的javax.ejb.Handle 和 javax.ejb.HomeHandle.它作为 一个引用,可以保存到一个文件或其它资源,并且用于后面的重新访问定时器. package javax.ejb; public interface TimerHandle extends java.io.Serializable { public Timer getTimer( ) throws NoSuchObjectLocalException, EJBException; } TimerHandle仅在定时器没有过期(如果它是一个single-aciton类型的定时器) 或被取消时有效.如果定时器不存在,调用TimerHandle.getTimer( )方法会抛了 一个javax.ejb.NoSuchObjectException .异常.TimerHandle对象是本地的,意 味着他们不能够在容器系统外被生成.传递TimerHandle作为参数到远程或端点 接口是不合法的,因为本地企业Bean必需用于相同的容器系统中. 13.2.2.5.异常 在Timer接口中的所有方法定义抛出两种异常: javax.ejb.NoSuchObjectLocalException 如果调用一个期满的single-action类型定时器或一个被取消的定时器的任意方 法,会抛出此异常. javax.ejb.EJBException 当一些系统级类型的异常发生在定时器服务时这个异常会被抛出. 13.3.事务 当一个Bean调用createTimer(),这个操作要在当前事务范围内执行.如果事务回 滚,定时期不会被完成;它不会被创建(或,更好,未被创建).例如,如果,Ship Maintenance EJB的scheduleMaintenance( ) 方法有一个事务属性RequiresNew, 当方法被调用时,一个新的事务会被创建.如果这个方法抛出异常,事务回滚并且 新的定时器事件不会被创建. 在大多数情况下,在组件类上调用超时的回调方法将有一个RequiresNew事务属 性.这确定工作执行在容器初始化的事务回调方法.在第十六章中将会详细介绍 事务. 13.4无状态会话Bean的定时器 我们的Ship Maintenance EJB 例子是一个无状态会话Bean使用定时器服务的一 个例子.无状态会话Bean可以使用统计或批处理.作为一个统计代理,一个无状态 会话Bean的定时器可以监听系统的状态确定任务的完成和数据的一致性.这种类 型的工作可能跨实体和多个数据源.如此EJB可以执行批下理工作,如数据库的清 除,记录的转移,等等.无状态会话Bean的定时器也可以部署成代理执行一些类型 的智能工作为他们的团体服务.一个代理可能被想像成是一个审计的扩充:它监 视系统,同时也会自动修复系统中的问题. 无状态会话Bean的定时器仅可以关联一个指定类型的会话Bean.当一个无状态会 话Bean的定时器过期时,容器选择一个无实态Bean类型的实例从实例池中并且调 用超时的回调方法.因为实例池中的所有无状态会话Bean的实逻辑上相等,所以 这有道理.任意实例可以服务于任何客户,包括容器自身. 无状态会话Bean的定时器常常用来管理任务;他们也用于当时间事件应用到一个 实体集合代替刚才的.例如,无状态会话Bean的定时器可能用于统计所有维护记 录来确定他们的状态和指导方针:在特定的时间间隔,一个定时器通知Bean查找 维护记录从所有的船上,并且生成一个报表.一个无状态会话定时器也可以用于 做一些像发送通知到所有的乘客为指定的巡航,如此,避免定时器攻击问题. 无状态会话Bean可以访问一个注入TimerService从SessionContext在 @PostConstruct, @PreDestroy ,或其它的业务方法,但是它不能访问定时器服 务从任何setter注入方法.这就意味着,一个客户端的调用,无状态会话 Bean(create或一个业务方法)必需为定时器设定特定的方法.这是保证定时器被 设定的唯一方法. 设置一个定时器在@PostConstruct 方法是有问题的.首先,不能够保证一个 @PostConstruct回调在何时被调用.一个@PostConstruct 回调方法的无状态会 话Bean调用有时是在Bean被实例后,在进入方法-就绪池之前.然而,一个容器可 能创建一个实例池直到第一个客户端访问那个Bean,所以,如果一个客户端(远程 或其它)永远不访问这个Bean,@PostConstruct回调可能永远不会被调用,并且定 时器将永远被设置.另一个问题是使用@PostConstruct它的调用在每个实例 进 入池之前;你必须阻止后来的例证 (建立的例证在第一个例证之后) 设定第一个 例证创造的定时器会已经做了.它尝试使用一个静态变量来避免重新创建定时器, 如下面的代码,但是这会引发问题: public class StatelessTimerBean javax.ejb.TimedObject { static boolean isTimerSet = false; @Resource TimerService timerService; @Resource SessionContext ctx; @PostConstruct public void init( ){ if( isTimerSet == false) { long expirationDate = (Long)ctx.lookup("expirationDate"); timerService.createTimer(expirationDate, null ); isTimerSet = true; } } 虽然,这看起来是一个很好的解决方法,它仅能工作在应用程序部署在一个服务 器同一个VM和一个类装载器.如果使用一个集群系统,一个服务器使用多个VM,或 多个类装载器(非常常见),它不会工作因为Bean的实例不会初始化在相同的VM 相同的类装载器将不能访问相同的静态变量.在这种情况下,很容易使用多个定 时器来做相同的事情.如果定时器已经建立,一种选择是使用@PostCreate访问和 移除所有先前存在的定时器.但是这可能会影响执行效率,因为有可能新的实例 将要被创建和添加到池中多次,造成多次@PostCreate回调,回此,多个调用到 TimerService.getTimers( ).同时,没有要求定时器服务跨跃集群,所以,一个集 群中节点设置的定时器对于其它集群节点中设置的定时器是不可见的. 关于无状态会话Bean,你可能不使用@PreDestroy回调方法取消或创建定时器.在 他们被从内存中被驱逐之前@PreDestroy 回调的调用在单独的实例中.它不能被 调用在响应客户端调用远程或本地移除方法.同时,@PreDestroy 回调不能够与 一个没有部署的Bean进行通讯.它仅指定单个实例,结果,从整体而言,调用 ejbRemove()方法不能够确定关于EJB的事情,而且你不能使用它来创建或取消定 时器. 当一个无状态会话Bean实现javax.ejb.TimedObject 接口,或包含一个 @javax.ejb.Timeout回调方法,它的生命周期的改变包括时间事件的维护.当一 个定时器期满的时候,定时器服务把组件的实例从实例池中取出.如果池中没有 实例 ,容器创建一个.如图所示的无状态会话Bean的生命周期,它实现了 TimerOut接口. Figure 13-1. Stateless session bean life cycle with TimedObject 13.5.消息驱动Bean的定时器 消息驱动Bean的定时器在很多方面与无状态会话Bean的定时器类似.定时器只关 联一种类型的Bean.当一个定时器期满,一个消息驱动Bean的实例被从池中选择 执行超时的回调方法.另外,消息驱动Bean可以用来执行统计或其它类型的批量 工作.消息驱动Bean的定时器与无状态会话Bean的定时器的主要不同是初始化: 定时器的创建用来响应一个输入消息,或,当容器支技它时,从配置文件中输入. 为了初始化消息驱动Bean的定时器从一个输入消息,你简单的放置调用到消息处 理方法的TimerService.createTimer( )方法.对于一个JMS为基础的消息驱动 Bean,方法调用在onMessage方法中: @MessageDriven public class JmsTimerBean implements MessageListener { @Resource TimerService timerService public void onMessage(Message message){ MapMessage mapMessage = (MapMessage)message; long expirationDate = mapMessage.getLong("expirationDate"); timerService.createTimer(expirationDate, null ); } @Timeout public void timeout( ){ // put timeout logic here } 输入的JMS消息将包含关于定时器的信息:开始日期,持续时间,或序列化的info 对象.组合JMS同定时器服务可以一些重要的设计选项为实现统计,批处理,和代 理解决. 虽然它没被标准化,但是提供商可能会允许消息驱动Bean定时器在部署时配置. 这会需要一个专有的解决,因为对于消息驱动Bean定时器的标准配置选项不存在. 这样配置定时器的优点是不需要客户端初始化一些定时器启动操作.当组件被部 署时,定时器被自动设置.这一个能力使消息驱动Bean定时器成为比较类似 Unix cron 工作,这被预先配置然后运行.查看厂商文档,看是否为消息驱Bean提供一 个专有的配置.如同无状态会话Bean的例子,TimedObject接口或@Timeout方法改 变消息驱动Bean的生命周期(看图13-2).当一个定时事件发生时,容器要从池中 取出一个消息驱动Bean的实例.如果在池中没有实例,那么一个实例从不存在状 态移到方法-就绪池中,在它接收到时间事件之前. 13.5.1.定时服务问题 定时器服务是 EJB 平台的优良附加物,但是它是有限制的.很多可以从cron,已 经在Unix中好几年列入计划功用了解到.这里是一些改进服务的建议.如果你公 对定里器如何工作感兴趣,和怎才能改进他们进行对照,觉得跳过这一单的其它 部分,它不是必需要求阅读的.由于哪一说, 理解定时器能被改良的方式帮助你 了解他们的限制.如果你有一些时间而且想要扩大你的定时器的理解, 继续阅 读. Figure 13-3. message-driven bean lifecycle with TimedObject 13.5.1.1.一个非常小的关于cron Cron是一个Unix程序,允许你定时执行脚本(类似于DOS中的批处理文件),命令, 和其它程序在指定的日期和时间.不同于EJB定时器服务,cron考虑到弹性,以日 历为基础的计划安排.Cron工作可以预定间隔的执行,包括一个小时中指定的分 钟,天中的那几个小时,一周中的几天,一个月中的几天,和一年中的几个月. 举例来说,你可以安非一个Cron工作运行在每个星期五的下午12:15.或者每不时, 或者每个月的第一天.这样精细的划分听起来很复杂,不过叙述起来很容易.Cron 使用整型值的五个字段简单的文本格式.通过空格或制表符进行分隔,描述间隔 需要脚本的运行.Figure 13-3表示栏位位置和他们的意义. 栏位的顺序很重要,因为,每一个指定不同的日历指示:分钟,小时,月,和一周中的 天.下面的例子表示如何安排cron工作: 20 ****---> 20 minutes after every hour. (00:20, 01:20, etc.) 5 22 ***---> Every day at 10:05 p.m. 0 8 1 **---> The first day of every month at 8:00 a.m. 0 8 4 7 *---> The fourth of July at 8:00 a.m. 15 12 ** 5---> Every Friday at 12:15 p.m. 下为译文: 20 ****---> 每小时的20分钟之后 (00:20, 01:20, 等等.) 5 22 ***---> 每天下午的10:05分 0 8 1 **---> 每月第一天的上午8:00 0 8 4 7 *---> 七月第四天的上午8:00 15 12 ** 5---> 每周五的下午12:15 一个星号指出所有的数值是有效的. 举例来说,如果你使用一个星号作为分钟的 栏位, 你正在安排计划 cron在每小时的那几分钟运行工作.你可以定义更复杂 的间隔通过指定多值,分开通过逗号,为一个栏位.除此之外, 你能叙述使用连字 号的时间的范围: 0 8 ** 1,3,5 --->Every Monday, Wednesday, and Friday at 8:00 a.m. 0 8 1,15 **--->The first and 15th of every month at 8:00 a.m. 0 8-17 ** 1-5--->Every hour from 8:00 a.m. through 5:00 p.m., Mon- Fri. 译文: 0 8 ** 1,3,5 --->每个周一,周三,和周五的上午8:00 0 8 1,15 **--->每个月的第一和第十五天的上午8:00 0 8-17 ** 1-5--->周一到周五的每天上午8:00到下午的5:00 Cron 工作被预定使用 crontab 文件,这是一个简单的文本文件,你可以配置日 期/时间字段和命令,一个常运行的脚本命令. 13.5.1.2. 改良定时器 cron 日期/时间格式现在提供的更有弹性比EJB定时器. 定时器服务需要你指定 间隔精确到毫秒, 这有一点笨拙 (你可以转换 天,小时,和分钟到毫秒), 但是 更重要,它对许多真实的计划安排需要是不够弹性的. 举例来说,没有方法确定 一个定时器期满在每个月的第一或第十五天,或者在上午的8:00到下午的5:00中 的每个小时,周一到周五.你可以放置更复杂的间隔,但是要付出的代价是增加逻 辑到你的组件代码中来计算他们,并且在更复杂的情节中,你将会为相同的任务 设置多个定时器. Cron 也不是完美的.计划安排工作像在一台录象机上设定一个定时器: 依照时 钟和日历,每件事物被预设.你能指定 cron 在那年的特定天的特定时间运行一 个工作,但是你在来自一个任意的出发点的比较的间隔不能让它运行一个工作. 举例来说, cron 的日期/时间格式不让你排程一个工作每 10 分钟运行,从现在 开始.你必须安排它在每小时的特定分钟运行 (举例来 说,0,10,20,30,40,50).Cron 也被限制到回到工作的计划安排; 你不能建立一 个单一行动定时器,而且你不能设定一个开始日期.cron 和 EJB 定时器服务的 问题是你不能规划 , 一个停止约会一个定时器将会自动地取消它本身的日期. 你也可能已经注意 cron 间隔心度是对分钟不愿对毫秒.起先一瞥, 这看起来像 弱点一样, 但是在习惯,它非容易接受.因为日历驱动的计划安排, 较多的精确 只是不是很有用. 如果它可以处理像 cron 一样的日期/时间格式,藉由开始日期和结束日期,定时 器服务介接口将被改进.而非丢弃目前的 createTimer()调用(它是有用的, 尤 其对于单一行动定时器和任意的毫秒间隔),它可能会较好的用像 cron 一样的 语意增加一个新的方法.而非使用 0-6 指定星期的天,使用数值 Sun,Mon,Tue,Wed,Thu,Fri,和Sat(如同在Linux版本下面的cron).例如,代码确 定一个定时器,它运行在2006年,从10月1日起每周的下午11:00.2007年也是如 此: TimerService timerService = ejbContext.getTimerService( ); Calendar start = Calendar.getInstance( ).set(2006, Calendar.OCTOBER, 1); Calendar end = Calendar.getInstance( ).set(2007, Calendar.MAY, 31); String dateTimeString = "23 *** Mon-Fri"; timerService.createTimer(dateTimeString, start, end, null); 因为他们非常有用,所以对定时器服务的这个被提议的变化保存以毫秒为单位的 createTimer() 方法.像cron配置一样重要,不过它不是一个银的子弹.如果你想 要确定一个定时器每30秒启动(或者在任何的任意点及时)一次,你需要使用现有 createTimer() 方法之一.真正精确到毫秒很困难; 首先,正常处理和线程很容 易延迟响应时间,一个服务器时钟一定适当地与正确的时间 (也就是, UTC) 一 起同步化[*]到毫秒,可大部分不是. [*] 协调通用时间 (UTC) 是国际的标准叁照时间. 服务器能与使用网络时间协 定 (NTP) 和公共时间服务器的 UTC 一起协调. 被协调的全世界的时间在标准 化国家之中被缩写如一个妥协的 UTC . 一种完全的解释在 http://www.boulder.nist.gov/timefreq/general/misc.htm#Anchor-14550上 是由标准和技术的常见疑问的国立学会提供 13.5.1.3.消息驱动Bean:标准配置属性 虽然前面的建议会提高一些可用性,但是他们对EJB的定时器并不起决定性作用. 有一个很大的可能性是使用消息驱动Bean像cron一样的工作配置在部署时和自 动地运行.不幸地, 没有标准的方法在配置时间配置一个信息驱动Bean的定时器. 一些厂商可能支持这,但是其它的不是.提前配置消息驱动Bean的定时器是有很 高的要求的对于开发者,想要确定消息驱动Bean执行工作在特定的日期和时间. 没有部署-时间配置的支持,唯一可靠的方法是安排一个企业Bean定时器有一个 客户端的调用方法或发送一个JMS消息.这不是一个合理的解决.开发者需要配置 -时间的配置,而且它应该被加到规格的下一个版本.在前面的部分构建一个cron 一样的语意目的在于,它可以很容易的设计一个标准的活动属性来配置消息驱动 Bean的定时器在部署时.例如,下面的代码配置一个消息驱动子,Audit EJB运行 在2006年的10月1日起每周一到周午的下午11:00到2007看的5月31日结束(开始 和结束日期不是必需的): Run Monday through Friday at 11:00 p.m. Starting on Oct 1st,2003 until May 31st, 2004 dateTimeFields 23 *** Mon-Fri startDate October 1, 2003 endDate May 31, 2004 如果他们提高对cron这样的支持,这种类型的配置会让提供者更容易的现.除此 之外,你可以配置信息驱动Bean使用以毫秒为基础的定时器 EJB 2.1 支持. 13.5.1.4. 定时器API的其它问题 在语义上一个Timer对象传递一些信息关于对象自身的.没有方法决定一个定时 器是否是一个单一行动定时器或一个间距定时器.如果它是一个间距定时器, 没 有方法决定被配置的间隔,否则是否定时器已经运行它的第一次期满.为了解决 这些问题,另外的方法应该被加到Timer接口上,提供这一信息.权益之计, 把一 个info对象放置这种信息是一个好的想法,所以它可以被需要它的应用程序存 取. 13.6. 结束语 在这一章中,是否要点的改变被EJB专家组所采用.这应该是EJB开发者社区应该 回答的.它是有可能的,找到其它的方法来改进这些提议.不管结果如何,定时器 服务的语义上的限制和缺乏配置消息驱动Bean定时器的支持问题.当你在开发定 时器时,你将很快的发现需要一个更好的方法来描述需要,和一些配置定时器的 方法在部署时,代替使用客户端应用程序来初始化一个确定性事件. 第十四章 JNDI ENC和注入 每一个EJB容器在部署到应用程序服务器时有它自己的内部调用的企业命名的上 下文(ENC).这个ENC实现通过JNDI并且EJB容器可以拥有关于它的环境引用的盒 子.想像它是EJB容器的一个个人通讯地址本,它记录下J2EE服务的不同服务的地 址,在它的业务逻辑里面使用. 在第十一章中,我们开始讨论了一点关于ENC,演示了怎样使用批注像 @javax.annotation.EJB 和 @javax.annotation.Resource注入引用到J2EE服务 到组件的字段.这种注入的处理使用通过EJB容器的ENC.在本章中,我们将示怎 样移植ENC和使用它作为你自己的JNDI注册,并且我们演示你怎样使用它注入环 境引用到你的组件字段. 14.1.JNDI ENC ENC 已经是从最早的 1.0以后在 EJB 规范中引用.它最开始作为一个本地的 JNDI命名指定到一个EJB容器.开发者可以定义别名到资源中,EJB,和环境是在 JNDI ENC中是通过EJB的XML部署描述符进入的.这些别名可以直接在JNDI中进行 查找通过业务逻辑.在EJB3.0中,这种机制得到了扩展,所以JNDI ENC的引用可以 被直接注入到组件类的字段中.批注是做这件事的主要机制,但是,XML部署描述 符支持仍然有效对于想要使用抽象的人. 14.1.1. 什么能在 JNDI ENC 被登记? 很多不同的项目可以被绑定到ENC:引任EJB接口的引用,一个JMS队列或主题目的 地,JMS连接工厂,数据源,任意的JCA资源,和原始值.J2EE服务如 javax.transaction.UserTransaction, javax.ejb.TimerService, 和 org.omg.CORBA.ORB 也是有效的在ENC中. 14.1.2. JNDI ENC 如何组装? ENC的JNDI命名空间组装有两种不同的方式:通过XML或批注.任意引用,你可以定 义在XML中,服务或资源自动组装到JNDI ENC同名字的引用.在你组件类上的任意 环境的批注将会引起ENC的组装.当项目被绑定到EJB容器的JNDI ENC,它可以被 JNDI的查找进行引用. 14.1.3. XML方式 举例说明XML方式如何工作,让我们在第 11 章中写过的,定义一个引用到无状态 会话Bean. 在这里我们为 TravelAgent EJB 定义接口关于 ProcessPayment EJB 的参考: TravelAgentBean ejb/ProcessPayment Session com.titan.processpayment.ProcessPaymentLocal ProcessPaymentBean 元素告诉EJB容器,travelAgentBean想要引用ProcessPayment EJB.关于这个组件的引用在travelAgentBean 的 JNDI ENC 被登记命名为 ejb/ProcessPayment.它的定义通过元素.其它的引用,像资源和 JMS目的地,有类似的XML元素像指定并且引用将会被绑定到他 们的JNDI ENC.在J2EE的每个服务类型有自己的引用语法.我们将会在这一单中 的例子中看到他们的全部. 14.1.4. Annotation 方式 每一个引用类型也有一个同批注的通讯,也可以使用XML作为一种选择。如果你 指定这些批注在组件类中,他们将会导致 JNDI ENC 与在注解被定义的信息一起 引用: import javax.annotation.EJB; @Stateful @EJB(name="ejb/ProcessPayment", beanInterface=ProcessPaymentLocal.class, beanName="ProcessPaymentBean") public class TravelAgentBean implements TravelAgentRemote { ... } 在这一个例子中, 我们登记在 ejb/ProcessPayment 名字下面的关于 ProcessPayment EJB 的参考.运行travelAgentBean的业务逻辑可以做JNDI的查 找,找到这个引用.每一个环境注释,如@javax.annotation.EJB,有一个name() 属性,指定JNDI ENC名,你想要服务的引用被绑定.这一个章节的后半段描述每一 个这些不同的环境注解的所有的详细信息。 14.1.5. 事物如何叁考从ENC? 任何已注册的JNDI ENC可以查询通过类似java:comp/env 的上下文名字.comp部 分是名字的构成部分,如果你调用jndi.lookup("java:comp/env")在 travelAgentBean中,你将获取EJB容器的ENC.如果你在ProcessPaymentBean里面 做相同的工作,你将会获取一个不同的ENC注册指定的Bean.应用程序肥务器知道 什么时候执行查找的ENC是活动的.@Stateful @EJB(name="ejb/ProcessPayment", beanInterface=ProcessPaymentLocal.class, beanName="ProcessPaymentBean") public class TravelAgentBean implements TravelAgentRemote { public TicketDO bookPassage(CreditCardDO card, double amount) { ProcessPaymentLocal payment = null; try { javax.naming.InitialContext ctx = new InitialContext( ); payment = (ProcessPaymentLocal) ctx.lookup("java:comp/env/ejb/ProcessPayment"); } catch (javax.naming.NamingException ne) { throw new EJBException(ne); } payment.process(card, customer, amount); ... } 在这个例子中,TravelAgent EJB 的bookPassage()方法需要一个引用到 ProcessPayment EJB 以便它能为预定送帐单客户。通过使用@EJB批注组件类创 建一个ProcessPayment EJB的引用在TravelAgent的ENC中.前面的代码做一个 JNDI的查找,这个引用.当ProcessPayment.process( ) 方法被调 用.java:comp/env/ejb/ProcessPayment 引用不再有效,因为因为 ProcessPayment 的 ENC 是活跃的而非 TravelAgent 的 ENC 。 14.1.5.1.使用EJBContext 在第11和12章中我们谈论过一点关于javax.ejb.SessionContext 和 javax.ejb.MessageDrivenContext接口.他们都继承了javax.ejb.EJBContext并 且他们可以用于查找ENC.EJBContext接口有一个方便的ENC查找方法.因为它不 抛出一个没有检查的异常,所以,这个方法比直接查找JNDI简单些,它使用关联 的名字代替全部的java:comp/env 字符串.SessionContext 或 MessageDrivenContext 可以注入到你的会话或消息驱动Bean通过使用 @javax.annotation.Resource 注释. @Stateful @EJB(name="ejb/ProcessPayment", beanInterface=ProcessPaymentLocal.class, beanName="ProcessPaymentBean") public class TravelAgentBean implements TravelAgentRemote { @Resource private javax.ejb.SessionContext ejbContext; public TicketDO bookPassage(CreditCardDO card, double amount) { ProcessPaymentLocal payment = (ProcessPaymentLocal) ejbContext.lookup("ejb/ProcessPayment"); payment.process(card, customer, amount); ... } 这个例子中使用EJBContext.lookup( )方法来查找travelAgentBean的引用 ProcessPayment。上下文对象注入到ejbContext字段使用@Resource注释.你不需 要增加java:comp/env 字符串到名字中当执行查找时,仅仅代替使用关联定的名 字,定义在批注或XML中的引用. 14.1.5.2. 批注方式注入 代替ENC查找,ProcessPayment EJB引用可以直接注入到TravelAgent EJB中的成 员变量.这个注入可以通过环境相关的批注或一个XML部署描述符: @Stateful public class TravelAgentBean implements TravelAgentRemote { @EJB private ProcessPaymentLocal payment; ... } 通过使用@javax.ejb.EJB 注释在 travelAgentBean 类的payment字段,EJB容器 将会自动注入一个引用到 ProcessPayment EJB直接到payment字段,当 travelAgent组件实例被创建.作为一种选择,如果你不喜欢这种形式的注入,规 范中也支持注入通过组件的setter方法: @Stateful public class TravelAgentBean implements TravelAgentRemote { private ProcessPaymentLocal payment; @EJB public void setProcessPayment(ProcessPaymentLocal payment) { this.payment = payment; } 不同于前面的例子,当travelAgentBean 实例被分配,EJB 容器将会改为启动 setProcessPayment() 方法,传递EJB的引用作为一个参数.这种工作模式的所有 其它的注入注将会在这一章中进行讨论.Setter方法注入有一些冗长比直接注入 字段,但是它的优点是易于单元测试.一些不同的环境注解的描述如@EJB将会在 这一章节的后半段中详细介绍.他们的全部功能类似于下面讲的@EJB模式的用 法. 14.1.5.3. 默认ENC名字 注释一个字段或一个组件类的setter方法,创建一个条目在JNDI ENC中,为其注 入元素.这对所有的环境注解是正确的的, 不仅仅 @EJB。 如果注释批注提定 name()属性,那么,参考引用存储一个名字在ENC中.如果没有指定名字.那么ENC 名子从批注字段名或setter方法中提取.在这种情况下,一个默认的ENC名字来源 于完全合法的类的字段或方法名,连同栏位的基本名字或方法.因此,对于前面的 例子中的payment字段,ENC名字是 com.titan.travelagent.TravelAgentBean/payment.对于setProcessPayment( ) 方法默认的ENC名字是 com.titan.travelagent.TravelAgentBean/processPayment.这些注入EJB引用 可以查找通过 java:comp/env/com.titan.travelagent.TravelAgentBean/payment 和 java:comp/env/com.titan.travelagent.TravelAgentBean/processPayment各 自的.ENC的名字变得非常重要当你想要使用XML来覆盖注入注释. 14.1.5.4. XML 注入 如果你不喜欢使用批注来初始化你的组件类字段,那么你可以使用ejb-jar.xml 部部署描述符中的元素. TravelAgentBean ProcessPayment Session com.titan.processpayment.ProcessPaymentLocal ProcessPaymentBean com.titan.travelagent.TravelAgentBean payment 每个XML环境元素如 可以使用组装一个字 段或一个setter方法同引用它们的项.元素提定的类 是你的字段或方法的定义.这看起来不那么冗长,但是,当存在遗留的层次这会 变得很重要.指定目标字段或方法名到你想要引用的 注入.在这种情况下,我们注入payment字段.如果你想改为注入一个setter方法, 它会是processPayment. []你不能够注入一个字段或方使用相同的基名.举例来说,如果你有一个 setProcessPayment( ) 方法和一个processPayment字段,你不能够定义注入到 这两个项目.因为他们将代表相同的ENC名,并且EJB容器很难区分它们. 14.1.5.5. XML覆盖 使用批注来注入有时需要考虑硬编码配置到你的组件类的代码中.虽然在这里有 一个硬编码元素,EJB规范允许你覆盖注入的批注通过XML部署描述符.让我们再 次栓查我们的使用的@EJB 注解: @Stateful public class TravelAgentBean implements TravelAgentRemote { @EJB private ProcessPaymentLocal payment; ... } 在 TravelAgent EJB 的最初配置中, EJB 容器可以理解基于批注类型的 payment注入字段的EJB引用.ProcessPaymentLocal 在整个应用程序中是唯一 的。如果在一个新的配置中,多个处理付款的发动机被部署到相同的应用程序之 内可以吗?你可能想要配置,每一个应用程序的部署,处理付款的 travelAgentBean。你可以使用XML来覆盖这个批注: TravelAgentBean come.titan.travelagent.TravelAgentBean/payment Session com.titan.processpayment.ProcessPaymentLocal MasterCardProcessPaymentBean 在这个例子中,我们提供更精确的映射到@EJB批注使用XML.必需 与默认注入字段的ENC名字匹配.这样EJB容器知道你要覆盖一个批注的字 段.元素提供更多的关于处理信款的引用在TravelAgent EJB 将会使 用.如果@EJB批注使用了name()属性,那么 要与这配置. @Stateful public class TravelAgentBean implements TravelAgentRemote { @EJB(name="ejb/ProcessPayment") private ProcessPaymentLocal payment; ... } @EJB批注告诉EJB容器,它要注入一个EJB的ProcessPaymentLocal 接口到 payment字段.EJB的引用ejb/ProcessPayment将会在ENC中进行注册.XML必需使 用这个ENC的名字来覆盖注入: TravelAgentBean ejb/ProcessPayment Session com.titan.processpayment.ProcessPaymentLocal MasterCardProcessPaymentBean ejb/ProcessPayment名字的引用使用元素 []XML的优先级总是高于元数据注释.XML提供硬编码的重新本置. 14.1.5.6. 注入和继承 很有可能出现一个组件类是其它类的一部分.如果任意字段或方法都有注入批 注,他们仍然会被保留,但是特定的注入规则如下所示: public class BaseClass { @Resource DataSource data; @EJB(beanName="ProcessPaymentBean ") public void setProcessPayment(ProcessPaymentLocal pp) { ... } } @Stateless public class MySessionBean extends BaseClass implements MySessionRemote { ... } 在这个例子中,我们有一个无状态会话Bean类注入从基类.MySessionBean 的所 有实例也会注入适当的资源。如同setProcessPayment( ) 方法.通过实现和继承 子类来改变注入的setProcessPayment( )方法是可以的: @Stateless public class MySessionBean extends BaseClass implements MySessionRemote { @EJB(beanName="AcmeProcessPayment") public void setProcessPayment(ProcessPaymentLocal pp) { ... } ... } ProcessPaymentBean将不再被注入到setProcessPayment( )方法.取而代之的是 一个新的覆盖引用AcmeProcessPayment,将会被注入.对这一条规则的例外。如果 setProcessPayment( ) 方法在BaseClass是一个么有方法,并非是保护或公共方 法,那么基类仍会注入同旧的引用: Public class BaseClass { @Resource DataSource data; @EJB(beanName="ProcessPaymentBean") private void setProcessPayment(ProcessPaymentLocal pp) { ... } } @Stateless public class MySessionBean extends BaseClass implements MySessionRemote { @EJB(beanName="AcmeProcessPayment") public void setProcessPayment(ProcessPaymentLocal pp) { ... } ... } 所以,在前面的例子当中的两个setProcessPayment( )方法将会被调用并且设置 不同的ProcessPaymentLocal引用.BaseClass类的setProcessPayment( )方法将 要获取引用到ProcessPaymentBean,并且MySessionBean 的 setProcessPayment( )方法获取AcmeProcessPayment. 14.2. 叁考和注入类型 这一章的上半部分的重点集中在JNDI ENC的语意,和怎样引用它.你已经学了基 本语义上的批注和XML注入.这一部分分开不同的服务和配置引用从你的ENC中. 在本书的其它章节中已经接触了一些注入和参考类型,但是这一章组合所有的放 在一起进行讨论错综复杂的和乱的细节. 14.2.1. EJB参考 如在第11章和前面所看到的,你的EJB组件类可以参考和集合其它的EJB这JNDI ENC查找或这直接注入这些引用到成员字段. 14.2.1.1. @javax.ejb.EJB @javax.ejb.EJB批注可以用在你的组件类的setter方法上,成员字段,或直接在 类自身上: package javax.ejb; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface EJB { String name( ) default ""; Class beanInterface( ) default Object.class; String beanName( ) default ""; String mappedName( ) default ""; } name( )属性查找将要被参考的EJB的JNDI ENC名字.这个名字关联到 java:comp/env上下文. beanInterface( )属性是你感兴趣的接口,并且通常被容器用于区别你想要的一 个远程或本地EJB的参考.如果你的 EJB 需要与 EJB 2.1组件整 合,beanInterface() 也可能是关于一个home接口的参考。 beanName()是EJB的EJB名字叁考.它的值等价于@Stateless.name( ) 或 @Stateful.name( )批注中指定的值,或者是部署描述符中 元素指定 的值. mappedName() 属性是一个厂商-特定的识别符的一个占位符号。这一个识别符可 能是一个键标识厂商的全局注册.很多厂商存储EJB的引用使用全局的JNDI树,所 以客户端才可以引用他们,并且mappedName( )能引用全局JNDI名字. 当放置在组件类上,@EJB批注将会注册一个引用到Bean的JNDI ENC中: @Stateful @EJB(name="ejb/ProcessPayment", beanInterface=ProcessPaymentLocal.class) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,travelAgentBean的代码查找 ProcessPayment EJB通过 java:comp/env/ejb/ProcessPayment JNDI ENC名字.这里是一个客户端组件怎 样使用上下文来查找一个引用到ProcessPayment EJB: InitialContext jndiContext = new InitialContext( ); Object ref = jndiContext.lookup("java:comp/env/ejb/ProcessPayment"); ProcessPaymentLocal local = (ProcessPaymentLocal)ref; 当@EJB批注用在组件类上,name()和beanInterface()属性是必需的.大部分时间, 只有组件的接口需要区分你引用的EJB.有时,虽然你可能重用相同的业务接口, 为了多部署EJB.在那情况, beanName() 或 mappedName() 属性一定被用作提供 一个唯一识别符为你想要叁考的EJB. @EJB注释在你的组件类中仅能使用一次.当你需要引用多个EJB时,可以使用 @javax.ejb.EJBs注释: package javax.ejb; @Target({TYPE}) @Retention(RUNTIME) public @interface EJBs { EJB[] value( ); } 因为,只有给定类型的一个注释可以被应用到Java语方的任意给定组件,所以这 种注释是必需的.这种重复名字的部分是为了其它引用的批注的描述复制品,在 这一章中: @Stateful @EJBs({ @EJB (name="ProcessPayment", beanInterface=ProcessPaymentLocal.class), @EJB(name="CustomerReferralEngine", beanInterface=CustomerReferralLocal.class) }) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,travelAgentBean 创建一个ENC引用到ProcessPayment 和 CustomerReferralEngine本地接口. @EJB注释也可以被放置在一个setter方法或成员字段上,所以EJB的引用将会直 接注入到组件类的实例: @Stateful public class TravelAgentBean implements TravelAgentRemote { @EJB private ProcessPaymentLocal payment; ... } 当用在一个setter方法或成员字段,注释的属性beanName()不是必需的,业务接 口的类型可以这字段或方法的类型来确定. 14.2.1.2. XML为基础的远程EJB的参考 元素定义一个引用到远程EJB.它包含子元素 (可选的), (必需的), (必需的), (必需的), (可选的), (必需的), 和 (可选的),像这章 中的第一部分的 (可选的)元素.这是一个远程引用到 ProcessPayment EJB: TravelAgentBean ejb/ProcessPaymentRemote Session com.titan.processpayment.ProcessPaymentRemote 元素等价于@EJB批不的name()属性在那,它代表被引用的ENC的 名字将会被绑定.这个名字关联到java:comp/env上下文.这是一个客户端组件怎 样使用ENC上下文来查找一个引用到ProcessPayment EJB: InitialContext jndiContext = new InitialContext( ); Object ref = jndiContext.lookup("java:comp/env/ejb/ProcessPaymentRemote"); ProcessPaymentRemote remote = (ProcessPaymentRemote) javax.rmi.PortableRemoteObject.narrow(ref, ProcessPaymentRemote.class); 元素有两个值,Session或Entity,记录它是会话Bean或者一个 EJB2.1的实体Bean的home接口. 元素指明组件远程接口的完整类名。如果你引用一个EJB2.1的组件, 那 么,元素必需提供完囊的组件home接口的全名. 元素可以直接连接到一个指定的EJB容器通过使用元素. 与这个元素等价的是@EJB批注的beanName()属性.它参考被引用的EJB名字.EJB 的连接可以在相同的JAR部署如同引用的EJB,或者在其它的部署中同一个企业档 案(.ear)(EAR在第20章中解释). TravelAgentBean ejb/ProcessPaymentRemote Session com.titan.processpayment.ProcessPaymentRemote processPaymentEJB ProcessPaymentEJB com.titan.processpayment.ProcessPaymentRemote ... 元素等价于@EJB批注的mappedName( )属性,它是由厂商指定的, 可选的唯一标识符. 元素用于如果你想要注入EJB的引用到一个字段或setter方 法在你的组件类中.这是一个使用的例子: TravelAgentBean ejb/ProcessPaymentRemote Session com.titan.processpayment.ProcessPaymentRemote com.titan.travelagent.TravelAgentBean payment 在这个例子中,远程接口引用到ProcessPayment EJB注入payment这个成员字段 或传递它作为参数到setter方法,TRavelAgentBean类中的setPayment( )方法. 14.2.1.3. XML为基础的本地EJB参考 元素定义EJB的远程引用.它包含子元素 (可选 的), (必需的), (必需的), (必需 的), (可选的), (可选的), and (可选的),如同这一章第一部分的 (可选的)描述.下面是本 地引用到ProcessPayment EJB: TravelAgentBean ejb/ProcessPaymentRemote Session com.titan.processpayment.ProcessPaymentLocal 元素等价于@EJB注释的name()属性,代表引用的ENC名字的绑定. 这个名字与java:comp/env上下文相关. 元素有两种取值,Session或Entity,记录一个会话Bean或一个 EJB2.1实体Bean的home接口. 元素指定组件类本地接口的类名全称.如果你引用了EJB2.1的组件,那么 元素必需提供组件类的home接口的类的全名. 元素可以直接连接到提定的EJB容器通过使用元素.这个元 素等价于@EJB注释的beanName()属性.它引用被引用的EJB名字.EJB的连接可以 在相同的JAR部署中如同引用的EJB,或者在其它的部署同企业归档(.ear),我们 在第20章中进行讨论. TravelAgentBean ejb/ProcessPaymentLocal Session com.titan.processpayment.ProcessPaymentLocal processPaymentEJB ProcessPaymentEJB com.titan.processpayment.ProcessPaymentLocal ... 元素等价于@EJB批注的mappedName()属性,由厂商指定的,可选 的唯一标识符. 定义在 中的企业组件是本地的,所以他们不能使用 javax.rmi.PortableRemoteObject.narrow( )方法进行窄引用到适当的类型.相 返的你可以使用简单的原始类型转换操作. InitialContext jndiContext = new InitialContext( ); ProcessPaymentLocal local = (ProcessPaymentRemote) jndiContext.lookup("java:comp/env/ejb/ProcessPaymentLocal"); 如果你想要注入你的引用到EJB来代替在JNDI ENC中的查找,你可以使用 元素.的精确语义在前面描述过,但是 这是使用它的一个例子: TravelAgentBean ejb/ProcessPaymentLocal Session com.titan.processpayment.ProcessPaymentLocal com.titan.travelagent.TravelAgentBean payment 在这个例子中,本地的引用到ProcessPayment EJB被注入到一个成员字段 payment或传递一个参数到setter方法名字为setPayment( )在travelAgentBean 类中. 14.2.1.4. 不明确的和覆盖EJB名字 元素和任意的@Stateless.name( ) 或 @Stateful.name( )属性必需 是唯一的在一个给定的EJB JAR部署.不幸的是,这不是对于所有的EJB JAR部署 到一个企业归档(.ear文件的描述在第20章).在一个.ear文件中,EJB名字可以被 复制在不同的EJB-JAR部署.为了复制的不同参考,EJB规范有一个扩展语法和@EJB注释的beanName属性.这个扩展的语法有一个关联路径到JAR文件在 EJB的本地,跟着#字符,后面是引用的EJB组件名: @EJB(beanName="inventory-ejb.jar#InventoryEJB") InventoryLocal inventory; 在这个例中, inventory-ejb.jar 文件在EAR文件的根目录下,连同JAR引用EJB 被部署在其中.@EJB注释引用inventory.jar 中的InventoryEJB. 14.2.1.5.解析EJB的参考 使用@javax.ejb.EJB注释的最简单例子,不需要其它的注释属性: @EJB ProcessPaymentLocal processPayment; 规范中并没有详细的说明容器运行期间精确的分解参考.为了给你它是怎样处理 工作的理解,让我们看一下Jboss应用程序服务解是怎样分解这个参考的: 1.唯一可能标识EJB参考的是业务接口的类型.应用程序服务器首先查找一个唯 一的EJB在引用EJB的EJB-JAR部署中,使用ProcessPaymentLocal作为本地或远程 接口.如果有多于一个EJB使用相同的业务接口,它会抛出一个部署异常. 2.如果EJB-JAR是企业归档(.ear)的一部分,它顺便查找唯一的EJB使用 ProcessPaymentLoca接口的在其它的EJB-JARs 中.再次,如果超过一个EJB使用 相同类型业务接口,它会抛出一个部署异常. 3.如果EJB引用没有在.ear 文件中找到,它会查找其它的全局EJB-JAR部署. 如果beanName( ) 属性被指定,那么JBoss使用相同的查询处理,但是,使用 beanName()值 作为一个额外的标识符. 如果mappedName( )属性被指定,那么不会执行查询处理.应用程序服务器期待一 个指定的EJB绑定到全局JNDI通过mappedName()的值. 14.2.2. EntityManagerFactory引用 一个javax.persistence.EntityManagerFactory可以注册和注入到EJB的JNDI ENC中.直接地获得关于 EntityManagerFactory 的参考,有时是有用的以便你能 完全控制EntityManager和工作的持久化上下文.虽然你可以获取一个 EntityManagerFactory通过javax.persistence.Persistence API,使用j2EE比 较灵活,以便于应用程序可以控制EntityManagerFactory的生命周期.当你让应 用程序服务器移值在你的ENC或者注入到你的EntityManagerFactory,J2EE运行 时会处理 , 这一个例证的清除和你不一定要调用 EntityManagerFactory.close()方法.像所有的其它服务和资源,一个 EntityManagerFactory 可以绑定到JNDI ENC或注入到你的组件类这使用注释或 XML. 14.2.2.1. @javax.persistence.PersistenceUnit注释 @javax.persistence.PersistenceUnit注释可以用在你的组件类的setter方法 或成员字段,或直接在类的本身上: package javax.persistence; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface PersistenceUnit { String name( ) default ""; String unitName( ) default "";}} name()属性是将要被EntityManagerFactory引用的JNDI ENC名字.这个名字关联 到java:comp/env上下文. unitName( )属性标识那一个EntityManagerFactory你感兴趣的以及你定义在 persistence.xml 文件中的持久化单元的引用.如果没有指定,一个部署错误会 发生,除非EJB-JAR只有一个持久化单元部署在其中.在那种情况下,默认的是叭 一的持久化单元. 当放置在组件类中,@PersistenceUnit批注将注册一个引用到 EntityManagerFactory在EJB组件类的JNDI ENC中. @Stateful @PersistenceUnit(name="persistence/TitanDB", unitName="TitanDB") public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,travelAgentBean 中的代码可以查找一个EntityManagerFactory 管理一个TitanDB持久化单元通过java:comp/env/persistence/TitanDB JNDI ENC 名字.下面是一个客户组件使用上下文查找引用到这个 EntityManagerFactory: InitialContext jndiContext = new InitialContext( ); EntityManagerFactory titan = (EntityManagerFactory) jndiContext.lookup("java:comp/env/persistence/TitanDB"); 当@PersistenceUnit注释用在组件类上,name()属性总是必需的,所以EJB容器才 能知道JNDI ENC的绑定到EntityManagerFactory. @PersistenceUnit在一个组件类中仅能用一次.当你需要多个持久化单元时,可 以用@javax.persistence.PersistenceUnits注释: package javax.persistence; @Target({TYPE}) @Retention(RUNTIME) public @interface PersistenceUnits { PersistenceUnit[] value( ); } 这种注释是必需的,因为仅有一个给定类型的注释可以应用到给定的物件中,在 Java语言中: @Stateful @PersistenceUnits({ @PersistenceUnit(name="persistence/TitanDB", unitName="TitanDB"), @PersistenceUnit(name="Customers", unitName="crmDB") }) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,TRavelAgentBean创建一个ENC的引用到TitanDB和CrmDB持久化单 元中. @PersistenceUnit 注释同样也可以被放置在setter方法或成员字段上,所以 EntityManagerFactory的引用可以直接注入到组件类的实例中: @Stateful public class TravelAgentBean implements TravelAgentRemote { @PersistenceUnit(unitName="crmDB") private EntityManagerFactory crm; ... } 当使用在setter方法或成员字段中,没有注释属性是必需的,因为name()和 unitName()属性有默认的有效值,如同前面章节中的描述. 14.2.2.2. XML基础上的EntityManagerFactory引用 元素定义一个引用到一个给定的 EntityManagerFactory.它包含子元素description> (可选的), (必需的), and (必需的),如同这 一章中前面描述过的 (可选的)元素.下面是引用 TitanDB 持久化单元的例子: TravelAgentBean persistence/TitanDB TitanDB 元素等价于@PersistenceUnit批注的name()属性, 它代表被绑定的ENC名字.这里是客户端组件怎样使用ENC上下文来查找一个引用 到这个EntityManagerFactory: InitialContext jndiContext = new InitialContext( ); EntityManagerFactory titan = (EntityManagerFactory) jndiContext.lookup("java:comp/env/persistence/TitanDB"); 元素等价于@PersistenceUnit注释的userName()属性. 它代表你在persistence.xml中定义的同名部署描述符. 元素的作用是将EntityManagerFactory 注入到你的EJB组 件类中.下面是使用的一个例子: TravelAgentBean persistence/TitanDB TitanDB com.titan.travelagent.TravelAgentBean ships 在这个例子中,EntityManagerFactory 将会被注入到ships字段或作为参数传递 到一个setter方法,RavelAgentBean中的setShips()方法. 14.2.2.3.范围和覆盖单元名 一个持久化单元可以被定义在多个不同的地方.它可以定义在一个EJB-JAR,一个 EAR/lib JAR,或一个WAR文件(见第20章更多的关于此文件的详细信息).持久化 单元的范围,当定义在一个WAR或EJB-JAR文件并且他们不能够被归档外的组件引 用.部署在一个.ear的lib/directory下的JAR中的持久化单元对所有的企业归档 中的其它组件是有效的.有时,你可能有相同的持久化单元名定义在你的EJB或 WAR文件作为一个持久化单元名来定义,在EAR/lib目录下的JAR文件中.为了复制 持久化单元名到不能的参考,规范中有一个扩展的语法为 和@PersistenceUnit 注释的unitName()属性相对应.这个扩展的语法需 要关联包含持久化单元的Jar文件,跟着#字符,后面是持久化单元名: @PersistenceUnit(unitName="inventory.jar#InventoryDB") EntityManagerFactory inventory; 在这个例子中,在EAR文件中的相同目录中的inventory.jar作为持久化单元的 JAR文件.@PersistenceUnit注释参考inventory.jar中部署的持久化单元 InventoryDB. 14.2.3. EntityManager参考 一个EntityManager可以注册到一个EJB的JNDI ENC中.当你注册一个 EntityManager到JNDI ENC或注入它到你的EJB,EJB容器有对EntityManager持久 化上下文的生命周期的完体控制. EntityManager对象参考它自身,只是还不可 能存在真实的持久化上下文的周转一个代理,依赖于你注入的持久化上下文的类 型.当你让应用程序服务器在你的 ENC 或者注射你的 EntityManager 的时候, EJB 容器在处理这一个实例的清除时你不一定要调用EntityManager.close(). 实际上,调用注入EntityManager实例的close()方法是违法的,并且你这样做时 会抛出一个异常.像所有的其它服务和资源,一个 EntityManager 可以绑定到 JNDI ENC或注入到你的组件类通过使用注释或XML. 14.2.3.1. @javax.persistence.PersistenceContext @javax.persistence.PersistenceContext注释可以使用在组件类的setter方法 或成员字段,或直接在类上: package javax.persistence; public enum PersistenceContextType { TRANSACTION, EXTENDED } @Target({}) @Retention(RUNTIME) public @interface PersistenceProperty { String name( ); String value( ); } @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface PersistenceContext { String name( ) default ""; String unitName( ) default ""; PersistenceContextType type( ) default TRANSACTION; PersistenceProperty[] properties( ) default {}; } name()属性指定EntityManager参考的JNDI ENC名字.这个名字与java:comp/env 有关. unitName( )属性标识你感兴趣的引用持久化单元.这个标识与你在 persistence.xml文件中定义的相同,如果没有指定,一个部署错误将会发生, 除非EJB-JAR仅有一个持久化单元部署在其中.在那种情况下,默认的是唯一的持 久化单元. type( )指定持久化上下文的类型.PersistenceContextType.TRANSACTION指定 你想要使用事务范围的持久化上下文.这是默认 的.PersistenceContextType.EXTENDED给你一个扩展的持久化上下文.EXTENDED 类型仅用于有状态会话Bean.如果你使用它在其它类型的组件上你将会接收到一 个部署错误.复习一下第五章,有更多的信息关于这两种基本类型持久化上下文 的不同. properties( )属性允许你传递一个附加的由厂商指定的属性来创建 EntityManager 实例.设置这个属性使用一组 @javax.persistence.PersistenceProperty注释来定义. 当放置在组件类上,@PersistenceContext注释将会注册一个引用到 EntityManager在你的EJB组件类的JNDI ENC中: @Stateful @PersistenceContext(name="persistence/TitanDB", unitName="TitanDB" type=PersistenceContextType.EXTENDED) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,travelAgentBean中的代码可以查找一个EntityManager管理一个 TitanDB的持久化单元通过java:comp/env/persistence/TitanDB JNDI ENC 名 字.这里是客户端怎样使用上下文查找到这个EntityManager的引用: InitialContext jndiContext = new InitialContext( ); EntityManager titan = (EntityManagerFactory) jndiContext.lookup("java:comp/env/persistence/TitanDB"); 当@PersistenceContext注释用在组件类的时候,name()属性是必需的,所以容器 才能知道在JNDI ENC中那里绑定了 EntityManager。type()和unitName()属性有 正确的默认值. @PersistenceContext注释在一个组件类中只能用一次.当你需要使用多个引用 的持久化上下文时,可以使用@javax.persistence.PersistenceContexts注释: package javax.persistence; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface PersistenceContexts { PersistenceContext[] value( ); } 这种注释是必需的,因为仅有一个给定类型的注释可以应用到给的的物件上在 Java语言中: @Stateful @PersistenceContexts({ @PersistenceContext(name="persistence/TitanDB", unitName="TitanDB", type=PersistenceContextType.EXTENDED), @PersistenceContext(name="Customers", unitName="crmDB") }) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,TRavelAgentBean创建一个ENC引用到TitanDB和crmDB持久化单 元. @PersistenceContext注释也可以被放置在一个setter方法或一个成员字段上, 所以,EntityManager的引用可以直接注入到组件类的实例中: @Stateful public class TravelAgentBean implements TravelAgentRemote { @PersistenceContext(unitName="crmDB") private EntityManager crm; ... } 当用在一个setter方法或成员字段上时,注释的属性不再是必需的,如name( ), unitName( ), 和 type( )有有效的默认值. 14.2.3.2. XML为基础的EntityManager引用 元素定义一个引用到给定的EntityManager.它包括 子元素 (可选的), (必需的), (必需的), (可选 的), 和一个或多个 (可选的)元素,如同在本章中的第 一节中的 (optional)元素的描述.下面是一个引用到 TitanDB持久化单元的例子: TravelAgentBean persistence/TitanDB TitanDB EXTENDED hibernate.show_sql true 元素等价于@PersistenceContext批注的 name()属性,它代表ENC 名字引用的绑定.下面是一个客户端组件将要使用ENC 上下文来查找一个引用到这个EntityManager: InitialContext jndiContext = new InitialContext( ); EntityManager titan = (EntityManager) jndiContext.lookup("java:comp/env/persistence/TitanDB"); 元素等价于@PersistenceContext注释的unitName() 属性,它代表你在persistence.xml部署描述符中定义的相同名字. 元素等价于@PersistenceContext注释的properties( ) 属性.这个元素是可选的,你可以指定多个. 元素用于如果你想注入一个 EntityManager 到EJB中,下面 是一个使用它的例子: TravelAgentBean persistence/TitanDB TitanDB com.titan.travelagent.TravelAgentBean ships 在这个例子中,EntityManager将会被注入到shps字段或作为一个参数传递给一 个setShips()的setter方法在travelAgentBean类中. 14.2.3.3. 范围和重载单元名 一个被引用的EntityManager的持久化单元同样可能不明确,我们在这一章中前 面讨论过的"XML-based EntityManagerFactory references".不同的引用使用 相同的持久化单元名,规范中有一个扩展的语法为和 @PersistenceContext注释的unitName()属性.这种扩展的语法有一种关联路径 到包含持久化单元的JAR文件,,后面跟#字符,接着是持久化单元名: @PersistenceContext(unitName="inventory.jar#InventoryDB") EntityManager inventory; 在这个例子当中,inventory.jar 文件在EAR文件的相同目录下作为持久化单元 的硬文件.@PersistenceContext注释引用inventory.jar 文件中的InventoryDB 持久化单元. 14.2.4. 资源的引用 在第11章中,我们看到ProcessPayment EJB需要一个引用到JDBC数据源来执行它 的业务逻辑.企业Bean使用JNDI ENC来查找外部资源,如他们需要访问的数据库 连接。这种机制与参考其它EJB和环境项目使用的机制类似;外部资源在JNDI ENC 命名空间中映射成一个名字,并且可以注入到成员字段或组件实例的setter方 法。它的完成使用注释或一个XML部署描述符. 外部资源的类型可以是javax.sql.DataSource, javax.jms.Connection- Factory, javax.jms.QueueConnectionFactory, javax.jms.TopicConnectionFactory, javax.mail.Session, java.net.URL , 或 javax.resource.cci.ConnectionFactory ,或其它JCA资源适配类型.在这一 节中,我们的重点是javax.sql.DataSource的例子. 14.2.4.1. @javax.annotation.Resource注释 @javax.annotation.Resource注释用于引用一个外部资源.它可以应用在你的组 件类的setter方法或成员字段,或直接在类上.这个注释在J2EE规范中被高度的 重载和重用在外部资源上,它也可以用在JMS消息目的地的参考,环境项目, EJBContext和J2EE核心服务.现在,我们将重点放在使用这个注释来访问外部资 源. package javax.annotation; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface Resource { public enum AuthenticationType { CONTAINER, APPLICATION } String name( ) default ""; Class type( ) default Object.class; AuthenticationType authenticationType( ) default AuthenticationType.CONTAINER; boolean shareable( ) default true; String description( ) default ""; String mappedName( ) default ""; } name()属性对JNDI ENC名字引用外部资源.这个名子与java:comp/env上下文关 联. type()属性定义完全符合Java类型的资源类名.@Resource注释应用于组件类,这 个属性可能很重要对于EJB容器确定标识你感兴趣的资源.通常,这一个属性是不 需要的,而且默认的数值够好。 mappedName( )属性是厂商指定的用于标识外部资源.因为J2EE没有指定的机制 和全局注册为全局资源的查找,很多厂商要求这个属性,来定位和绑定资源.大 多数情况下,mappedName()属性将会等价于全局的JNDI名字. 当放置在组件类上,@Resource注释注册一个引用到外部资源到EJB组件类的JNDI ENC: @Stateful @Resource(name="jdbc/OracleDB", type=javax.sql.DataSource, mappedName="java:/DefaultDS") public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,@Resource注释绑定一个javax.sql.DataSource到 jdbc/OracleDB ENC名字.mappedName()属性提供一个全局的,厂商指定的标识 符,所以应用程序可以定位预定的资源.travelAgentBean中的代码可以定位这个 数据源这java:comp/env/jdbc/OracleDB JNDI ENC 名字.这里是客户端组件将 这上正文的查找引用: InitialContext jndiContext = new InitialContext( ); DataSource oracle = (DataSource) jndiContext.lookup("java:comp/env/jdbc/OracleDB"); 当@Resource注释用在组件类中,name()和type()属性是必需的.如前面所陈述 的,mappedName()或另外厂商指定的无数据注释要求通过厂商真确的标识资源. authenticationType( )属性告诉服务器,当资源被访问的时候,谁来负责鉴定. 它可以有两种取值:CONTAINER 或 APPLICATION.如果指定为CONTAINER,容器将 自动执行使用资源的鉴定,同指定的部署时间.如果指定为APPLICATION,组件将 自己执行鉴定在使用资源前.这里是当指定为APPLICATION时,一个组件可以签署 到一个连接工厂. @Stateful @Resource(name="jdbc/OracleDB", type=javax.sql.DataSource, authenticationType=AuthenticationType.APPLICATION, mappedName="java:/DefaultDS") public class TravelAgentBean implements TravelAgentRemote { @Resource SessionContext ejbContext; private java.sql.Connection getConnection( ){ DataSource source = (DataSource) ejbContext.lookup("jdbc/OracleDB"); String loginName = ejbContext.getCallerPrincipal().getName( ); String password = ...; // get password from somewhere // use login name and password to obtain a database connection java.sql.Connection con = source.getConnection(loginName, password); } 在这情况,连接将会被证明.CONTAINER选项,调用者主要从资源内部提取,或静态 的配置通过应用程序部署. @Resource 注释在一个组件类中仅能使用一次.当你想要引用多个持久化单元时, 可以使用@javax.annotation.Resources 注释: package javax.annotation; @Target({TYPE })@Retention(RUNTIME) public @interface Resources { Resource[] value( ); } 这种类型的注释是必需的,因为仅有一个给定类型的注释可以应用到任意给定的 物件上,在JAVA语言中: @Stateful @Resources({ @Resource(name="jdbc/OracleDB", type=javax.sql.DataSource.class, mappedName="java:/DefaultDS"), @Resource(name="jdbc/SybaseDB", type=javax.sql.DataSource.class, mappedName="java:/OtherDS") }) public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,travelAgentBean 创建一个ENC引用到OracleDB 和 SybaseDB数 据源. @Resource注释也可以被放置在一个setter方法或成员字段所以资源的引用将会 被直接注入到组件类的实例上: @Stateful public class TravelAgentBean implements TravelAgentRemote { @Resource(mappedName="java:/DefaultDS") private javax.sql.DataSource oracle; ... } 当使用在一个setter方法或成员字段,仅有mappedName( )属性是必需的标识资 源,如同类型和ENC名字可以确定从方法或字段的类型或名字. 14.2.4.2. 可共享的资源 当多个企业Bean在一个事务中使用相同的资源,你想要配置你的EJB服务来共享 资源.共享资源意味着每个EJB将使用相同的连接来访问资源(例如,数据库或JMS 提供者),一个策略比使用多个分开的资源连接更高效. 在数据库的术语中,EJB引用相同的数据库将使用相同的数据库连接在事务期间, 所以所有的CRUD操作将返回统一的结果.EJB容器通过默认共享资源,但是资源的 共享可以被关闭或打开,通过@Resource注释的shareable( )属性. 有时候,高级开发者可能陷入共享的资源不是想要的情形,而且选项被关闭是有 益的.除非你有关掉共享的资源的好理由, 我们推荐你设定shareable( ) 属性 为true。 14.2.4.3. XML为基础的资源参考 元素定义一个参考到给定的资源.它包括子元素 (可选的), (必需的), (必需的), (必需的), (可选的), and (可选的),如 同这一章第一节描述的 (可选的)元素.这里是引用TitanDB 数据源的例子: TravelAgentBean jdbc/OracleDB javax.sql.DataSource Container java:/DefaultDS 元素等价于@Resource注释的name()属性.等价于 type()属性.同authenticationType( )属性并且它有两个可选 值:Container 或 Application.元素等价于@Resource注释的 mappedName()属性. 如果你想要注释资源到你的EJB中使用元素,如同这一章中 前面描述过的,下面是这个元素的一个例子: TravelAgentBean jdbc/OracleDB javax.sql.DataSource Container java:/DefaultDS com.titan.travelagent.TravelAgentBean oracle 在这个例子中,数据源将会被注入到一个oracle字段或作为参数传递给 travelAgentBean 类的一个setter方法setOracle( ). 14.2.5. 资源环境和管理对象 资源环境的进入对象,不需要放入资源参考目录.一些资源可能有需要进入你的 组件类别之内被从 JNDI ENC 获得或注入的其他额外的被管理的对象。 一个执行对象是一个资源,在部署时配置,并且在运行期间被EJB容器所管理. 它们常常定义和部署通过一个JCA资源适配器. 此外的执行对象,资源环境项目被用到参考服务,像 javax.transaction.UserTransaction 和 javax.transaction.TransactionSynchronizationRegistry . 为了获取引用到其它的服务,可以使用@Resource注释。当使用这个注释 时,authenticationType( ) 和 shareable( )属性是无意义的,并且指定它是不 合法的. @Stateful public class TravelAgentBean implements TravelAgentRemote { @Resource private javax.transaction.UserTransaction utx; ... } 如果你使用XML,你必需使用一个分开的元素,它包含子元 素 (必需的), (必需的), and (可选的), 如同 (可选的)元素. 元素与JNDI ENC名字相关联.为引用的类型,的使用相同的方法 如同它们使用在: TravelAgentBean UserTransaction javax.transaction.UserTransaction com.titan.travelagent.TravelAgentBean utx XML攫取引用到一个javax.transaction.UserTransaction 对象并且注入到 travelAgentBean类的utx字段. 14.2.6. 环境项目 在第11章中,ProcessPayment EJB 有一个配置属性为最小检查数.这些类型的配 置属性被叫做环境项目.组件类使用环境项目来满足客户的需求. 虽然他们的定义可以使用注释,环境项目总是配置通过XML,通过他们配置数值 并且不是注释.元素用于定义他们.这个元素包含子元素 (可选的), (必需的), (必需的), and (可选的),如同 (可选 的)元素.这是一个典型的定义: ProcessPaymentBean minCheckNumber java.lang.Integer 2000 元素与java:comp/env上下文关联,例如,minCheckNumber项目 可以访问使用java:comp/env/minCheckNumber路径的JNDI ENC查找: InitialContext jndiContext = new InitialContext( ); int minValue = (Integer) jndiContext.lookup("java:comp/env/minCheckNumber"); 作为一种选择,它可以查找同 EJBContext.lookup( )方法使用minCheckNumber 名字. 的类型可以是String或多个原始包装类型中的一个,包括 Integer, Long, Double, Float, Byte, Boolean, 和Short. 是可选的.它的值的指定可以通过Bean的开发者或延期的应 用程序的集合或部署. 元素可以用于初始化一个字段或setter方法在环境项目值 中. ProcessPaymentBean minCheckNumber java.lang.Integer 2000 com.titan.processpayment.ProcessPaymentBean minCheckNumber 前面的XML将会注入2000到minCheckNumber字段或调用一个setter方法名字为 setMinCheckNumber( )在组件类中: @javax.annotation.Resource注释可以用于引用环境项目代替元素: @Resource(name="minCheckNumber") private int minCheckNumber = 100; 在这个例子中,2000将会被放入环境项目,描述在XML和注入到minCheckNumber 字段.如果没有XML配置这个值,默认值将会是100,但是没有项目被创建在ENC中. 一个通常的方式是注释你的字段使用@Resource和提供一个默认值为字段可以随 意的覆盖在XML中.使用@Resource注释同String或原始类型值标识作为一个环境 项目到EJB容器.当 @Resource指明环境项目,仅有name()属性是允许指定的.它 不会做很多的判断使用@Resource为环境项目在类级,同样的没有方法初始化值 在注释中. 14.2.7. 信息目的地叁考 信息目的地叁考用对一个 JMS 主题或队列引用在JNDI ENC中.如果你想要发送 消息,你需要这些引用在你的EJB中.在第12章中给了一个完整的这种类型的引 用,所以这里会只提供一个预览,额外的指令将会被注入使用的注释所代替. 14.2.7.1. XML基础之上的资源引用 元素定义一个引用到JMS消息目的地.它包括子元 素 (可选的), (必需的), (必需的), (必 需的), (可选的), 和 (可选的), 如同本章第一节中描述的 (可选的)元素.这是一个引用到 主题的例子: TravelAgentBean jms/TicketTopic javax.jms.Topic Produces Distributor topic/TicketTopic com.titan.travelagent.TravelAgentBean ticketTopic 元素是JNDI ENC名字,主题将会绑定到并且 关联到路径java:comp/env. 元素值是javax.jms.Topic 或一个 javax.jms.Queue并且是必需的. 元素指定是否EJB的生产者或消费者消息从目的 地. 元素创建消息流,如第12章中描述过的 有时厂商需要指定唯一标识,并且可选的实现这一角色. 关于所有的其它资类型的引用,元素可以被用来注入目的地 到一个字段或setter方法. 14.2.7.2. 使用 @Resource注释 @javax.annotation.Resource注释重载来支持引用JMS目的地.不幸的是,规范中 没有设提供元数据注释来设置一个消息目的地的连接,所以你要依赖XML来完成 这种事情. 当你把它放置到组件类上时,@Resource 注释注册一个引用到JMS队列或主题目 的地到EJB组件类的JNDI ENC: @Stateful @Resource(name="jms/TicketTopic", type=javax.jms.Topic, mappedName="topic/TicketTopic") public class TravelAgentBean implements TravelAgentRemote { ... } 在这个例子中,@Resource注释绑定一个javax.jms.Topic 到jms/TicketTopic ENC名字.mappedName( )属性提供一个全局的,商厂指定的唯一标识,所以应用程 序可以找到期望的目标. 当@Resource注释用在组件类上时,name()和type()属性是必需的.如同前面的陈 述,mappedName()或额外的厂商指定元数据注释可以是必需的,通过厂商来确定 标识的资源.只有当这三个属性被应用程序的代码设定时.所有的其它是违法的 和他们将会产生一个配置错误. @Resource注释也可以被放置在一个setter方法或一个成员变量,所以目的地的 引用将会被直接注入到组件类的实例: @Stateful public class TravelAgentBean implements TravelAgentRemote { @Resource(mappedName="topic/TicketTopic") private javax.jms.Topic ticketTopic; ... } 当使用在一个setter方法或成员字段,只有mappedName( )属性是必需的用来标 识资源.同样,类型和ENC名字可以从字段的类型和名字中确定. 14.2.8. Web Service 引用 Web service参考服务接口或服务结点接口,指向所在的JNDI ENC.在第19章中给 一个完全的引用类型的描述,所以给你提供一个预览,同另外的指令,怎样使用注 释注入来代替. 14.2.8.1. XML基础之上的资源引用 元素定义一个引用到JAX-WS服务接口.从这个引用,一个存根实现 服务终点接口,可以获取并调用.这个元素包含子元素 (可选的), (必需的), (必需的), (必需的), (必需的), (必需 的), and (可选的),如同本章第一节中描述的 (可选的)元素. 这里是元素的用法: TravelAgentBean service/ChargeItProcessorService com.charge_it.ProcdessorService META-INF/wsdl/ChargeItProcessor.wsdl META-INF/mapping.xml chargeIt:ProcessorService webservice/ChargeItProcessorService com.titan.travelagent.TravelAgentBean chargeService 元素定义JNDI ENC查找的JAX-RPC服务名字:它总是与 java:comp/env上下文关联.更多的关于Web Service部署描述符见第19章. 14.2.8.2. 使用@javax.xml.ws.WebServiceRef注释 @javax.xml.ws.WebServiceRef注释可以用于许多单一化的网页服务.它可以被 用于参考或注入一个服务接口或一个服务端点接口: package javax.xml.ws; @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface WebServiceRef { String name( ) default ""; String wsdlLocation( ) default ""; Class type( ) default Object.class; Class value( ) default Object.class; String mappedName( ) default ""; } name()属性用于映射JNDI ENC名字.wsdlLocation( )属性用于定义查找本地 WSDL文件.你可以使它为空,并且部署将自动为你生成和定位到它.当直接引用一 个服务终点,value()属性可以真充服务接口类从获取的端点.mappedName()属性 可以用于指定一个厂商指定的引用.下面是一个获取服务接口和服务终点接口的 例子: @Stateful public class TravelAgentBean implements TravelAgentRemote { @WebServiceRef ProcessorService service; @WebServiceRef(ProcessorService.class) Processor endpoint; ... } 更多的信息见第19章. 第十五章 拦截 拦截机能够在会话和消息驱动Bean的生命周期事件或方法调用上插入自身的对 象.他们让你应用程序的大部分减少通常的行为.这些行为通常是你不想要污染 你的业务逻辑的代码.在对EJB3.0的大部分变化设计中,使EJB变得更容易被开发 者使用.拦截是一个很好的特性提供另一种方式的应用程序模块甚到扩充EJB容 器的高级功能.这一章中教你可何写一个拦截器和各种各样的不同的真实的例 子. 15.1.拦截方法 为了明白什么时候使用拦截器,我们看一下对TravelAgent EJB 的 bookPassage( )方法的修改.这个应用程序的代码编写者增加一些切换的逻辑, 多久bookPassage()方法将会被调用.很简单的放置当前的时间在方法的开始,并 且打印出到结束时的时间在finally块内: @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { long startTime = System.currentTimeMillis( ); try { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); return ticket; } catch(Exception e) { throw new EJBException(e); } } finally { long endTime = System.currentTimeMillis( )- startTime; System.out.println("bookPassage( ) took: " + endTime + "(ms)"); } } 虽然这个代码可以编译运行,它存在一些设计缺点: ●bookPassage( )方法与跟程序无关的业务逻辑代码放在一起。不仅是在程序中 加入6行代码,使得程序阅读困难. ●它很难被消掉,而且在你批注之后要重新编译组件类. ●这种紧密的逻辑很显然可以在整个应用程序的许多方法中重用一个模板.这种 压缩的代码使你的EJB方法变得混乱,你需要潜在的编辑很多不同的类来扩展压 缩逻辑的作用. 拦截机制提供了一个压缩逻辑和一个很容易应用到你的方法不需要读硬编码的 一种机制.拦截机提供一个结构为这种类型的行为,以便它能很容易的扩展,并 且在一个类中进行扩展.最后,他们提供一个简单又容易配置的机制给你喜欢应 用它们的地方. 15.1.1. 拦截器类 装入这种逻辑到一个拦截器,像创建一个简单的普通的Java类有一个方法使用 @javax.interceptor.AroundInvoke注释并且看下面的信息: @AroundInvoke Object (javax.interceptor.InvocationContext invocation) throws Exception; 使用@AroundInvoke注释的方法意味着它是拦截器类.它包装在你访问的业务方 法和实际调用的相同Java访问的栈周围和在相同的事务和安全上下文,同样的在 被拦截的组件方法上.javax.interceptor.InvocationContext参数是用户端调 用业务方法的一般表示法.你可以在调用期间获取如目标组件实例的信息,对对 象的队列参数进行存取.并且关于java.lang.reflect.Method 对象的引用,是一 般的表示实际调用的方法.InvocationContext 也被用来驱动调用.让我们转换 压缩的逻辑到一个@AroundInvoke 方法: 1 import javax.ejb.*; 2 3 public class Profiler { 4 @AroundInvoke 5 public Object profile(InvocationContext invocation) throws Exception { 6 long startTime = System.currentTimeMillis( ); 7 try { 8 return invocation.proceed( ); 9 } finally { 10 long endTime = System.currentTimeMillis( )- startTime; 11 System.out.println("Method " + invocation.getMethod( ) 12 + " took " + endTime + "(ms)"); 13 } 14 } 15 } 拦截器类的@AroundInvoke方法是profile().它看起来与bookPassage()方法非 常的类似,除了左边的业务逻辑不见了而且所有的一般的压缩的业务逻辑也不见 了.在第8行中,InvocationContext.proceed( )方法被调用.如果另外的拦截器 必需被调用作为方法调用的一部分,那么proceed( )调用其它拦截器的 @AroundInvoke 方法.如果没有其它的拦截器需要执行,那么EJB容器调用组件方 法在客户端调用上.因为profile( ) 方法被调用在相同的Java堆栈中作为业务 方法在你调用时,proceed()必需被调用通过拦截器代码或实际的EJB方法一点也 不需要调用. 在第10和11行,profile()方法计算执行时间和打印出方法执行时 间.InvocationContext.getMethod( )操作为profile()代码访问 java.lang.reflect.Method 对象代表实际组件方法调用.它用于11行到打印出 调用的方法名.此外 getMethod(), InvocationContext 接口有一些其他的有趣 方法: package javax.interceptor; public interface InvocationContext { public Object getTarget( ); public Method getMethod( ); public Object[] getParameters( ); public void setParameters(Object[] newArgs); public java.util.Map getContextData( ); public Object proceed( ) throws Exception; } getTarget( )方法返回一个引用到目标组件的实例.我们需要改变profile()方 法也可以打印出调用组件的方法参数使用getParameters( )方 法.setParameters( )方法允许你修改被调用方法的参数.使用它要小 心.getContextData( )方法返回一个Map对象,那对整个方法是活跃的。拦截器可 以使用这个Map传递关联数据在每个其它的相同方法调用. 15.1.2.应用拦截器 现在拦截器类被写出来了,现在需要应用它到一个EJB.一个或多个拦截器可以被 应用到所有EJB在一个部署中(默认拦截器),到一个EJB的所有方法,或到一个EJB 的单个方法.拦截器可以被应用通过注释或XML部署描述符.这一节我们讨论所有 的选项. 15.1.2.1. 注释方法和类 @javax.interceptor.Interceptors注释用于应用拦截器到单个方法或EJB组件 类的每个方法: package javax.interceptor; import java.lang.annotation.*; @Retention(RetentionType.RUNTIME) @Target({ElementType.CLASS, ElementType.METHOD}) public @interface Interceptors { Class[] value( ); } TravelAgent EJB的bookPassage( )方法很容易应用@Interceptors注释: @Remove @Interceptors(Profiler.class) public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); return ticket; } catch(Exception e) { throw new EJBException(e); } } 当@Interceptors 应用到单个方法时,拦截器的执行仅当指定方法被调用.如果 你使用@Interceptors 注释在组件类上,所有的拦截器类列出将会被提出的EJB 的每个方法调用的业务方法: @Stateful @Interceptors(Profiler.class) public class TravelAgentBean implement TravelAgentRemote { @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { ... } ... } 是否你想要应用@Interceptors注释在类或方法上,是由你使用的拦截器来决定. 15.1.2.2.应用拦截器通过XML 通过@Interceptors 注释允许你应用拦截器很简单,它不用强制你修改或重编译 类在每次你想要移除压缩或增加一个更详细的方法或EJB.除非拦截器是你业务 逻辑的一部分,它不是一个很好的主意注释在你的代码中;使用XML绑定来代替是 一个更好的方法.因为EJB3.0规范支持局部XML部署描述符,它会完全无痛的和容 易应用拦截器通过XML: TravelAgentBean com.titan.Profiler bookPassage com.titan.CreditCardDO double 前面的XML完成部署描述.元素提定我们想要Profiler拦 截器被执行当TravelAgent EJB的bookPassage( ) 方法被调用时.因为 bookPassage( )方法没有重载在组件类中,元素是必需的. 如果你想要应用一个拦截器到指定的EJB的每个业务方法上,那么移除元素: TravelAgentBean com.titan.Profiler 如你所看到的,你可以使用一个XML部署描述符仅当你需要它并且遗弃其它的元 数据注释.在指定拦截器的使用情况下,使用XML比注释要更明智,因为在开发时 你可能需要做一些压缩,不在一个生产应用程序中. 15.1.2.3.默认的拦截器 XML有一些其它的优点.例如, 元素中的元素 可以使用通配符.在这种情况下,你可以应用一个或多个拦截器,这样你可以定义 在interceptor-binding到每个EJB在指定的JAR文件部署: * com.titan.Profiler 15.1.2.4. 关闭拦截器 如果你使用默认的拦截器或类级拦截器,当你想要关闭他们为指定的EJB或指定 EJB的方法.你可以这样做,通过一个注释或XML.让我们首先看一下关闭默认的拦 截器: * com.titan.Profiler 在前面的XML中,我们允许Profiler拦截器为每个EJB部署在指定的JAR文件放置 XML.当我们不想Profiler被执行为我们的TravelAgent EJB。我们可以关闭默认 的拦截器通过使用@javax.interceptor.ExcludeDefaultInterceptors注释: @Stateful @ExcludeDefaultInterceptors @Interceptors (com.titan.SomeOtherInterceptor.class) public class TravelAgentBean implements TravelAgentRemote { ... } 在前面的例子中,Profiler将不会被执行.仅仅因为 @ExcludeDefaultInterceptors 注释被使用.它并不意味着我们不能指定一个 @Interceptors 注释来触发其它的拦截器类.这种排除最好在XML中来做: > * com.titan.Profiler TravelAgentBean com.titan.SomeOtherInterceptor 我们在前面的例子真睚做的是给TravelAgent EJB一个新的拦截栈覆盖和代替任 何默认的拦截器.相同的重载和关闭拦截可以在方法级别上使用.你可以关闭拦 截器完全的使用@javax.interceptor.ExcludeDefaultInterceptors 和 @javax.interceptor.ExcludeClassInterceptors注释来指定: @Stateful @Interceptors (com.titan.SomeOtherInterceptor.class) public class TravelAgentBean implements TravelAgentRemote { ... @Remove @ExcludeClassInterceptors @ExcludeDefaultInterceptors public TicketDO bookPassage(CreditCardDO cc, double amount) { ... } ... } @ExcludeClassInterceptors注释关闭任何应用在类级的拦截器,同样 的,@ExcludeDefaultInterceptors 注释关闭任何默认的拦截器定义在XML中的. 你可可以指定一个@Interceptors 注释在bookPassage( )方法来定义一个不同 的拦截器栈比较组件类的其它方法.这同样是有效的在XML格式中: * com.titan.Profiler TravelAgentBean com.titan.SomeOtherInterceptor TravelAgentBean com.titan.MyMethodInterceptor bookPassage com.titan.CreditCardDO double 通常,你并不关心关闭的拦截器,但是你最好知道它,如果你需要有工具来做. 15.2.拦截器和注入 拦截器属于相同的ENC作为EJB被拦截.拦截器类完全支持所有的注入注释,像注 释一样好通过XML.所以,你可以使用注释像@Resource, @EJB, 和 @PersistenceContext在你的拦截器类中,如果你希望这样.让我们在一个统计拦 截器中举例说明这个: package com.titan.interceptors; import javax.ejb.*; import javax.persistence.*; import javax.annotation.Resource; import javax.interceptor.*; public class AuditInterceptor { @Resource EJBContext ctx; @PersistenceContext(unitName="auditdb") EntityManager manager; @AroundInvoke public Object audit(InvocationContext invocation) throws Exception { Audit audit = new Audit( ); audit.setMethod(invocation.getMethod().toString( )); audit.setUser(ctx.getCallerPrincipal().toString( )); audit.setTime(new Date( )); try { Object returnValue = invocation.proceed( ); } catch (Exception ex) { audit.setFailure(ex.getMessage( )); throw ex; } finally { manager.persist(audit); } } } 这个拦截器的目的是记录在一个数据库中的每个方法调用在指定的组件上,所以 一个统计记录被建立.从这个统计记录中,系统管理员可以重新搜索安全突破口 或重演指定用户的行动.拦截器获取调用,用户通过调用getCallerPrincipal( ) 在javax.ejb.EJBContext注入的ctx成员变量.它分配一个统计实体组件和集合 属性像方法被调用,首要的调用,并且在当前情时间.如果方法抛出一个异常,这 也会存储到一个Audit实体.在最后的@AroundInvoke方法,Audit实体被持久化到 数据库中通过EntityManager注入的manager成员变量. 关于组件类别,拦截器注入注释创建额外的实体在EJB绑定的拦截器类的的ENC中. 这意味着持久化上下文的引用通过manager字段也可以使用JNDI的字符串 java:comp/env/com.titan.interceptors.AuditInterceptor/manager. 15.2.1.XML注入 如果你不想使用注释来依赖注入你的拦截器类,作为一种选择XML定义一个 元素在你的ejb-jar.xml部署描述符中: com.titan.interceptors.AuditInterceptor audit com.titan.interceptors.AuditInterceptor/manager auditdb com.titan.interceptors.AuditInterceptor manager 元素是新的,的顶级元素.元素接受任 何有效的EJB描述符中的环境项目.同样,如果你选择不使用批注在你的类定义中, 你可以指定元数据像@AroundInvoke 使用 元素.有时你可能想 要覆盖每个EJB上的指定拦截器类的注入.可以通过使用注释或XML来完成.例如, 假定你想要使用一个不同的持久化上下文为绑定到TravelAgent EJB 上的拦截 器类,auditdb作为当前的持久化单元被注入到你的AuditInterceptor类中.你可 以这样做,通过覆盖在你的EJB中定义的注入属性的环境名: @Stateful @PersistenceContext (name="com.titan.interceptors.AuditInterceptor/manager", unitName="EnterpriseWideAuditDB") public class TravelAgentBean implements TravelAgentRemote { ... } 因为所有的注入是在ENC名字的基础之上,前面的代码使用@PersistenceContext 注释在EJB组件类上覆盖拦截器的持久化上下文参考.这恐怕最好在XML上来做: TravelAgentBean com.titan.interceptors.AuditInterceptor/manager auditdb AuditInterceptor类中的manager字段引用 元素中定义的默认的ENC名字. 15.3.拦截生命周期事件 你不仅能拦截EJB方法的调用,也可以拦截EJB生命周期事件.这些回调可以被用 于初始化你的EJB组件类的状态,如同拦截器类本身.生命周期的拦截非常类似于 @AroundInvoke 类型: @ void (InvocationContext ctx); 要想拦截一个EJB的回调,定义一个方法在你的拦截器类中与你感兴趣的回调一 起注释.方法的返回值必需是void因为EJB回调没有返回值.方法名可以是任意的 并且必需不抛出任何检查异常(没有throws子句).InvocationContext是这个方 法的唯一参数.同@AroundInvoke方法,回调拦截的调用在一个大的Java调用栈中. 这意味着你必需调用InvocationContext.proceed( )方法来完成生命周期事件. 当proceed( )被调用时下一个拦截器类的相同回调被调用.如果没有其它的回调 方法,那么proceed( )是一个no-op.因为没有回调方法 InvocationContext.getMethod( )方法总是返回null. 15.3.1.定制注入注释 为什么要拦截一个EJB的回调?一个具体的例子是当你想要创建和定义你自己的 注入注释.EJB规范中有一组注释为你注入J2EE资源,服务,和EJB引用到你的组件 类中.一些应用程序服务器或应用程序像使用JNDI作为全局注册配置或没有J2EE 服务的.不幸的是,规范定义没有方法直接注入全局的JNDI到你的组件中.我们怎 样做来定义我们自己的注释提供这种功能和实现它作为一个拦截器. 首先我们要做的是定义我们将要使用从JNDI注入的注释: package com.titan.annotations; import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface JndiInjected { String value( ); } @com.titan.annotations.JndiInjected注释的value()属性是对象的全局JNDI 名字,我们想要注入的字段或setter方法.这是一个我们怎样使用自定义注释的 例子: @Stateless public class MySessionBean implements MySession { @JndiInject("java:/TransactionManager") private javax.transaction.TransactionManager tm; ... } 一些应用程序可能感兴趣的是获取一个引用到J2EE JTA事务管理服务.很多应用 程序服务存储一个引用到这个服务在全局JNDI中.在这种情况下,我们使用 @JndiInjected注释进入到会话Bean的字段直接的引用事务管理.现在我们定义 了我们自定义的注入注释并且我们定义了怎样使用它,我们需要实现拦截器类的 代码实现它的行为: package com.titan.interceptors; import java.lang.reflect.*; import com.titan.annotations.JndiInjected; import javax.ejb.*; import javax.naming.*; import javax.interceptor.*; import javax.annotation.*; public class JndiInjector { @PostConstruct public void jndiInject(InvocationContext invocation) { Object target = invocation.getTarget( ); Field[] fields = target.getClass().getDeclaredFields( ); Method[] methods = target.getClass().getDeclaredMethods( ); // find all @JndiInjected fields/methods and set them try { InitialContext ctx = new InitialContext( ); for (Method method : methods) { JndiInjected inject = method.getAnnotation(JndiInjected.class); if (inject != null) { Object obj = ctx.lookup(inject.value( )); method.setAccessible(true); method.invoke(target, obj); } } for (Field field : fields) { JndiInjected inject = field.getAnnotation(JndiInjected.class); if (inject != null) { Object obj = ctx.lookup(inject.value( )); field.setAccessible(true); field.set(target, obj); } } invocation.proceed( ); } catch (Exception ex) { throw new EJBException ("Failed to execute @JndiInjected", ex); } } } 使用@javax.annotation.PostConstruct注释的jndiInject( )方法,告诉EJB容 器JndiInjector拦截器感兴趣的是拦截指定的EJB回调.方法开始通过获取一个 引用到组件实例它截取.通过反射来查找@JndiInjected对象的所有方法和字段. 查找引用的JNDI名字,并且初始化目标组件实例的字段或方法.注意这是在 try/catch 块中来做的.当拦截一个回调方法,你可以不用抛出检查异常:因此, 所有的检查异常必需被捕获和包装在一个EJBException. 现在拦截器类已经被实现,我们可以应用拦截器到我们的组件当中: * com.titan.interceptors.JndiInjector 这个例子中一个特别有趣而且显著的事情是你可以使用EJB拦截器作为一个框架 为你写的自定义注释增加你想要的行为到EJB中.默认的拦截器,通过XML,给你一 个简洁,简单的应用拦截器的实现行为在注释中.最后,这种自定义的行为是轻便 的并且可以用在任意厂商来实现.不仅仅是EJB3.0容易使用,此外,最终还要易于 扩展. 15.4.异常处理 拦截器的异常处理简单而有力.因为拦截器在组件方法的Java调用栈或回调被调 用,你可以放置一个try/catch/finally块在InvocationContext.proceed( )方 法的周围.你可以中断调用在它到达实际组件方法之前通过抛出一个异常在 @AroundInvoke 或回调方法.你也可以允许捕获一个组件方法抛出异常和抛出一 个不同的异常,或禁止异常.关于@AroundInvoke 拦截器,你甚至被允许重试组件 方法调用在从组件方法捕获一个异常后.让我们看一些例子. 15.4.1. 终止一个方法调用 参数确认是检查见到的业务方法的参数在以方法的逻辑着手进行有效值确认的 业务逻辑.ProcessPayment EJB的byCheck()方法,使用确认来确定是否CheckDO 参数有一个最小的检查值.也许我们卖我们的泰坦巡航系统软件作为一个ERP系 统到不同的巡航公司到世办各地.我们可能想要关闭检查确认为我们的 ProcessPayment EJB的一个部署.为其它的部署,我们增加更复杂的验证,如,检 查一个和数库中的名字和支票帐号不符的帐户.拦截器给我们装载验证逻辑的能 力,在拦截器类中与配置应用它作为需要到ProcessPayment EJB的不同部署中. 因为拦截器类允许你终止EJB方法的调用在拦截器类自身中,在实际到达的组件 方法之前,这种方式使得模块化验证成为可能: package com.titan.interceptors; import javax.ejb.*; import javax.annotation.*; import javax.interceptor.*; public class CheckValidation { @Resource int minimumCheckNumber; @AroundInvoke public Object validateCheck(InvocationContext ctx) throws Exception { CheckDO check = ctx.getParameters( )[1]; if (check.checkNumber < minimumCheckNumber) { throw new PaymentException ("Check number is too low"); } return ctx.proceed( ); } } CheckValidation类中包装了ProcessPaymentBean.byCheck( )方法中的验证逻 辑.它使用InvocationContext.getParameters( )方法来获取CheckDO参数.最小 的检查从一个中的注入到拦截器类的minimumCheckNumber变 量.validateCheck( )方法验证检查CheckDO中的数值大于最小的检查数值.如果 验证失败它会终止调用同时抛出PaymentException. 验证仅仅是一个你想要终止EJB调用使用@AroundInvoke的方法.另一个例子是你 实现自定义安全框架的情况下.EJB安全是很漂亮的基础,而且有时你有很大的安 全需要.举例来说,你可能想要集成规则引挚到你的EJB中分析使用者作为方法和 参数来确定是否允许使用者调用方法.这也可以使用一个拦截器来做. 15.4.2.捕获和重新抛出异常 除了终止一个给定的方法调用,你也可以捕获组件中使用被拦截器 @AroundInvoke 方法抛出的异常.例如,你可以使用拦截器类作为一个抽像机制 来创建异常处理框架.考虑到JDBC和java.sql.SQLException.当一个 SQLException在你的程序代码中被抛出,不知道异常发生的原因,没有查看错误 数或异常消息.不幸的是,不同的数据库提供商的错误代码和消息有所不同,因此, 如果你想要处理确定情况的异常在确定的方法中,你的代码可以轻便的在不同数 据库厂商之间. 让我们看两种常见的SQLException发生情况:死锁和游标无效.首先,我们将创建 具体的异常继承SQLException: @ApplicationException(rollback=true) public class DatabaseDeadlockException extends java.sql.SQLException { public DatabaseDeadlockException(Exception cause) { Super(cause); } } @ApplicationException(rollback=true) public class DatabaseCursorNotAvailable extends java.sql.SQLException { public DatabaseCursorNotAvailable(Exception cause) { super(cause); } } 由于这些异常,我们依赖这些错误数来确定实际发生的数据库错误.客户端代码 使用这些异常轻便的方式并且不用关心底层数据库提供商.但是在我们使用这些 异常前,我们需要写拦截器类的异常处理: public class MySQLExceptionHandler { @AroundInvoke public Object handleException(InvocationContext ctx) Exception { try { return ctx.proceed( ); } catch (SQLException sql) { int ernum = sql.getErrorCode( ); switch(ernum) { case 32343: throw new DatabaseDeadlockException(sql); case 22211: throw new DatabaseCursorNotAvailable(sql); ... default: throw new RollbackAlwaysOnException(sql); } } } @AroundInvoke方简单捕获任何SQLException被组件方法抛出的,并且转换它到 一个适当的客户端代码可以捕获的异常类型.当然,有一个异常处理拦截器类对 于每个数据库提供商.这里的代码是你的应用程序如果利用拦截器的行为: // application client code { try { ejbref.invokeSomeDatabaseOperation( ); } catch (DatabaseDeadlockException deadlock) { // handle this specific error case in a special way } } 所以,组合异常处理拦截器同EJB调用允许你有指定的代码来处理指定数据库的 错误像死锁,不需要任何担心你的代码在商厂之间. 15.5.拦截器的生命周期 拦截器类有想同的生命周期作为EJB他们拦截.考虑到一个拦截器类作为EJB组件 实例的扩展.他们连同组件实例一同被创建.他们的销毁,钝化,和激活也是连同 组件的实例.同样注意重要的是拦截器类有相同的限制作为组件它们绑定的.例 如,你不能注入一个扩展持久化上下文到一个拦截器类,如果拦截器没有拦截一 个有状会话Bean. 因为拦截器有生命周期并且挂起到生命周期事件,所有他们不能保存内部状态. 这可能非常的有用当你想要拦截器获取一个连接到远程系统和关闭连接在销毁 时.你也可以对感兴趣的组件类实例保存其状态,在拦截器类上拦截.可能你有一 个自定义注入注释,你需要建立和它需要指定清除在组件实例销毁之后.你可以 保存内部状态同拦截器类和做清理工作当拦截器类和组件实例被销毁后. 15.6. 组件类中的@AroundInvoke 方法 这一章中主要讨论拦截器类.@AroundInvoke方法也可以存在于EJB组件类中.当 使用在一个组件类中,@AroundInvoke方法将会被最后“interceptor”被调用在 实际组件方法之前: @Stateless public class MySessionBean implements MySessionRemote { public void businessMethod( ){ ... } @AroundInvoke public Object beanClassInterceptor(InvocationContext ctx) { try { System.out.println("entering: " + ctx.getMethod( )); return ctx.proceed( ); } finally { System.out.println("leaving: " + ctx.getMethod( )); } } } 这是一个简单的使用@AroundInvoke方法的例子.那一类是你想要使用的?你可能 想要动态实现你的组件类,或你可能有一个拦截器逻辑指定到组件. 15.7.未来拦截器的改进 EJB3.0专家组研讨一些其它拦截器的特性规范正在写,但是并不会去掉发布的最 终草稿.让我们详细看一下它们中的一员,以便你能为你的决定判断它是一个好 的想法. 15.7.1. 行为的注解 让我们回到本章中前面的统计的例子中.为了应用统计行为,你需要 @Interceptors注释方法或指定一些XML在ejb-jar.xml.在这种情况下会出现一 对问题.如果你使用@Interceptors 注释,那么你的组件类绑定到统计实现.如果 使用XML,好的,你要写一个冗长的XML文件和增加它到你的配置中.如果你仅仅使 用它自己的注释来表达这一统计功能,不是很好吗? @Interceptors(com.titan.interceptors.AuditInterceptor) public @interface Audit { } 主意是注释你的自定义注释到拦截器功能上你想要应用的组件.当写自定义注释 时,你应用@Interceptors 注释定义那个注释.这将会自动的引起拦截器被应用, 不管是否使用@Audit 注释: @Stateful public class TravelAgentBean implements TravelAgentRemote { @Audit public TicketDO bookPassage(CreditCardDO cc, double amount) { ... } } 前面的例子中使用了新定我的@Audit注释.应用@Audit到bookPassage()将会触 发附着的AuditInterceptor方法调用.它是非常清楚的发生了什么并且@Audit注 释给你,作为拦截器的开发者,另一个间接的水平. 15.7.2.欢迎反馈 EJB3.0专家组欢迎您的反馈.如果你喜欢这一特性或想要一个不同的,可以免费 发送电子邮件到专家组ejb3-feedback@sun.com. 第十六章 事务 16.1. ACID事务特性 要想了解事务是如何工作的,我们将重新看一下TravelAgent EJB,在第11章中开 发的无状态会话Bean包装了处理客户预定巡航的业务.TravelAgent EJB的 bookPassage()方法如下: public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price); entityManager.persist(reservation); this.processPayment.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); return ticket; } catch(Exception e) { throw new EJBException(e); } } TravelAgent EJB是一个简单而清晰的会话Bean,而用它使用其它EJB的典型业务 对象设计和任务.不幸的是,好的业务对象设计并没有应用这些EJB在商业应用程 序中.问题在于没有定义EJB或任务:问题是,一个好的设计,不在于它本身,保证 TravelAgent EJB 的 bookPassage( ) 方法在一个好的事务.想要了解为什么, 我们将看一下什么是事务和可信赖的一个标准事务. 在业务上,一个事务通常包括改变的两个部分.当你购买一个冰淇淋,你会改变买 食物的钱;当你为一个公司工作,你会改变技能和时间为金钱(用于买更多的冰淇 淋).当你做相关的改变时,检测结果是确定你不是"ripped off".如果你组冰淇 淋商20美元的帐单,在他没有给你之前你不希望他离开:同样,你想要确保你的每 小时的工作的薪水被取回.通过这些业务交流,你可以确保交易的可靠性.确保每 次交易是每个人所期望的. 在商业软件中,一个事务体现一个业务交流的观念.一个交易的业务系统(简称 交易)是一个工作单元的执行,访问一个或更多的资源,通常是数据库.一个工 作单元是一系列彼此关联的活动,和一定要在一起被完成.预定处理是一个工作 单元,由多个活动组成;记录一经保留,将一张信用卡记录借方,并生成一张票. 一个事务对象是执行一个工作单元,可靠交流的一个单位.这里是一些典型的应 用事务的业务系统: ATM ATM(自动柜员机)你可以用它存放,撤消,和转移资金作为一个事务的工作单元 来执行.在一个ATM撤消中,例如,ATM检查确保不被透支;然后记录帐户和吐出钱. Online book order 你可能购买很多的Java书籍,甚到是从网上书店购买的.这种类型的购买也是作 为一个工作单元在一个事务中.在一个在线购买中,你提交信用卡号,它是有效 的,并且收取书的价钱.然后所运送的命令送到书店的仓库. Medical system 在一个医务系统中,每天都记录关于病人的重要数据,包括临床,医闻手术,处方 和药物过敏等信息.在医生开药时,系统检查病人是否对药物过敏,禁忌,和适当 的剂量.如果所有的测试通过,可以开药.这些任务构成一个工作单元.一个医务 系统的工作单像是财政,但是同样的重要.不能够辨别了一个病人的药物过敏症 是致命的错误. 如你所看到的,事务通常是复杂的并且常常操作一些数据.要为数据中的错误付 出很多的代价.事务必需保证数据的正确性,意味着事务中的工作完全被执行或 根本不执行.这是一件相当困难的事.这样的要求是很难实现的,然而,当它得到 商业化的时候,错误就不会被产生了.单元工作总是包括钱和其它值,因为错误的 原因,所以需要相当高的可信度. 必需有一个好的方法来准确的处理,关于事务发生的系统,减少错误的发生.ATM 提供对它的银行帐户方便存取给客户并且表现个人的银行处理百分比.通过ATM 的事务处理是简单的,但是给我们提供一个更好的例子来预防错误.假如一个银 行有100个ATM机在一个大城市内,并且每个ATM处理300个存款(存款,取款,传 输)一天,每天总共处理30,000.如果每个事务包括存款,取消,或 $100 的平均 传递。那么大约$3百万将会移动通过ATM系统每天.在一年期间,至少超过十亿: 365 天 x 100 ATMs x 300 个操作 x $100 = $1,095,000,000 很多的ATM执行都被视为可靠的吗?为函数中的独立变数,ATM执行操作的正确率 为99.99%每次.这看起来似乎很恰当:毕竟,在每10,000处理中只有一个是不正 确的运行.但是如果你计算一下,在一看之内错误 造成的次数将超过$100,000! $1,095,000,000 x 。01%=$109,500 显然,这个例子是一个问题的简单化,但是它演示了小的错误在一个系统中是无 法接受的.处于这种原因,专家提出了事务的四种特性必需被实现考虑到一个系 统的安全.事务处理一定是原子的, 一致的,隔离的,和持久的(ACID)四种事 务服务特性: 原子性 原子事务必需被完全执行或一点也不执行.这就意味着每个工作单元必需被无误 的运行.如果其中的任意任务失败,进入的工作单元或事务被终止,意味着任何数 据的改变将会不被执行.如果所有的任务执行成功,事务提交,那将意味着数据的 改变被持久化. 一致性 一致性提级到底层的数据存储的完整性.它必需被强制通过事务系统和应用程序 开发者.处理系统实现职务通过确保事务是原子的,隔离的和持久的.应用程序开 发者必需确保数据库有适当的约束(主键,完整性参考,和前面四种)和工作单元 的业务逻辑没有结果的限制数据集(例如,事据不是真实世界协调的代表).在一 个帐户的转让过程中,例如,借方的账户必需与另一个帐户的存储相同. 隔离性 隔离性意味着事务必需被允许执行无需引用从其它的进程或事务.换句话来说, 在一个事务中访问的数据不能够影响系统的其它部分直到事务或工作单元完成. 持久性 持久性意味着所有的改变在一个事务的过程中必需被写入到一些物理存储设备 在事务完成之后.这样来确定改变在系统当机后不会被丢失. 为了要得到原则意指的较好的概念,我们将检查TravelAgent EJB在四个ACID属 性间. 16.1.1.TravelAgent EJB符合原子性吗? 我们衡量TravelAgent EJB可靠性的第一个条件是原子性:确定事务执行完成或 没有执行?我们真正关心的是关键任务的改变或创建信息.在bookPassage()方法, 一个Reservation实体被创建,ProcessPayment EJB 记入借方的信用卡,并且一 个TicketDO对象被创建.所有的这些任务必需被完成在进入事务中被完成. 要想理解原子性的重要性,假想子任务执行失败时发生.如果,例如,创建一个 Reservation 实体失败但是所有的其它任务完成,你的客户或许会结束获取从巡 航或共享的船舱同一个陌生人.就旅行代理程序来说, bookPassage() 因为一个 TicketDO 对象被生成,所以方法成功地运行。如果一个票被成生没有创建一个 预定,业务系统的状态变成无限制同事实因为客户为一张票付款,但是预定没有 被记录.同样的,如果ProcessPayment EJB失败管理客户的信用卡,客户获取一个 锡费的巡航.他可能会很高兴,但是管理者却不是.最后,如果TicketDO对象从来 没有被创建,客户将在事务过程中无记录或许不会被允许在船上. 所以bookPassage( ) 方法完成仅有一种方法是所有的关键任务执行成功.如果 有一些事情发生错误,全部的处理被终止.除此之外,事务中的所有任务被重做. 如果, 举例来说, 预定实体的创建和ProcessPayment.byCredit( )方法完成,但 是TicketDO对象创建失败(抛出一个异常从构造中)预定和付款记录不会被增加 到数据库中. 16.1.2.TravelAgent EJB符合一致性吗? 为了使处理一致,业务系统必需理智的在事务完成之后.换句话来讲,业务系统的 状态必需被统一同实际的业务.这要求事务强制执行原子性,隔离性,和持久性, 并且它也要求应用程序开发者强制执行完整性约束.如果,例如,应用程序的开发 者忘记了包含信用卡管理操作在bookPassage()方法中,客户将被确定一张票但 是永远不会被管理.数据将不会被限制在预期的客户业务逻辑将被管理被处理. 另外,数据库中必需强制执行完整性约束,例如,它可能不被记录增加到 RESERVATION表除非CABIN_ID, CRUISE_ID , 和 CUSTOMER_ID外键映射关联记录 在CABIN,CRUISE,和CUSTOMER表,各自的.如果一个CUSTOMER_ID没有被用于映射 到CUSTOMER记录,完整性约束将会引发数据库抛出一个错误消息. 16.1.3.TravelAgent EJB 附合隔离性吗? 如果你非常熟释Java语言中的线程同步或行锁定在关系数据库中的概念,隔离性 将是一个非常熟悉的概念.被隔离,一个事务必需保护数据访问从其它的事务中. 这是必需的阴止其它事务交互使用数据在事务中.在TravelAgent EJB中,事务隔 离阻止其它事务修改实体和表被更新.假定问题发生,如果分开处理允许改变任 何实体在任意时间事务将会撤离.一些客户可能很容易预定相同的船舱因为旅行 代理发生预定在相同的时间. 通过EJB的数据隔离并不意味着整个应用程序在事务期间停止.仅当那些实体组 件和数据直接受到隔离事务的影响.在TravelAgent EJB中,例 如,事务隔离仅当 Reservation EJB被创建.很多的Reservation实体可能存在;没有原因其它EJB不 能访问通过其它的事务. 16.1.4.TravelAgent EJB符合持久性吗? 在持久化之前,bookPassage()方法必需写入所有的改变和新数据到一个持久化 数据存储在它被认为完成之前.这有点像不用脑想都知道的问题,不过通常它不 是真实生活的反映.在效率的名字中,变化通常操作在内存在需要很长的时间在 保存到硬盘驱动器前.这种思想减少磁盘的访问,在配置较低的系统只有周期性 的写入累积的数据改变提高效率.这种方式的执行效率很高,不过当系统下降, 而且存储器被彻底损坏的时候,数据会被丢失,所以它也是很危险的.当事务成 功完成后,持久性需要系统反所有的事务中的更新存档.从而保持数据的完整性. 在TravelAgent EJB中,这意味着新的RESERVATION 和 PAYMENT 记录插入持久化 在事务成功完成后.只有当数据被持久化的时候那些特定的记录可访问来自其它 事务中的实体.因此,持久性也扮演隔离性的一个角色.当记录中的数据没有被成 功的记录,事务是不会完成的. 确保事务的坚持ACID原则需要仔细的设计.系统必须检测处理的进步确定它做它 的全部工作,数据正确地被改变,事务之间彼此互不干扰,而且变化可以安全的度 过当系统当机时.工程学上所有的函数进入一个系统有许多工作,并且不是所有 的东西你想要的重新使用在每个业务系统在你的工作中.幸运的是,EJB的设计支 持事务自动完成,使开发事务系统很容易.这一章中的其它检查EJB如果隐式(通 过定义事务属性)的和显式的支持事务(通过Java事务API,或JTA). 16.2.定义事务管理 企业 JavaBeans 的主要利益之一是它考虑到明确的的定义事务管理。没有这个 特性,事务控制必需使用明确的事务划分.包括非常复杂API像OMG的对象事务服 务(OTS)或它的JAVA语方实现,Java事务服务(JTS).如果你使用上述的 API ,充 其量,明确的划界很困难,特别的是对事务系统是新的.另外它要求将事务相关的 代码写入业务逻辑,减少代码的透明度.我们谈论更多的关天直接事务管理和EJB 在本章的后面。 定义明确的事务管理,EJB的事务行为控制可以使用 @javax.ejb.TransactionAttribute批注或EJB部署描述符,两者都可以设置事务 属性为单个企业组件方法.这意味着一个EJB的事务行为可以被改变在不改变EJB 业务逻辑的情况下,通过简单的注释在不同的方法上或修改XML文件.定义事务管 理减少复杂的事务为EJB开发者和应用开发者并且使它容易被创建事务应用程 序. 16.2.1. 事务范围 事务的范围概念是理解事务的关键.在这个上下文,事务范围涉及那些EJB的会话 和实体参加到指定的事务中.在TravelAgent EJB的bookPassage( )方法中,所有 的EJB都是相同事务范围的部分.事务的范围始于客户端调用TravelAgent EJB的 bookPassage( ).一旦事务开始,它被传播到实体管理服务负责创建预定和 ProcessPayment EJB. 如你所知道的,一个事务是由一个或多个任务组成的工作单元.在一个事务中, 所有的任务组成工作单元必需成功的进和整个事务被完成;换句话来讲,事务必 需是原子的.如果任意任务失败,事务中的其它的更新任务将要被回滚或重作.在 EJB中,任务通过企业Bean的法方完成,并且一个工作单元包括每个企业Bean方法 调用在一个事务中.事务的范围是包括所有的参加工作单元中的EJB. 通过线程的执行很容易跟踪事务范围.如果调用bookPassage( )方法时开始了一 个事务,那么,理论上,事务结束是在方法完成时.bookPassage( )的事务范围将 包括TravelAgent EJB,EntityManager服务,和ProcessPayment EJB.通过 bookPassage()方法每个EJB或事务意识服务被接触.事务被传递到一个EJB,当 EJB的方法被调用和包括在事务范围.事务也被传递到EntityManager的持久化上 下文.如果事务成功,持久化上下文保持跟踪改变到持久化管理对象和提交他们. 如果bookPassage( )方法在执行过程中抛出一个异常一个事务将会结束.异常的 抛出可能从其它的EJB中的一个或从bookPassage()方法本身.一个异常可能或不 可能引起一个回滚,依赖它的类型.稍后我们会讨论异常和事务的更多细节. 线程的执行不是决定一个EJB包含在事务范围内的唯一因数;EJB 的事务属性也 扮演一个角色。决定是否一个EJB参与到任意工作单元的事务范围隐式的完成, 使用EJB的事务属性,或明确的使用JTA. 16.2.2. 事务属性 作为一个应用程序的开发者,你通常不需要明确的控制事务当使用EJB服务器 时.EJB服务器可以隐式的管理事务,事务属性的建立是在部署时.当一个EJB被部 署,你可以设置它运行时的事务属性在@javax.ejb.TransactionAttribute批注 或部署描述符到多种选择中的一个: NotSupported Supports Required RequiresNew Mandatory Never 你可以为全部的EJB设置一个事务属性(在哪一个情况它适用于所有的方法)或 你可以设置不同的事务属性为个别的方法.前面的方法比较简单而且错误较少, 但是设置方法级的属性提供更多的弹性.在接下来的部分代码演示如何设置一个 EJB的默认事务属性在EJB的部署描述符中. 16.2.2.1. 使用 @TransactionAttribute 批注 @javax.ejb.TransactionAttribute批可以使用在你的EJB组件类的事务属性.属 性的定义使用javax.ejb.TransactionAttributeType Java枚举类型: public enum TransactionAttributeType { MANDATORY, REQUIRED, REQUIRES_NEW, SUPPORTS, NOT_SUPPORTED, NEVER } @Target({METHOD, TYPE}) public @interface TransactionAttribute { TransactionAttributeType value( ) default TransactionAttributeType.REQUIRED; } @transactionAttribute可以应用到每个方法,或者你可以使用它在组件类上定 义默认的事为属性为整个组件类: import static TransactionAttributeType.*; @Stateless @TransactionAttribute(NOT_SUPPORTED) public class TravelAgentBean implements TravelAgentRemote { public void setCustomer(Customer cust) {...} @TransactionAttribute(REQUIRED) public TicketDO bookPassage(CreditCardDO card, double price) { ... } } 在这个例子中,默认的事务属性将会是NOT_SUPPORTED为类中的每个方法因为我 们应用@transactionAttribute批注到组件类上.这种默认的可以被应用到个别 的bookPassage( ) 方法上的@TRansactionAttribute覆盖,因为它有REQUIRED事 务属性. 注意:如果你没有指定任何@transactionAttribute 和没有XML部署描述符,默认 的事务属性将会是REQUIRED.在EJB3.0之后的思想之一是提供通用的默认值,所 以你不需要明确事务的划分.在大多数情况下,EJB方法是事务管理的,尤其是如 果他们与实体管理相交互. 16.2.2.2.在XML中设置一个事务属性 在XML部署描述符中,一个元素提定事务属性为EJB的 部署描述符: TravelAgentEJB * NotSupported TravelAgentEJB bookPassage Required 部署描述符为TravelAgent EJB指定事务属性.每个元 素指定一个方法和方法的事务属性.第一个元素指定 所有的方法有一个默认的事务属性为NotSupported;*通配符代表TravelAgent EJB中的所有方法.第二个元素覆盖默认设置到指定的 bookPassage( ) 方法,使用Required事务属性.注意:我们必需指定是那个的EJB 引用即 元素;一个XML部署描述符可以包含多个EJB. 16.2.2.3.事务属性的定义 这里是前面列出的事务属性列表.在少数的定义中, 用户端处理被描述为中止 的。这就意味着事务不被传递到调用的企业Bean方法;传递的事务是临时挂起直 到企业Bean的方法返回时.要想使其更简单,我们将谈论属性类型作为组件类型: 例如,我们将说"a Required EJB" 简称为"一个企业Bean存在Required事务属性 "属性是: NotSupported 在一个EJB上的方法调用同这种事务方法持起事务直到方法被完成.这意味着事 务范围是不被传递到NotSupported事务属性的EJB,或到任何它调用的EJB.在 NotSupported EJB 上的一次方法被做,最初的处理重新开始它的运行。 Figure 16-1显示一个NotSupported EJB 不会传递到客户端事务当它的一个方 法被调用时. Supports 这个属性意味着企业Bean方法将会被包括在事务范围如果它的调用在一个事务 中.换句话来说,如果EJB或客户端调用Supports EJB是事务范围的一部 分,Supports EJB和所有的EJB访问通过它,因为它是最原始的事务的一部分.然 而,Supports EJB不是事务范围的一部分可以同客户端和其它EJB互动在事务范 围内. Figure 16-2a显示Supports EJB被调用通过一个事务客户端和传递的事务. Figure 16-2b显示Supports EJB被调用通过一个非事务客户端. Required 这个属性意味着企业Bean方法必需在事务范围内被调用.如果客户端调用或EJB 是事务的一部分, Required EJB自动包含在事务范围.如果, 然而,客户端调用或EJB 不与事务关联,Required EJB 开始它自己的新事务.新的事务范围仅覆盖 Required EJB和所有其它通过它访问的EJB.一旦Required EJB的方法调用完成, 新的事务范围结束. Figure 16-3a显示Required EJB被调用通过一个事务的客户端的传递的事 务.Figure 16-3b显示Required EJB被调用通过一个无事务的客户端,开始它自 己事务的原因. RequiresNew 这个属性意味着总是开始一个新事务.当调用时,不管是客户端调用或EJB是否是 事务的一部分,使用了RequiresNew 属性的方法将开始一个新的事务.如果客户 端调用已经在一个事务中使用,事务将会暂停直到RequiresNew EJB的方法调用 返回. 新的事务范围覆盖仅包括RequiresNew EJB 和所有的EJB访问通过这个事 务的.一旦RequiresNew EJB上的方法调用做完,新的事务范围结不和原始事务重 新开始. Figure 16-4a显示RequiresNew EJB的调用通过一个客户端事务.客户端事务暂 停直到EJB执行在它自己的事务内.Figure 16-4b 显示RequiresNew EJB被调用 通过一个无事务的客户端;RequiresNew EJB执行在它自己的事务内. Mandatory(托管) 这个属性意味着企业Bean方法必需总是客户端调用的事务范围内的一部分.EJB 不能够开始自己的事务;事务必需由客户端传递过来.如果客户端调用不是一个 事务的一部分,调用将会失败.抛出 javax.ejb.EJBTransactionRequiredException异常. Figure 16-5a 显示 Mandatory EJB 调用通过一个事务客户端和传递的事务 Figure 16-5b 显示 Mandatory EJB 调用通过一个无事务的客户端;方法抛出一 个EJBTransactionRequiredException因为没有事务范围. Never 这个属性意味着企业Bean的调用不与一个事务范围相关.如果客户端调用或EJB 是事务的一部分,Never EJB 将会抛出一个EJBException异常.然而,如果客户端 调用或EJB不与一个事务相关,Never EJB将会正常执行在无事务状态下. Figure 16-6a显示Never EJB 被一个无事务的客户端调用. Figure 16-6b显示Never EJB 被一个事务客户端调用;方法抛出一个 EJBException异常到EJB客户端,因为一个客户端或EJB包含一个事务不能被方法 调用。 16.2.2.4. EJB 3.0持久化和事务属性 EJB规范强烈建议EntityManagers 访问在JTA事务范围内,如果你包装访问你的 持久化实体同EJB,仅使用Required,RequiresNew,和Mandatory事务属性.这种限 制确保所有的数据库访问发生在同一个事务的上下文,非常重要的一点是容器自 动管理持久化.当和有状态会话Bean一同时使用扩展的持久化上下文将会抛出一 个有效异常到这种规则,但是这些异常将会在这一章的后面谈论到. 16.2.2.5. 消息驱动Bean和事务属性 消息驱动Bean仅何以定义NotSupported 或 Required 事务属性.其它的事务属 性并不会被消息驱动Bean所感知,因为他们应用到客户端初始化事务.Supports, RequiresNew, Mandatory, 和 Never属性关联到客户端的事务上下文.例 如,Mandatory属性要求客户端有一个事务处理在调用企业Bean方法之前.这对消 息驱动Bean是无意义的,从客户端被分离. NotSupported 事务属性指示消息将会被处理不在一个事务中.Required事务属 性指示消息的处理在一个容器初始化的事务中. 16.2.2.6. EJB 端点和事务属性 Mandatory事务属性不能用于EJB端点因为一个EJB端点不能传递一个客户端事务. 当Web服务事处变成标准化时,这可能必变,但是现在,使用Mandatory作为一个 EJB端点是被禁止的. 16.2.3. 事务传播 为了演示事务属性的影响,我们再看一次TravelAgent EJB的bookPassage()方法. 为了bookPassage()执行作为一个成功的事务,Reservation 实体的创建和管理 客户两者都必需成功.这意味着两者的操作必需在相同的事务中.如果其中的一 个失败,整个事务也会失败.我们可以指定Required事务属性作为默认的事务属 性为全部关联的EJB,因为这个属性强制我们需要的策徊必需执行在一个事务中 和确保数据的一致性. 作为一个事务的监听者,一个EJB服务器观察事务中的每个方法调用.如果任意一 个更新操作失败,EJB中的所有更新和全部反转或回滚.回滚类似于undo(重做)命 令.如果你工作在关系型数据库,你将会很熟悉回滚的概念.一旦更新被执行,你 可以提交更新或回滚.一个提交使得改变请求的更新被持久化;一个回滚终止更 新和返回到数据库的原始状态.使EJB事务提供相同类型的回滚/提交控制.例如, 如果Reservation实体不能被EntityManager创建,管理使得ProcessPayment EJB 回滚.事务使得更新变为一个全有或全无的建议.这样来确保工作单元,像 bookPassage()方法.当要运行,它阻止不一致的数据被写到数据库。 为防止万一,容器暗中管理事务,提交和回滚的决定处理是自动的.当事务是明确 的管理同一个企业Bean或通过客户端,职责落在了企业Bean或应用程序的开发者 来提交或回滚事务.事务的划分将会在这一章中的后面被覆盖. 我们假设TravelAgent EJB的创建和使用在客户端,如所示: TravelAgent agent = (TravelAgent)jndi.lookoup("TravelAgent"); agent.setCabinID(cabin_id); agent.setCruiseID(cruise_id); try { agent.bookPassage(card,price); } catch(Exception e) { System.out.println("Transaction failed!"); } 此外,我们假设bookPassage()方法使用RequiresNew事务属性.在这种情况下, 客户端调用bookPassage()方法不是自身事务的一部分.当调用TravelAgent EJB 的bookPassage()方法时,一个新的事务被创建,如 RequiresNew 属性所命令。 这意味着TravelAgent EJB 注册自身自用EJB服务的事务管理.它将自动管理事 务.处理事务坐标.传递事务范围从一个EJB到下一个确定的所有EJB接触通过事 和包含的工作单元.那样,事务管理可以监视更新模式通过每个企业Bean和决定, 在成功的更新基础上,是否提交所有必变通过企业Bean到数据库,或回滚.如果系 统异常或一个回滚应用异常被bookPassage()方法抛出,事务自动回滚.在本章的 后面我们会讨论更多的关于异常. 当bookPassage( )里的byCredit()方法被调用,ProcessPayment EJB同事务管理 在为TravelAgent EJB创建的持久化上下文;事务的上下文传递到 ProcessPayment EJB.当新的Reservation被实体管理器持久化时,实体管理器的 持久化上下文也注册同事务管理在相同的事务中.当所有的EJB和持久化上下文 都注册和更新完成,事务管理器检查确认他们的更新工作. 如果所有的更新完成, 事务管理器允许改变变成持久的.如果EJB或实体管理器报告一个错误或失败,任 何改变模式在ProcessPayment 或 TravelAgent EJB 回滚被事务管理器.Figure 16-7图示传递和管理TravelAgent EJB的事务上下文. 另外,管理事务在它自己的环境中,一个EJB服务器可以定位其它的事务系统,如 果,例如,ProcessPayment EJB实际上与TravelAgent EJB来自不同的应用服务器,两 个应用程序服务器可以联合管理事务作为一个工作单元.这被叫做分布式事务. 分布式事务要求一个被称为两除段的提交(2­PC或TPC)。一个2-PC 允许事务管理 跨跃不同的服务器和资源(例如,数据库和JMS提供者).详细的2­PC超出本书的范 围,但是作为一个系统支持它不需要额外的操作通过一个EJB或应用程序的开发 者.如果支持分布式事务,事务传递协议,在前面讨论,将会被支持.换句话来讲, 作为一个应用程序或EJB开发者,你不需要注意本地和分布式事务的不同. ※不是所有的EJB服务器都支持分部式事务 ◆关于事务处理和2-PC的书很多.也许最好的书是Principles of Transaction Processing (Morgan Kaufmann) 和 Transaction Processing: Concepts and Techniques (Morgan Kaufmann).一个轻量资源系列"XA Exposed"文章(I, II, 和 III)由Mike Spille所写,你可以查找在 http://jroller.com/page/pyrasun/?anchor=xa_exposed. 16.2.3.1. 事务和持久化上下文的传递 有多种事务传递的规则供你考虑当调用在多个不同的EJB上使用实体管理器在相 同的事务中.例如,如果ProcessPayment EJB重新实现使用Java持久化非JDBC记 录付款.它的实体管理将共享相同的持久化上下文为TRavelAgentBean的 bookPassage( )方法.这是因为bookPassage( )方法的调用同 ProcessPaymentBean在相同的事务中.这里列出更多的传递持久化上下文的规则 细节: ◆当一个事务范围的实体管理调用在事务范围外,它为持续的方法调用创建一个 持久化上下文.在方法调用完成后,任何被调用的对象将会立即的被分离.第五章 给出了允行在事务外调用的方法的详细列表. ◆如果一个事务范围的实体管理调用来自一个事务,一个新的持久化上下文被创 建,如果没有一个已存在并且关联的事务.持久化上下文被传递在相同事务的EJB 调用之间. ◆如果一个实体管理调用的持久化上下文已经关联事务 ,使用持持久化上下文. 持久化上下文传递在相同事务的EJB调用之间.这意味着如果一个EJB同一个注入 的实体管理交互和那个调用在另一个EJB在相同的事务内,EJB调用将使用相同的 持久化上下文. ◆如果一个EJB同一个事务范围的持久化上下文调用在一个有状态会话Bean上使 用一个扩展的持久化上下文,一个错误会被抛出. ◆如果一个有状态会话Bean同一个扩展的持久化上下文调用已经注入到一个事 务范围的持久化上下文的另外的EJB,扩展的持久化上下文被传递. ◆如果一个EJB调用另外一个在不同事务范围内的EJB,持久化上下文,是否被扩 展,没有被传递. ◆如果一个扩展持久化上下文的有状态会话Bean调用另外一个扩展持久化上下 文的非注入的有状态会话Bean,一个错误会发生.如在第11章中看到的,如果注入 一个有状态会话Bean到另一个有状态会话Bean,那些组件共享相同的扩展持久化 上下文.然而,如果你手工创建一个有状态会话,不会共享持久化上下文. 16.3.隔离性和数据库锁 事务隔离性("I"是ACID中的I)是事务系统的关键部分.这一部分解释隔离条件, 数据库锁,和事务隔离级别.当你开发任何的事务系统时这些概念非常重要. 16.3.1.脏读,可重复读,和虚读 事务隔离性的定义条件被称作赃读,可重复读,和虚读.这些条件描述当两个或更 多的事务操作相同的数据时会发生件么.举例说明这些条件,假设我们使用两个 分开的客户端的各自的TravelAgent EJB 访问相同的数据,一个主键为99的记录. 这些例子围绕着RESERVATION表,访问通过这一章开始讨论的和第11章中讨论过 的bookPassage()方法,和一个新的使用EJB QL来查询cabin列表的 listAvailableCabins( ) 方法: 隔离性条件的详细被ANSI SQL-92 规范所覆盖,文件编号:ANSI X3. 135-1992 (R1998). public List listAvailableCabins(int bedCount) throws IncompleteConversationalState { if (cruise == null) throw new IncompleteConversationalState( ); Query query = entityManager.createQuery("SELECT name FROM Cabin c WHERE c.ship = :ship AND c.bedCount = :beds AND NOT ANY (SELECT cabin from Reservation res WHERE res.cruise = :cruise"); query.setParameter("ship", cruise.getShip( )); query.setParameter("beds", bedCount); query.setParameter("cruise", cruise); return query.getResultList( ); } 当两个用户同时执行这些方法,不同的问题能升至水面或者完全地被避免,依赖 于数据库的隔离级别,例如,假定两个方法有相同的事务属性Required. 16.3.1.1.赃读 赃读发生在一个事务读取到另一个事务未被提交的改变.如果前一个事务回滚, 被第二个事务读取的数据变成无效的了,因为回滚重做的改变.第二个事务将不 会意识到它所读取的数据已经是无效的了.这里是脏读发生的细节(Figure 16-8 所示): 1.时间10:00:00:客户1执行travelAgent.bookPassage( )方法.连同Customer 和Cruise实体,客户1先选择了屋号为99的小屋被包含在预定中. 2.时间10:00:01:客户1的TravelAgent EJB的bookPassage()方法创建一个 Reservation实体.EntityManager插入一个记录到RESERVATION表,保留Cabin 99. 3.时间10:00:02:客户2执行travelAgent.listAvailableCabins( ).客户1预留 Cabin 99,所以它不在有效的Cabins列表中,返回从这个方法. 4.时间10:00:03:客户1的TravelAgent EJB执行bookPassage()方法内的 ProcessPayment.byCredit( )方法.byCredit( )方法抛出一个异常,因为传递的 日期是过期的. 5.时间10:00:04:异常被ProcessPayment EJB抛出,因为进入bookPassage()方法 的事务被回滚.结果,当Reservation EJB被创建没有持久化插入到RESERVATION 表中的记录(例如,它被移除).Cabin 99现在是有效的. 客户2现在使用一个无效的可用的Cabins列表,因为Cabin 99是用效的,但是不包 含在列表中.因为客户2报告的巡航被订购的信息是错误的,如果Cabin 99是最后 可用的小屋,这冗长是很严重的.消费者可能会试着去预定一个巡航在完成的航 线. 16.3.1.2.重复读 重复读发生在当数据读取保证看起来在相同的时间,如果再一次读取在相同的事 务间.可重复读保证在两种方法的一种:或者是使用锁来防止改变,或者是不反映 改变的快照.如果数据被加锁,任何的其它事务不能够改变它直到当前事务结束. 如果数据作为一个快照,其它的事务可以改变数据,但是这些改变不能够被这个 事务看到如果是重复读.这里是可重复读的一个例子(示例 Figure 16-9): 1.时间 10:00;00:客户1开始一个显示的javax.transaction.UserTransaction. 2.时间 10:00:01:客户1执行travelAgent.listAvailableCabins(2),寻找有两 个床的Cabins的有效列表.Cabin 99在有效的Cabins列表内. 3.时间 10:00:03:客户2工作同一个管理Cabins的接口.客户2试着去改变床位数 在Cabin99从2到3. 4.时间 10:00:03:客户端1 重新执行 TRavelAgent.listAvailableCabins(2).Cabin 99仍在有效的Cabins列表中. 因为它使用 javax.transaction.UserTransaction ,所以这一个例子略微不寻 常,这在这一个章节中被稍后更详细地覆盖。本质上, 它所做的是让一个用户端 应用程序明确地控制事务的范围。在这种情况下,客户1放置事务调用的边界到 listAvailableCabins( ),所以他们是相同事务的一部分.如果客户1不这样做, 两个listAvailableCabins( )方法将会在分开的事务中执行并且重复读的条件 不会发生. 通过客户2尝试改变床位数为Cabin99到3,Cabin99仍然显示在客户1调用的 listAvailableCabins( )当一个床位数为2被请求时.或者客户2被禁止改变(因 为一个锁)或者是客户2允许改变,但是客户1工作同一个数据的快照不反映数据 的变化. 一个不可重复读的发生当数据重新的得到,在并发的读取在相同的事务中返回不 同的结果.换句话来讲,并发的阅读可以看到其它的事务所做的改变. 16.3.1.3.虚读 虚读发生在当记录被增加到数据库中,被事务读到开始的插入数据.查询将会包 括在被其他处理增加之后的记录 , 他们的事务已经启动。这里是包含虚读的细 节:(在图 16-10 中举例) 1.时间 10:00:00: 客户1开始一个显式的 javax.transaction.UserTransaction. 2.时间 10:00:01: 客户1执行travelAgent.listAvailableCabins(2),查询有两 张床的有效的Cabins列表.Cabin 99在Cabins的有效列表内. 3.时间 10:00:02: 客户2执行bookPassage( )和创建一个预定.预定插入一个新 的记录到RESERVATION表,预定Cabin 99. 4.时间 10:00:03: 客户1重新执行 travelAgent.listAvailableCabins(2).Cabin 99 不再在有效的Cabins列表中. 客户1放置事务的边界在两次listAvailableCabins( ) 调用,所以他们是相同的 事务的一部分.在这种情况下,预定完成在相同的事务的 listAvailableCabins( )查询之间.因此,插入到RESERVATION表中的记录不存在 当第一次listAvailableCabins( ) 方法调用,但是它不存在和看不见当第二次 listAvailableCabins( ) 方法被调用.插入的记录被称做虚读. 16.3.2.数据库锁 数据库,尢其是关系型数据库,通常使用不同的锁技术.最常用的是读锁,写锁, 和排它的写锁.(增加特殊的“快照”到这个技术列表,虽然它不是正式的条件) 这些锁的机制控制事务如何并发的访问数据.锁的机制影响读条件在前面章节中 描述过.这些类型锁的简单的概念在JAVA语言规范中有说明,但是我们将以后进 行讨论.数据库提供商实现这些锁的方式不同,所以,你最好明确你所使用的数据 库地址这些锁的机制,最好能预言这部分隔离性是怎样工作的. 四种类型的锁: 读锁: 读锁阻止从改变数据读的其它事务直到事务结束,因此,防止非重复读.其它 的事务可以读数据但不能写.当前事务禁止改变.一个锁仅锁定记录的读,一个锁 记录,或整个表依赖所使用的数据库. 写锁 写锁用于更新.一个写锁阻其它事务改变数据直到当前事务完成,但是允许其 它事务的赃读和当前事务本身.换句话来说,事务可以读取它自己未提交的改变. 排它写锁 排它写锁用于更新,一个排它写锁阻止其它事务读写数据,直到当前事务 完成.它也阻止其它事务的脏读.一些数据库不允许事务读他们自己的数据当它 使用排它锁. 快照 快照是当一个事务开始时,数据的一个冻结视图.一些数据库获取锁通过每个 事务同时有它自己的快照.快照可以防止脏读,不可重复读,和虚读.因为数据不 是即时的,所以可能是有问题的;快照中的数据是旧的. 16.3.3. 事务的隔离级别 事务的隔离性根据隔离性条件(脏读,可重复读,和虚读)被定义.隔离级别遍的 应用于数据库系统,描述在一个事务中怎样将锁应用到数据上.下面的条件用于 讨论隔离级别. 隔离条件的详细信息被ANSI SQL-92 规范覆盖,文件编号:ANSI X3.135- 1992 (R1998). 读未提交的数据 事务可以读取未提交的数据(例如,通过不同事务的数据改变仍在处理中). 脏读,不可重复读,和虚读可能发生.这种隔离级别的Bean方法可以读取未提交的 改变. 读已提交的数据 事务不能读取未提交的数据:在不同事务改变的数据不能够被读取.脏读被 阻止.不可重复读和虚读可能发生.这种隔离性的Bean方法不能读取未提交的数 据. 重复读 事务不能够改变数据被不同的事务读取.脏读和不可重复读被阻止;虚读可能发 生.这种隔离级别的Bean方法有相同的限制如同那些在读提交数据级和可以仅执 行重复读级. 串行化 事务可以排除读和更新权限;不同的事务可以读或写相同的数据.脏读,不 可重复读,和虚读被阻止.这种隔离级别是最严格的限制. 这些隔离级别类似于JDBC的定义.明确地,他们映射到java.sql.Connection类中 的静态变量.在Connection类中的隔离级别的行为模型类似于描述的行为.这些 隔离级别的行为精确性依赖于底层的数据库或资源的锁定机制.隔离级工作大部 分取决于你的数据库对他们的支持. 在EJB中,如果容器管理事务,开发者设置事务隔离级别在厂商指定的方式.如果 企业Bean管理自己的事务,EJB开发者设置事务的隔离级别.这一点上,我们仅讨 论容器管理的事务;我们将会在后面讨论Bean管理的事务. 16.3.4.一致性的平衡表现 一般说来,如果隔离级别更严格,执行效率会变得低下,因为事务阻止访问相同的 数据.如果隔离级别非常格,换句话来说,如果所有的事务在串行级,甚至简单的 读,必需等待行执行.结果是系统变得非常慢.EJB系统处理大量的并发事务并且 需要非常快的,因此,避免串行隔离级别那不是必需的. 隔离级别,然而,也强调数据的一致性.更严格的隔离级别帮你确认无效的数据不 应用到执行更新.古语云,"垃圾进,垃圾出",应用.串行化隔离级别确认数据从 来不被并发的事务访问,因此确保数据总是一致的. 选择正确的隔离级别要求你对数据库有一定的研究以及如何处理锁定.你也必需 小心的分析在你的应用程序中的数据块如何被使用.举例来说, 几乎每一个泰坦 巡航系统中的实体将会有很难改变的数据.一个船的名字从来不改变.船上的小 屋的数目很少改变.因为顾客必需把假期安排计划建立在这个信息的基础之上, 所以,巡航的名子和日期从不改变.即使这些类型的数据改变,他们也不会影响系 统的完整性.因此,当一片业务逻辑使用这种类型的数据时,可以使用低隔离性. 另一方面,预定,在很大的方面影响泰坦系统的完整性.如果你仔细看 bookPassage()方法.你可能看到它,指定巡航上的双倍预定小屋.如果两个客户 并发的使用一个预定为相同的小船和巡航,将会发生双倍的预定,因为没有在 bookPassage()中进行检查阻止.让我们修复这个问题: public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Query isReserved = entityManager.createQuery( "select count(res) from Reservation res" + "where res.cabin = :cabin AND res.cruise = :cruise"); isReserved.setParameter("cabin", cabin); isReserved.setParameter("cruise", cruise); int count = (Integer) isReserved.getSingleResult( ); if (count > 0) throw new EJBException("Cabin already reserved"); Reservation reservation = new Reservation( customer, cruise, cabin, price); entityManager.persist(reservation); this.process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); return ticket; } catch(Exception e) { throw new EJBException(e); } } 我们需要做的是增加一个isReserved查询到bookPassage()将会检查,如果一个 Cabin已经被预定为一个指定的巡航.如果查询返回Cabins数目,那么预定不能够 被创建.如果bookPassage( )方法与EntityManager交互同时数据源使用串行化 数据库连接的隔离级别,那么执行isReserved查询将获得必要的锁在数据库中, 来确保查询返回持续事务有效的返回值.因为其它的隔离级别不能够隔离来自其 它事务所做的改变从其中的查询,所以除了串行化隔离级别没有其它的隔离级别 了. 16.3.4.1. 控制隔离级别 不同的EJB肥务器允许不的隔离级别粒度;一些服务器服从数据库中的这种职责. 大多数EJB服务器和EntityManager实现控制隔离级别是通过资源访问API(例 如,JDBC和JMS)和允许不同的资源有不同的隔离级别.然而,在单个事务中,他们 一般要求一个一致性的隔离级别来访问相同的资源.参考你的提供商文档来查找 服务器的控制级别. Bean管理的事务在会话Bean和消息驱动Bean中,仍然,允许你指定事务的隔离级 别使用数据库的API.JDBC API,例如,提供一种机制来指定数据库连接时的隔离 级别.例如: DataSource source = (javax.sql.DataSource) jndiCntxt.lookup("java:comp/env/jdbc/titanDB"); Connection con = source.getConnection( ); con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); 你可以在相同事务中为不同的资源设置不同的隔离级别,但是所有的企业Bean使 用相同资源在一个事务中应该使用相同的隔离级别. 16.3.5.乐观锁 我们早在前面的isReserved查询中使用串行化隔离级别允许bookPassage( )方 法阻止两次相同的预定.这个查询应该放在比较少的客户.这种防止双份定的解 决方法存在一系列的执行缺陷,可是.为了isReserved 方查询工作,没有其它的 预定参考Cabin的客户试着去预定,或者查询是无效的.在大多数的数据库中,排 它锁将会被获取在整个RESERVATION表使它工作.这意味着任何bookPassage()或 ListAvailableCabin()调用可能会发生在同一时间里.这是一个极大的可量测性 问题。当泰坦的业务增长并且增加更多的船,巡航,和客户时,我们的系统将不 能够处理新的负荷,无论我们购买多少机器或CPU的升级.这是因为将有一个更高 的连接在共享资源上(例如,在RESERVATION表上). 所以,我们要怎样才能解决并发问题呢?一种方式是使用乐观锁设计模式.乐观 锁并不是把所有的传统的感觉锁在里面.它在bookPassage( ) 方法下工作,我们 假设没有其它的用户在相同时间里预定相同的Cabin.那么,在时务提交时,我们 让数据库决定是否Cabin被预定.如果被预定,我们抛出一个异常并回滚事务.换 句话来说,我们是乐观的,那么没有其它的用户来预定相同的Cabin.这如何运作? 这如何避免表级锁? 好吧,为了使用乐观锁,我们必需把泰坦预定系统稍做修改 和使用Java持久化的特殊功能. 我们需要做的第一件事是创建一个新的实体类保存关于一个指定巡航上指定小 屋的信息.让我们调用这个新的实体类CruiseCabin。一个CruiseCabin实体类将 会被创建为每次巡航的每个小屋: package com.titan.domain; import javax.persistence.*; @Entity public class CruiseCabin { private int id; private Cabin cabin; private Cruise cruise; private boolean isReserved; private long version; @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } @OneToOne public Cabin getCabin( ){ return cabin; } public void setCabin(Cabin cabin) { this.cabin = cabin; } @OneToOne public Cruise getCruise( ){ return cruise; } public void setCruise(Cruise cruise) { this.cruise = cruise; } public boolean getIsReserved( ){ return isReserved; } public void setIsReserved(boolean is) { isReserved = is; } @Version protected long getVersion( ){ return version; } protected void setVersion(long version) { this.version = version; } } CruiseCabin实体类参考小屋和它属于的巡航.isReserved 属性让我们知道是否 有人预定小屋为巡航.一个新的有趣的属性是version,它使用注释 @javax.persistence.Version.一个@Version属性是一个栏位在CruiseCabin表, 它将保存一个指定的CruiseCabin 记录的版本ID.只要CruiseCabin 实体被更新, 版本栏会增长.当事务开始提交处理和业务逻辑更新CruiseCabin,实体管理器首 先检查是否内存在CruiseCabin实例匹配的version属性,数据库中现在存储的版 本栏.如果版本匹配,那么version属性增长.如果他们不匹配,那么实体管理抛出 一个异常,并且整个事务回滚.让我们改变bookPassage()使用新功能: public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Query getCruiseCabin = entityManager.createQuery( "SELECT cc FROM CruiseCabin cc WHERE" + "cc.cabin = :cabin AND cc.cruise = :cruise"); getCruiseCabin.setParameter("cabin", cabin); getCruiseCabin.setParameter("cruise", cruise); CruiseCabin cc = (CruiseCabin)getCruiseCabin.getSingleResult( ); if (cc.getIsReserved( )) throw new EJBException("Cabin is already reserved for cruise"); cc.setIsReserved(true); Reservation reservation = new Reservation( customer, cruise, cabin, price); entityManager.persist(reservation); this.process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); return ticket; } catch(Exception e) { throw new EJBException(e); } } bookPassage( )方法为相关的CruiseCabin实体类操纵查询.如果它被预定,它终 止事务。如果不是,那么设置isReserved属性和继续同其它的方法.在事务提交 时,实体管理调用一个SQL查询验让和增长实体的version栏.我们假定查询 CruiseCabin有一个ID为1并且当前version是101: update CRUISE_CABIN set isReserved=true, version=version + 1 where id = 1 AND version = 101; 如果这个更新返回零个修改记录,那么实体管理知道CruiseCabin被更新通过其 它的事务,并且一个并发的事务会发生.在这种错误情形下,它抛出 javax.persistence.OptimisticLock-Exception 异常和回滚事务.另外,事务成 功完成,并且查询CruiseCabin被更新为预定和它的version属性增长.这种乐观 锁解创建一个快速的写锁在一我们的数据库中的一行代替串行级别的大范围的 表锁,已经在前面章节中呈现过. 应该注意的是,乐观锁的设计模式并不是一直工作的.如果你数据库中有一行有 更高的数据写争夺,那么使用乐观锁模式会降低效率,因为,它将会创建很多回 滚,会增加更多的系统开支.在那种情况下,串行化解决可能会更好.重新设计你 的数据模式可能会更适合在这种情形,然而,如果在你的数据库中有更高的并发 访问同一个指定行,那么,你的系统可能不依比例决定. 16.3.6. 程序中使用锁定 EntityManager接口有一个特殊的lock()方法为了执行实体锁定.使用它,你传递 一个你想要锁定的实体对象和指示你想要的一个读或写锁: package javax.persistence; public enum LockModeType{ READ, WRITE } public interface EntityManager { void lock(Object entity, LockModeType type); } LockModeType.READ确保没有脏读和无重复读发生在锁定的实 体.LockModeType.WRITE同READ有相同的语义,但是它也强制一个实体的 @Version属性增长.为了实现这些语义,数据库常常执行行级锁(例如:SELECT ... FOR UPDATE) 厂商实现并不要求支持在实体上的锁有@Version属性. 程序中使用锁定变得非常重要当你想要确保不可重复读在实体Bean上,那么可以 在事务中进行读取但不能更新. 16.4.事务外的EJB 事务范围外的Bean常常提供一些无状态的服务,不操作数据存储中的数据.当这 些类型的企业Bean在一个事务中是必需的,他们不需要符合ACID条件.考虑一个 事务外的无状态会话Bean,引用EJB,提供原始经历的引用.这EJB可能响应从一 个购买库存的事务EJB的请求.购买库存的成功或失作为一个事务并不会影响状 态或操作引用的EJB,所以它不需要是事务的一部分.事务中关联的Bean隶属于隔 离ACID属性,意味着服务不能被共享,在事务的生命期间.使企业Bean事务的运行 变得非常昂贵.定义一个事务外的EJB(例如,NotSupported)事务范围不填,那一 个可以改进执行效率和有效的服务. 16.5.外在的事务管理 虽然这部分包含JTA,强列建议你不要试着外在管理事务.通过事务属性,企业 JAVABean提供一个全面的和简单的机制为事务划分界线,在方法级和自动事务传 播.仅对事务系统有明确的理解的开发者才去尝试使用JTA同EJB. EJB提供隐式的事务管理在方法级:我们可以定义事务的界线通过执行方法的范 围.这是EJB的主要优点之一覆盖自然的分布对象实现;它可以减少复杂性,所以, 程序错误.另外,定义事务的划分,在EJB中使用,分离事务的行为从业务逻辑中; 一个改变到事务行为不要求改变业务逻辑.在极少的情况下,必需使用外在的事 务控制. 外在的事务管理通常完成使用OMG的对象事务服务(OTS)或JAVA实现的OTS,Java 事务服务(JTS).OTS和JTS提供API,允许开发者工作直接同事务管理和资源(命 例如:数据库和JMS提供者).OTS 的 JTS 实施是强健的和完全的,它不容易与API 一同工作;它要求干净和有意图的控制注册范围在事务中. 企业JavaBeans支持更简单的API,Java事务API(JTA),为了同事务工作.这个API 的实现在javax.transaction 包中.JTA实际上由两个组件组成;一个高级的客户 端事务接口和一个低级的X/Open XA接口.我们关心高级客户端接口,因为它访问 企业Bean和被推荐为客户端应用程序.代级的XA接口用于被EJB服务器和容器定 位事务同资源,如数据库. 你使用外在的事务管理,或许焦点在简单的接 口:javax.transaction.UserTransaction.UserTransaction允许你管理外在的 事务范围.这里是外在的划分用在一个EJB或客户端的应用程序: TravelAgent tr1 = (TravelAgent)getInitialContext( ).lookup("TravelAgentRemote"); tr1.setCruiseID(cruiseID); tr1.setCabinID(cabin_1); tr1.setCustomer(customer0; TravelAgent tr2 = (TravelAgent)getInitialContext( ).lookup("TravelAgentRemote");; tr2.setCruiseID(cruiseID); tr2.setCabinID(cabin_2); tr2.setCustomer(customer); javax.transaction.UserTransaction tran = ...; // Get the UserTransaction. tran.begin( ); tr1.bookPassage(visaCard,price); tr2.bookPassage(visaCard,price); tran.commit( ); 客户端应用程序需要预定两个小屋为相同的客户.在这种情况下,客户买一个小 屋为自己和他的孩子.除非他能获取到两个,因此,客户端应用程序设计在相同 的事务中包括两个预定,否则客户不想订购任一小屋.完成通过外在的标记事务 的界线通过使用javax.transaction.UserTransaction对象.每个 UserTransaction.commit( )方法包含在相同的事务范围,记录企业Bean方法调 用的事务属性. 显然,这个例子是人为的,但是重点是表示清楚.事务可以被直接控制,代替依赖 于方法范围的界线划分.使用外在事务划分的优点是客户端控制事务的界线.这 个例子的客户端,可能是一个客户端应用程序或另一个企业Bean.在任一情况, 相同的 javax.transaction。UserTransaction 被用,但是获取不同的资源,依 赖于是否它需要在客户端或在企业Bean获取资源. 只有Bean的定义为管理自己的事务(Bean管理事务方式)时可以使用 UserTransaction接口. Java企业版(J2EE)规定一个客户端应用程序怎样获取一个UserTransaction 对 象使用JNDI.这里是一个客户端获取一个UserTransaction 对象,如果EJB容器是 J2EE系统的一部分(J2EE和EJB的详细关系在第18章中介绍). Context jndiCntx = new InitialContext( ); UserTransaction tran = (UserTransaction) jndiCntx.lookup("java:comp/UserTransaction"); utx.begin( ); ... utx.commit( ); 企业Bean也可以管理外在的事务.只有会话Bean和消息驱动Bean定义一个 javax.ejb.TransactionManagementType类型的组件使用 @javax.ejb.TransactionManager 注释可以管理他们自己的事务.企业Bean频繁 的管理他们自己的事务被称作Bean管理的事务(BMT).实体Bean无法是BMT Bean. BMT Bean不能够定义事务属性为其方法.这是,一个会话Bean的定义怎样管理外 在的事务: import javax.ejb.* ; import javax.annotation.* ; import javax.transaction.UserTransaction; @Stateless @TransactionManagement(TransactionManagerType.BEAN) public class HypotheticalBean implements HypotheticalLocal { ... } 为了管理它自己的事务,一个企业Bean需要获取一个UserTransaction对象.一个 企业Bean获取一个引用到UserTransaction从EJBContext或从@Resource注入: import javax.ejb.* ; import javax.annotation.* ; import javax.transaction.UserTransaction; @Stateless @TransactionManagement(TransactionManagerType.BEAN) public class HypotheticalBean implements HypotheticalLocal { @Resource SessionContext ejbContext; public void someMethod( ){ try { UserTransaction ut = ejbContext.getUserTransaction( ); ut.begin( ); // Do some work. ut.commit( ); } catch(IllegalStateException ise) {...} catch(SystemException se) {...} catch(TransactionRolledbackException tre) {...} catch(HeuristicRollbackException hre) {...} catch(HeuristicMixedException hme) {...} 作为一种选择,UserTransaction可以直接注入到Bean中: import javax.ejb.* ; import javax.annotation.* ; import javax.transaction.UserTransaction; @Stateless @TransactionManagement(TransactionManagerType.BEAN) public class HypotheticalBean implements HypotheticalLocal { @Resource UserTransaction ut; ... } 最后,一个企业Bean也可以访问UserTransaction 从JNDI ENC.企业Bean执行查 找java:comp/env/UserTransaction上下文: InitialContext jndiCntx = new InitialContext( ); UserTransaction tran = (UserTransaction) jndiCntx.lookup("java:comp/env/UserTransaction"); 16.5.1. Bean管理事务方式下的事务传递 同无状态会话Bean,事务被管理使用UserTransaction必需开始和完成在相同的 方法中.换句话来讲,UserTransaction事务不能开始在一个方法和结束在另一个 方法.因为无状态会话Bean实例共享跨跃多个客户端,所以这是有道理的;当一个 无状态实例服务于一个客户的第一个请求,一个完全不同的实例可能并发的请求 通过相同的客户端.同有状态会话Bean,然而,一个事务可以开始在一个方法和提 交在另一个方法,因为一个有关状态会话Bean仅服务于一个客户.因此,一个有 状态会话Bean可以关联它自己同一个事务跨跃很多不同的客户端方法调用.作为 一个例子,假定TravelAgent EJB作为BMT Bean.在下面的代码中,事务开始在 setCruiseID( )方法和结束在bookPassage( )方法.这允许TravelAgent EJB的 方法关联在相同的事务.travelAgentBean 的定义像这样: import com.titan.reservation.*; import javax.ejb.EJBException; @Stateful @TransactionManagement(TransactionManagerType.BEAN) public class TravelAgentBean implements TravelAgentRemote { ... public void setCruiseID(Integer cruiseID) { try { ejbContext.getUserTransaction().begin( ); cruise = entityManager.getReference(Cruise.class, cruiseID); } catch(Exception re) { throw new EJBException(re); } } public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { try { if (ejbContext.getUserTransaction().getStatus( )!= javax.transaction.Status.STATUS_ACTIVE) { throw new EJBException("Transaction is not active"); } } catch(javax.transaction.SystemException se) { throw new EJBException(se); } if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation(customer, cruise, cabin, price); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); ejbContext.getUserTransaction().commit( ); return ticket; } catch(Exception e) { throw new EJBException(e); } } ... } 重复调用EJBContext.getUserTransaction( ) 方法,返回一个引用到相同的 UserTransaction对象.容器要求保贸事务和有状态会话Bean实例之间的关联,访 问多个客户端调用,直到事务终止. 在bookPassage( ) 方法中,我们可以检查事务的状态来确保它仍被激活.如果事 务不再被激活,我们抛出一个异常.getStatus() 的使用在这一个章节中被稍后 更详细地覆盖。 当一个客户端已经关联在一个事务调用的Bean管理的事务方法,客户端的事务暂 停直到方法返回.暂停发生不管是否BMT Bean外在的启动它的事务在方法里或事 务被启动在前一个方法调用.客户端事务总是暂停直到BMT方法返回. 事务控制跨跃方法强列不被推荐因为它的结果是错误的管理事务和长期的事务 锁定,或资源的泄漏. 16.5.1.1.消息驱动Bean和Bean管理的事务 消息驱动Bean也有管理它自己的事务的选项.在 MDBs 的情况,事务必需开始和 结束在onMessage()方法内,Bean管理的事务跨跃onMessage( ) 调用是不可能的. 你可以转换在第12章中创建的ReservationProcessor EJB到BMTBean简单的通过 改变javax.ejb.TransactionManagementType值到Bean: @MessageDriven @TransactionManagement(BEAN) public class ReservationProcessorBean implements MessageListener { ... } 在这种情况下,ReservationProcessorBean类可以被修改,使用 javax.transaction.UserTransaction标记事务的开始和结束: @MessageDriven @TransactionManagement(BEAN) public class ReservationProcessorBean implements javax.jms.MessageListener { @PersistenceContext(unitName="titanDB") private EntityManager em; @EJB private ProcessPaymentLocal process; @Resource(name="ConnectionFactory") private ConnectionFactory connectionFactory; @Resource UserTransaction ut; public void onMessage(Message message) { try { ut.begin( ); MapMessage reservationMsg = (MapMessage)message; int customerPk = reservationMsg.getInt("CustomerID"); int cruisePk = reservationMsg.getInt("CruiseID"); int cabinPk = reservationMsg.getInt("CabinID"); double price = reservationMsg.getDouble("Price"); // get the credit card Date expirationDate = new Date(reservationMsg.getLong("CreditCardExpDate")); String cardNumber = reservationMsg.getString("CreditCardNum"); String cardType = reservationMsg.getString("CreditCardType"); CreditCardDO card = new CreditCardDO(cardNumber, expirationDate, cardType); Customer customer = em.getReference(Customer.class, customerPk); Cruise cruise = em.getReference(Cruise.class, cruisePk); Cabin cabin = em.getReference(Cabin.class, cabinPk); Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); em.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); deliverTicket(reservationMsg, ticket); ut.commit( );//跨跃多个方法提交,有状态会话Bean, BMT } catch(Exception e) { throw new EJBException(e); } } ... 重要的是了解,在BMT中,被MDB消耗的信息不是事务的部份。 当一个MDB使用容 器管理事务,消息是事务处理的一部分,所以如果事务被回滚,消费的消息也被回 滚,强制JMS提供者重复提交消息.但是同Bean管理的事务,消息不是事务的一部 分,所以如果BMT被回滚,JMS提供者并不会意识到事务的失败.然而,所有的不会 丢失,因为JMS提供者仍可以依赖消息承认书来确定是否消息被成功递送. EJB容器将会答谢消息如果onMessage()方法返回成功.如果,然而,一个 RuntimeException被onMessage( ) 方法抛出,容器将不承认信息,而且 JMS 提 供者将会怀疑一个问题而且或许尝试再递送信息。如果当处理失败的时候,信息 的重复递送很重要,行动的你最好方针是确定 onMessage() 方法抛出一个 EJBException 以便容器将不承认从 JMS 提供者被收到的信息。 *厂商使用专有的 (能明白的) 机制叙述尝试的数目再递送给 BMT/NotSupported MDBs 的讯息 " 失败" 承认收据。如果他们不能够成功的处 理记录审计,JMS-MDB提供者可能会放置一个像“死消息”的讯息.系统管理能检 测到死的信息区域以便递送信息能用手被发现而且处理。 除了信息之外, UserTransaction.begin() 和 UserTransaction.commit() 之 间的每件事情都是相同事务的一部份。包括创建一个新的Reservation EJB和处 理信用卡使用ProcessPayment EJB.如果一个事务发生失败,那些操作将会被回 滚.事务也包含使用JMS API在deliverTicket( )方中发送票消息.如果一个事务 发生失败,票信息将不被发送. 16.5.2. 启发式的决断 事务通常被一个事务管理器控制(通常是EJB服务器),管理ACID特生跨跃多个企 业Bean,数据库,和服务器.事务管理器使用两阶段提交(2-PC)管理.2-PC是一个 协议为管理事务,提交更新分为两个步骤.2-PC是复杂的,但是基本上,它需要服 务器和数据库的协作,通过一个中撞的事务管理器定制,确保所有的数据一同被 持久化.一些EJB服务器支持2-PC,有一些不支持,而这一个处理机制的值是一些 争辩的来源.重点是记录事务管理器控制事务;把它建立在反对民意测验的资源 (数据库, JMS 提供者和其他的资源)基础之上,它决定是否所有的更新被提交或 回滚.一个启发式决断被放置,当一个资源决定单向的提交或回滚没有被事务管 理器允许.当一个启发式决定已经被作出的时候,事务的原子性丢失,并且数据完 整性错会发生.UserTransaction抛出一个不同的异常关联到启发式决定;这些在 下列的区段被讨论。 16.5.3. UserTransaction接口 EJB服务器必需支持UserTransaction但不是必需支持其它的JTA,他们不是必需 的使用JTS作为事务服务器.UserTransaction的定义如下: public interface javax.transaction.UserTransaction { public abstract void begin( ) throws IllegalStateException, SystemException; public abstract void commit( ) throws IllegalStateException, SystemException, TransactionRolledbackException, HeuristicRollbackException, HeuristicMixedException; public abstract int getStatus( ); public abstract void rollback( ) throws IllegalStateException, SecurityException, SystemException; public abstract void setRollbackOnly( ) throws IllegalStateException, SystemException; public abstract void setTransactionTimeout(int seconds) throws SystemException; } 这里是方法的定义在这个接口中: begin() 调用这个方法创建一个新的事务.线程执行这个方法实现同新的事务关联, 被传递到任何支持已存在事务的EJB.begin( )方法可以抛出两个检查异常之一. 一个IllegalStateException被抛出当begin()被调用通过一个已关联一个事务 的线程.你必需完成任何事务的关联同线程在开始一个新的事务之前.一个 SystemException 被抛出如果事务管理器(例如:EJB服务器)遇到一个非预期的 错误条件. commit() 该方法完成与当前线程关联的事务.当commit( )被执行,当前的线程不 再关联一个事务.这个方法能抛出几个检查异常.一个IllegalStateException 被抛出如果当前线程没有关联一个事务.一个SystemException被抛出如果事务 管理器(EJB服务器)遇到一个非预期的错误条件.一个TRansactionRolled- backException被抛出当整个事务回滚胜于提交时.这可能发生,如果一个资源不 能够执行一个更新或如果UserTransaction.rollBackOnly( )方法被调用.一个 HeuristicRollbackException指出资淅使用启发式决定来回滚和提交事务;那 是,一些资淅决定回滚和其它决定提交. rollback( ) 该方法调用回滚事务和取消更新.这个方法可以抛出三种不同的检查 异常中的一种.一个SecurityException被抛出,如果线程使用UserTransaction 对象不允许回滚事务.一个IllegalStateException被抛出如果当前线程没有同 一个事务关联.一个SystemException被抛出如果事务管理器(EJB服务器)遇到一 个非预期的错误条件. setRollbackOnly( ) setRollbackOnly( )方法被调用标记事务回滚.这意味着,不管执行 更新的事务是否成功,事务必需被回滚当完成时.这个方法可以被任何BMT EJB 调用参与到事务中,或者被或户端应用程序.这个方法可以抛出两中检查异常之 一.一个IllegalStateException被抛出如果当前线程没有同一个事务关联;一个 SystemException被抛出如果事务管理器(EJB服务器)遇到一个非预期的错误条 件. setTransactionTimeout(int seconds) 该方法设置事务的生命周期.例如,在超时之前多久.事务必需被完成在 事务超时前.如果这个方法没有被调用,事务管理器(EJB服务器)自动设置超时. 如果方调用值为0秒,默认的事务管理超时将会被使用.这个方法调用必需在 begin()方法之后.一个SystemException被抛出如果事务管理器(EJB服务器)遇 到一个非预期的错误条件. getStatus() getStatus( )方法返回一个与javax.transaction.Status接口中定义的常量对 应的整数,一个有经验的程序员可以使用这个方法来确定与事务关联的 UserTransaction 对象的状态.一个SystemException被抛出如果事务管理器 (EJB服务器)遇到一个非预期的错误条件. 16.5.4. Status接口 Status是一个简单的接口,只包含常量没有方法.它的唯一的目的是提供一个常 量集合描述事务对象的一种状态,UserTransaction: interface javax.transaction.Status { public final static int STATUS_ACTIVE; public final static int STATUS_COMMITTED; public final static int STATUS_COMMITTING; public final static int STATUS_MARKED_ROLLBACK; public final static int STATUS_NO_TRANSACTION; public final static int STATUS_PREPARED; public final static int STATUS_PREPARING; public final static int STATUS_ROLLEDBACK; public final static int STATUS_ROLLING_BACK; public final static int STATUS_UNKNOWN; } getStatus( )的返回值告诉客户端使用UserTransaction的事务状态.这里是常 量的意义: STATUS_ACTIVE 一个活动的事务同UserTransaction对象关联.这个状态返回在一个事务已经被 启动和前一个事务管理器开始一个两阶段的提交(2-PC). (事务被暂停直到被激活.) STATUS_COMMITTED 一个事务同UserTransaction 对象关联;事务被提交.是有可能的是,启发决断已 经被作出;另外,事务被破坏,并且,STATUS_NO_TRANSACTION常量被代替返回. STATUS_COMMITTING 一个事务同UserTransaction 对象关联;事务正在处理提交.UserTransaction对 象返这种状态,如果事务管理器决定提交,但是没有完成处理. STATUS_MARKED_ROLLBACK 一个事务同UserTransaction 对象关联;事务被标记灵回滚,也许 UserTransaction.setRollbackOnly( )操作的结果,被应用程序的某部分调用. STATUS_NO_TRANSACTION 没有事务关联当前的 UserTransaction对象.这发生在事务被完成之后或者如果 没有事务被创建. 这个值的返回代替抛出IllegalStateException 异常. STATUS_PREPARED 一个事务同UserTransaction 对象关联;事务已经被准备,这意味着两阶段提交 处理的第一阶段已经完成. STATUS_PREPARING 一个事务同UserTransaction 对象关联;事务正在处理准备,意味着,事务管理在 二阶段提交中的第一阶段中执行. STATUS_ROLLEDBACK 一个事务同UserTransaction 对象关联;事务的结果已经被确认为回滚.是有可 能的是,启发决断已经被作出; 以别的方式,处理被破坏,而且 STATUS_NO_TRANSACTION 常量被返回. STATUS_ROLLING_BACK 一个事务同UserTransaction 对象关联;当前状态不能被确定.这是一个短暂的 条件,而且并发的调用最终会返回一个不同的状态. STATUS_UNKNOWN 一个事务同UserTransaction 对象关联 16.5.5. EJBContext回滚方法 只有BMT Bean可以访问UserTransaction 从EJBContext和JNDI ENC.容器管理的 事务(CMT)不能使用UserTransaction.CMT Bean使用setRollbackOnly( )和 geTRollbackOnly( )方法交互同当前事务互动.在这一章的后面,我们将看到事 务回滚后的异常.setRollbackOnly( )方法给一个EJB否决一个事务的能力,当事 务完成后,EJB发现会引起不一致数据的数据条件被提交时,这可能被使用.一旦 EJB调用setRollbackOnly( )方法,当前事务被标记为回滚并且不能被其它的事 务参与者提交,包括容器. 如果当前事务被标记为回滚geTRollbackOnly( )方法返回True.这个信息用于避 免运行工作不能被提交.例如,如果一个异常被抛出并且捕获在一个EJB的方法 内.getrollbackOnly( )可以用于确定是否异常引起当前事务被回滚.如果是,继 续处理是无意义的.如果不是,EJB有机会更正问题并且重试失败任务.只有EJB开 发专家可以尝试重试任同在一个事务.作为选择,如果异常不能够引起回滚(例 如,geTRollbackOnly( ) 返回false),一个回滚可以被强制使用 setRollbackOnly( )方法. BTM Bean不能使用EJBContext的setRollbackOnly( ) 和 getrollbackOnly( ) 方法.BMT Bean不能使用getStatus( )和rollback( )方法在 UserTransaction 对象上来检查回滚和强制回滚,分别的. 16.6. 异常和事务 异常对事务的处理结果有重大的影响. 16.6.1. 应用程序异常和系统异常的比较 系统异常代表未知的内部错误.EJB容器抛出系统异常当它遇到一个内部应用程 序服务器失败.业务逻辑可以抛出系统异常,当它想终止业务处理.应用程序异常 是业务逻辑的部分异常.他们指示一个强列的类型定义一个指定的业务问题或失 败,但是,不是必需终止或回滚业务处理. 16.6.1.1. 系统异常 系统异常包括java.lang.RuntimeException和它的子集.EJBException是 RuntimeException的子集,因此,它被看作是系统异常.系统异常也包括 java.rmi.RemoteException和它的子集.RuntimeException 和RemoteException 的子集不同在他们变成应用程序异常使用@javax.ejb.ApplicationException注 释.这个注释的讨论在本章的事面. 系统异常总是会引起一个事务的回滚当他们被抛出从EJB的方法中.任何 RuntimeException没有@ApplicationException注释在 bookPassage( )方法(例 如,EJBException, NullPointerException, IndexOutOfBoundsException ,等 等)中被抛出.容器会自动处理和结果会使事务回滚.在 Java中,RuntimeException 类型不需要定义方法的throws子句或处理使用 try/catch 块;他们会在方法中自动的被抛出. 容器自动处理系统异常和它将总会如下所做: ●回滚事务 ●记录异常警告系统管理员. ●放弃EJB实例 当一个系统异常被任何方法调用抛出时(@PostConstruct, @PostActivate等等), 它将以异常的方式被对待,从其它的业务方法中被抛出. 虽然EJB的系统异常必需被记录,它不指定他们怎样记录或格式化日志文件.记录 异常和报告他们给系统管理员的的准确机制留给厂商. 当一个系统异常发生,EJB实例被放弃,意味着它被解除参照和被垃圾收集.容器 呈现出EJB可能坏的变量或其它不稳定和因此而不安全的使用. 废弃的EJB实例依赖于EJB的类型.在无状态会话Bean的情况下,客户端并不通知 实列已经被废弃.那些实例类型不是专为某一个客户服务的;他们在实例池中进 行交换进入和出,所以每个实例可以服务于一个新的请求.同有状态会话Bean,然 而,对客户的影响是巨大的.有状态会话Bean服务于单一客户并且维持会话状态. 丢弃一个有状态会话Bean的实例毁坏实例的会话状态并且无效客户的引用到EJB. 当有状态会话Bean的关例被丢弃,并发的调用EJB的方法通过客户端结果会是一 **个NoSuchEJBException,它是RuntimeException异常的子集.虽然实例与 RuntimeException一同被丢弃,但是在远程引用的影响依赖于厂商. 同消息驱动Bean,一个系统异常被onMessage( )方法抛出或一个回调方法 (@PostConstruct 或 @PreDestroy)将会引起Bean的实例被丢弃.如果MDB是BMT Bean,消息处理可能被或不被传递,依赖于EJB容器的应答传递.在容器管理事务 的情况下,容器将会回滚事务,所以消息不被应答和重复传递. 在会话Bean中,当一个系统异常发生并且实例被丢弃,总是抛出一个 RuntimeException 异常不管是远程或是一个本地的调用.如果客户端开始事务, 传递到EJB,一个系统异常(抛出被企业Bean的方法)将会被容器捕获,并且重新 作为一个javax.ejb.EJBTransactionRolledbackException异常抛 出.EJBTransactionRolledbackException是RuntimeException的子类并且给出 了更多的指示到客户端,发生回滚.如果客户端不传递一个事务到EJB,系统异常 将会被作为EJBException.异常被捕获和重新抛出. 一个EJBException会被抛出当一个非业务子系统抛出一个异常,就像JDBC抛出 SQLException异常或JMS抛出JMSException.在一些情况下,然而,Bean的开发者 尝试着去处理异常并且重试一个操作代替抛出一个EJBException异常.只有当异 常被子系统和他们的反射在事务很好的被理解的时候,才应该这样做.同样的规 则,重新抛出无业务的子系统异常如EJBExceptions和允许EJB容器回滚事务和自 动丢弃Bean的实例. 16.6.1.2.应用程序异常 一个应用程序异常通常抛出一个响应到业务错误的异常,与系统异常相反.应用 程序异常总是直接传递到客户端不用重新包装为一个EJBException类型.默认情 况下,它们不会引起事务的回滚.在这种情况下,客户端有机会获得在一个应用程 序异常被抛出之后.例如,bookPassage( )方法抛出一个应用程序异常叫做 IncompleteConversationalState;这是一个应用程序异常,因为它不继承 RuntimeException 或 RemoteException异常.IncompleteConversationalState 异常被抛出如果有一个传递到bookPassage( ) 方法中的参数为null.(应用程序 错误频繁的发生用于报告验证错误在习惯上)出于这种情况,在任务动之前和明 确子系统失败的结果抛出异常.(例如:JDBC, JMS, Java RMI, 和 JNDI). 因为它是一个应用程序异常,一个IncompleteConversationalState异常不能够 导致事务的回滚,在默认的情况下.异常的抛出在任何工作之前,避免 bookPassage( )方法的不必要的处理和提供给客户端(企业Bean或应用程序调用 bookPassage( )方法)一个机会来重新获得与重试方法调用使用有效的参 数.@javax.ejb.ApplicationException注释用于强制应用程序异常自动的回滚 事务: package javax.ejb; @Target(TYPE) @Retention(RUNTIME) public @interface ApplicationException { boolean rollback( ) default false; } 例如,在第11章中的ProcessPayment EJB使用PaymentException异常.是一个引 起自动回滚的应用程序异常的好的例子: @ApplicationException(rollback=true) public class PaymentException extends java.lang.Exception { public PaymentException( ){ super( ); } public PaymentException(String msg) { super(msg); } } 我们想要事务被自动的回滚,但是业务逻辑能够捕获PaymentExceptions 并且重 试自动事务(假如另一个信用卡在文件上,例如). @ApplicationException注释也可以用在java.lang.RuntimeException 和 java.rmi.RemoteException异常的子类.这非常的有用因为你不想抛出一个 RuntimeException到包装在EJBException中,或你不想要指定的 RemoteException的子类回滚异常. 应用程序异常也可以定义在XML,在 元素内: java.sql.SQLException true 元素是的子元素.XML提供给 你增加定义第三方异常的能力如同应用程序异常.在这个例子当中,我们使 java.sql.SQLException一个应用程序异常引起一个回滚.我们可以让 ProcessPayment EJB 直接抛出SQLExceptions代替包装它们到EJBException. 表16-1总结了在不同类型的异常和事务之间的会话和实体. 事务范围 事务类型属性 抛出异常 容器的动作 客户视图 客户端初始事 务从客户端(应 用程序或EJB) 开始并且传到 企业Bean的方 法 事务-类型= 容器 事务-属性= Required | Mandatory |Supports 应用程序异常 如果EJB调用 setRollbackOn ly( ) 或应用 程序异常被使 用 @ApplicationE xception(roll back=true)注 释,标记客户端 事务回滚,重新 抛出应用程序 异常. 接受应用程序 异常.客户端的 事务或许或不 被标记为回滚. 系统异常 标记客户端的 事务回滚.记录 错误.丢弃实例. 重新抛出 javax.ejb.EJB TransactionRo lledbackExcep tion. 用户端接收 JTA javax.ejb.EJB TransactionRo lledbackExcep tion 。 用户端 的事务被回滚. 容器管理事务 事务开始,当 EJB的方法被调 用和结束当方 法完成的时候 事务-类型= 容器 事务-属性= Required | RequiresNew 应用程序异常 如果EJB调用 setRollbackOn ly( )或者应用 程序异常使用 @ApplicationE xception(roll back=true)注 释,回滚事务和 重新抛出应用 程序异常。如果 EJB不明确的回 滚事务,尝试提 交事务和重新 抛出应用程序 接收应用程序 异常.EJB的事 务也许或也许 不被回滚。客户 端的事和不被 影响. 异常。 系统异常 回滚事务.记录 错误.丢弃实例. 重新抛出 RemoteExcepti on 或 EJBException. 远程客户端接 收 RemoteExcepti on 或 EJBException 异常.EJB的事 务被回滚.客户 端的事务或许 被标记为回滚, 依赖于厂商. Bean不是事务 的一部分.EJB 被调用但是不 被传递到客户 端的事务和不 开始它自己的 事务. 事务-类型= 容器 事务-属性= Never | NotSupported | Supports | 应用程序异常 重新抛出应用 程序异常. 接收应用程序 异常.客户端的 事务不受影响. 系统异常 记录错误.丢弃 实例.重新抛出 RemoteExcepti on 或 EJBException 异常. 远程客户接收 RemoteExcepti on 或 EJBException 异常.客户端的 事务或许或不 标记回滚,依赖 于提供商. Bean管理的事 务. 有状态或无状 态会话EJB使用 EJBContext明 确的管理它自 己的事务 事务-类型 =Bean 事务属性= Bean管理事务 EJB不使用事务 属性. 应用程序异常 重新抛出应用 程序异常. 接收应用程序 异常.客户端事 务不受影响. 系统异常 回滚事务.记录 错误。丢弃实例. 重新抛出 RemoteExcepti on 或 EJBException 异常. 远程客户接收 RemoteExcepti on 或 EJBException 异常. 客户端的事务 不受影响. Table 16-2.消息驱动Bean的异常总结 16.7.处理有状态会话Bean 会话可以直接同数据库交互,像其它EJB管理任务一样容易.ProcessPayment EJB, 例如,当byCredit( )方法被调用时,插入到PAYMENT表,并且当 listAvailableCabins( ) 方法被调用时,TravelAgent EJB直接到数据库中去查 询.无状态会话Bean像ProcessPayment EJB 不能保存状态,所以,每次的方法调 用必需立即改变到数据库.同有状态会话Bean,然而,我们不想改变到数据库直到 事务完成.记住,一个有状态会话Bean可能是一个事务的众多参与者之一,所以它 可能会建议延迟数据库的更新直到整个事务被提交或如果事务回滚避免更新. 有很多不同的情况在一个有状态会话Bean可能被缓冲改变在应用到数据库之前. 事务范围 事务类型属性 抛出异常 容器的动作 容器初始化事务。 事务开始在 onMessage( )方 法被调用之前,并 且结束在方法完 成. 事务-类型=容 器 事务-属性= Required 系统异常 回滚事务.记录日 志.丢弃实例 应用程序异常 如果实例调用 setRollbackOnly ()或使用 @ApplicationExc eption(rollback =true)注释的异 常,回滚事务并且 重新抛出资源适 配器. 容器初始化事务。 没有事务开始 事务-类型=容 器 事务-属性= NotSupported 系统异常 记录错误.丢弃实 例 应用程序异常 重抛异常到资源 适配 Bean管理的事务. 消息驱动Bean使 用EJBContext显 式的管理它自己 的事务 事务-类型= Bean 事务-属性= Bean管理事务EJB 不使用事务属性 系统异常 回滚事务.记录错 误,丢弃实例. 应用程序异常 重新抛出异常到 资源适配器. 举例来说,试想一个商店的购物车由有状态会话Bean来实现,聚集了很多购买的 东西.如果有状态会话Bean实现SessionSynchronization.它可以被缓存项目并 且写入到数据库仅当事务完成. javax.ejb.SessionSynchronization接口允许一个会话Bean接收事务的额外通 知.额外的事务回调方法通过SessionSynchronization接口扩展EJB的生命周期 包括一个新状态,Transactional Method-Ready.这是第三个状态,虽然没有在第 11章中进行讨论,是处理有状态会话Bean的生命周期的一部分.简单的实现 SessionSynchronization 接口,使它可视的到EJB.图 16-11图示有状态会话 Bean同其另外的状态. SessionSynchronization接口的定义如下: package javax.ejb; public interface javax.ejb.SessionSynchronization { public abstract void afterBegin( ) throws RemoteException; public abstract void beforeCompletion( ) throws RemoteException; public abstract void afterCompletion(boolean committed) throws RemoteException; } 当SessionSynchronization 的一个方法在事务范围外被调用时,方法执行在方 法-就绪状态如在第11章中讨论的.然而,当一个方法被调用在一个事务范围(或 创建一个新事务),EJB移至Transactional Method-Ready状态. 16.7.1.Transactional Method-Ready 状态 SessionSynchronization方法被调用在Transactional Method-Ready状态. 16.7.1.1. 转换到Transactional Method-Ready状态 当一个处理事务方法在SessionSynchronization Bean上被调用,有状态会话 Bean变成事务的一部分,因为afterBegin( ) 方法定义在 SessionSynchronization 接口上被调用.这个方法可能会仔细的读取任何数据 从数据库并且排序在组件的实例字段.afterBegin( )方法被调用在EJB对象代表 业务-方法被调用EJB的实例之前. 16.7.1.2. Transactional Method-Ready状态的生命周期 当afterBegin( )回调方法完成,业务方法首先被客户的EJB实例执行调用.任何 并发的业务方法调用在相同的事务将会直接代表EJB的实例. 一旦有状态会话Bean是一个事务的一部分,不管它实现SessionSynchronization 或没实现都不能被其它事务的上下文所访问.这是真实的,不管是否客户端试着 去访问EJB使用不同的上下文或EJB的拥有者方法创建一个新的上下文。例如,使 用RequiresNew事务属性的一个方法被调用,新的事务上下文会引起一个错误被 抛出.因为NotSupported 和 Never 属性指定一个不同的事务上下文(无上下文), 调用一个附带这些属性的方法也会引起错误.一个有状态会话Bean不能被移除当 它在一个事务中被关联.这就意味着调用一个@Remove 注释过的方法,当 SessionSynchronization Bean在一个事务中时,将会引起错误被抛出. 在一些点上,SessionSynchronization bean中的事务被标记为结束.如果事务被 提交,SessionSynchronization Bean将会被通知通过beforeCompletion( )方法. 在这时,EJB将会写缓存到数据库.如果事务被回滚,beforeCompletion( )方法不 被调用,避免写入改变无意义,不提交到数据库.afterCompletion( )方法总会被 调用,不管事务成功的结束提交或是不成功的回滚.如果事务成功,那意味着 beforeCompletion( )被调用,afterCompletion( )方法的committed 参数为 true.如果事务没有成功,那么committed参数为false. 有可能重设有状态会话Bean的实例变量到一些初始状态,如果, afterCompletion( )方法的事务被回滚. 16.8. 会话的持久上下文 参与到事务中的实体管理就像其它资源一样.我们在这本书中可以看到例子.扩 展的持久化上下文有很多有趣的事务行为你可以使用.允许你调用 EntityManager的操作,像persist( ), merge( ), 和remove( )在一个事务外当 你与一个扩展的持久化上下文交互时.那些插入,更新,和删除排队直到扩展持 久化上下文支持一个活动的事务和提交.换句话来讲,数据库不被涉及直到持久 化上下文变成关联一个事务时.同时,任何被运行的查询不被保存到数据库中连 接在他们完成之后.让我们看这样的一个例子. 1 EntityManager manager = entityManagerFactory.createEntityManager(EXTENDED); 2 manager.persist(newCabin); 3 manager.merge(someCustomer); 4 manager.remove(someReservation); 5 6 userTransaction.begin( ); 7 manager.flush( ); 8 userTransaction.commit( ); 第一行创建一个扩展的持久化上下文.第2-4行创建,更新,和删除一些实体.这些 动作排队直到持久化上下文变成支持一个事务在第6行.调用一个EntityManager 方法的行为在一个事务的持久化上下文内.批量动作的提交在第7行. 你可以真正的开发这种行为通过使用有状态会话Bean.前面,我们显示的创建两 个reservations 通过TRavelAgentBean的例子: TravelAgent tr1 = (TravelAgent)getInitialContext( ).lookup("TravelAgentRemote"); tr1.setCruiseID(cruiseID); tr1.setCabinID(cabin_1); tr1.setCustomer(customer0); TravelAgent tr2 = (TravelAgent)getInitialContext( ).lookup("TravelAgentRemote");; tr2.setCruiseID(cruiseID); tr2.setCabinID(cabin_2); tr2.setCustomer(customer); javax.transaction.UserTransaction tran = ...; // Get the UserTransaction. tran.begin( ); tr1.bookPassage(visaCard,price); tr2.bookPassage(visaCard,price); tran.commit( ); 这个想法是我们有一个客户想要创建多个预定为他的家人.这些多个预定将会参 加到一个事务中,由客户端初始化.这实际上是一个非常差的系统设计,因为你 有一个事务跨路多个远程调用.远程客户,通常,不可靠的实体,尤其是被人操作 的客户端.数据库更新锁保持远程 bookPassage( ) 调用和数据库的连接是打开 的.如果是人工的旅行代理决定去吃午餐在事务执行期间,这些资源和连接将会 被保持直到事务超时.为了解决这个问题,我们可以使用查询非事务行为的实体 管理操作.让我们修改TRavelAgentBean 使用乐观锁的例子: import javax.ejb.*; import javax.persistence.*; import static javax.persistence.PersistenceContextType.*; import static javax.ejb.TransactionAttributeType.*; @Stateful @TransactionAttribute(NOT_SUPPORTED) public class TravelAgentBean implements TravelAgentRemote { @PersistenceContext(unitName="titan", EXTENDED) private EntityManager entityManager; @EJB ProcessPaymentLocal processPayment; private Customer customer; private Cruise cruise; private Cabin cabin; public Customer findOrCreateCustomer(String first, String last) { ... } public void setCabinID(int id) { ... } public void setCruiseID(int id) { ... } public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Query getCruiseCabin = entityManager.createQuery( "SELECT cc FROM CruiseCabin cc WHERE" + "cc.cabin = :cabin AND cc.cruise = :cruise"); getCruiseCabin.setParameter("cabin", cabin); getCruiseCabin.setParameter("cruise", cruise); CruiseCabin cc = (CruiseCabin)getCruiseCabin.getSingleResult( ); if (cc.getIsReserved( )) throw new EJBException ("Cabin is already reserved"); cc.setIsReserved(true); Reservation reservation = new Reservation( customer, cruise, cabin, price); entityManager.persist(reservation); this.process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); return ticket; } catch(Exception e) { throw new EJBException(e); } } > @TransactionAttribute (REQUIRED) > @Remove public void checkout( ){ entityManager.flush( );// really not necessary } } 我们对前面的例子稍做修改.我们首先要做的是使每个非事务的业务方法通过 @TRansactionAttribute(NOT_SUPPORTED)注释在组件类上.下一步是,一个扩展 的持久化上下文被注入到entityManager字段,所以我们可以同一个外在的事务 交互.@Remove注释被移动从bookPassage()到一个新的checkout()方法,所以多 个bookPassage()方法调用可以在相同的TravelAgent会话.因为bookPassage( ) 方法现在是非事务的,任何预定可以被这个方法创建,现在查询和无数据库资源 被保存在它们的调用之间.预定被提交到数据库,当扩展的持久化上下文支持一 个事务在checkout()方法内. checkout( ) 方法的EntityManager.flush( )操作不是必需的;扩展的持久化上 下文自动被支持当方法开始时.它是一个好的习惯,无论如何都要调用flush()方 法,因为,它提醒开发者读取实际发生的代码. 最后的事情是修改ProcessPayment EJB.因为,ProcessPaymentBean使用原一的 JDBC记录付款,这些付款没有向实体管理操作排队.为了促进这个,我们需要写另 外一个实体组件代表一个付款和改变ProcessPayment EJB 使用一个实体管理, 并非JDBC: @Entity public class Payment implements java.io.Serializable { private int id; private Customer customer; private double amount; private String type; private String checkBarCode; private int checkNumber; private String creditCard; private Date creditCardExpiration; @Id @GeneratedValue public int getId( ){ return id; } public void setId(int id) { this.id = id; } @ManyToOne public Customer getCustomer( ){ return customer; } public void setCustomer(Customer cust) { this.customer = cust; } public double getAmount( ){ return amount; } public void setAmount(double amount) { this.amount = amount; } public String getType( ){ return type; } public void setType(String type) { this.type = type; } public String getCheckBarCode( ){ return checkBarCode; } public void setCheckBarCode(String checkBarCode) { this.checkBarCode = checkBarCode; } public int getCheckNumber( ){ return checkNumber; } public void setCheckNumber(int checkNumber) { this.checkNumber = checkNumber; } public String getCreditCard( ){ return creditCard; } public void setCreditCard(String creditCard) { this.creditCard = creditCard; } public Date getCreditCardExpiration( ){ return creditCardExpiration; } public void setCreditCardExpiration(Date creditCardExpiration) { this.creditCardExpiration = creditCardExpiration; } } 这个实体代表在前面ProcessPayment EJB例子中使用的PAYMENT表.下一步,让我 们修改ProcessPaymentBean 使用这个新实体: package com.titan.processpayment; import com.titan.domain.*; import javax.ejb.*; import javax.annotation.Resource; import javax.persistence.*; import static javax.ejb.TransactionAttributeType.*; import static javax.persistence.PersistenceContextType.*; @Stateful @TransactionAttribute(SUPPORTS) public class ProcessPaymentBean implements ProcessPaymentLocal { final public static String CASH = "CASH"; final public static String CREDIT = "CREDIT"; final public static String CHECK = "CHECK"; @PersistenceContext(unitName="titan", type=EXTENDED) private EntityManager entityManager; @Resource(name="min") int minCheckNumber = 100; public boolean byCash(Customer customer, double amount) throws PaymentException { return process(customer, amount, CASH, null, -1, null, null); } public boolean byCheck(Customer customer, CheckDO check, double amount) throws PaymentException { if (check.checkNumber > minCheckNumber) { return process(customer, amount, CHECK, check.checkBarCode, check.checkNumber, null, null); } else { throw new PaymentException("Check number is too low. Must be at least "+minCheckNumber); } } public boolean byCredit(Customer customer, CreditCardDO card, double amount) throws PaymentException { if (card.expiration.before(new java.util.Date( ))) { throw new PaymentException("Expiration date has passed"); } else { return process(customer, amount, CREDIT, null, -1, card.number, new java.sql.Date(card.expiration.getTime( ))); } } private boolean process(Customer cust, double amount, String type, String checkBarCode, int checkNumber, String creditNumber, java.sql.Date creditExpDate) throws PaymentException { Payment payment = new Payment( ); payment.setCustomer(cust); payment.setAmount(amount); payment.setType(type); payment.setCheckBarCode(checkBarCode); payment.setCheckNumber(checkNumber); payment.setCreditCard(creditNumber); payment.setCreditCardExpiration(creditExpDate); entityManager.persist(payment); return true; } } ProcessPaymentBean 类使用@transactionAttribute (SUPPORTS)注释,因为它 可能或不执行在一个事务中.很容易改变process( ) 方法并且移除很多冗余的 JDBC语法从原始的版本.我们必需有一些作做的事情,不合实际的事情在 ProcessPayment EJB中强制这个例子工作.JAVA持久化规范不允许你传递一个扩 展的持久化上下文到一个无状态会话Bean,当没有事务时.因此,ProcessPayment 变成一个有状态会话Bean,有一个扩展的持久化上下文注入到其中.因为 ProcessPayment是一个EJB被嵌入到travelAgentBean,他们共享相同的扩展持久 化上下文(见第11章中更多的细节).我们可以使用这一点绕开传递问题. 提供不理想的变化来使ProcessPayment 例子工作,我不建议使用扩展持久化上 下文的查询特性如果你需要交互同其它的EJB.它是不幸的 EJB 3.0 专家的组无 法修复明显的可用性问题,暴露在这一段中.因为专家组的一两个顽固成员无法 决定解决这一问题,两个厂商决定修复这些问题在它们的实现中. 同travelAgentBean 和 ProcessPaymentBean 改变的完成,我们现在焦点放在重 新实现的客户端: TravelAgent tr1 = (TravelAgent)getInitialContext( ).lookup("TravelAgentRemote"); tr1.setCruiseID(cruiseID); tr1.setCabinID(cabin_1); tr1.setCustomer(customer0); tr1.bookPassage(visaCard,price); tr1.setCruiseID(cruiseID); tr1.setCabinID(cabin_2); tr1.setCustomer(customer); tr1.bookPassage(visaCard,price); tr1.checkout( ); 比较客户端的代码从原始的例子中,这个代码已经变得非常单一化了.如你所看 到的,我们管理很少的travelAgentBean中的因数和简单的客户端逻辑.我们解决 了我们原始的使用多个bookPassage( )调用的情况.同时,我们管理使用数据库 资源更有效并且仍代表一个工作单元. 联合有状态会话Bean,扩展持久化上下文,和指定事务划分给你很多优化和控制 会话状态会动力。没有这些完整的特性,你可能会更辛苦的管理这些状态改变, 在一个有状态会话Bean本身.现在你可以让实体管理做更多的工作为你. 第 17 章. 安全 大多数的J2EE应用程序需要提供身分给用户,谁访问它们并且安全的访问.应用 程序可以阻止非法用户进入系统.他们也可能想要限制使用他们系统的个体的行 动。 J2EE和EJB规范提供了应用程序开发者可以完全整合定义和编程的安全服务 的核心组件.这些包括: 鉴定 鉴定是确认处理用户的身份,谁在试图访问一个安全系统.当鉴定的时候,应用 程序服务器验证用户的实际存在和提供正确的信任程度,如密码. 授权 一旦一个用户在系统中被鉴定,它将会与应用程序互动.授权包括确定是否允许 一个用户执行一个可靠的动作.授权可以管辖一个用户访问子系统,数据,和业 务对象,或它可以监视更多的一般行为.确定用户,例如,可能会允许更新信息, 虽然其它仅被允许查看数据.因为Web应用程序,也许仅确定用户通行让去访问确 的URL.因为是EJB应用程序,用户可以被在每个方法基础之上进行授权. 机密和完整性保护 当一个用户交互同应用程序跨跃一个网络通过浏览器或通过远程EJB调用,如果 连接没有被保护,有一些恶意用户的拦截网络包并且解释数据.数据传输应该被 保户并且攻击者不能够读或修改数据在传输中.数据传输可以被保护通过密码服 务,如SSL.加密是厂商指定的并且不在本章中覆盖. 通过一个小的编程API交互同J2EE安全服务,用户很少定任何代码保掮 他们的应 用程序,因为,设置安全通常是静态的定义处理.只有会话Bean可以被EJB保 护.Java持久化尚未提供一个机制提来安全存取,但是它可以依赖于关系型数据 库系统使用分配的权限在数据库级.本章的重点在于怎样设置鉴定和授权为你的 会话Bean. 17.1. 鉴定和身份 在一个安全的EJB应用程序中,监定包括验证一个用户她说她是谁.当一个远程客 户登录EJB系统时,会话期间它被关联一个安全身份.一旦一个远程客户端应用程 序被关联一个安全身份,它准备使用Bean完成一些任务.当客户调用一个方法在 Bean上时,EJB服务器隐式的传递客户端的身份同方法调用.当EJB对象接收方法 调用,它会检查身份确认客户是否用效,并且是否允许调用方法. 不幸的是(或者是幸运的,依赖于你的想法),EJB规范没有指定样发生鉴定.虽 然它定义安全信息如何从一个客户端传递到服务器(通过CORBA/IIOP)被繁殖.但 是它并没有指定客户端应该如何获取并且关联身份和信任度同一个EJB调用.它 也没有定义应用服务器存储和重新获得鉴定信息.提供商必需决定怎样打包和提 供这些服务在客户端和服务器. 当调用一个远程的EJB,很多应用程序服务器完成鉴定通过使用JNDI API.例如, 一个客户端使用JNDI可以提供鉴定信息,使用JNDI API访问服务器或资源在服务 器上.这个信息会被频繁的传递当客户端尝试初始化一个JNDI连接在服务器上. 下面的代码显示客户端的密码和用户名可以被增加到连接属性为获取一个JNDI 连接到EJB服务器: properties.put(Context.SECURITY_PRINCIPAL, userName); properties.put(Context.SECURITY_CREDENTIALS, userPassword); InitialContext ctx = new InitialContext(properties); Object ref = jndiContext.lookup("TravelAgent"); TravelAgentRemote remote = (TravelAgentRemote) PortableRemoteObject.narrow(ref, TravelAgentRemote.class); 在这个例子中,用户被鉴定在连接的JNDI InititalContext.用户名和密码关联 同客户线程并且传递到服务器内,当调用完成在远程EJB中. 虽然JNDI是一个通常的方法为大多数的应用程序服务器执行鉴定,有时用户需要 更抽像的获取安全信息.举例来说,怎样使用指纹代替密码?许多应用程序服务器 除了 JNDI 之外,还提供一种机制来鉴定.举例来说, JBoss 应用程序服务器使 用 JAAS 规格,这提供一个丰富的 API 执行鉴定。 17.2. 授权 一旦一个用户被鉴别通过厂商的机制,他必需检查是否被允许调用一个指定的 EJB方法.授权执被执行在J2EE和EJB通过关联一个或多个规则同给定的用户,并 且,在其角色的基础上之上分配方法给用户.当列子中的用户可能是"Scott"或 "Gavin",规则用于标识一组用户,例如,"administrator," "manager," 或 "employee." 在EJB中,你的分配控制在每个方法基础上.你不能够分配这些许可 在每个用户基础之上,但是,宁可在每个角色基础这上. 角色用于描述授权考虑逻辑规则因为他们并不直接影响用户,组,或任何其它的 安全身份在一个指定的操作环境.EJB的安全规则被映射到真实世界用户组和用 户当组件被部署时.这种映射允许组件是便携的;每次组件被部署到一个新系统 中,规则可以被映射到用户和组指定操作环境. 不同于鉴定,授权在EJB规范中明确定义.你可以从定义角色访问程序代码开始. 分配许可为你的类中每个方法.可以通过Java批注或ejb-jar.xml部署描述符来 做.为了要举例说明, 让我们巩固对 ProcessPayment EJB 的存取在第 11 章中 定义了. 17.2.1. 分配方法许可 当决定的时候,泰坦巡航一定非常小心的给予访问ProcessPayment EJB的存取. 这个Bean允许用户管理钱代替用户的信用卡,所以它在泰坦系统中是最有趣的确 定客户安体给他们信用卡数.只有经过认可的用户旅行代理程序才会允许处理泰 坦巡航付款.此外,唯一经过府可的代理程序会自动核对,对于检查银行帐户的 期骗,系统会自动作出检查付款.然而,任何有效的用户被允许使用现金付款. 为了分配方法许可到一个EJB的方法,使用 @javax.annotation.security.RolesAllowed 注释: package javax.annotation.security; @Target({TYPE, METHOD}) @Retention(RUNTIME) public @interface RolesAllowed { String[] value( ); } 这个注释定义一个或更多的业务角色到允许访问的方法。当放置在组件类上, @RolesAllowed注释指定默认的规则设置允许访问Bean的方法.每个EJB方法可以 被覆盖这个行为通过使用相同的注释. @javax.annotation.security.PermitAll注释指定任何鉴别用户被许可调用方 法.同@RolesAllowed,你可以将这个注释放置在组件类上,定义默认的为整个组 件类,或你可以使用在它的每个方法上.@PermitAll也是默认值,如果没有默认或 外在的安全无数据提供给方法.这意味着,如果你不使用任何安全注释在你的组 件类上,每个用护被同意为无限制访问. 让我们应用这些注释到ProcessPaymentBean使用许我是面讨论过的许可: package com.titan.processpayment; import com.titan.domain.*; import javax.ejb.*; import javax.annotation.Resource; import javax.annotation.security.*; @Stateless @RolesAllowed("AUTHORIZED_TRAVEL_AGENT") public class ProcessPaymentBean implements ProcessPaymentRemote, ProcessPaymentLocal { ... @PermitAll public boolean byCash(Customer customer, double amount) throws PaymentException { ... } @RolesAllowed("CHECK_FRAUD_ENABLED") public boolean byCheck(Customer customer, CheckDO check, double amount) throws PaymentException { ... } public boolean byCredit(Customer customer, CreditCardDO card, double amount) throws PaymentException { ... } private boolean process(int customerID, double amount, String type,String checkBarCode, int checkNumber, String creditNumber, java.sql.Date creditExpDate) throws PaymentException { ... } } AUTHORIZED_MERCHANT 角色标识泰坦用户谁被鉴定使用付款在系统中.组件类使 用@RolesAllowed注释指定ProcessPaymentBean中的所有方法,默认情况下,可以 执行的仅有AUTHORIZED_MERCHANT用户.byCredit( ) 方法继承默认的设置.泰坦 系统将会接受任何人的现金支付,所以byCash( )方法被注释为@PermitAll来允 许支付从任何用户.byCheck( )方法,我们有另外的要求,仅商人有 CHECK_FRAUD_ENABLED 被允许和理检查付款.对于这一个方法,另外的 @RolesAllowed注释被用来覆盖在组件上的默认注释.当客户端调用在一个EJB方 法,但是没有适当的允可,EJB容器会抛出一个javax.ejb.EJBAccessException. 让我们应用安全元数据使用XML来代替: This role represents an authorized merchant AUTHORIZED_MERCHANT This role represents a merchant that has check fraud enabled CHECK_FRAUD_ENABLED AUTHORIZED_MERCHANT ProcessPaymentBean byCredit CHECK_FRAUD_ENABLED ProcessPaymentBean byCheck ProcessPaymentBean byCheck 方法许可定义在 元素内.每个规则用于映射方法认让的 必需首先标识在元素内.这元素有一个选项元素 描述规则的使用.元素定义规则将被用于部署.它不易理解的是为什 么元素是必需的在规范中,因为,引用规则必需被确定通过查找 所有的元素.它看起来像完整句法的糖. 方法许可自身被定义在多个元素内.每个元素定义一个或多个元素定义规则允许访问的一个指 定.元素等价于@PermitAll批注.元素定义你想要 保护的方法.元素允行使用*通配符.如果一个定义可以被应用到一个或多个组件的方法,然后管接被采取。 17.2.2. 在XML中识别特定的方法 元素在元素内用于标识一个指定的方法组在指定 的Bean中.元素总是包含一个元素,指定Bean名和元素指定方法.也可以能包含一个元素,元 素指定那个一个方法参数将会被用于关联重载方法,并且一个 元 素指定是否方法属于无程或本地接口.最后的元素决定可能有相同的方法名用于 多个接口. 17.2.2.1.通配符的定义 元素的方法名可以是简单的(*)通配符.一个通配符可以应用到组件的 所有方法,本地和远程接口.例如: ProcessPaymentBean * 虽然,组合通配符同其它字符很诱人,不要这样做.例如get*,举例来说,是违法的. 星号字符只能单独使用. 17.2.2.2.命名方法定义 命名方法定义应用到所有组件的远程或本地接口方法上指定名字.例如:, ProcessPaymentBean byCheck 这个定义应用到给定名字的所有方法在任意业务接口.它不同于方法重载.例如, 如果,ProcessPayment EJB 的本地接口被修改,所有它有三种重载的byCheck() 方法,前面的定义将应用到所有方法,作为演示这里: @Local public interface ProcessPaymentLocal { boolean byCheck(Customer cust, CheckDO check, double amount); boolean byCheck(double[] amounts); boolean byCheck( ); } 17.2.2.3. 特定的方法声明 特定方法定义使用 元素,指定一个方法被列出它的参数,允许 不同的重载方法.元素包含零个或多个元素的 关联,井然有序地, 每个参数类型(包括多维数组)定义在方法内.想要指定一个 无参的,使用元素在其中不嵌入元素. 例如,让我们再看一下ProcessPayment EJB,我们增加一些byCheck( )的重载方 法.这里有三个 元素,每个明确的指定方法被列出它的参数: ProcessPaymentBean byCheck com.titan.domain.Customer com.titan.processpayment.CheckDO double ProcessPaymentBean byCheck ProcessPaymentBean byCheck double[] 17.2.2.4. Remote/home/local的不同 剩下一个问题。 同名的方法可能被用于远程接口和本地接口.解决这个不明确性, 增加方法到方法的定义作为一个修改者. 元素允许 五个值:Remote, Home, LocalHome, Local, 和ServiceEndpoint. 所有的这些方法风格可以被定义任意组合和任何元素使用元素. 元素是从一个规则-方法认证合并的组合.例如,下面的代 码,元素定义仅有管理员可以访问ProcessPayment EJB的 远程方法: administrator ProcessPaymentBean * Remote 17.2.3. 排除方法 EJB安全元数据有一个很少使用的特性,允许你禁止访问一个或多个方法.排除方 法使用@javax.annotation.security.DenyAll批注标识,或同元 素.禁止访问一个指定的方法同一个注释不是一个非常好的建议.为什么要写一 个组件类方法,增加它到业务接口,并且注释它,它不能够被使用?在XML中使用这 一特性,可是,它没有什么实用性.如果,ProcessPayment EJB是第三方厂商库的 一部分?元素可能被用于关闭API部署的变量特性.让我们看一个 例子: ProcessPaymentBean byCash 这个例子不允许所有访问到ProcessPaymentBean.byCash( )方法.元素可以有一个或多个方法签名. 17.3. RunAs 安全身份 除了指定访问一个企业Bean的方法外,部署者也可以指定runAS规则为全部企业 Bean.当@RolesAllowed注释和元素指定访问Bean方法的那 一个规则,runAS指定在其规则下的方法将会被运行.换句话来讲,runAS规则用来 作为企业Bean的标识当它试着调用在其它Bean上,这标识不是必需同当前访问的 Bean相同.@javax.annotation.security.RunAs注释用来指定专用的规则.虽然 他们不允许使用方法许可,消息驱动Bean可以使用@RunAs特性: package javax.annotation.security; public @interface RunAs { String value( );} @RunAs可以用于简单化我们的规则为映射到ProcessPayment EJB.我们可以标记 travelAgentBean到运行作为一个AUTHORIZED_MERCHANT,所以我们不需要映射任 何TRavelAgentBean的规则到ProcessPayment EJB的规则.这很重要如果一个第 三方厂商买ProcessPayment EJB从泰坦巡航.让我们看一下TRavelAgentBean怎 样使用@RunAs批注: package com.titan.travelagent; import javax.ejb.*; import javax.annotation.security.*; @Stateful @RunAs("AUTHORIZED_MERCHANT") public class TravelAgentBean implements TravelAgentRemote { ... } 这也可以在ejb-jar.xml中表达: TravelAgentBean AUTHORIZED_MERCHANT 元素是会话Bean或消息驱动Bean的子元素为了定义元素.元素定义你想要分配到EJB的角色在调用者成功的鉴定和授权 之后. 指定一个企业Bean将被执行在调用者的身分而非一个传递run-as的身 份,规则包含单个空元素,. 下面的定义指定,EmployeeService EJB总是执行在调用者的身份: EmployeeService 使用 应用到会话Bean.消息驱动Bean仅有一个runAs身份 ;他们从来不以调用者的身份执行,因为,没有“调用者”.一个消息驱动Bean的 消息,不被处理考虑调用,并且客户端发送他们不关联消息.因为没有调用者身份 被繁殖,消息驱动Bean必需总是指定一个runAS安全身份,如果他们同其它安全的 会话Bean交互. 17.4.计划性安全 在这一章中的大部分的安全特性都单独的聚集在安全元数据的定义上,或在一个 应用程序运行前静态的定义元数据甚至是运行.EJB也有一个小的计划性API为了 收集关于安全会话的信息.尤其是,javax.ejb.EJBContext 接口,有一个方法为 了确定具体用户的调用在EJB上.它也有一个方法允许你检查是否当前用户属于 一个确定的角色: package javax.ejb; public interface EJBContext { javax.security.Principal getCallerPrincipal( ); boolean isCallerInRole (String roleName); } getCallerPrincipal( ) 方法返回一个标准的j2SEjavax.security.Principal 安全接口.一个Principal对象代表在EJB上正在调用的独立用户.我们可以扩展 ProcessPayment EJB 来存储旅行代理程序记录用户的付款.如果没有任何事务 问题,这个附加允许我们统计付款. package com.titan.processpayment; import javax.security.Principal; import javax.ejb.*; import javax.annotation.*; @Stateless public class ProcessPaymentBean implements ProcessPaymentLocal { @Resource SessionContext ctx; ... private boolean process(int customerID, double amount, String type,String checkBarCode, int checkNumber,String creditNumber, java.sql.Date creditExpDate) throws PaymentException { Principal caller = ctx.getCallerPrincipal( ); String travelAgent = caller.getName( ); // add travelAgent to payment record ... }} EJBContext.isCallerInRole( )方法允许你确定是否当前调用的用户属于一个 确定的角色.例如,我们想要禁止大的付款事务为一个年轻的旅行代理. 如果你想要使用isCallerInRole,你必需提供一点语法上的糖果.因为EJB规则是 逻辑的,应用程序服务器需要意识到所有的规则与EJB交互的,所以它能映射他们 到真实世界共同的环境.EJB容器可以简单的确定方法认证规则通过内置的注释 和XML元数据定义为EJB.一个艰苦的工作确定是否计划性的安全引用一个确定规 则,没有定义在元数据中.要使EJB的容器工作简单,你必需定义所有的计划性规 则使用@javax.annotation.security.DeclareRoles annotation注释: package javax.annotation.security; @Target(TYPE) @Retention(RUNTIME) public @interface DeclareRoles { String[] value( ); } 让我们在ProcessBean EJB中使用isCallerInRole()方法: package com.titan.processpayment; import javax.security.Principal; import javax.ejb.*; import javax.annotation.*; import javax.annotation.security.*; @Stateless @DeclareRoles ("JUNIOR_TRAVEL_AGENT") public class ProcessPaymentBean implements ProcessPaymentLocal { @Resource SessionContext ctx; @Resource double maximumJuniorTrade = 10000.0; ... private boolean process(int customerID, double amount, String type, String checkBarCode, int checkNumber,String creditNumber, java.sql.Date creditExpDate) throws PaymentException { if (amount > maximumJuniorTrade && ctx.isCallerInRole("JUNIOR_TRAVEL_AGENT")) throw new PaymentException("Travel agent is not authorized to make such a large purchase. Manager approval is required."); ... } } 在这个例子中,我们扩展了私有方法process()来检查是否付款大于确定的数量 并且是否系统用户是一个年青的旅行代理.如果他是,那么他需要一个管理员赞 成出售并且一个PaymentException被抛出.因为我们使用 EJBContext.isCallerInRole( ) 方法实现这个行为,我们必需使用 @DeclareRoles来注释组件类,指定我们引用的JUNIOR_TRAVEL_AGENT 角色.如果 你不使用@DeclareRoles 注释,那么你必需使用元素在会 话Bean的定义: ProcessPaymentBean JUNIOR_TRAVEL_AGENT 元素定义在 元素内.它 有一个子元素,,那个名子是被引用的角色.

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 5 金币 [ 分享文档获得金币 ] 2 人已下载

下载文档

相关文档