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

08-12-18 xmuzyu
                   

目前持久层框架都有一个持久化上下文的概念,下面以比较流行的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修改过]

                   

2
xmuzyu
2008-12-18 00:53

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

在讨论此问题之前,首先要明确一个问题,什么是会话?会话简单的可以理解为跨多个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修改过]

xmuzyu
2008-12-18 00:58

请banq老师帮忙改一下主题贴的题目,我回复了就不能改了。

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

banq
2008-12-18 10:14

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

可能有的人觉得奇怪,怎么涉及事务,就变得好像很复杂?其根本原因还在于多线程和锁,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修改过]

IceQi
2008-12-18 12:29

好文章,收获不少。

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

3Go 1 2 3 下一页