论坛首页 Java企业应用论坛

JSF的加减法与Seam(二)之对java的改进

浏览 17150 次
该帖已经被评为良好帖
作者 正文
   发表时间:2007-11-02  
引用

题记:Seam对开发的简化,对各种不同软件的统一能力,我想其主要来源不是创造了conversation域,不是使用了元注解,不是依赖于JSF,不是使用了反射机制,而是一种更加全局的域管理机制。那些其它因素都是为了这一目的服务的,或多或少的使这一目的的实现变得更加方便可靠。如果说Seam有什么决定性的区别于其它框架的东西,那就是它的全局域管理机制。

说明:1楼和2楼铺垫了很多基础的东西。如果没有耐性,可以把它们略过去,直接从第3楼读起。
另: 文章里的命,生命周期(lifecycle),上下文(context),域(scope)在这里指同一内容,可以互相替换。


不说Seam诞生的大环境和Seam产生的语言基础,只是说Seam本身的功能,大概也可以,不过我认为非常多的外在功能都只是某些环境外在因素和基本内在因素所决定了的。大环境和内在可能定了之后,细节的东西只是做就可以了。所以没有办法,还是得绕开Seam本身说些题外话。

JSF的加减法与Seam(一)http://www.iteye.com/topic/137027大概说了一下 Seam诞生的环境,这是外在机会,是融合各种技术的可能性,是广的纬度。 这篇文章说说java上的可能性,是何让Seam具有了融合的本事,是内在能力,从深的纬度上说吧。

其实也不深,因为说JAVA,其实得从最基本的说起:

1. Java 对象的命

1.1 概述
这个命呢,就是生命周期,就是什么时候生下来可以用它,什么时候死了不能再用了。一个对象在内存中的时间和生命周期是不一定相等的,因为即使一个对象完全没有办法再用了,它仍然可能还在内存里。比如
void doIt() {
Object o = new Object();
o = null;
}

这个方法执行完了之后,o是没办法再用了,但是o可能仍然还在内存里,因为内存的垃圾回收有延时,术语叫做best effort,意思就是我尽我最大能力去回收了,但是回收回得来回不来,得看情况。这篇文章里说到的生命周期,是不考虑这个延时的,如果我没有办法再用这个对象了,我就认为它已经死掉了。

1.2 假如无状态
我举一个非常理想化的例子,假如java的所有的对象的都不能有状态,那对生命周期会有什么影响呢?
如果没有状态,那么所有对象都只能用方法调用。假设我有类A,B,C;而a,b,c是它们的对象。底下老这么说费事,凡是大写都是类,凡是小写都是对象。而a.getB()则假设a有属性b并且有它的getter和setter方法。
如果所有对象的状态不能改变,则只能是这样:

public static void main() {
  A a = new A();
  a.doSomthing();
}
class A() {
  doSomething() {
    B b = new B();
    b.doSomething();
  }
}
class B() {
  doSomehing() {
    C c = new C();
    c.doSomething;
  }
}


每个对象都得在某个方法里被创建(出生),而且必须在那个方法内被销毁(死亡)。那么所有的对象的生命都是和调用它的方法一样长的。这样产生了一个现象就是所有的被调用的对象一定比调用它的对象的命短,而且它出生于调用它的对象出生之后,死亡于调用它的对象死亡之前。 我们把一段生命周期称为“域”,则前者存在的域一定被包含在后者存在的域中。
如何断定的呢? 因为调用者的那个方法的域(也就是命,也就是生命周期)一定包含于调用者本身存在的域──因为任何时候我调用那个方法,拥有该方法的那个对象一定存在,否则该方法也就不能存在了(静态方法是特殊情况等会再说)。而被调用者存在的域一定被包含在调用者方法的域中,这刚才已经讨论了。所以:
对于任意 a通过它的方法doXYZ()来调用b来说:
b的域 包含于(被包含)  doXYZ()的域,  而 doXYZ() 的域 包含于 a的域。
由此可知 b的域包含于a的域。
以此类推,如果a调用b,b调用c,c调用d…… 那么后者的域总是被包含在前者的域里,也就是说越成为被调用者,命越短,越成为调用者,命越长。且被调用者存在时,调用者必然存在。
   发表时间:2007-11-02  
