Spring Mvc 视图解析

在 Spring Mvc 中,我们自己编写的控制器方法 (Controller) 并没有直接去渲染结果,使用 response 去输出到浏览器。方法返回的是 ModelAndView,甚至只是一个 String 类型的视图名,那 Spring Mvc 是怎么把模型数据填充到视图的呢?如果控制器能通过逻辑视图名来了解视图的话,那 Spring Mvc 如何确定使用哪一个视图实现来渲染模型呢?

了解视图解析

解析过程

  • DispatcherServlet
  • HandlerMapping
  • HandlerAdapter
  • ViewResolver
  • View

对于控制器的方法,无论其返回值是 String、View、ModelMap 或是 ModelAndView,Spring MVC 都会在内部HandlerAdapter 将它们封装为一个 ModelAndView 对象再进行返回

1
2
3
4
5
6
public interface HandlerAdapter {

ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

...
}

然后 Spring MVC 会借助视图解析器 ViewResolver 得到最终的视图对象 View

1
2
3
4
public interface ViewResolver {
//通过 viewname 解析 view
View resolveViewName(String viewName, Locale locale) throws Exception;
}

View 接口表示一个响应给用户的视图如 jsp 文件,pdf 文件,html 文件。getContentType 方法会返回视图的内容类型,render 方法接收模型以及 Servlet 的 request 和 response 对象,并将输出结果渲染到 response 中

1
2
3
4
5
6
7
8
9
public interface View {
...

default String getContentType() {
return null;
}

void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}

ViewResolver 实现

下面是 Spring 中 ViewResolver 的一些实现

视图解析器 描述
AbstractCachingViewResolver 一个抽象的视图解析器类,提供了缓存视图的功能。通常视图在能够被使用之前需要经过准备。继承这个基类的视图解析器即可以获得缓存视图的能力
XmlViewResolver 接受使用与 Spring 的 XML bean工厂相同的 DTD 编写的 XML 配置文件。默认配置文件是 /WEB-INF/views.xml
ResourceBundleViewResolver 将视图解析为资源bundle(一般为属性文件)
UrlBasedViewResolver 直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义
InternalResourceViewResolver UrlBasedViewResolver 的子类,将视图解析为 Web 应用的内部资源(一般为JSP)
FreeMarkerViewResolver UrlBasedViewResolver 的子类,将视图解析为 FreeMarker 模板
ContentNegotiatingViewResolver 根据请求文件名或 Accept 标头解析视图,通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器

视图解析

重定向和转发

  • redirect:
  • forward:

视图名称中使用前缀 redirect: 执行重定向,UrlBasedViewResolver(及其子类) 会认为这是一条需要重定向的指令,视图名称的其余部分是重定向URL。如果是转发的话,使用前缀 forward:

1
2
3
4
@RequestMapping("/index")
public String index(){
return "redirect:index.jsp";
}

我们可以看一下 UrlBasedViewResolver 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {
...
@Override
protected View createView(String viewName, Locale locale) throws Exception {

if (!canHandle(viewName, locale)) {
return null;
}

// Check for special "redirect:" prefix.
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}

// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}

// Else fall back to superclass implementation: calling loadView.
return super.createView(viewName, locale);
}
...
}

使用 JSP 视图

有一些视图解析器(如 ResourceBundleViewResolver)会直接将逻辑视图名映射为特定的 View 接口实现,而InternalResourceViewResolver 所采取的方式并不那么直接。它遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个 Web 应用中视图资源的物理路径

1
2
3
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/"
p:suffix=".jsp" />

InternalResourceViewResolver 配置就绪之后,它就会将逻辑视图名解析为 JSP 文件路径。"index" 会被解析成 "/WEB-INF/jsp/index.jsp""blog/index" 会被解析成 "/WEB-INF/jsp/blog/index.jsp"

如果这些 JSP 使用 JSTL 标签来处理格式化和信息的话,那么我们会希望 InternalResourceViewResolver 将视图解析为 JstlView

1
2
3
4
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/"
p:suffix=".jsp"
p:viewClass="org.springframework.web.servlet.view.JstlView" />

如下面的例子 "index" 会被解析成 "/WEB-INF/jsp/index.jsp"

1
2
3
4
5
@RequestMapping("/index")
public String index(String name,Model model){
model.addAttribute("name",name);
return "index";
}

Freemarker 模板视图解析

添加 maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>

FreeMarker 配置和解析器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- FreeMarker 配置 -->
<bean id="freeMarkerConfigurer" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/" />
<property name="defaultEncoding" value="UTF-8" />
<property name="freemarkerSettings">
<props>
<prop key="template_update_delay">10</prop>
<prop key="locale">zh_CN</prop>
<prop key="datetime_format">yyyy-MM-dd HH:mm:ss</prop>
<prop key="date_format">yyyy-MM-dd</prop>
<prop key="number_format">#.##</prop>
</props>
</property>
</bean>

<!-- FreeMarker视图解析 -->
<bean id="freeMarkerViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" />
<property name="contentType" value="text/html;charset=UTF-8" />
<property name="prefix" value="/ftl/" />
<property name="suffix" value=".ftl" />
<property name="exposeRequestAttributes" value="true" />
<property name="exposeSessionAttributes" value="true" />
<property name="exposeSpringMacroHelpers" value="true" />
</bean>

