第27章 Spring MVC之扩展篇

本章内容

  • Spring MVC与Convention Over Configuration

  • Spring 3.0展望

27.1 Spring MVC与Convention Over Configuration

随着Ruby On Rails这一Web开发框架的流行,Convention Over Configuration的理念也越来越受到大家的欢迎。而Spring MVC的良好设计,也使得扩展它并加入Convention Over Configuration的元素变得并非什么难事。

注意:SpringMVC提供的Convention Over Configuration支持,是在Spring2.0发布之后引入的。在Spring2.5发布之后,我们更倾向于基于注解的Controller。在扩展篇依然对SpringMVC中Convention Over Configuration进行介绍的目的是,帮助大家扩展思路,让大家可以进一步地了解SpringMVC的良好设计所赋予我们的扩展现有框架的能力。

27.1.1 Convention Over Configuration简介

Convention Over Configuration,国内技术社区通常称其为“惯例优于配置”或者“惯例优先原则”或者其他称呼。它所倡导的理念是,如果系统中某些映射关系可以通过事先约定的规则确定,那么优先使用事先约定的规则来确定当前映射关系,这样可以避免将所有的映射关系通过配置的方式来表述,进而大大减少配置量,提高开发效率。

在Java平台的ORM产品中,PO(Persistence Object)与数据库表之间的映射关系,之前大多是通过配置文件来表述的,而Ruby On Rails所使用的ActiveRecord这一ORM解决方案则按照约定规则,直接根据对象类名推测该类所映射到的数据库表。比如,User类将映射到users数据库表,也就是说,对象与数据库表之间的命名约定确定了二者之间的映射关系,而不需要任何附加的配置文件来明确这一点。相对于Java平台ORM产品早先的做法,后者将大大减少配置的工作量。

Java平台最初更多偏重于使用外部配置,是因为外部配置可以带给应用程序和开发人员很大的灵活性。但随着系统规模的增长,配置的工作量显然也是有增无减,通过Convention Over Configuration的方式,虽然失去了部分的灵活性,但可以极大减少配置的工作量,提高日常开发效率。权衡利弊之后,我们就可以做出自己的选择了。

实际上,Convention Over Configuration的理念与使用外部配置并不冲突。使用Convention Over Configuration并不意味着我们就得丢弃外部配置方式,我们依然可以通过外部配置来覆盖默认约定所确定的映射关系。甚至,在通过事先的约定无法确定所需要的映射关系的情况下,外部配置将成为“后备军”以确保万无一失。所以,在Convention Over Configuration中,Convention与Configuration的关系更多是协作关系,只是,Convention会优先上场罢啦!从这一角度来说,将Convention Over Configuration称为“约定优先于配置”或许更贴切一些。不过,为了避免翻译造成的混乱,余下部分我们将一直使用原生的Convention Over Configuration来表述这一概念。

27.1.2 Spring MVC中的Convention Over Configuration

Spring从来不会将好的理念和实践方式拒之门外,所以,Spring框架发布2.0版本的同时,为Spring MVC添加了Convention Over Configuration的支持,并借此进一步简化了Spring MVC Web框架的使用。

为了演示SpringMVC如何支持Convention Over Configuration,我们先对Web请求处理过程中所涉及的角色之间的映射关系做事先约定:

  • 客户端以http://server:por/application/sample.do的形式发起Web请求;

  • 服务器端将根据请求的URL,选用类名为SampleController的Handler来处理当前请求;

  • 请求处理完成后,同样根据请求的URL,选择名称为sample.jsp的模板文件进行视图渲染;

下面让我们看看,在SpringMVC中如何实现这一约定下的请求处理过程。

1. Web请求与Handler之间的约定

