13.5. 视图与视图解析

所有web应用的MVC框架都有它们处理视图的方式。Spring提供了视图解析器供你在浏览器显示模型数据,而不必被束缚在特定的视图技术上。Spring内置了对JSP,Velocity模版和XSLT视图的支持。 第 14 章 集成视图技术这一章详细说明了Spring如何与不同的视图技术集成。

ViewResolverView是Spring的视图处理方式中特别重要的两个接口。 ViewResolver提供了从视图名称到实际视图的映射。View处理请求的准备工作, 并将该请求提交给某种具体的视图技术。

13.5.1. 视图解析器

正如前面(第 13.3 节 “控制器”)所讨论的,SpringWeb框架的所有控制器都返回一个ModelAndView实例。Sprnig中的视图以名字为标识,视图解析器通过名字来解析视图。Spring提供了多种视图解析器。我们将举例加以说明。

表 13.5. 视图解析器

ViewResolver 描述
AbstractCachingViewResolver 抽象视图解析器实现了对视图的缓存。在视图被投入使用之前,通常需要进行一些准备工作。从它继承的视图解析器将对要解析的视图进行缓存。
XmlViewResolver XmlViewResolver实现ViewResolver,支持XML格式的配置文件。该配置文件必须采用与Spring XML Bean Factory相同的DTD。默认的配置文件是 /WEB-INF/views.xml
ResourceBundleViewResolver ResourceBundleViewResolver实现ViewResolver,在一个ResourceBundle中寻找所需bean的定义。这个bundle通常定义在一个位于classpath中的属性文件中。默认的属性文件是views.properties
UrlBasedViewResolver UrlBasedViewResolver实现ViewResolver,将视图名直接解析成对应的URL,不需要显式的映射定义。如果你的视图名和视图资源的名字是一致的,就可使用该解析器,而无需进行映射。
InternalResourceViewResolver 作为UrlBasedViewResolver的子类,它支持InternalResourceView(对Servlet和JSP的包装),以及其子类JstlViewTilesView。通过setViewClass方法,可以指定用于该解析器生成视图使用的视图类。更多信息请参考UrlBasedViewResolver的Javadoc。
VelocityViewResolver / FreeMarkerViewResolver 作为UrlBasedViewResolver的子类,它能支持VelocityView(对Velocity模版的包装)和FreeMarkerView以及它们的子类。

举例来说,当使用JSP作为视图层技术时,就可以使用UrlBasedViewResolver。这个视图解析器会将视图名解析成URL,并将请求传递给RequestDispatcher来显示视图。

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

当返回的视图名为test时,这个视图解析器将请求传递给RequestDispatcherRequestDispatcher再将请求传递给/WEB-INF/jsp/test.jsp

当在一个web应用中混合使用不同的视图技术时,你可以使用ResourceBundleViewResolver:

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
    <property name="defaultParentView" value="parentView"/>
</bean>

ResourceBundleViewResolver通过basename所指定的ResourceBundle解析视图名。对每个待解析的视图,ResourceBundle里的[视图名].class所对应的值就是实现该视图的类。同样,[视图名].url所对应的值是该视图所对应的URL。从上面的例子里你也可以发现,你可以指定一个parent view,其它的视图都可以从parent view扩展。用这种方法,你可以声明一个默认的视图。

关于视图缓存的注意事项 --继承AbstractCachingViewResolver的解析器可以缓存它曾经解析过的视图。当使用某些视图技术时,这可以大幅度的提升性能。你也可以关掉缓存功能,只要把cache属性设成false就可以了。而且,如果你需要在系统运行时动态地更新某些视图(比如,当一个Velocity模板被修改了),你可以调用removeFromCache(String viewName, Locale loc)方法来达到目的。

13.5.2. 视图解析链

Spring支持多个视图解析器一起使用。你可以把它们当作一个解析链。这样有很多好处,比如在特定情况下重新定义某些视图。定义视图解析链很容易,只要在应用上下文中定义多个解析器就可以了。必要时,也可以通过order属性来声明每个解析器的序列。你要记住的是,某个解析器的order越高, 它在解析链中的位置越靠后。

下面这个例子展示了一个包含两个解析器的解析链。一个是InternalResourceViewResolver,这个解析器总是被自动的放到链的末端。另一个是XmlViewResolver,它 支持解析Excel视图(而InternalResourceViewResolver不可以)。

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

<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
  <property name="order" value="1"/>
  <property name="location" value="/WEB-INF/views.xml"/>
</bean>

<!-- in views.xml -->

<beans>
  <bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>

如果某个解析器没有找到合适的视图,Spring会在上下文中寻找是否配置了其它的解析器。如果有,它会继续进行解析,否则,Srping会抛出一个Exception

