Spring 4 官方文档学习(十一)Web MVC 框架之resolving views 解析视图

时间:2021-11-29 22:36:33

接前面的Spring 4 官方文档学习(十一)Web MVC 框架,那篇太长,故另起一篇。

针对web应用的所有的MVC框架,都会提供一种呈现views的方式。Spring提供了view resolvers,可以让你在浏览器中render model,而不必绑定到某种特定的view技术上。开箱即用,例如,Spring可以让你使用JSPs、Velocity目标和XSLT views。See Chapter 23, View technologies for a discussion of how to integrate and use a number of disparate view technologies.

Spring处理views的非常重要的两个接口是ViewResolver和View。ViewResolver提供了view names和实际的views之间的映射。View接口则致力于request的准备工作、将request交给一种具体的view技术。

1、使用ViewResolver接口来resolve views

在前面有提到,Spring MVC controllers中的所有handler methods都必须resolve到一个logical view name,显式的(如返回String、View、ModelAndView),或隐式的(例如,基于惯例)。Spring中的views都以logical view name呈现,并由一个view resolver来resolve。 Spring 提供了很多view resolvers。下表列出了部分:

Table 22.3. View resolvers

ViewResolver Description
AbstractCachingViewResolver

Abstract view resolver that caches views. Often views need preparation before they can be used; extending this view resolver provides caching.

可以缓存views的抽象view resolver。继承其可以提供缓存功能。

XmlViewResolver

Implementation of ViewResolver that accepts a configuration file written in XML with the same DTD as Spring’s XML bean factories. The default configuration file is /WEB-INF/views.xml.

ViewResolver的实现,接收XML配置文件。默认的配置文件是 /WEB-INF/views.xml。

ResourceBundleViewResolver

Implementation of ViewResolver that uses bean definitions in a ResourceBundle, specified by the bundle base name. Typically you define the bundle in a properties file, located in the classpath. The default file name is views.properties.

VIewResolver的实现,可以使用ResourceBundle中的bean definitions,该ResourceBundle由bundle base name指定。通常会在一个properties文件中定义该bundle。默认的文件名是view.properties。

UrlBasedViewResolver

Simple implementation of the ViewResolver interface that effects the direct resolution of logical view names to URLs, without an explicit mapping definition. This is appropriate if your logical names match the names of your view resources in a straightforward manner, without the need for arbitrary mappings.

VIewResolver的简单实现,将logical view names直接解析成URLs,不需要显式的映射定义。如果你的logical names直接匹配你的view resources名字,用它很合适。

InternalResourceViewResolver

Convenient subclass of UrlBasedViewResolver that supports InternalResourceView (in effect, Servlets and JSPs) and subclasses such as JstlView and TilesView. You can specify the view class for all views generated by this resolver by using setViewClass(..). See the UrlBasedViewResolver javadocs for details.

UrlBasedViewResovler的子类,很便利,支持InternalResourceView (Servlet和JSPs) 和子类如JstlView和TilesView。所有由该resolver生成的views都可以使用setViewClass(..)来指定view class。详见javadocs。

VelocityViewResolver / FreeMarkerViewResolver

Convenient subclass of UrlBasedViewResolver that supports VelocityView (in effect, Velocity templates) or FreeMarkerView ,respectively, and custom subclasses of them.

UrlBasedViewResolver的子类,很便利,支持VelocityView (Velocity模板)或FreeMarkerView,以及相应的,它们的自定义子类。

ContentNegotiatingViewResolver

Implementation of the ViewResolver interface that resolves a view based on the request file name or Accept header. See Section 22.5.4, “ContentNegotiatingViewResolver”.

ViewResolver的实现,可以resolve基于request file name 或 Accept header的view。

如果使用JSP view技术,可以使用URLBasedViewResolver。 该view resolver 将view name翻译成URL,并将request交给RequestDispatcher来render:

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

当返回test作为logical view name时,该resolver会将request forward给RequestDispatcher,后者会将request发送到 /WEB-INF/jsp/test.jsp。

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

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

该resolver检查由basename指定的ResourceBundle,针对每个由其resolve的view,它会使用property [viewname].(class)的值作为view class,使用property [viewname].url的值作为view url。 下一章会给出例子。 如你所见,你可以指定一个parent view。

注意:AbstractCachingViewResolver的子类,会缓存他们resolve的view实例。缓存提高了特定的view技术的性能。也可以通过设置其cache property为false来关闭缓存。 甚至,如果你必须在运行时来刷新一个特定的view(例如,当一个Velocity模板被修改过),你可以使用removeFromCache(String viewName, Locale loc) 方法。--修改模板,立即生效!!!

