J2EE持久层持久化上下文的传播以及会话(Conversation)实现

目前持久层框架都有一个持久化上下文的概念,下面以比较流行的hibernate以及JPA来做一总结。
如果我们采用OO的方式开发系统,那么势必为了减低耦合,增加内聚,我们会通过细粒度的类来实现业务功能,那么这样就产生了一个问题,如何将持久化上下文在不同的类(这里面其实就是Dao类或者DDD里面的repository)中传播,比如传统的开发方式中,一个service里通过不同的Dao来访问数据库,那么怎么保证不同的Dao类中用的session以及以及与session对应的持久化上下文是同一个呢?这就涉及到了持久化上下文如何传播的问题。下面以我熟悉的hibernate以及JPA来来说明一下,在hibernate中,根据采用的底层事务的不同,需要采用不同的策略来实现:

1 Hibernate中持久化上下文的传播

1.1 采用jdbc事务
此时hibernate通过Threadlocal将当前的session与当前的线程绑定在一起,这样以来只要是同一个线程中的调用,那么获得的session都是同一个,具体来说就是配置hibernate.current_session_context_class属性为thread,这样以来hibernate内部就会通过CurrentSessionContext接口的实现类ThreadlocalSessionContext通过threadlocal来将session和当前线程绑定在一起。当调用SessionFactory的getCurrentSession()方法,返回的就是与当前线程绑定的session,从而解决了持久层上下文传播的问题。

1.2 采用JTA事务
此时hibernate内部是将当前的session以及对应的持久化上下文绑定到了全局的JTA事务上,这样以来我们通过sessionFactory的getCurrentSession()方法获得的就是与当前的JTA事务绑定的session.具体一点就是配置属性hibernate.current_session_context_class为jta,这样以来hibernate内部就是通过CurrentSessionContext接口的实现类JTASessionContext来将session与当前的JTA全局事务绑定在一起,因此当我们通过sf.getCurrentSessionContext()来获取session时,获得的就是与当前的JTA绑定到一起的session.

但是在此种情况下,需要特别注意一个问题:不能同时使用hibernate的Transaction接口与getCurrentSession(),因为当前的session是绑定到全局JTA事务中的,如果通过session.beginTransaction()来开始事务,这说明以前没有事务,既然没有事务存在,我们的session又是怎么绑定到全局JTA事务上的呢?所以一定要注意:当使用JTA事务,并且用了getCurrentSession()的方法时,一定不要用hiernate的native transaction 接口。但是我们如果我们用openSession(),那么就可以通过通过hibernate的native Transaction接口来控制JTA事务。

最后还需要弄清楚一点,Hibernate中还有一种绑定持久化上下文的方法,那就是通过设置hibernate.current_session_context_class属性为managed,hiberante内部就是通过CurrentSessionContext接口的实现类ManagedSessionContext来绑定的。在此种情况下主要是为了实现会话,会话在hibernate中的实现介绍。

2 JPA中持久化上下文的传播
JPA中持久化上下文的传播根据采用不同的事务模型而不同,下面分别来说明:

2.1 采用resource-local事务模型
如果采用resource-local事务模型,此种情况也就是在非J2EE应用服务器的支持下使用。那么我们的持久化上下文的生命周期是与当前的EntityManager绑定到一起的,所以我们可以在不同的类中传播相同的EntityManager实例来达到传播事务上下文的传播。

2.2 采用JTA事务模型
在此种模型下面,我们有EJB容器的支持,持久化上下文的传播是借助于事务上下文来传播的,在说明如何传播前首先要明确EJB组件中两种不同的事务上下文的生命周期:

对于stateless session bean,持久化上下文的生命周期是与当前的系统事务一致的,这也就是无状态会话bean的事务型的持久化上下文,每当事务结束,持久化上下文也就结束了,所有持久化对象也就变为了脱管的(detached).

对于statefull session bean,持久化上下文的生命周期是与当前的有状态会话bean一致的,只有当有状态的会话bean从系统中移除的时候,持久化上下文才关闭,这也就是有状态会话bean的扩展的事务上下文。