1.3 延寿
上面是一种假想的状态。 假如一个对象确实需要活的比刚才那种情况长怎么办呢? 这样的例子太多了,比如我如果需要在某个类中调用某个ResourceBundle,如果在刚才的假想状态下,那该ResourceBundle的域永远被包含于(因此也短于)调用它的对象的域,那么我们就不避免的要生出无数个该类的对象来。怎样使一个对象寿命变长呢? 我们知道垃圾回收机制里只要有对该对象的引用,该对象就不会被回收。那么只要能让这个对象靠上某个更长命的引用就行了。这个对象的命就肯定大于等于(包含)该引用的命。 说大于是因为也可能该对象不只靠上一个引用,这样的情形下该对象的命(域)就是它的所有引用的域的并集。
罗嗦这么多就是为了说明一个公式:
对象的域 = 所有指向它的引用的域的并集
如果 String a = "废话"; String b = a; String c = b;
那么对于字符串"废话",它就有了三个指向它的引用a,b,c。如果b,c的域很短,a的域长,则该"废话"的域也长,等于 |a|并|b|并|c|。 |x|表示x对象存在的域,下同。
现在的问题从怎么延长寿命就变成了怎么才能搞到一个长寿命的引用来。只要该对象看得到某个长寿命的引用,并且该引用和自己是同一类型,它就可以通过靠上这个引用来延长自己的寿命。


1.4 把长寿的引用给我
对于1.2里的情形,显然一个对象是没有办法获得长寿的引用的。想得到长寿的引用,只有一种情形就是该引用隶属于某个(更长寿的)对象,并且短寿对象可以获得该长寿对象的引用。
想要让1.2里的c变得和a一样长命,我在a里加入一个属性c,并使对象c(对象c是对象,属性c是引用,不一回事)可以获得a,则
a.setC(c);  

OK了,c就和a.c一样寿命了。虽然可能比a的寿命短(因为总是要先消除a对象的属性才能清除a对象),但是对于只能和b的某个方法一样短命,还是长了不少了。
那么怎样才能获得a呢? 在c的方法里加一个A类型的参数,或者干脆在c里加入一个A类型的属性,然后c.setA(a); 然后再在c的某个方法里this.a.setC(this);  就OK了。
0 请登录后投票
   发表时间:2007-11-02  
2. 全局的域管理

java对象总是通过new关键字开始它的生命周期,通过断开所有指向它的引用结束它的生命周期。过去的所有应用也都是这样做的,各个对象分布在整个应用里,有的诞生了,有的死亡了,它们的生命周期若是用图形表现出来是呈现参差不齐的图样的。现在我们有一个问题是,既然我们可以知道JAVA对象如何诞生,如何死亡,那么我们可以人为的造出若干大小不同的“域”来,通过把这些域的指针传递给对象,使对象可以把自己绑定在域上面,我们就可以让这些对象从应用自己管理的生命周期解脱出来,让某个容器或是框架来管理。

如果如1.2里面的方式,我只想让对象在一个方法里诞生,在同一个方法内死亡,我只需要告诉容器或者框架,给我方法域。当我得到了方法域,我就可以向方法域发出请求,给我我想要的对象。如果方法域里面已经有了一个符合我的要求的对象,方法域就把这个对象给我。如果方法域中没有,它就根据设定,或者返回null,或者抛出异常,或者用new关键字新建一个符合询问类型的对象,返回给我。

如果方法域这个例子看起来用处不大,则更实用的例子是Servlet里面的请求域(request scope)和会话域(session scope)。使用的方法和方法域的例子一样,总是先获得某个域的引用(在java里,获得某个对象的引用和获得某个对象指得是一回事),然后向该域发出请求。

