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

Spring Framework

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

对于很多项目来说,遵从已有的惯例和使用合理的缺省选项大概是最合情合理的做法。现在Spring Web MVC框架也明确支持这种惯例优先的配置。具体来说,如果你在项目中遵守一定的惯例(比如命名规范),你可以显著地减少系统需要的配置(比如处理器映射,视图解析器配置,ModelAndView的声明,等等)。这对快速系统建模(rapid prototyping)是非常有利的。如果你打算进一步把模型完成为可以工作的系统,这样写出的代码也具有很好的一致性。

提示

Spring的开发包中有一个web应用的范例。这个范例演示了这一节提到的惯例优先原则。你可以在samples/showcases/mvc-convention目录中找到这个范例。

This convention over configuration support address the three core areas of MVC - namely, the models, views, and controllers.

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

13.11.1. 对控制器的支持: ControllerClassNameHandlerMapping

ControllerClassNameHandlerMappingHandlerMapping接口的一个实现。它检查请求的URL,然后通过惯例来决定与之相对应的控制器。

比如,下面有个非常简单的控制器实现,请特别注意这个类的名字

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。把这些bean的名字中Controller后缀去掉,就得到了每个控制器所能处理的URL。

让我们通过举例来进一步解释这个映射的工作原理。

  • WelcomeController映射到'/welcome*'这个URL

  • HomeController映射到'/home*'这个URL

  • IndexController映射到'/index*'这个URL

  • RegisterController映射到'/register*'这个URL

  • DisplayShoppingCartController映射到'/displayshoppingcart*'这个URL

    (注意字母的大小写。URL全部都用小写,但在Java类名中每个单词的第一个字母要大写。)

当控制器是MultiActionController的子类时,自动生成的映射就稍有点复杂,但应该还是比较好理解的。下面例子中这几个控制器都是MultiActionController

  • AdminController映射到 '/admin/*' 这个URL。

  • CatalogController映射到 '/catalog/*' 这个URL。

如果你的控制器类遵守这些命名规范(xxxController),ControllerClassNameHandlerMapping可以自动生成映射,这样你就不必费劲的定义和维护一长串SimpleUrlHandlerMapping(或者类似的映射策略)。

ControllerClassNameHandlerMappingAbstractHandlerMapping的子类,所以你仍旧可以像对待其他HandlerMapping实例一样来定义HandlerInterceptor的实例。

13.11.2. 对模型的支持:ModelMap (ModelAndView)

ModelMap是一个加强版的Map实现。在这个Map里,每个对象的键都遵守一个命名规范,然后这些对象就可以显示在视图中。这个类的使用其实很简单,不需要长篇大论。下面让我们看几个例子,然后我们结合例子进行讲解。

下面是一个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 a'ain ma!

        return mav;
    }
}

ModelAndView在内部使用了ModelMapModelMap是一个自定义的Map实现,它可以为加入其中的每个对象自动生成一个键名。这些键名是有规律的。当存入的对象是存储单值的对象(scalar object),比如User,生成的键名就是对象的类名(不包括包的名字)。下面几个例子详细解释了这个命名规范:

  • x.y.User这个类的实例对应user

  • x.y.Registration 这个类的实例对应registration

  • x.y.Foo这个类的实例对应foo

  • java.util.HashMap的实例对应hashMap(在这种情况下你最好还是自己声明键名,hashMap这个名字不那么直观)

  • 当你视图把null这个值加入Map时,你会得到IllegalArgumentException。 所以如果你的某个对象可能为null,你最好也自己声明键名。

当你加入ModelAndView中的对象是SetList或者数组时,Spring会检查这个集合,取出这个集合中的第一个对象,然后用它的类名,加上List后缀,就是最终生成的名字。下面几个例子进一步解释了这个命名规则:

  • 一个由 x.y.User组成的数组对应userList这个名字。

  • 一个由x.y.Foo组成的数组对应fooList这个名字。

  • 一个由x.y.User组成的java.util.ArrayList对应userList这个名字。

  • 一个由x.y.Foo组成的java.util.HashSet对应fooList这个名字。

  • 一个空的java.util.ArrayList根本不可能被加到这个Map中。(在这种情况下,adObject(..)其实什么都没做)。

13.11.3. 对视图的支持: RequestToViewNameTranslator

RequestToViewNameTranslator这个接口的功能是自动寻找请求所对应的视图名(当某个视图名没有明确配置的时候)。这个接口目前只有一个实现,类名为DefaultRequestToViewNameTranslator

为了解释DefaultRequestToViewNameTranslator是如何将请求的URL映射到视图名,最好的方法就是举例说明。下面是一个Controller的实现,和它对应的配置文件。

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.RegistrationControllerController">
        <!-- 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中分析出逻辑视图名(logical view name)的是DefaultRequestToViewNameTranslator。在上面这个例子中,RegistrationControllerControllerControllerClassNameHandlerMapping在一起使用,所有对http://localhost/registration.html这个URL的请求将会对应registration这个逻辑视图名。这个视图名正是由DefaultRequestToViewNameTranslator解析出来的。然后,InternalResourceViewResolver这个bean会将这个逻辑视图名进一步解析成/WEB-INF/jsp/registration.jsp这个视图。

提示

你甚至不需要配置类型为DefaultRequestToViewNameTranslator的bean。如果DefaultRequestToViewNameTranslator的缺省行为已经符合你的要求,你就可以使用这个类。当你没有明确声明时,Spring Web MVC 中DispatcherServlet这个类会自动生成一个DefaultRequestToViewNameTranslator的实例。

当然,如果你有自己特殊的要求,你就需要配置DefaultRequestToViewNameTranslator bean。如果你需要知道这个类有哪些可以设置的参数,请参阅DefaultRequestToViewNameTranslator的Javadoc。