搞清楚了这两种不同的事务上下文的生命周期以后,我们来说一下持久化上下文如何传播的问题。持久化上下文是通过当前系统事务来传播的,当一个EJB组件调用另一个EJB组件的时候,如果两个EJB组件的事务范围是一样的,那么持久化上下文就会传播,下面分几种情况来说明:

无状态会话bean之间调用:此时如果两个无状态会话bean在同一个事务中调用,那么持久化上下文就是同一个,通过当前事务来传播。
有状态会话bean调用无状态会话bean:此种情况下如果两者在同一个事务中,那么有状态会话bean的扩展的事务上下文会传播到无状态会话bean里,其实还是通过事务来传播。但是如果被调用的无状态会话bean不支持事务的话(事务属性设置为not support 或者never),那么此时持久化上下文不能传播(JPA规范规定扩展的持久化上下文是不能传播到无事务的stateless session bean)。
有状态会话bean之间调用:此时无论两个有状态会话bean是否支持事务,那么扩展的持久化上下文都会传播,此时就不是通过系统事务来传播的,而是通过statefull session bean的实例来传播(但是此时一定要注意有状态会话bean必须是通过容器注入的或者显示通过JNDI查找)。
无状态的会话bean调用有状态会话bean:一个有事务范围的持久化上下文的stateless session bean调用一个statefull session bean会引发一个错误,因为当前的事务上下文不能传播)

综上所述,EJB之间的持久化上下文传播是通过我们的系统事务来传播的,如果EJB不支持事务(事务属性设置为not support或者never)),那么持久化上下文就不会传播,但是对于扩展的持久化上下文,是通过statefull session bean来传播的,即使没有事务也可以传播。
前面说的是关于持久化如何在持久层的不同的类之间传播的问题,其实无外乎就是通过当前的线程和当前的系统事务来传播,不过对与statefull session bean的扩展的持久化上下文,传播是通过实例来传播的,在下面的实现会话的讨论中,我还会说到这个问题。当我们掌握了原理的时候,遇到一些问题的时候,我们就会很快找到解决方案。

[该贴被admin于2008-12-18 09:47修改过]

此贴接着说一下,如何通过持久层框架来实现会话:

在讨论此问题之前,首先要明确一个问题,什么是会话?会话简单的可以理解为跨多个request生命周期,就拿那个投修改简历的问题来说,第一个事务读取简历,用户修改,然后再通过另外一个事务去更新简历,那么读取和更新是在不同的事务中完成的,那么我们怎么实现这种用例呢?如果采用ORM框架,实现起来会轻松很多,下面就以hibernate和JPA来做一总结。

1. Hibernate实现会话。
在hibernate中实现会话可以采用两种策略。
第一种就是采用detached object,在此种情况下,我们只需要用hibernate提供的reattach和merge操作将脱管对象重附或者是合并到当前的持久化上下文,hibernate会为我们检测更新并最后决定同步到数据库。

第二种就是通过ManagedSessionContext来扩展持久化上下文,使其跨越几个request生命周期。在使用ManagedSessionContext来实现扩展持久化上下文时,一定要记得设置session的flushmode为FlushMode.MANUAL.这样在每次事务提交的时候就不会同步持久化上下文。(不幸的是JPA规范中没有此选项,我们只能通过非事务型的操作来实现,但是如果采用EJB模型,那么我们就可以采用statefull session bean默认的扩展的持久化上下文)。

下面我主要说一下如何在JPA中来实现会话:

2. JPA中实现会话。
在JPA中实现会话,我主要结合EJB的编程模型来做一总结,因为EJB编程模型为我们做了很多简化,免得我们自己动手实现,但是如果要在非J2EE托管环境下使用JPA,就要自己实现。

第一种策略:采用脱管对象来实现,在这之前首先要明确一个问题,那就是JPA只提供了对脱管对象的merge(合并)操作,没有提供reattach(重附操作),所以我们采用脱管对象实现会话时,在会话的最后一个request生命周期中,我们通过合并来将脱管对象合并到持久化上下文,当然detached object对象,我们一般是保存到httpsession里面,但是这样一个不好的地方就是将业务逻辑泄露到了表现层,不利于层的内聚性,所以我们可以采取EJB编程模型给我们带来的便利方式来实现,通过扩展的持久化上下文来实现,下面就来说说如何通过扩展的持久化上下文来实现会话。

