Seam的谈话(Conversation)

曾经购物狂的往事

人有时间喜欢怀旧,既浪漫而且对将来很有指导意义。想来在国内早期的电子商务时期,8848.com非常火爆、卓越刚刚崛起之际,网络购物这个新潮淘金行业最吸引年青人的眼球。在网上冲浪的同时还能买到便宜实惠的商品,尤其是当时国内市场买不到的舶来品。

当年我也经常浪迹于8840和卓越网,一次偶然的机会发生了一个必然的趣事:在网站购买崭新的刻录光驱时,通过购物网站提供的工具栏把选好的刻录机装进购物篮后。在保持当前网页打开的状态下,我又打开了另一个网页开始在网站上闲逛,又选了几张音乐CD之后提交订单前自己计算了一下发现钱不够了,只得放弃了那几张经典CD,然后含泪提交订单,结果太雷人了!之前选择的刻录机赫然出现在选购商品清单中,俺的钱还是不够!

那时还不知道是 session捣得鬼,只是心中暗骂了一下网站开发者,怎么开发这么摧残人的程序!现在想来已经明白了原来开发者用了同一个session存储购物,而我在原来页面的基础上新开窗口的话还是使用同一个session,这就造成了俺的购物悲剧。从此之后每每购物时都小心翼翼地使用单页面了,直到前几年才发现这个bug已经灰飞烟灭了!感谢那些伟大的程序员!

你想知道他们怎么做到的么?那我就结合最近阅读的《Seam in action》谈一下。

什么是Conversation

如果具有数据库常识的话,你一定了解数据库事务(Transation)。而Seam的Conversation和Transation有异曲同工之妙:

  • 数据库厂商从DBA的角度定义Transation,使DBA或者操作数据库的程序在经过多次数据库操作后才最终提交变化到数据库;
  • Seam 从用户的角度从新定义工作单元,它包含了用户为了完成某种目的而希望的所有操作步骤,例如购物时的选购、装载到购物篮、下订单、结算。这个过程被认为是一个工作单元,即Conversation。在Conversation里面,数据库事务、页面请求和其它原子操作单元可以前进或者后退,但直到用户目的达到时才完成整个工作处理。这样就解除了由于HTTP协议的无状态性而带来的数据丢失和竞争问题。


打个形象地比喻,Conversation就像是我们每日上下班又恨又爱的地铁,它把乘客从起始地一直运送至目的站,而其间路经的各站可以看作是页面的不断跳转、一次次request请求。而我们传统的开发模式可比喻为一位拉肚子的生病乘客,每到一站,他都要跑下车冲进厕所,而痛快一下之后回到站台等待下一趟地跌,然后乘坐这辆地铁去往下一站,继续此循环。


从这个角度来讲,Conversation更加匹配用户需求,而且大大降低了由于临时数据存储而对数据库进行反复操作的压力。

Conversation怎么工作

提到Conversation的工作原理,当然不能离开Conversation Context!Conversation Context是Seam引入的两个Context之一,它和Servlet生命周期正相反,它服务于业务领域的时间框架。

Conversation Context逃出了HTTP Session的狱垒,在空旷的大地上狂啸着:“我自由了!”。这场景让我想起了电影《肖申克的救赎》。与《肖申克的救赎》中的主角一样的聪明精干,Conversation Context把Session分割为n等份的存储相互独立的内存片段,这些内存片段被称为“Workspace”。


Seam把Conversation Context作为其上下文变量的工作区域。虽然Conversation Context使用了Session进行谈话数据的存储,但并不会重蹈前面提到Session问题的覆辙。


首先,conversation的生命期按照分钟计算,而Session的生命期却大都以小时计算。造成这个不同的原因是蜗居于Session中的 Conversation具有自己的生命期。每个Conversation都具有自己的超时期限,这个期限的默认值取自全局超时设置。另外,并发的 Conversation也是相互保持独立的,而不像Session的属性那样脆弱地易于覆盖。