你要记住,当一个视图解析器找不到合适的视图时,它可能 返回null值。但是,不是每个解析器都这么做。 这是因为,在某些情况下,解析器可能无法侦测出符合要求的视图是否存在。比如,InternalResourceViewResolver在内部调用了RequestDispatcher。请求分发是检查一个JSP文件是否存在的唯一方法,不幸的是,这个方法只能用一次。同样的问题在VelocityViewResolver和其它解析器中也有。当你使用这些解析器时,你最好仔细阅读它们的Javadoc,看看你需要的解析器是否无法发现不存在的视图。这个问题产生的副作用是,如果InternalResourceViewResolver解析器没有放在链的末端,InternalResourceViewResolver后面的那些解析器根本得不到使用,因为InternalResourceViewResolver总是返回一个视图!

13.5.3. 重定向(Rediret)到另一个视图

在前面我们提到过,一个控制器通常会返回视图名,然后由视图解析器解析到某种视图实现。对于像JSP这样实际上由Servlet/JSP引擎处理的视图,我们通常使用InternalResourceViewResolverInternalResourceView。这种视图实现最终会调用Servlet API的RequestDispatcher.forward(..)方法或RequestDispatcher.include()方法将用户指向最终页面。对于别的视图技术而言(比如Velocity、XSLT等等), 视图本身就会生成返回给用户的内容。

有些时候,在视图显示以前,我们可能需要给用户发一个HTTP redirect重定向指令。比如,一个控制器成功的处理了一个表单提交(数据以HTTP POST的方式发送),它最终可能委托给另一个控制器来完成剩下的工作。在这种情况下,如果我们使用内部forward,接手工作的那个控制器将会得到所有以POST方式提交的表单数据,这可能会引起潜在的混淆,干扰那个控制器的正常工作。 另一个在显示视图之前返回HTTP redirect的原因是这可以防止用户重复提交同一表单。具体一点讲,浏览器先用POST的方式提交表单,然后它接收到重定向的指令,它继续用GET的方式去下载新的页面。从浏览器的角度看,这个新的页面不是POST的返回结果,而是GET的。这样,用户不可能在点击刷新的时候不小心再次提交表单,因为刷新的结果是再次用GET 去下载表单提交后的结果页面,而不是重新提交表单。

13.5.3.1. RedirectView

在控制器中强制重定向的方法之一是让控制器生成并返回一个RedirectView的实例。在这种情况下,DispatcherServlet不会使用通常的视图解析机制,既然它已经拿到了一个(重定向)视图,它就让这个视图去做剩下的工作。

RedirectView会调用HttpServletResponse.sendRedirect()方法,其结果是给用户的浏览器发回一个HTTP redirect。所有的模型属性都被转换成以HTTP请求的访问参数。这意味着这个模型只能包含可以被简便的转换成string形式的HTTP请求访问参数的对象,比如String或者可以被转换成String的类型。

如果你使用RedirectView视图,并且它是由控制器生成的,重定向的URL最好是用Spring所提供的IoC功能注射到控制器里。这样这个URL就可以和视图名一起在上下文中被声明,而不是固化在控制器内。

13.5.3.2. redirect:前缀

尽管RedirectView帮我们达到了目的,但是如果控制器生成RedirectView的话,控制器不可避免地要知道某个请求的结果是让用户重定向到另一个页面。这不是最佳的实现,因为这使得系统不同模块之间结合得过于紧密。其实控制器不应该过问返回结果是怎么生成的,通常情况下,它应该只关心提供给它的视图名。

解决上述问题的方法是依靠redirect:前缀。如果返回的视图名包含redirect:前缀,UrlBasedViewResolver (以及它的子类) 会知道系统要生成一个HTTP redirect。 视图名其余的部分会被当作重定向URL。

这样做的最终结果跟控制器返回RedirectView是一样的,但现在控制器只需要和逻辑上的视图名打交道。 redirect:/my/response/controller.html这个逻辑视图名中的URL是当前servlet context中的相对路径。与之相比,redirect:http://myhost.com/some/arbitrary/path.html中的URL是绝对路径。 重要的是,只要这个重定向视图名和其他视图名以相同的方式注射到控制器中,控制器根本不知道重定向是否发生了。

13.5.3.3. forward:前缀

类似的,我们也可以使用包含有forward:前缀的视图名。这些视图名会被UrlBasedViewResolver和它的子类正确解析。解析的内部实现是生成一个InternalResourceView,这个视图最终会调用RequestDispatcher.forward()方法,将forward视图名的其余部分作为URL。所以,当你使用InternalResourceViewResolver/InternalResourceView,并且你所用的视图技术是JSP时,你没有必要使用这个前缀。但是,当你主要使用其它的视图技术,但仍需要对Servlet/JSP engine处理的页面强制forward时,这个forward前缀还是很有用的(但就这个问题而言,如果你不想用forward前缀,你也可以使用视图解析链)。

redirect:前缀一样,如果含有forward前缀的视图名和其他视图名一样被注入控制器,控制器根本不知道forward是否发生了。