第二种策略:采用statefull session bean扩展的持久化上下文来实现。因为在EJB3.0中,有状态会话bean默认采取扩展的持久化上下文(extended persistence context),持久化上下文的生命周期与statefull session bean的生命周期是一致的,所以我们可以通过此扩展的持久化上下文来实现会话。为了更加清楚,我采用以下代码来描述:
//注:以下bean默认的事务属性是REQUIRED
@Statefull
@Remote(BussinessInterface.class)
public class ConversationBean implements BusinessInterface{

@PersistenceContext(type = PersistenceContextType.EXTENDED)
EntityManager em ;

public OperationResult firstOperation(...){
//此方法实现会话的第一个请求
}

//省略会话的中间阶段的请求处理方法,我只用一个来描述
public OperationResult middleOperation(..){
//此方法可以实现会话的一些中间操作,比如查询等
}

..........

//会话的最后一个请求处理方法
public OperationResult lastOperation(...){


}


}
但是此种情况下实现会话需要注意一个问题,因为EJB3.0规范没有像hibernate那样的人工刷新持久化上下文的选项,所以此时要想会话的中间阶段不刷新持久化上下文,为了让会话的中间阶段不刷新持久化上下文(同步持久化上下文到数据库),那么或者采用hibernate扩展或者采用非事务型的EJB组件方法调用(因为JPA规范只有两种持久化上下文的刷新模式,一种是AUTO,它是默认的刷新选项,第二种是commit,第一种要求在执行查询和事务提交的时候都刷新持久化上下文)。下面分两种情况来描述:

采用Hibernate扩展:此种情况下,我们需要增加以下注释:
........
@PersistenceContext(type=PersistenceContextType.EXTENDE,
propertites = @PersistenceProperty(name="org.hibernate.flushMode", value="MANUAL")

EntityManager em;

......

PS:以上的注释也可以采用等价的XML部署描述来替换。

通过以上的注释,当我们的客户端调用会话中间操作时,当前与ConversationBean绑定的扩展的持久化上下文就不会刷新(因为默认会在middleOperation()方法调用的时候启动事务,结束后提交事务,而如果按照JPA标准的话,flushMode无论是AUTO,还是COMMIT,都会刷新持久化上下文,并且如果我们在中间操作进行查询操作,也会触发刷新持久化上下文的操作),当然因为当前的持久化上下文是人工刷新的,那么我们就需要在会话的最后一个操作中,本例中,也就是lastOperation中手动刷新数据库,代码如下:

public OperationResult lastOperation(...){

em.flush();
//注意此时不需要自己手动提交事务,因为默认在方法调用后会自动提交事务。
}

此时我们还可以将一个会话通过面向对象的设计来进行分解,比如我们可能还需要其它的EJB来实现会话,那么我们可以将其通过@EJB注释让容器帮我们注入,这样以来不同的EJB之间的传播会通过当前的系统事务来传播持久化上下文(具体请参考上篇持久化上下文传播)

采用目前JPA规范建议方法:非事务型的EJB方法调用。我们可以修改ConversationBean的代码如下:
@Statefull
@Remote(BussinessInterface.class)
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORT)
public class ConversationBean implements BusinessInterface{

@PersistenceContext(type = PersistenceContextType.EXTENDED)
EntityManager em ;

public OperationResult firstOperation(...){
//此方法实现会话的第一个请求
}

//省略会话的中间阶段的请求方法,我只用一个来描述
public OperationResult middleOperation(..){
//此方法可以实现会话的一些中间操作,比如查询等
}

..........

//会话的最后一个请求
@Remove
@TransactionAttribute(TransactionAttributeType,REQUIRED)
public OperationResult lastOperation(...){

em.flush();
//注意此时此时不需要收到提交事务,因为默认在方法调用后会自动提交事务。

}
}

PS:因为此时整个bean不支持事务,所以会话的最后一个方法要设置为REQUIRED,以便会话的最后一个阶段能同步持久层上下文