由于conversation存储在Session,有两个必要的条件要提一下:

  • conversation范围内的(conversation-scoped)组件必须实现java.io.Serializable接口
  • 在web.xml中设置的Session超时取值,必须大于所有的conversation超时值


每个Conversation有一套清晰的生命周期边界,此边界与对应的用户case一致。当用户触发条件开始进入Conversation时,一个新的 HTTP Session区域被分配给此Conversation。同时Conversation Id这个唯一标识也被生成并与分配好的Session区域相关联。接着,Conversation Id被作为参数传递给下一个request,而参数的形式不止一种:隐藏form field、JSF视图根属性。幸运的是,Conversation Id的传递是由Seam全权管理的,对于开发者和用户来讲由于水晶般透明。由于Conversation Id会和Session token一起发送给Server,因此Conversation就建立起了与Session、request之间的联系。Seam在接收到 request后,取出其中的Conversation Id,并根据后者方便地将Conversation从Session这个大抽屉中取出使用。

Conversation Context的确是在整个Conversation旅行过程中放置数据的一次性安全保险箱!它利用了Session的存储复杂对象的能力而又具有独立的工作空间和特有的生命期,不会造成内存泄漏或并发问题。

什么适合装入Conversation大皮箱

 

 

  • 非持久化数据:例如一套检索定义或记录标识的集合。用户可以在一个request中创建状态,然后在下一个request中接收这些数据。当然这类数据也包括配置,例如pageflow的定义。
  • 临时实体数据(Transient entity data):临时的实体实例可以被建立和作为页面向导数据的一部分。当向导完成时,这个实体实例将和Conversation的Workspace分离,之后被持久化保存。
  • 被管理的实体数据:Conversation的Workspace为我们提供了一个聪明的方式与被数据库管理的、要更新field取值的那些实体数据一起工作。这个实体实例被放置在Conversation的Workspace中,然后被构造成页面form。当用户提交form时,form中的数据将被用于实体实例上,这样变化的数据就会透明地更新到数据库中。
  • 资源Session:Conversation Context提供了巧妙的机制来维护对企业资源的应用。例如,持久化上下文(persistence context)能被保存到Conversation中,来防止实体实例不过早地进入分离状态。

Conversation的传递

Conversation 通常有两种状态:临时(temporary)、长期运行(long-running)。当然还有第三种不大常用的状态:内嵌(nested),它就像寄生虫一样内嵌在长期运行的Conversation中。而状态之间的切换通常称为Conversation传递(Conversation Propagation)。我们可以通过设置Conversation传递指令来设置Conversation的边界。但我们不能够人工初始化、撤销 Conversation,而可以通过玩转盘游戏一般的不断地转换Conversation状态。

临时 VS 长期运行

大多数情况,当人们谈起Seam的Conversation时,他们提及的是长期运行的Conversation。一个长期运行的 Conversation跨越了一系列request、通过传输Conversation Id来保持行动力,就像用Conversation Id作为一条丝线把N个闪亮的request穿在一起那样。与临时的Conversation相比,它很像一个签了合同(Conversation Id)的合约职员。

如果没有使用Conversation传递指令设置为长期运行的话,Seam将自动创建一个临时Conversation来为当前request服务。这个临时的Conversation就像一个临时工,它紧跟着JSF生命周期的恢复视图阶段进入工作状态(马上被初始化),在SF的渲染响应阶段之后被销毁。这位临时工的工作时间只在JSF生命周期之内。

