2009年3月14日
本文的主要内容是对 Grails 1.1 中 PetClinic-MVC 实例的代码分析。这个实例主要验证了如何在非 Grails
的项目中使用 GORM。此外也演示了 Spring MVC 2.5 的一些用法。本文假设大家对 Spring、Spring
MVC、Groovy 和 Grails 有着初步的了解。OK, let's go!
Domain 层
目前 Grails 只提供了在普通 Spring 应用中独立使用 GORM 的方法(现在 Groovy 和 Grails 已是
SpringSource 旗下的产品了,自然要首先对自家产品提供支持)。不过相信以后在其它 Java EE 框架中也可独立使用 GORM。
在 Grails 以外使用 GORM,只需在 Domain 类上使用 grails.persistence.Entity 注释即可。其它的就像在 Grails 中的一样。
在配置方面也很简单,只要使用 <gorm:sessionFactory ... /> 即可。详细请见 Grails 的 PetClinic,这里就不拗述了。
Controller 层
同 Domain 层一样,Controller 层也全部使用 Groovy 实现。主要是因为 GORM 的大部分方法都是动态的。
1. ClinicController: ClinicController 实现了 InitializingBean
接口。这个接口只有一个方法 afterPropertiesSet。这个方法会在 Bean 完成属性设置之后执行。PetClinic
用这个方式实现了类似 Grails 中 BootStrap 的部分功能。在这个方法中我们可以看到 GORM 中的 save() 方法和
addTo* 动态方法都是可用的。
1. public void afterPropertiesSet() {
2. if (!Speciality.count()) {
3. def rad = new Speciality(name: "radiology").save()
4.
5. new Vet(firstName: "Linda", lastName: "Douglas").
6. addToSpecialities(sur).save()
7.
8.
9. ['dog', 'lizard', 'cat', 'snake', 'bird', 'hamster'].each {
10. new PetType(name: it).save()
11. }
12. }
13. }
可能有人会问,afterPropertiesSet()
这个方法的作用就是增加一些预定义的数据,为什么不使用构造函数来实现。实际上,在本例中,afterPropertiesSet()
的作用的确可以用构造函数来实现(经过测试)。至于作者是出于什么目的而选择了实现 InitializingBean 接口我也说不好。
ClinicController 的其它部分就是一些简单的不能再简单的 Http 请求处理函数了。需要说明的一点是 @RequestMapping 中的值代表了请求的 URL,这个是不用带后缀的,也就是说 ".do" 是多余的。
2. AddPetForm:
在 AddPetForm 类的级别上有三个注释,分别是 @Controller, @RequestMapping 和
@SessionAttributes。@Controller 不用多说,作用是这个类声明为一个 Spring Controller,并添加到
Context 中。下面介绍一下 @RequestMapping 和 @SessionAttributes 的功能:
- @RequestMapping 可以定义处理请求的路径和方法类型,即可放在类声明上,也可放在方法声明上。在 AddPetForm
中,类和方法上都有 @RequestMapping,类上的 @RequestMapping 用来定义请求路径。两个
@RequestMapping 的方法分别负责处理 get 请求和 post 请求。但我认为更好的方式是在一个 Controller
类中定义多个处理请求的方法,就像 Grails 的 Controller 那样。需要注意的是方法上面的 @RequestMapping
上面的值并不会覆盖类上的 @RequestMapping 中的值。
- @SessionAttributes 中的值代表了 org.springframework.ui.Model 中应该被设置为
Session 范围的变量。在 Spring MVC 应用中,这些变量往往是起到 ActionForm(Struts) 或
CommandObject(Spring MVC) 的作用。之所以要将表单对象放在 Session
中,是为了使其能够在表单显示和之后的表单提交这样过程中不丢失。@SessionAttributes 的这种用法是源于 Spring MVC
2.0 AbstractFormController 的 SessionForm 模式。
实际上,使用 @SessionAttributes
是很成问题的,有时是不可接受的。考虑以下场景:在 PetClinic 应用中,新建两个 Owner,然后为这两个 Owner 都添加新的
Pet。注意,只要显示出来 Add Pet 的页面即可。这时,这两个页面的 URL 应该是 addPet.do?ownerId=num1 和
addPet.do?ownerId=num2 。这时将先打开的表单
提交
,假设是 num1 的那个。你会发现你
实际上是为 num2 Owner
添加了一个 Pet。
这个问题的原因其实是很简单的,我就不解释了。从上面这个事实来看,@SessionAttributes
的缺点还是比较明显。是否采用这种方式还要看这个问题对你的应用的影响是否是致命的了。Spring MVC 有必要改进一下,像 Seam
那样可以很方便的使用 Conversation 是解决问题的一个好办法。(当然也可以使用 hidden 域来保存 id)。
Spring MVC 之所以要使用这种有较明显缺陷的方式的原因是要使用 Spring MVC
的表单数据绑定功能。由于目前还没有找到更好的方式,所以只能使用 @SessionAttribute 了。这一块还是 Spring MVC
的一个短板,需要在新版本中改进。
除此以外,AddPetForm 中还有两个特殊的方法,一个是注释了 @ModelAttribute 的方法;另一个是注释了 @InitBinder 的方法。下面分别介绍一下:
- @ModelAttribute 可以用在方法上,也可以用在形参上。如果放在方法上,那它会在进入请求处理方法是准备一些数据,作用和
SimpleFormController 的 referenceData() 一样。@ModelAttribute 的这种用法可能会让人联想到
Seam 中的 @Factory。但被 @ModelAttribute 注释的方法只会在同一个 Controller 中被注释了
@RequestMapping 的方法调用之前被调用,而 @Factory 方法则是全局的。
你可以在 setupForm 方法中向 request 或 session 中设置一些需要显示在页面上的数据,但是如果只放在 request
里的话,在表单验证发生时,数据便会丢失;放在 session 缺点自不必多说。在 Spring Web Flow 新加入的 flow 和 conversation 等新的 scope
无法很容易地在 Spring MVC 中使用的情况下,使用 @ModelAttribute 无疑是更好的选择。并且使用 @ModelAttribute 可以在多个
@RequestMapping 方法之间实现代码的重用。
- @InitBinder 适用于注册只属于本 Controller 的属性编辑器。但在 AddPetForm 中,@InitBinder
方法只是告诉 Spring MVC,路径为(<form: input path="id" />) id
的值不要使用属性编辑器。不过页面里也压根没有 路径为 id 的值。所以,我觉得这个又是一个多余的代码。(我试过将 @InitBinder
方法注释掉,对程序没有影响)
Controller 层中两个特殊的类:ClinicBindingInitializer 和 PetTypeEditor
- ClinicBindingInitializer 的作用是注册全句的属性编辑器
- PetTypeEditor 是用于处理 PetType 类型数据绑定的属性编辑器。对于像 PetType 这样的实体之间的关联属性,恰当地使用属性编辑器可以简化程序的开发。
(不知 Grails 是否实现了一个动态的属性编辑器,用于关联属性的数据绑定)
配置
1. applicationContext.xml
applicationContext 中大部分都是常见的 Spring 配置。特殊一点的配置除了
<gorm:sessionFactory ... /> 以外,还有 Hibernate StatisticsService JMX
的配置。
2. dispatcher-servlet.xml
Spring MVC 配置文件(也是一个 Spring 的配置文件)的默认名是 dispatcher-servlet.xml。
从上至下,第一个配置表明哪个 Package 下的 Class 使用了 Spring 的 @Controller
注释。这样 Spring 容器在启动的时候会自动扫描其下的 Class,将加注了 @Controller 注释的类加入到 Spring
的容器中。
第二个配置 DefaultAnnotationHandlerMapping
是 HandlerMapping
接口的一个最常用的实现类。用于映射 Handler 和请求路径之间的关系。通过这样的配置,Controller 中的 @RequestMapping 中的路径值就可以真正地和 Http 请求对应起来。HandlerMapping 的另外一个常用的实现类是 ControllerClassNameHandlerMapping
。
第三个配置 AnnotationMethodHandlerAdapter
的作用是使处理请求的 Handler 能够真正的具有处理请求的能力而提供一些服务,包括为请求参数绑定应用相应的属性编辑器(通过注册 webBindingInitializer 实现)。
==============================结束的分割线==============================
写到这里,PetClinic-MVC
中值得分析学习的代码都提到了。PetClinic-MVC 是一个很小的应用,但是其中演示了 GORM 在普通 Spring 中的应用和
Spring MVC 2.5 的使用。细看的话还是能学到不少东西的。我所写的只是我的理解,难免会有一些不足和错误,欢迎纠正。
posted @
2009-03-14 12:33 阿里 阅读(1605) |
评论 (1) |
编辑 收藏
2008年6月14日
本文将简单谈谈我对 EJB 3.0 的两种 Persistence Context 和 Seam-managed Persistence Context 的不同点的理解、所要解决的问题和我自己所疑惑的问题。
EJB 3.0 (JPA) 的 Persistence Context
大家在使用 EJB 3.0 的时候会注意到 EJB 3.0 中的容器管理 Persistence Context
有两种类型,一种是 Transaction,另一种是 Extended。这是一个较 Hibernate 的 Session
所没有的概念,Session 没有两种不同的类型,而且最重要的是 Session 不是容器管理的,这里的容器指的是 App Server
容器。这里暂时不谈论 Persistence Context 与 Session 之间的异同,主要谈谈两种 Persistence
Context 之间的不同。学过 ORM 的同学都知道,当 Persistence Context 是打开状态的时候,Model
就处于被管理的状态中;当 Persistence Context 关闭之后,Model 就处于了 Detached 状态。
上面这些特性对于 Transaction 或 Extended 的 Persistence Context
都是一样的,不同的地方在于 Persistence Context 何时被打开关闭。由于绝大多数情况下 Persistence Context
是被容器管理的(如果你不嫌累也可以自己控制 Persistence Context),所以在 EJB 3.0 应用中看不到打开或关闭
Persistence Context 的代码(Spring + Hibernate 的应用也同样如此,Hibernate Session
的管理工作可以交给 Spring 来做)。
其实,Transaction 和 Extended Persistence Context
的不同之处也就在于容器何时打开或关闭 Persistence Context。Transaction 类型的 Persistence
Context 的打开和关闭是和事务的打开和关闭是同步的。也就是说在一个事务开始之后,Persistence Context
才会开始;在事务关闭的时候,相应的 Persistence Context 也会被关闭。
Extended 类型的 Persistence Context 的打开和关闭是和 Stateful Session Bean
的生命周期同步的,是跨越事务的。也就是说,从 SFSB 的初始化开始,直到销毁,Persistence Context
都是存在的。你可以在事务之外执行写操作,但是这是并不会执行真正的数据库操作,写操作只是放入了队列,直到下一个事务,写操作才会真正地被执行。两者的
不同简单说来就是 Extended Persistence Context 存在的时间更长。那为什么要有两种不同的 Persistence
Context 呢?
当一个 Web
请求到来时,服务器会打开一个线程,这个线程可能会调用一个事务方法,这是一个事务便开始了,当这个请求结束时,线程关闭,事务也随之结束。由于
Transaction 类型的 Persistence Context 的生存周期是在事务范围之内的,所以一个 Web
请求的结束也意味着相应的 Persistence Context 的关闭。由于多数 Web 应用在一次 Web
请求内即可完成一个独立的操作,所以大部分情况下 Transaction 的 Persistence Context
是适用的。但是对于一些复杂的应用,一次操作需要跨越多次请求。这种情况下,如果依旧使用 Transcation 的 Persistence
Context,由于每次请求结束后,相应的 Persistence Context 都被关闭,相应的 Model 也就变为 Detached
状态。如果接下来的请求仍然需要这些已经变为 Detached 状态的 Model 就需要重新 load,使用 merge()
方法来持久化。稍有不适就会产生 LazyInitializationException 和
NonUniqueObjectException。同时,这也提高了操作的复杂程度。
如果使用 Extended Persistence Context 就能解决这些问题。由于 Extended
Persistence Context 的生命周期是与 SFSB 的生命周期同步的,所以只要多次请求调用的都是同一个 SFSB
中的方法,有多少次的请求,Persistence Context 总是同一个,其中的 Model 也始终是被管理的。很好地解决了
Persistence Context 在线程之间传递的问题,也不会有 LazyInitializationException 和
NonUniqueObjectException 问题的发生。
Seam-managed Persistence Context
EJB 3.0 容器管理之下的 Persistence Context
很不错,能解决很多问题,但是还是有些问题无法解决。Seam 很强大,如果有些问题 EJB 容器解决不了了,没关系,把 Persistence
Context 交由 Seam 来管理就 OK 了。那 Seam 都能解决哪些 EJB 不能解决的问题呢?先考虑下面两个问题:
-
Extended Persistence Context 虽然可以跨越多个事务,但是每个事务照旧调教不误,这对于想在想让整个操作作为一次事务的话,该如何去做
- 如果一个业务的一组请求只是调用同一个 SFSB 的话,那么 EJB 的 Extended Persistence Context
可以在线程之间传递,使 SFSB 的整个生命周期都使用同一个 Persistence Context。但如果业务需要调用不同的 SFSB
的话,如何在 SFSB 之间传递。
对于第一个问题,由于 Seam 的 JPA 实现提供者是 Hibernate,而且 Hibernate 提供了一个扩展的
FlushModeType - "Manual"。通过使用这个 FlushModeType,我们可以手工控制何时执行 flush() 操作。在
Seam 的文档中有关于这部分的介绍 《
Seam-managed persistence contexts and atomic conversations
》。文档中使用了一段简单的代码展示 Seam 如何实现所谓的 "atomic
conversations"(关于代码的内容我就不介绍了,大家通过我提供的链接来浏览 Seam
的文档)。通过这种方式,事务貌似是跨越了整个事务,但我认为 SFSB 中除了调用 flush()
的方法以外的其它方法不是事务的。其实也没有必要,因为这些方法并没有执行数据库操作,所以没有必要使用事务。当然,如果是乐观事物的话,使用了对性能影
响也不大(这只是我的一点浅薄的理解,欢迎指出错误)。只有最后的调用了 flush() 的方法有事务的必要。
这就引发了一个令我不解的问题。请先看这篇文章《
Extended Persistence Context in Stateful Session Beans
》。在这篇文章介绍了如何只使用 EJB 3.0 去解决“问题1”。文中的 SFSB 的默认事务属性是
"NOT_SUPPORTED",也就是说这个 SFSB 中的方法默认不是事务的。只有最后的调用 flush() 的方法使用了
"REQUIRED" 的事务属性去覆盖默认设置。也就是最后的方法是事务的,其它的不是。这和 Seam 所做的有区别吗?感觉没有区别。但我认为像
Gavin King 这样的大牛绝不会做无用功的,那问题就是 Seam 实现 "atomic conversations"
的内部细节到底是什么呢?欢迎大家回答这个问题。
对于第二个问题,可以通过使用 Seam-Managed Persistence Context
来解决。Seam-manged Persistence Context 需要在 components.xml 文件中进行配置,并使用 @In
注入到 Seam 的组件中。由于 Seam 是一个比较新的框架技术,所以关于 Seam 是如何使 Persistence Context
在组件中传递并没有详细的介绍。应该只是声明,然后透明地使用即可。在一个 jBPM 流程中被使用到的 Seam 组件,其中的
Persistence Context 应该是可以很容易地被传递。(本不应该使用不确定的和模糊的词语,但无奈现在关于 Seam
的文章资料还是有限,所以我也没有找到关于 Persistence Context 在组件之间调用的例子)
结束语:这篇文章解释的问题不多,不过都是我自己的理解。可能有错误
的地方,欢迎大家指出。同时,又有一些新问题产生了,应该可以通过阅读 Seam 提供的例子和源代码得到解释。但 Seam
的例子只看过三个,更多的还没有看。看过的 Seam 的源代码更可以忽略不计。继续努力吧!!
posted @
2008-06-14 21:59 阿里 阅读(1470) |
评论 (0) |
编辑 收藏