2、链接ViewResolvers

Spring支持多个view resolvers。 因此,你可以链接它们以及,例如,在特定的环境下覆盖特定的views。你可以将多于一个的resolver添加到你的application context中,使用order property来指定顺序。记住,越高的order,在chain中越靠后。

下面的例子中,view resolver chain由两个resolver组成,一个是InternalResourceViewResolver -- 通常会被自动作为最后一个resolver,一个是XmlViewResolver -- 用于Excel views。 InternalResourceViewResolver不支持 Excel views。

<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>

如果一个特定的view resolver没有产生一个view,Spring会检查context中的其他view resolvers。如果其他view resolvers存在,Spring会继续检查他们,直到一个view被resolved了。如果最终没有返回一个view,Spring会抛出ServletException。

视图解析器规定如果没有找到视图就返回null。但不是所有的view resolvers都这样做,因为某些情况下,resolver不能判断view是否存在。例如,InternalResourceViewResolver内部使用了RequestDispatcher,dispatching是唯一的方式来确认一个JSP是否存在,但这样的行为只能被执行一次。VelocityViewResolver和其它的一些解析器也这样,区分这些视图解析器能否在找不到视图的情况下返回null,最好的方法就是看官方文档中它是否支持了。

所以需要将InternalResourceViewResolver放在最后,因为它总是返回一个view!

  1. The contract of a view resolver specifies that a view resolver can return null to indicate the view could not be found. Not all view resolvers do this, however, because in some cases, the resolver simply cannot detect whether or not the view exists. For example, the InternalResourceViewResolver uses the RequestDispatcher internally, and dispatching is the only way to figure out if a JSP exists, but this action can only execute once. The same holds for the VelocityViewResolver and some others. Check the javadocs of the specific view resolver to see whether it reports non-existing views. Thus, putting an InternalResourceViewResolver in the chain in a place other than the last results in the chain not being fully inspected, because the InternalResourceViewResolver will always return a view!

3、 重定向到views

如前所述,controller通常返回一个逻辑视图名称,然后一个view resolver会将其resolve到特定的view技术。

对于诸如JSPs之类的view技术来说(JSPs需要由Servlet或JSP引擎来处理),该resolution通常是由InternalResourceViewResolver和InternalResourceView共同处理的,这样会导致一个内部的forward或使用Servlet API的RequestDispatcher.forward(..)或RequestDispatcher.include()。对于其他view技术来说,例如Velocity、XSLT等等,view本身会将内容直接写到响应流。

有时候需要将HTTP redirect返回客户端 -- 在view被渲染之前。例如,当一个controller通过POST data被调用时,其响应是代理到另一个controller(例如在成功提交时)。这种情况下,常规的内部forward意味着另一个controller也会看见POST data,这可能导致问题。另一个原因是消灭用户反复提交的可能。在这种情景下,浏览器会先发出一个POST,然后接收响应,重定向到一个不同的URL,最终浏览器会执行GET操作。因此,从浏览器角度看,当前页面不会反映POST的结果,而是GET的结果。最终效果就是,用户不会因为刷新而意外的重复POST操作。

重定向视图

一种强制重定向的方式是让controller返回一个Spring RedirectView实例。这时,DispatcherServlet不会使用常规view resolution机制。因为已经含有redirect view,DispatcherServlet会简单地让view做其本身的工作。RedirectView的工作就是调用HttpServletResponse.sendRedirect()。

如果你使用RedirectView,且view是由controller本身创建的,我们推荐你配置redirect URL。

传递数据给重定向目标

默认,所有model attributes都被暴露为URI模板变量,在重定向URL中使用。基本类型或基本类型的集合/数组的attributes,都会被自动附加为query parameters。

可以在@RequestMapping method中声明RedirectAttributes类型的参数,用其来指定具体的可用于RedirectView的attributes。-- 如果该method重定向,那RedirectAttributes的内容会被使用。否则正常使用。

RequestMappingHandlerAdapter提供了一个flag叫做"ignoreDefaultModelOnRedirect",可用于指明默认Model的内容是否该被忽略--在controller method redirect情况下。此时controller method应该声明RedirectAttributes,如果不声明,no attributes会被传给RedirectView。MVC namespace和MVC Java config都将其设为false,以向后兼容。但对于新项目,我们推荐将其设为true。

注意,当前请求的URI模板变量会自动可用 -- 当展开一个重定向URL时,因此不需要显式的使用Model或RedirectAttributes添加。例如:

@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}

另一种将数据传给重定向目标的方式是通过Flash Attributes。不像其他的重定向attributes,flash attributes是被保存在HTTP session中的 (因此也不会呈现在URL中)。

重定向:前缀

当RedirectView工作的很好时,如果controller本身创建了RedirectView,就无法避免controller意识到重定向发生了。这是亚优化的,且耦合了。controller不应关心response如何被处理。它应只操作被注入的view names。

特殊的前缀"redirect:"可以让你做到这样。如果view name带有该前缀,UrlBasedViewResolver(和其所有子类)会将其识别为特别的指示:需要重定向。该view name的其他部分会被当成redirect URL来对待。

其效果与返回RedirectView完全一致,但现在controller只需要简单的操作逻辑视图名。一个逻辑视图名如"redirect:/myapp/some/resource" 会重定向到当前Servlet context的相对路径,而"redirect:http://..."则会重定向到绝对路径。

注意,若controller带有@ResponseStatus注解,该注解的值会优于RedirectView中设置的response status值。

转发:前缀

在view names中同样也可以使用"forward:"前缀,UrlBasedViewResolver和其所有子类会resolve它。这创建了一个InternalResourceView(最终会RequestDispatcher.forward())--围绕view name的其他部分。因此,该前缀在InternalResourceViewResolver和InternalResourceView(如JSPs)时无效。当你使用其他view技术,且仍然想强制forward一个资源以被Servlet/JSP引擎处理时,它会很有用。(或者,你可能性chain多个view resolvers)

4、 ContentNegotiatingViewResolver

本身不会resolver views,而是代理到其他view resolvers,选择类似客户端请求的表现形式(representation)的view。客户端请求representation有两种策略

  • 为每个资源使用不同的URI,通常在URI中使用不同的文件扩展名。例如,URI "http://www.example.com/users/fred.pdf",会请求一个PDF representation,而"http://www.example.com/users/fred.xml" 则请求一个XML representation。
  • 使用相同的URI,但设置Accept HTTP request header 以列出它理解的media types。例如,设为"application/pdf"、"text/xml"。该策略就是内容协商 content negotiation

Accept header的一个问题就是,无法在浏览器中设置。例如,在Firefox中,其被固定为: "Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"。因此,使用不同的URI更常见。

为了支持一个资源的不同的representations,Spring提供了ContentNegotiatingViewResolver来resolve基于文件扩展名或Accept header的view。ContentNegotiatingViewResolver本身不会执行view resolution,而是代理到一个view resolver列表 -- 可以通过bean property "ViewResolvers"来指定。

ContentNegotiatingViewResolver选择一个合适的View来处理请求 -- 通过比较请求media type(s)和View支持的media type (也被叫做Content-Type)。列表中第一个兼容Content-Type的View会返回representation给客户端。如果ViewResolver chain无法提供兼容的view,会咨询通过DefaultViews property指定的view 列表。后者适合单例Views--可以为当前资源渲染合适的representation而无视逻辑视图名。Accept header可能含有通配符,例如"text/*",此时Content-Type为"text/xml"的View是兼容适配。

为了支持view的自定义resolution -- 基于文件扩展名,使用ContentNegotiationManager。

这里是ContentNegotiatingViewResolver的一个示例配置:

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</list>
</property>
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</list>
</property>
</bean>
<bean id="content" class="com.foo.samples.rest.SampleContentAtomView"/>

InternalResourceViewResolver处理 view names和JSP页面 的翻译工作,而BeanNameViewResolver返回基于bean name的view。在该例中,content bean是继承自AbstractAtomFeedView的类,会返回一个Atom RSS feed。详见Atom Views。

在上面的配置中,如果请求是以".html"为扩展名,该view resolver会查找匹配"text/html" media type的view。InternalResourceViewResolver提供了匹配"text/html"的view。 如果请求以".atom"为扩展名,则会查找匹配"application/atom+xml" media type的view。该view由BeanNameViewResolver提供,会映射到SampleContentAtomView--如果返回的view name是"content"。 如果".json",DefaultViews列表中的MappingJackson2JsonView实例会被选中,而无视view name (卧槽,这句话,深究)。或者,客户端请求可以使用Accept header,同样的处理。

注意:如果ContentNegotiatingViewResolver 的ViewResolvers列表没有显式的配置,它会自动使用所有定义在application context中的ViewResolvers。

"http://localhost/content.atom" 和Accept header为"application/atom+xml"的"http://localhost/content" 示例:

@Controller
public class ContentController {
private List<SampleContent> contentList = new ArrayList<SampleContent>();
@GetMapping("/content")
public ModelAndView getContent() {
ModelAndView mav = new ModelAndView();
mav.setViewName("content");
mav.addObject("sampleContentList", contentList);
return mav;
}
}