实际上Servlet里面的请求域与会话域大部分情况下只是被用做存放参数和某些非常重要的东西,一般的对象是不向里面放的。它们产生的原因也仅仅是用做临时的储存环境,而全然不是考虑要对对象的生命周期进行管理。在JSF里面有请求域,会话域和应用域(application scope),它们的范围依次增大。应用域的范围在一个java虚拟机内的整个应用,也就是在同一虚拟机内应用启动了它就启动,应用结束它就结束。会话域跨越一个用户会话,一般是通过jsessionid的值来确定是同一用户,在这个用户活动的时间内,只要会话没有过期失效,就一直是同一个会话域。请求域跨越一个用户请求,服务器收到浏览器的请求,请求域开始;服务器发给该请求回复页面,请求域结束。一个会话域总是包含了若干个请求域,一个应用域总是包含了若干个会话域。

JSF里面的域更向全局的域管理走了一步,一个受JSF管理的托管Bean,可以被放在请求域,会话域或是应用域里,根据它们对内存和刷新的需求不同。通过faces-config.xml的配置文件,一个普通的POJO可以被用作托管bean,放在其中的某个域中。这样,这些托管bean的生命周期实际上是交由了JSF管理。

然而JSF仅仅是向着这个方向又走了一小步,是很难把许多POJO都定义为托管bean的。主要问题是xml文件会膨胀的不可维护(也是spring的老毛病),ejb不可以被定义为托管bean,还有如果放入过多的对象到session里面,会造成session的过度膨胀,让无用信息消耗大量的服务器内存。

Seam则接过JSF的接力棒,让全局的域管理变得更接近了。它为了解决xml膨胀的问题,引入了用元注解(annotation)代替xml配置,如有需要xml的配置可以覆盖元注解的配置,这样配置实际上被分散到了源文件中,而且如需作改动仅需要在一处改就可以了;它为了解决session膨胀问题,引入了一个新的域叫做对话域(conversation scope),这个域的范围介于请求域和会话域之间,它代表了用户做某种有意义的一个整体的行为时所跨的纬度,通常包含了若干个请求:比如用户输入某个表单,可能需要提交若干个页面,那么这整个表单就构成一个对话域,在表单提交结束时,对话域结束,这样它把有意义的内容组合在一个更小的范围内,避免了session的膨胀;它也解决了ejb的不可以被管理的问题(这大概主要归功于ejb的改进),在Seam中ejb是和pojo组件一样可以被元注解设定放在某个域中的。

Seam还引入了其它范围的域(在seam中不说scope,说context,是一回事):方法域(method context),页域(page scope),商业域(business context)。它们范围各有不同,各有其适用的场合。

在JSR 299 的Web Bean规范中,全局的域管理才真正有所显现,规范规定用户可以自己创建新的域。现在该规范仍然在早期草案审阅(early draft review)阶段,但是已经很让人期待了。

0 请登录后投票
   发表时间:2007-11-04  
3. 对全局域管理的探讨

无论是Servlet,JSF,SEAM 或是 Web Bean,它们都只是逐渐地向全局域管理的方向前进,但是仍然没有任何关于这个方向的理论研究,它们甚至没有意识到全局域管理本身才是给这些应用带来巨大便利的主要因素。而且全局的域管理其实是一个JAVA问题,并非WEB问题,在WEB领域出现只是因为 WEB 领域有天然的容器历史,但是实际上在J2SE领域它是一样适用的。现在对这个问题作一些初步的探讨:

3.1 全局的域管理是如何增加软件开发效率的

3.1.1 生命周期的管理与应用代码分离

一个显而易见的改进是现在对象的生命周期由原来的应用自己控制,改作了交由容器或者框架集中来控制,这样就把在应用代码中混杂的生命周期控制代码和真正的逻辑代码分隔开了。应用本身不再由new关键字或者Class.forName()来初始化对象──这种控制方式是混杂在所有代码里面的。如Seam的做法,它使用反射机制和元注解,使对该类的对象的生命周期定义放在该类的源代码中:
@Name("payment")
@Scope(ScopeType.CONVERSATION)
public class Payment {
    // 类属性与业务方法
}

如果在不同的场合,该类需要被放在不同的的域内(有不同的生命周期),那么可以给它起若干个名字,并定义每个名字所在的域:
@Name("payment")
@Scope(ScopeType.CONVERSATION)
@Role(name="cachedPayment", scope=ScopeType.SESSION)
public class Payment {
    // 类属性与业务方法
}