Conversation传递路线图

  1. 在request达到Server时,如果request参数中含有Conversation Id的话,一个响应的长期运行的Conversation被恢复。
  2. 如果没有Conversation Id或者Conversation Id失效,Seam将初始化一个全新的临时Conversation。
  3. 在处理request的任意点,只要遇到Conversation传递指令,Conversation都可以改变状态:
  4. begin指令把临时Conversation转换为长期运行状态;
  5. join指令和begin指令作用相似,除了它能够进入到一个已存在的长期运行Conversation中,而相同的情况下begin指令将抛出异常;
  6. nest指令也开始一个长期运行Conversation,但如果已经存在一个长期运行Conversation,则新的Conversation被创建后,已存在的那个将被临时挂起。
  7. end指令将长期运行Conversation打回到临时状态。
  8. 在request结束时,这个临时Conversation将被撤销;而长期运行Conversation将卷起铺盖住进Session旅馆,等待下一个request来叫醒。


如果被恢复的Conversation是无效的或者早已超时,那么用户将被提示并转发到在Seam中预先好的fallback页面。

 

  • 2009-08-06Seam2骨架——给Seam2拍张X光片 - [seam]


    说起Seam2的另类,并不言过其实,因为它身为JSF与J2EE之间的“粘合剂”,当然要有所作为。而由于貌似尴尬地处于汉堡包肉饼的位置,这些行为有点古怪,也不难理解。通过我阅读《Seam2 in action》感觉Seam2浩如烟海般的组件库,初次见到让人直接晕掉,想必也是不少初学者碰壁之处。在我撞得头破血流之后,发现Seam2体现了“大道至简”的原则,在这里简单精炼地说一下体会,以供和大家交流。

    完整的生命周期:

    四个生命周期的关系:

    四个周期:Servlet上下文的生命周期、Request的生命周期、JSF生命周期、Seam生命周期。

    • Servlet上下文的生命周期:代表了整个web应用的寿命,它被用于引导服务启动,比如Seam容器;
    • Request的生命周期:是单一request支配的生命周期,它包含了JSF和Seam的生命周期,它从由浏览器发起url请求而诞生、服务器完成处理发生响应回到浏览器而终结;
    • JSF生命周期:十分短暂,它被限制在JSF Servlet的service()方法内,并不关心其它非JSF请求;
    • Seam 生命周期:相对长一些,它和JSF生命周期相随,将额外的服务植入策略那些定义好的扩展点。另外,它超越了JSF的生命周期:在垂直方向上,它能捕捉 JSF Servlet范围之外发生的事件;水平方向上,它参与非JSF请求的处理。可以认为Seam生命周期是一个JSF生命周期的进化版本。


    四个周期范围包含关系:Servlet上下文>Request>Seam>JSF

    Seam如何参与到Servlet容器的生命周期中:

    • 应用启动初始阶段:Servlet容器引导Seam启动,此时Seam装置它自己的富有上下文关系(contextual )的容器、扫描组件、开始服务创建组件实例。
    • 应用运行阶段:和常见的J2EE应用相同,在运行过程中,当HTTP session被打开和关闭时,Servlet容器也会通知Seam。Seam通过注册一个Servlet过滤器和一个JSF时期(phase)Listener来加入到处理Servlet请求的工作中来。这样通过处理这些Servlet和JSF周期事件,Seam可以管理它的容器并加强JSF。

     

    勤劳的焊接机:

    Seam作为JSF与J2EE间的“焊接机”,它通过一系列的基础设施完成“热焊接”:

    SeamListener类:

    Servlet 生命周期Lisener。它在web.xml中配置为应用初始化启动的Servlet,只要应用启动它就会得到通知。Seam使用这个生命周期事件来自引导。每次Seam被调用时,它开始扫描组件所在的classpath。这个组件扫描器将组件的定义放置在Seam容器中。任何被标记为 application-scoped启动的组件(例如:使用@Startup和@Scope(ScopeType.APPLICATION)的组件)将被自动初始化。这种启动组件常用于执行引导逻辑,比如更新数据库或注册模块。

    SeamListener同时也捕捉新HTTP session开始时的通知,并初始化在此session范围内的启动组件(例如:使用@Startup和 @Scope(ScopeType.SESSION)的组件)。其它所有组件在处理HTTP请求的过程中按需被初始化。

    FacesServlet类:

    Seam要和JSF一起工作,当然就缺不了这个Servlet类,在处理JSF请求过程中和Seam相关的工作都在这里进行。FaceServlet的配置文件中定义的映射模板是*.seam,即此类负责处理以.seam为结尾的请求。

    FaceletViewHandler类:

    Seam 开发团队力荐开发者使用Facelet来代替传统的JSP作为JSF视图处理器。JSF和JSP存在一个令开发者超不爽的错配。JSP的目的是来生产动态数据,而JSF组件tag则企图生成一个具有自渲染能力的UI组件模式。这在runtime时会造成完全不同的目标冲突。而Facelet是一个轻量级、基于XML的视图技术,它通过解析XML文档单独生成JSF UI组件树。它提供组件tag,这些tag可以被本地转换为UI组件,并把那些非JSF标签(包括EL)封装到JSF text组件中。因此,来自JSP tag层的需求,以及JSP编译的整体消耗,都可以解决。

    Facelet处理流程,可以比喻为“一场精彩的4X1接力赛”:

    • 当一个进入的JSF请求需要被处理、被转换到UI组件树时,JSF Servlet映射扩展(.seam)告诉Servlet容器重定向请求到JSF Servlet,即FacesServlet;
    • 然后JSF servlet将view ID传递给Seam启动时早已经注册过的view handler,即FaceletViewHandler;
    • Facelet使用view ID来定位模板,然后解析文档创建UI组件树。

     


    SeamResourceServlet类:

    JSF 规范中没有提供如何推送如图片、CSS、JavaScript这类支持性资源给浏览器的指导。最常见的解决方式是使用自定义的JSF时期Listener 将这些请求匹配相应的路径。而这样却搅乱了JSF生命周期,Seam使用自定义Servlet处理这些资源,通过回避生命周期从而避免不必要的麻烦。使用 SeamResourceServlet是非常合理的,因为在处理资源过程中的调用步骤和那些处理应用页面截然不同,前者消除了复杂生命周期的需要。

    SeamFilter类与Seam内建的Filter反应链:

    Serlvet 过滤器把整个request的处理过程包裹得严严实实,可以在处理请求之前和处理请求之后执行逻辑。Seam使用单独一个SeamFilter类来封装 JSF Servlet,以使其在JSF生命周期之外“节外生枝”或者是处理JSF没有捕捉的请求。但这个过滤器并不限于处理JSF请求。它处置所有的请求,也允许非JSF请求访问Seam容器。Seam可以不依赖于过滤器工作,但这些被安装的过滤器对Seam额外添加的服务很有用处。

    尽管只是安装了一个独立的过滤器,但Seam实际上使用此过滤器像传送带一般带动着一整条内建Filter反应链,通过委派将请求调配给相应安装在Seam容器内部的过滤器。这种委派模式使web.xml的配置实现最小化。当SeamFilter被安装好后,剩下的Seam内建Filter配置就可在Seam组件描述符(/WEB-INF/components.
    xml)中完成了!

    SeamPhaseListener类:

    这是将Seam打入JSF生命周期的“隐形侦察机”。在Seam 2.0,Seam phase listener被声明在facesconfig.
    xml文件中,这个文件被包含在核心Seam JAR文件——jboss-seam.jar中。因此,只要你包含此JAR文件在你的应用里面,这个phase listener就有效。

     

    容器与上下文:

    双向注入机制:

    和Spring的注入依赖(DI)类似,Seam2也有此类东东,但更加方便:

    双向注入(Bijection):它是向内注入(injection)和向外注入(outjection)的合成物。Bijection通过一个方法拦截器管理。injection发生在组件方法调用之前,outjection发生在调用完成时。可以认为上下文(context)变量参与这个交互过程。 injection从Seam容器提取context变量,然后赋值给相应组件实例的属性;而outjection把组件实例的属性值“推”到相应 context变量中。

  • 你可能感兴趣的:(数据库,工作,servlet,session,JSF,seam)