在Spring MVC中,Web请求与请求处理Handler之间的关系是由HandlerMapping来管理的。不过,通常情况下使用的BeanNameUrlHandlerMapping或者SimpleUrlHandlerMapping都是通过配置来管理请求与Handler之间的映射关系,要使用Convention Over Configuration显然不能依靠它们。为了让Dispatcherservlet能够根据约定,使用请求URL所“极力呼吁”的Controller实现类,我们需要提供BeanNameUrlHandlerMapping或者SimpleUrlHandlerMapping之外的HandlerMapping实现。该自定义HandlerMapping实现类将根据请求的URL,到容器中选取约定名称或者约定类型的Controller实现类来进行当前Web请求的处理。要真去实现这么一个自定义HandlerMapping实现类也并非难事,不过好消息是,2.0版本之后的SpringMVC已经提供了org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping来完成同一历史使命。

ControllerClassNameHandlerMapping在处理Controller与具体Web请求之间的映射关系的时候,使用的约定规则,如下所述。

  • 对于简单的Controller实现来说,ControllerClassNameHandlerMapping将去除了“Controller“后缀的类名作为映射的请求路径,比如:

    CoCController -> /coc*

    SpringController -> /spring*

    默认情况下,转换后的结果均为小写,但我们可以通过将ControllerClassNameHandlerMapping的caseSensitive设置为true更改这一默认行为。

  • 对于MultiActionController来说,生成映射的规则略有改变,采用了类似Servlet映射中的前缀映射规则:

    CoCController -> /coc/*

    SpringController -> /spring/*

    结合InternalPathMethodNameResolver使用的话,恰好可以达到与RubyOnRails通常做法类似的映射效果。如果我们对默认的约定规则还不满意,通过设置或者更改ControllerClassNameHandlerMapping的相应配置项可以进一步对约定规则进行定制,如下所述。

pathPrefix。该属性允许在默认约定规则的基础上,让相应的Controller去处理带有指定路径前缀的请求,比如,在如下ControllerClassNameHandlerMapping基础上:

<bean id="coCHandlerMapping" class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping" p:order="1" p:pathPrefix="module/>

SampleController处理的请求变成对应http://server:port/application/module/sample.do的形式。

basePackage。假设我们的Controller实现类按照功能模块划分,定义在不同的包下:

cn.spring21.simplefx.controller.group.SampleController

cn.spring21.simplefx.controller.rate.SampleController

cn.spring21.simplefx.controller.report.SampleController

那么,在将ControllerClassNameHandlerMapping的basePackage属性设定为cn.spring21.simplefx.controller.的时候,对应三种不同源代码包下的Controller的请求路径就变成:

cn.spring21.simplefx.controller.group.SampleController->group/sample.do

cn.spring21.simplefx.controller.rate.SampleController->rate/sample.do

cn.spring21.simplefx.controller.report.SampleController->report/sample.do

这对于按照功能模块进行管理的实践比较实用。

其他可以使用的定制属性还有excludedClasses和excludedPackages,允许我们将某些Controller实现类排除在ControllerClassNameHandlerMapping所管理的映射之外。如果需要,可以参阅ControllerClassNameHandlerMapping的Javadoc文档以获取更多信息。

如果没有为DispatcherServlet明确声明一个可用的HandlerMapping实例,那么DispatcherServlet将在初始化的时候默认启用一个ControllerClassNameHandlerMapping(随同的还有BeanNameUrlHandlerMapping)。除非要对ControllerClassNameHandlerMapping的约定规则进行定制,通常情况下,不需要在DispatcherServlet的WebApplicationContext中声明它,就能使用Convention Over Configuration的支持了。现在,如果要为http://server:port/application/sample.do形式的请求提供Controller实现的话,只需要按照以上约定,定义一个SampleController,然后注册到IoC容器中即可,如下所示:

<bean id="sample" class="..SampleControl1er"/>

ControllerClassNameHandlerMapping将可保证Web请求到Controller的正确映射。如果是使用BeanNameUrlHandlerMapping或者SimpleUrlHandlerMapping,除了添加Controller的bean定义之外,还得添加映射信息的配置。现在,这些都可以免了。

注意:最初在注册相应Controller实例的时候,可能需要必须指定符合约定的bean定义名称,比如id=“sample”。不过,最新的Spring版本中,即使不声明bean定义名称也是可以的。

2. ModelAndView中的约定

在相应的视图模板访问模型数据的时候,需要根据数据项的键(key)来获取对应的数据值。所以,通常情况下,我们都是按照如下形式将模型数据添加到ModelAndView的:

ModelAndView mav = new ModelAndView();
mav.addObject("key1", value1);
mav.addObject("key2", value2);
...

但现在引入ConventionOverConfiguration理念之后,即使我们不指定添加到ModelAndView中的数据所对应的键,按照约定,ModelAndView依然可以为添加的数据提供一个默认的键(key)来标志它,例如:

ModelAnaview mav = new ModelAndView();
Customer cust = ...;
mav.addObject(cust); // ->key=customer

Customer[] customers = ...;
mav.addObject(customers); //-> key=customerList
...

基本上,如果添加的数据类型为单一对象的话,数据对应的类名转换为小写,即为其所对应的键。而如果添加的数据类型为集合或者数组的话,集合或者数字中的对象类型所对应的类名转换为小写然后添加List后缀所得的结果,将作为其在模型中的键(key)。更多信息可以参考Spring Reference文档,此处不再赘述。

3. Web请求与视图之间的约定

相应Controller处理完当前Web请求之后,会通过ModelAndView返回后继流程要使用的逻辑视图名。但通常情况下,我们都是明确指定ModelAndView所返回的逻辑视图名是什么。对于Convention Over Configuration来说,这显然是多此一举的。所以,要朝着Convention Over Configuration迈进的话,我们就不应该每次去明确指定当前请求对应的逻辑视图名是什么。可是,这样的话,ViewResolver如何决定返回哪个视图呢?

坦率地讲,不能从ModelAndView中取得一个逻辑视图名的话,ViewResolver确实无能为力,它不能凭空造一个视图出来,或者随便挑一个返回,而且,ViewResolver接口定义中并不涉及HttpServletRequest,它显然也无法根据请求的URL来推测可能使用的逻辑视图名是哪个。为了能够让ViewResolver从请求URL中根据约定提取默认的逻辑视图名,Spring MVC引入了org.springframework.web.servlet.RequestToViewNameTranslator。当框架内部不能从ModelAndView中获取可用的逻辑视图名的时候,由RequestToViewNameTranslator根据约定从当前请求的URL中提取。显然,该接口必定与HttpServletRequest有所关联,如下所示:

public interface RequestToViewNameTranslator {
	String getViewName(HttpServletRequest request) throws Exception;
}

在RequestToViewNameTranslator按照约定从请求的URL中提取到可用的逻辑视图名之后,ViewResolver就可以正常工作了。

RequestToViewNameTranslator接口在Spring MVC框架内只有一个实现,即org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator。DispatcherServlet在初始化的时候,将默认使用该实现类来处理ConventionOverConfiguration情况下,请求URL到视图的映射关系。DefaultRequestToViewNameTranslator在处理请求到视图名的映射关系的时候,行为上与ControllerClassNameHandlerMapping处理请求到Handler的映射关系是类似的,如

http://.../application/sample.do -> sample

http://.../application/module/sample.do -> module/sample

另外,我们同样可以通过DefaultRequestToViewNameTranslator的相应属性,对默认的映射约定进行定制,比如通过prefix或者suffix属性为默认提取的视图名添加相应的前缀或者后缀等。

当ViewResolver根据RequestToViewNameTranslator从请求URL中提取的逻辑视图名返回对应的视图的时候,整个按照Convention Over Configuration方式处理Web请求的过程就算完成了!

27.2 Spring3.0展望

在Spring3.0中,针对Spring MVC的更新幅度应该算是最大的了吧!

首先,在Spring2.5发布之后,“基于注解的Controller”就已经成为最被推荐的Spring MVC实践方式。在Spring3.0中,这种实践倡导将被进一步增强,也就是说,SpringMVC将更倾向于成为Spring@MVC,以注解驱动的Spring MVC开发将在Spring3.0中红遍整个天。

其次,Spring团队在发布了Spring3.0 M2之后的博客中阐述到,他们还可能考虑在Spring3.0中追加对JSF2.0的正式支持,但这依然没有成为定局,能不能成为现实,还要看增加这样的支持是否会对现有的Spring MVC框架造成很大的变更,或者也有可能针对JSF2.0变更现有的SpringMVC实现架构。所有这些都只能在Spring3.0发布之后,才可知晓Spring团队是如何做出决定的。

在Spring3.0中,针对REST的支持将成为重头戏,按照Spring团队博客上的相关信息可以知道(可以参看http:/blog.springsource/2009/03/08/rest-in-spring-3-mvc/),针对REST的支持将被集成到SpringMVC的核心当中,这将大大简化使用SpringMVC或者说Spring@MVC开发REST应用的难度。

根据已知的信息可以知道,Spring3.0将为SpringMVC更新或者说增加如下装备。

ContentNegotiatingViewResolver。为了能够适应REST对视图类型显示的需要,自定义的ViewResolver被引入;

  • URITemplate。结合@Pathvarible,我们可以定义满足URITemplate形式的请求处理映射,比如:

    // 以下代码摘自SpringSource Blog
    @RequestMapping(value="/hotels/{hotel}/bookings/{booking}", method=RequestMethod.GET)
    public String getBooking(@PathVariable("hotel")1ong hotelld, @PathVariable("booking") long bookingId, Model model) {
      Hotel hotel = hotelService.getHotel(hotelId);
      Booking booking = hotel.getBooking(bookingId);
      model.addAttribute("booking", booking);
      return "booking";
    }
    

    通过@PathVarible标注的方法参数将被绑定请求映射中相应位置的值,比如,针对以上代码定义,在/hotels/1/bookings/2的情况下,1将被绑定到hotelld方法参数,而2将被绑定到bookingId方法参数。

  • 各种新的View实现。这包括AbstractAtomFeedView、AbstractRssFeedView、MarshallingView以及JacksonJsonView。

  • HiddenHttpMethodFilter。虽然HTTP定义有PUT、POST、GET和DELETE四种方法,但通常HTML只支持GET和POST两种。要实现REST形式的服务,我们就得“曲线救国”,比如通过JavaScript来发送相应请求。HiddenHttpMethodFilter是一个Servlet的Filter实现,其作用在于,它可以拦截并转换相应的请求方法,使得这一切对后端透明。Spring3.0中通过增强的form标签库和HiddenHttpMethodFilter为基于REST的应用开发提供了一套完备的服务。

另外,Spring3.0也将对Spring Portal MVC进行升级,以支持Portlet 2.0 API。与Spring MVC将更倾向于Spring@MVC相类似,Spring Portal MVC注定也将向这个方向发展,相应的注解将被添加或者被强化。

总之,让我们拭目以待,期待Spring 3.0为我们带来更多的惊喜!

27.3 小结

显然,Spring MVC良好的架构设计使得引入Convention Over Configuration的理念变得极其轻松,只提供一个自定义的HandlerMapping和扩展的RequestToViewNameTranslator就搞定了。实际上,纵观Spring MVC,我们会发现,无论是Spring2.5后引入基于注解的Controller,还是Spring2.0之后引入的Convention Over Configuration,它们都是在保持Spring MVC框架总体一致性的基础上添加的自定义扩展而已。如果需要,我们同样可以通过扩展Spring MVC来引入更多新的特性或者理念,不是吗?

更多推荐

第27章 Spring MVC之扩展篇