以上代码改变的地方就是我们将ConversationBean的事务属性改为了not support(默认情况下是REQUIRED),这样以来我们的ConverstationBean的调用就不会在事务中进行,所有的会话操作都会放在一个队列中,当我们的扩展的持久化上下文重新与事务关联的时候,当事务提交时同步到数据库。但是有个问题,如果ConversationBean还会调用其它的EJB bean怎么办?因为当前的bean不支持事务,那么持久化上下文是不能通过事务来传播的,比如ConversationBean会与另外一个bean进行协作,我们定义如下:
@Stateless
@Remote(AnotherBussinessInterface.class)
public class AssistBean implements AnotherBusinessInterface{

.........
}

此时我们需要增加如下代码到ConversationBean中:
.......
//通过@EJB注释,EJB容器会将实例池里的一个实例注入到当前的 ConversationBean里。
@EJB
AnotherBusinessInterface assistBean;
........

这样以来,我们遇到了持久化上下文传播的问题,因为此时ConversationBean不支持事务,所以扩展的持久化上下文是传播不到没有无状态会话bean里的(这个是JPA规范规定的,不能在没有事务的情况下,传播扩展的持久化上下文到stateless session bean),那么既然持久化上下文不能得到传播,那么我们同一个会话中就会有不同的持久化上下文,这样就不能保证会话的完整性。那么我们有没有办法来采取采取措施补救呢?幸亏还有哈哈,那就是我们可以将AssistBean改为有状态的会话bean,这样以来,因为扩展的持久化上下文此时通过bean实例来传播的,所以不同的有状态会话bean里会采用同一个持久化上下文。所以我们要想通过有状态会话bean的扩展的持久化上下文来实现会话,会话设计到的所有的bean必须都要是statefull,造成这样不方便的原因也就在与当前JPA标准不支持FlushMode.MANUAL,如果JPA也支持了此选项,那么我们通过扩展的持久化上下文实现会话将变得更加容易,希望以后JPA标准能加入FlushMode.MANUAL选项。

综上所述,实现会话我们可以采取两种措施,第一种就是通过detached object,这样的不好地方就是业务状态都保存到了表现层,不利于分层架构,不利于层的内聚性,所以为了将业务状态保存到业务层,那么我们可以采取扩展的持久化上下文,虽然hibernate支持flushModel.MANUAL的选项,但是要自己动手实现扩展的持久化上下文(非J2EE托管环境下),而EJB提供了扩展的持久化上下文,但是又没有提供FlushMode.MANUAL的选项,所以如果各有个的好处,遗憾的是不能两全其美。但是当采用EJB的扩展的持久化上下文,这样不用自己动手实现,唯一不好的地方就是要么通过hiberante的扩展来关闭事务提交自动刷新持久化上下文,要么通过非事务型的EJB方法调用来关闭事务提交自动刷新持久化上下文,所以如果我们EJB容器采用了Hiberante做为JPA标准的实现,那么我们最好借助与Hiberante的扩展,以及statefull session bean的扩展的持久化上下文来实现会话。
[该贴被xmuzyu于2008-12-18 10:20修改过]

请banq老师帮忙改一下主题贴的题目,我回复了就不能改了。
“J2EE持久层持久化上下文的传播以及会话(Conversation)实现”

写得很好,总结了持久层框架的事务应用模式。

可能有的人觉得奇怪,怎么涉及事务,就变得好像很复杂?其根本原因还在于多线程和锁,JTA事务是借助线程Monitor和锁等机制完成。

JTA事务和JDBC事务区别在于,JDBC事务是和数据库连接有关,一个连接打开关闭期间就是JDBC事务,而通过我们很多业务需要跨数据表操作,不可能将一个数据库连接长期开启,因为数据库连接是被线程Hold住的,所以,长期开启带来两个问题:
1. 资源消耗,数据库连接数量本身有限,维持一个长连接消耗资源相当惊人,在高并发下是危险的。
2. 线程适合做短瞬间的任务,你让一个线程长期举着300公斤的重资源,会出问题的,因为线程是由CPU轮流执行,可能造成这个线程暂停甚至死亡。出现莫名其妙的任务中断。