当应用需要获得某个对象的时候,它不再自己初始化这个对象,而是总是和容器框架联系。它会向容器框架发出请求,而容器框架则会视情况返回不同的结果。
在Seam里,应用代码和Seam框架联系的方法有两种,一是通过静态方法,一是通过反射注入。

静态方法很容易理解,Seam有一个全局的叫做Contexts的类,它有若干个静态方法,用以获得容器所管理的域或者直接获得容器所管理的对象。
获得容器管理的域的方法有这些:
public static Context getEventContext();
public static Context getMethodContext();
public static Context getPageContext();
public static Context getSessionContext();
public static Context getApplicationContext();
public static Context getConversationContext();
public static Context getBusinessProcessContext();

这些方法返回一个Context对象,是所有域的总接口。应用可以通过调用context.get("name")来获得在某一域中的符合该名字的对象。
直接获取容器所管理的对象可以调用
public static Object lookupInStatefulContexts(String name);

Seam会按照如下顺序在各域进行搜索:method, event, page, conversation,session, business process, application。任何先找到的符合名称的对象它会返回给应用代码。

静态方法比较容易理解,但是可能会以Seam特有的类污染应用代码,因为这样任何一个需要与域进行联系的类都依赖于Contexts类了。一个比较少污染的方式是通过反射注入,比如:
@Name("paymentControl")
public class PaymentControl {
    @In private Payment payment;
    @In EntityManager entityManager;
    public void store() {
        entityManager.persist(payment);
    }
}

这样一个和数据库连接并储存payment对象到数据库的类就写完了。完全不需要任何连接代码,Seam来完成所有这些任务。Seam通过寻找@In元注解来对属性注入,默认情形属性的名字就是Seam要找的对象的名字,当然这是可调的:
@In("payment") private Payment expensivePayment;

通过对元注解加参,Seam会根据指定的参数作为名字来寻找相应的对象。
0 请登录后投票
   发表时间:2007-11-04  
3.1.2 类即对象(仿静态方法调用)

我们先看看Spring是如何作反射机制的配置的:
<bean id="paymentControl" class="org.javaeye.PaymentControl">
    <property name="payment">
        <ref bean="payment"/>
    </property>
</bean>
<bean id="payment" class="org.javaeye.Payment">
    <constructor-arg>
        <value>I think this is what I owe zaya.</value>
    </constructor-arg>
    <property name="amout">
        <value>1000RMB</zaya>
    </property>
</bean>

这里定义了两个Bean,分别对应于上一小节里所示的两个类。PaymentControl依赖于Payment。
这里Bean的id属性相当于Seam里的@Name元注解,但是仔细看会发现有非常大的不同。Seam的名字是起给对象的,而Spring的名字是起给类的。在Seam中,只要我询问某个名字,我就知道肯定是某个对象而不是拥有该名字的类的另一个对象。而在Spring中没有生命周期管理的概念,它只是根据简单的依赖关系把对象依次初始化。对象本身的生命周期并不依赖于某个域,而仅仅依赖于指向它的引用的生命周期,也就是依赖于它的那个对象的生命周期。这句话比较绕但是是这样:在Spring中,一个对象的生命周期依赖于依赖它的那个对象的生命周期。如果应用本身不手动得把某个对象联结到更大域(更长命)的其它对象上,那么该对象就总是短命于依赖它的那个对象,并且其生命周期被包含于那个对象的生命周期。

Seam对所有域的统一管理导致了一个很有意思的现象,这就是每一个对象都变得像它的类了,每一个对象的非静态方法都变得像静态方法了。
我们知道为何静态方法可以在应用代码的任意位置直接调用是因为静态方法在内存中只有一份,任何时候我去请求这个类,jvm就立刻知道就是那个内存地址,它是和对象有着相似特征的。对象的特征即是任何时候我在应用代码中写这个对象的名字,jvm就知道是哪个地址,静态资源也是如此。一个应用需要通过new或反射或参数传递来获取一个类的某个对象的引用,是因为没有办法直接根据类的名字来锁定某个内存地址。