Thymeleaf 模板视图解析

  • ThymeleafViewResolver:将逻辑视图名称解析为 Thymeleaf 模板视图
  • SpringTemplateEngine:处理模板并渲染结果
  • TemplateResolver:加载Thymeleaf模板

添加 maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>

配置 Thymeleaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/templates/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
<property name="cacheable" value="false" />
<property name="characterEncoding" value="UTF-8"/>
</bean>

<bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>

<bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="characterEncoding" value="UTF-8" />
</bean>

ThymeleafViewResolver 是 Spring MVC 中 ViewResolver 的实现类。它把接收的逻辑视图名称解析一个 Thymeleaf 模板视图

ThymeleafViewResolver bean 中注入了一个对 SpringTemplateEngine bean 的引
用。SpringTemplateEngine 会在 Spring 中启用 Thymeleaf 引擎,用来解析模板,并基于这些模板渲染结果。

多视图解析处理

当我们同时使用多种视图解析的时候,该如何解析呢?

这里要分情况讨论了

多视图解析处理

可以声明多个解析器,并在必要时通过设置 order 属性指定排序。order 属性值越高,视图解析器在链中的位置越晚。

一个 ViewResolver 是可以返回 null 的,表示无法找到该视图。如果一个视图解析器不能返回一个视图,那么 Spring 会继续检查上下文中其他的视图解析器。此时如果存在其他的解析器,Spring 会继续调用它们,直到产生一个视图返回为止。

但是 InternalResourceViewResolver 会执行调度 RequestDispatcher 来确定 jsp 是否存在。因此,必须把 InternalResourceViewResolver 在视图解析器链中的顺序设置为最后一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!--jsp 视图解析-->
<!-- 这里jsp的 order 要设置比freemarker的大,不然都会解析成 jsp -->
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/jsp/"
p:suffix=".jsp"
p:viewClass="org.springframework.web.servlet.view.JstlView"
p:order="1" />

<!-- FreeMarker 配置 -->
<bean id="freeMarkerConfigurer" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/" />
<property name="defaultEncoding" value="UTF-8" />
<property name="freemarkerSettings">
<props>
<prop key="template_update_delay">10</prop>
<prop key="locale">zh_CN</prop>
<prop key="datetime_format">yyyy-MM-dd HH:mm:ss</prop>
<prop key="date_format">yyyy-MM-dd</prop>
<prop key="number_format">#.##</prop>
</props>
</property>
</bean>

<!-- FreeMarker视图解析 -->
<bean id="freeMarkerViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" />
<property name="contentType" value="text/html;charset=UTF-8" />
<property name="prefix" value="/ftl/" />
<property name="suffix" value=".ftl" />
<property name="exposeRequestAttributes" value="true" />
<property name="exposeSessionAttributes" value="true" />
<property name="exposeSpringMacroHelpers" value="true" />
<property name="order" value="0"/>
</bean>

这里jsp的 order 要设置比freemarker的大,不然都会解析成 jsp

以上的配置,假如逻辑视图名称为 test,它会先在 /ftl/ 下找 test.ftl 模板,找到就解析成 freemarker,没有就使用 jsp 解析器去找 jsp 文件

另外可以配置 viewNames 属性,对视图名称加以区分。(如 同时配置了 thymeleaf 和 jsp )

配置 viewNames 属性,值可以使用通配符匹配,如
<property name="viewNames" value="*.html,*.xhtml" />
<property name="viewNames" value="thymeleaf/*" />
<property name="viewNames" value="*.jsp" />

内容协商

ContentNegotiatingViewResolver 能根据请求文件名或Accept标头解析视图,通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器

比如请求只是 accept-type 不同,如 /aa, HTTP Request Header 中的 Accept 分别是 text/jsp, text/pdf, text/xml,text/json, 无 Accept 请求头

ContentNegotiatingViewResolver 可以一个 @RequestMapping,返回多个不同的 View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="favorParameter" value="true"/>
<property name="favorPathExtension" value="true"/>
<property name="mediaTypes">
<map>
<entry key="xml" value="application/xml"/>
<entry key="json" value="application/json"/>
<entry key="xls" value="application/vnd.ms-excel"/>
</map>
</property>
<property name="viewResolvers">
<list>
<ref bean="jaxb2MarshallingXmlViewResolver"></ref>
<ref bean="jsonViewResolver"></ref>
<ref bean="excelViewResolver"></ref>
</list>
</property>
</bean>

自定义多视图解析器

另外,还可以自定义多视图解析器,根据返回视图名称的后缀不同或是参数值不同分别委托给其他的视图解析器。这里就不介绍了

MVC 配置视图解析器

MVC 配置简化了视图解析器的注册,需要使用 mvc 命名模式

以下示例使用 JSP 和 Jackson 来配置内容协商视图解析

1
2
3
4
5
6
7
8
9
10
<mvc:annotation-driven/>

<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
</mvc:view-resolvers>

以下是 FreeMarker 的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mvc:annotation-driven/>

<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache="false"/>
</mvc:view-resolvers>

<mvc:freemarker-configurer>
<mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>