所以,JTA事务是针对业务的一个长事务,也是我们经常使用的一种事务,JTA事务有扁平和嵌套两种,扁平就是一个事务内调用一个事务,大家都是共享同一个事务,当然这里面取决于被调用者的事务设置,如果设置为required.NEW之类那就重启另外一个事务,不和调用者混在一起了,这些都取决于你业务要求。

所谓上下文,就是指事务的对接,就是调用者的事务上文怎么和被调用者的事务下文衔接在一起,这比如写作文或接龙诗歌,别人出个上联,你对下联,上下文意思要衔接上,所以,编程也很象文学诗歌哦,多读点文学修养,增加你很多对抽象事物的形象认识。

有的人如果觉得持久层框架的上下文很复杂,那么就采取Spring/EJB这类业务层框架,他们可以简化甚至屏蔽这些复杂的上下文背景知识,但是深入使用还是了解楼主上面谈的这些知识。

当然,如果你使用Jdon框架+Hibernate架构,更要了解这些知识,因为事务是没有象Spring/EJB那样整入Jdon框架中,这是有我自己的考虑:丑媳妇总要见公婆,当你的项目简单时,可能就不需要事务,所以,何必在你不知晓它原理和应用模式的情况下,硬是塞给你,然后在你运行时,报个事务错误,吓一跳,这是初学者接触Spring常碰到的事情。如果你确实在做关键业务,需要事务了,那么你肯定要了解楼主上面的知识,你只有深刻理解了,才能使用,因为事务本质就是多线程,用不好就死锁,性能慢,当你深刻掌握了,你就能够直接控制事务和上下文,何必我框架又给你添加一道关口,再收费,虽然你知道了,但是你必须通过Spring/EJB才能接触事务,得,你又必须学习Spring/EJB的事务机制,这些在Jdon框架都没有,这才是真正“优雅简单”没有自我的快速开发框架(本来是JTA事务组件的事情,不是你的事情,不要打着简化旗帜将事务揽到自己身上搞复杂化,个人观点)。

btw: 有的人一看我谈到Jdon框架就反感,其实你多学一个框架增长知识开拓视野也是好的啊,哪怕是坏的经验。现在中国很流行“山寨”一词,山寨手机除了没有牌子,哪个比名牌差,待机长,娱乐功能强,这些都是垄断名牌做不出来的,话说回来了,Jdon框架也不是山寨框架,Jdon框架出来时,类似Seam以及DDD框架和语言Ruby/Grails等都没出来,不但不是复制,而且是创新.

[该贴被banq于2008-12-18 11:48修改过]

好文章,收获不少。

我正在开始Java EE的学习,刚开始看第一本教材ejb-3_0-fr-spec-ejbcore.pdf。想到了一个问题:为什么一个bean需要remote 和 local 2个不同的接口?他面向本地和远程的客户需要提供不同的业务过程么,还是local接口提供面向容器的管理调用?

>>他面向本地和远程的客户需要提供不同的业务过程么,还是local接口提供面向容器的管理调用?
首先要清楚,EJB是个分布式组件,既然是分布式组件,那么我们如何实现分布式呢?EJB采用了RMI-IIOP协议进行,因为RMI是远程调用(也就是客户端和EJB组件不在同一个进程,或者同一个JVM中),所以就要通过Remote接口(其实这些是EJB2.X的知识,2.X里要求实现远程接口和本地接口)而提供本地接口就是为了方便本地调用,减低开销,因为远程调用要通过JAVA的序列化机制来实现,有网络开销,这样客户端可以和EJB组件部署在同一个JVM中,此时就可以通过本地接口调用,并且被部署在通过一个容器中的EJB组件之间可以通过local接口进行调用,减低了远程调用开销。

>>话说回来了,Jdon框架也不是山寨框架,Jdon框架出来时,类似Seam以及DDD框架和语言Ruby/Grails等都没出来,不但不是复制,而且是创新.

我是比较佩服banq老师,jdon中的缓存,以及model管理,自动分页,CRUD配置实现,以及closeSessionView都是比较有创新性的,并且banq老师在JDON倡导的思想,是站在了一个高度来看软件设计的,是走在软件设计前沿的,支持JDON.

在一个bean向容器托管后,就不再需要直接找到实例本身而都是通过容器提供的包装后的接口进行调用了,就算在同一个容器或者JVM中也是这样的过程,是这样么。

是的,当我们的bean部署到容器后,容器会帮我们生成支持类,这样其实底层还是很多类在协作。

比如远程调用,大致过程是这样的:

client->RMI-stub(存根)-->RMI-skelton(骨架)->ejb object->ejb bean
所以容器会帮我们生成业务bean的远程会话stub,服务器端的骨架(注意这些stub,skelton是基于具体的协议的,目前就是RMI-IIOP协议,为了方便与其他异构系统的集成)以及ejb object(ejb object是EJB2.X的部分,但是它还存在EJB3中,虽然不用我们写EjbObjcet和EjbLocalObject,但是真是EJBObject的存在,才使得关注点分离啊,比如ejb bean只负责业务逻辑,EJBObject会在调用bean的方法时,开启事务或者进行安全验证等,都是通过EJBOject来实现的),所以要想理解EJB3.0,最好复习EJB2.X的基础,方便理解。


BTW:我再说一点关于EJB组件的看法,这样有利于理解。首先EJB为什么称之为组件呢?因为EJB体现了关注点分离的原则,在我们系统开发中,可以分为功能(业务)部分和技术部分(事务,安全等),我们的组件(比如各种EJB bean)完成了功能部分(这是每个系统不同的地方),而每个系统共同的地方怎么实现呢?好,来咱把它抽象,抽象成什么呢?答案就是容器,现在又来一个问题,既然把功能和技术部分分开了,那么如何结合起来呢?比如如何让bean具有事务性和安全性呢?好,这需要一个粘合剂,这个粘合剂是什么呢?在EJB中就是注释和XML部署描述符。所以EJB体现了一种编程时分离,部署时粘合,运行时真正统一的思想,这种思想是很先进的。其他组件CORBA,COM+组件,也都是这个原理。

说了这些希望对你有点帮助,学习EJB,最好还是学习EJB2.X的原理部分。

其实我觉得很多东西看你怎么理解他。比如你现在写程序,调用一个函数,然后返回一个值(或者对象)。那么大体的过程是找到这个函数的地址空间,执行它,然后返回一个值。但是网络上这个地址不是在本机,而是在远端的服务器上,那么怎么找到它,然后调用它。编译的时候生成的是本机地址空间,是固定位置,但是在网络上是变化的,要描述一个地址需要知道目标服务器,以及服务器上确定的地址空间。如果你自己写比较麻烦,不过EJB替你做了。

多谢帮助,受到不少启发。

》》学习EJB,最好还是学习EJB2.X的原理部分。
我正在看的是JSR 220: Enterprise JavaBeansTM,Version 3.0 EJB Core Contracts and Requirements从sun上下来的。所以想问一下知道EJB的原理部分在这里面有描述么,还是需要去看2.X的spec。

RMI其实就是典型的代理,代理的话就得保证接口不变,所以不管愿意不愿意,用EJB就得整个接口进来。另外jboss5好像必须显示的声明local,不像4那样不声明就默认local。
本地传址:
OrderRep.add(Order order);//OrderRepBean implements OrderRep
//OrderRepBean里有一个生成日期的方法给order加了一个日期,然后
assertNotNull(order.getDate());//这在本地就可以(传址),远程就不行。order被ser后也没持久上下文管理了,如果恰好合适的话当个DTO用也行。
另外webservice不用想也必须是remote的(我们现在用的webservice客户端工具老是覆盖掉结构里的同名类,搞的必须得整个没意义的包名生成再重构)

>>所以想问一下知道EJB的原理部分在这里面有描述么,还是需要去看2.X的spec。

我可以推荐你两本书看看,我是看这两本书来学习EJB的。
第一本:精通EJB3.0(这本书前面章有讲EJB2.X的知识)
第二本:Enterprise javabean 3.0 (这本书例子比较多,并且可以下载代码,你可以运行试试,多试几次就知道规律了)

其它的可以google了。

[该贴被xmuzyu于2008-12-19 00:30修改过]

收到