而Seam的@Name元注解里所起的名字,是给对象起的而不是给类起的,这就使应用代码总是可以通过在类中定义的某个名字来锁定内存地址,这就好像是所有的类代码的方法都变成了静态方法一样。

为何Seam能够做到这一点呢?其秘诀恰在于它是统一对对象进行生命周期管理的。过去的应用一个类可以在应用中有若干个对象,甚至连可能初始化的对象的个数都无法确定,这样子自然无法给每一个初始化的对象在编译时(compile-time)都起名字。然而有了集中式的域管理以后,每个类的对象所能存在的时间空间被缩小了,这样在每一段被缩小了的时间空间域内,便可以保证该类只会有一个对象。当然如上一小节所示,如果应用希望某个类在一段缩小了的时间空间内有多个实例,它也可以给该类起若干个名字,虽然在实际应用中这样的情形很少发生,往往可以通过缩小对象存在的域来使该对象在给它规定的域内只有一个实例。

静态方法本身是很好使用的,在java类库中有不少已有的静态方法。静态方法最大的好处就是完全不用考虑对象的生命周期,任何时候想用就用,不用了就放一边不用管它。但是有很多因素使一个应用不可能大量依赖静态方法。静态方法是不可能被接口封装的,它也无法被扩展,只要那些非常确定的肯定不会扩展的类的方法才有可能写成静态的,而且静态方法往往被大量的其它类所依赖,稍有改动就会造成大面积的重写。

而在Seam里,在全局域管理下的仿静态方法,有静态方法的所有好处,却完全没有静态方法的弊端。实际上我们会看到在Seam里,类实际上是比往常的应用更容易扩展了,而且对依赖类的改动所造成的牵动效应也比往常的应用更小,这些我后面会写到。
0 请登录后投票
   发表时间:2007-11-04  
想请教一下,不清楚是一二楼那样的风格好一些,还是3、4的风格好一些?

专业词汇用得多,会比较枯燥么? 或者还是觉得还是不要举例子,作类比了,直接就用专业词汇比较好理解些?

还有个问题,怎么才能把文章转发到Seam圈子里面去?

谢谢了

0 请登录后投票
   发表时间:2007-11-05  
引用
而在Spring中没有生命周期管理的概念,它只是根据简单的依赖关系把对象依次初始化。对象本身的生命周期并不依赖于某个域,而仅仅依赖于指向它的引用的生命周期,也就是依赖于它的那个对象的生命周期。这句话比较绕但是是这样:在Spring中,一个对象的生命周期依赖于依赖它的那个对象的生命周期。


spring有生命周期的概念吧。。。spring容器管理对象的创建和消亡。。。。
我感觉最近多了很多seam的抢手啊
0 请登录后投票
   发表时间:2007-11-05  
这么多种context,有点恐怖。虽然解决一些问题,但整个系统失去简单性。
0 请登录后投票
   发表时间:2007-11-06  
分析得很不错,看了觉得有点矛塞顿开的感觉。
0 请登录后投票
   发表时间:2007-11-06  
dingyuan 写道

spring有生命周期的概念吧。。。spring容器管理对象的创建和消亡。。。。
我感觉最近多了很多seam的抢手啊



呵呵,那我就是其中之一了。

其实很多框架都有生命周期的概念,但是Seam所有的是“更全局化”的生命周期管理。

就像在Spring之前就已经有了反射机制,但是是Spring的大规模使用反射,才使EE世界向轻量级转变;在Seam之前也早已经有了生命周期管理,但是Seam对生命周期管理的大规模使用,同样会引发编程模式的变革。

新的东西往往未必最好,用得最好方是最好。


nihongye 写道
这么多种context,有点恐怖。虽然解决一些问题,但整个系统失去简单性。

正常使用,request,session,application,conversation就可以了。 前三个域别得系统都有,第四个域可以解决一些其它框架不能解决的问题,比如hibernate的OSIV问题,比如可以对浏览器的每个Tab分别保存状态,比如可以在一个用户session里面放置多个并行且相互独立的状态线,并且可以在这些状态线间随意切换,比如可以把对数据库的访问作更有效的缓存,减少hit。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics