13.11. 惯例优先原则(convention over configuration)

对于很多项目来说,严格遵从已有惯例和使用合理的缺省选项大概是这些项目需要的……现在Spring Web MVC明确的支持了这种惯例优先原则的主旨。 这意味着,如果建立了一套命名规范,诸如此类,就可以显著地减少系统所需配置项目的数量, 来建立处理器映射、视图解析器、ModelAndView实例,等等。 这为快速原型开发提供了很大方便。同时提供了一定程度的(通常是好事情)代码库的一致性,进而可以从中选择并发展为成型产品。

提示

Spring分发版本包含了一个展现了惯例优先原则支持的Web应用程序,我们将在这一节描述这一原则。 这个应用程序可以在samples/showcases/mvc-convention目录中找到。

惯例优先原则支持体现在MVC的三个核心领域:模型、视图和控制器。

13.11.1. 对控制器的支持:ControllerClassNameHandlerMapping

ControllerClassNameHandlerMapping类是HandlerMapping接口的一个实现。 它使用惯例来确定请求的URL和用于处理它们的Controller实例间的映射关系。

举个例子,考虑下面的(直观的)Controller实现, 请特别注意这个类的名称

public class ViewShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // the implementation is not hugely important for this example...
    }
}

下面是与之伴随的Spring Web MVC配置文件的一个片段:

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
                
<bean id="viewShoppingCart" class="x.y.z.ViewShoppingCartController">
    <!-- inject dependencies as required... -->
</bean>

ControllerClassNameHandlerMapping在它的应用上下文中找出所有不同的处理器(handler)(或Controller)bean, 并去掉名称中的Controller,来定义它的处理器映射。

让我们看更多的例子,这样其中的中心思想就马上就清楚了。

  • WelcomeController映射到“/welcome*”请求URL

  • HomeController映射到“/home*”请求URL

  • IndexController映射到“/index*”请求URL

  • RegisterController映射到“/register*”请求URL

  • DisplayShoppingCartController映射到“/displayshoppingcart*请求URL

    (注意大小写——全部小写——对于驼峰式大小写(第一个词的首字母小写,随后的每个词首字母大写)的Controller类名。)

当控制器是MultiActionController处理器类时,生成的映射就(有一点点)更为复杂,但幸而没有更难理解。 下面例子中的几个Controller名字假设都是MultiActionController的实现。

  • AdminController映射到“/admin/*”请求URL

  • CatalogController映射到“/catalog/*”请求URL

如果遵循漂亮而且标准的规范把你的Controller实现命名为xxxController, 那么ControllerClassNameHandlerMapping将使你免于忍受必须首先定义它们, 然后还要维护冗——长——的——SimpleUrlHandlerMapping(或者类似的东西)的枯燥。

ControllerClassNameHandlerMappingAbstractHandlerMapping的子类, 从而使你能够像对待大量其他HandlerMapping实现一样的定义HandlerInterceptor实例和其他任何东西。

13.11.2. 对模型的支持:ModelMapModelAndView

ModelMap类首先是一个绚丽的Map实现, 它可以使新增的将要显示在View中(或上)的对象也遵循同一命名规范。 考虑下面的Controller实现,注意对象被加入ModelAndView, 而并没有指定任何名称。

public class DisplayShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        
        List cartItems = // get a List of CartItem objects
        User user = // get the User doing the shopping
        
        ModelAndView mav = new ModelAndView("displayShoppingCart"); <-- the logical view name

        mav.addObject(cartItems); <-- look ma, no name, just the object
        mav.addObject(user); <-- and again ma!

        return mav;
    }
}

ModelAndView类使用的ModelMap类是一个自定义的Map的实现。 当有一个新对象加入的时候,它就被用于为这个对象自动生成一个键。 决定某个加入的对象的名字的策略是,当它是一个标量对象(scalar object),比如User时, 就使用这个对象所属类的简短类名。下面的几个例子中,几个为标量对象生成的名字被加入ModelMap实例中。

  • 将会为一个新增的x.y.User实例生成“user”作为名称

  • 将会为一个新增的x.y.Registration实例生成“registration”作为名称

  • 将会为一个新增的x.y.Foo实例生成“foo”作为名称

  • 将会为一个新增的java.util.HashMap实例生成“hashMap”作为名字(在这个情形下你可能想要让名字更加明确一些,因为“hashMap不太直观)。

  • 新增null将会导致抛出一个IllegalArgumentException。 如果正在加入的这个(或这些)对象可能潜在的是null的话,就要让名字更明确一些。

在加入一个SetList或者对象数组之后, 生成名称的策略是深入这个集合,取出集合中第一个对象的简短类名,并使用这个名称并在后面加上List。 一些例子将会让集合的名称生成方式更清晰……

  • 将会为一个新增的包含了一个或多个x.y.User元素的x.y.User[]实例生成“userList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.User元素的x.y.Foo[]实例生成“fooList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.User元素的java.util.ArrayList实例生成“userList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.Foo元素的java.util.HashSet实例生成“fooList”作为名称

  • 一个java.util.ArrayList根本不会被加入(也就是说,addObject(..)调用其实什么都没做)。

13.11.3. 对视图的支持:RequestToViewNameTranslator

RequestToViewNameTranslator接口的功能是当没有显式的提供这样一个逻辑视图名称的时候, 确定一个逻辑的View名称。 这个接口只有一个实现,精明的命名为DefaultRequestToViewNameTranslator

为了解释DefaultRequestToViewNameTranslator将请求的URL映射到逻辑的视图名的方式, 最好还是求助于一个例子。

public class RegistrationController implements Controller {
                
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // process the request...
        ModelAndView mav = new ModelAndView();
        // add data as necessary to the model...
        return mav;
        // notice that no View or logical view name has been set
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>

    <!-- this bean with the well known name generates view names for us -->
    <bean id="viewNameTranslator" class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>

    <bean class="x.y.RegistrationController">
        <!-- inject dependencies as necessary -->
    </bean>
    
    <!-- maps request URLs to Controller names -->
    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

请注意,在这个handleRequest(..)方法的实现中, 没有在返回的ModelAndView上设置任何的View或者逻辑视图名称。 而是把从请求的URL生成一个逻辑视图名称的任务交给了DefaultRequestToViewNameTranslator。 在上面这个RegistrationControllerControllerClassNameHandlerMapping联合使用的例子中, 一个“http://localhost/registration.html”请求URL将会由DefaultRequestToViewNameTranslator生成一个“registration”逻辑视图名称。 这个逻辑视图名称接下来就会被InternalResourceViewResolver bean解析为“/WEB-INF/jsp/registration.jsp”视图。

提示

甚至不需要显式的定义一个DefaultRequestToViewNameTranslator bean。 如果DefaultRequestToViewNameTranslator的缺省设置符合你的要求, 就可以依赖这样一个事实,Spring Web MVC的DispatcherServlet将会在没有显式配置的情况下自动的生成这个类的一个实例。

当然,如果需要修改缺省设置,那么就需要显式的配置自己的DefaultRequestToViewNameTranslator bean。 关于可以设置的各种属性的细节,请参阅DefaultRequestToViewNameTranslator的相当详细的Javadoc。