乐趣区

SpringMVC源码分析6SpringMVC的视图解析原理


title: SpringMVC 源码分析 6:SpringMVC 的视图解析原理
date: 2018-06-07 11:03:19
tags:

- SpringMVC

categories:

- 后端
- SpringMVC

转自 [SpringMVC 视图机制详解[附带源码分析]](https://www.cnblogs.com/fangj…

本系列文章首发于我的个人博客:https://h2pl.github.io/

欢迎阅览我的 CSDN 专栏:Spring 源码解析 https://blog.csdn.net/column/…

部分代码会放在我的的 Github:https://github.com/h2pl/
<!– more –>

目录

  • 前言
  • 重要接口和类介绍
  • 源码分析
  • 编码自定义的 ViewResolver
  • 总结
  • 参考资料

前言

SpringMVC 是目前主流的 Web MVC 框架之一。

如果有同学对它不熟悉,那么请参考它的入门 blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html

本文将分析 SpringMVC 的视图这部分内容,让读者了解 SpringMVC 视图的设计原理。

重要接口和类介绍

1. View 接口

视图基础接口,它的各种实现类是无状态的,因此是线程安全的。该接口定义了两个方法:

2. AbstractView 抽象类

View 接口的基础实现类。我们稍微介绍一下这个抽象类。

首先看下这个类的属性:

再看下抽象类中接口方法的实现:

getContentType 方法直接返回 contentType 属性即可。

  render 方法:

3. AbstractUrlBasedView 抽象类

继承自 AbstractView 抽象类,增加了 1 个类型为 String 的 url 参数。

4. InternalResourceView 类

继承自 AbstractUrlBasedView 抽象类的类,表示 JSP 视图。

我们看下这个类的 renderMergedOutputModel 方法(AbstractView 抽象类定义的抽象方法,为 View 接口提供的 render 方法服务)。

5. JstlView 类

JSTL 视图,继承自 InternalResourceView,该类大致上与 InternalResourceView 类一致。

6. AbstractTemplateView 抽象类

继承自 AbstractUrlBasedView 抽象类,重写了 renderMergedOutputModel 方法,在该方法中会调用 renderMergedTemplateModel 方法,renderMergedTemplateModel 方法为新定义的抽象方法。

该抽象类有几个 boolean 属性 exposeSessionAttributes,exposeRequestAttributes。设置为 true 的话会将 request 和 session 中的键值和值丢入到 renderMergedTemplateModel 方法中的 model 这个 Map 参数中。

这个类是某些模板引擎视图类的父类。比如 FreemarkerView,VelocityView。

7. FreeMarkerView 类

继承自 AbstractTemplateView 抽象类。

直接看 renderMergedTemplateModel 方法,renderMergedTemplateModel 内部会调用 doRender 方法:

8. RedirectView 类

继承自 AbstractUrlBasedView,并实现 SmartView 接口。SmartView 接口定义了 1 个 boolean isRedirectView(); 方法。

该视图的 renderMergedOutputModel 方法主要就是通过 response.sendRedirect 进行重定向。

有关 RedirectView 方面的知识楼主另外写了 1 篇博客。http://www.cnblogs.com/fangjian0423/p/springMVC-redirectView-analysis.html

9. ViewResolver 接口

视图解释器,用来解析视图 View,与 View 接口配合使用。

该接口只有 1 个方法,通过视图名称 viewName 和 Locale 对象得到 View 接口实现类:

  View resolveViewName(String viewName, Locale locale) throws Exception;

10. AbstractCachingViewResolver 抽象类

带有缓存功能的 ViewResolver 接口基础实现抽象类,该类有个属性名为 viewAccessCache 的以 “viewName_locale” 为 key,View 接口为 value 的 Map。

该抽象类实现的 resolveViewName 方法内部会调用 createView 方法,方法内部会调用 loadView 抽象方法。

11. UrlBasedViewResolver 类

继承自 AbstractCachingViewResolver 抽象类、并实现 Ordered 接口的类,是 ViewResolver 接口简单的实现类。

该类复写了 createView 方法:

父类 (AbstractCachingViewResolver) 的 createView 方法内部会调用 loadView 抽象方法,UrlBasedViewResolver 实现了这个抽象方法:

下面对 UrlBasedViewResolver 做 1 个 test,配置如下:

<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver">
  <property name="prefix" value="/WEB-INF/view/"/>
  <property name="suffix" value=".jsp"/>
  <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceView"/>
  <property name="viewNames">
      
      <value type="java.lang.String">*</value>  
        
  </property>  
  <property name="contentType" value="text/html;charset=utf-8"/>
  <property name="attributesMap">
    <map>
      <entry key="mytest" value="mytestvalue"/>
    </map>
  </property>
  <property name="attributes">
    <props>
      <prop key="test">testvalue</prop>
    </props>
  </property>
</bean>

我们看到:以 InternalResourceView 这个 JSP 视图作为视图;viewNames 我们设置了 ,这里的 代表全部视图名(这个 viewNames 属性不设置也可以,代表全部视图名都处理);http 响应头部 contentType 信息:text/html;charset=utf-8;attributesMap 和 attributes 传入的 Map 和 Properties 参数都会被丢入到 staticAttributes 属性中,这个 staticAttributes 会被设置成 AbstractView 的 staticAttributes 属性,也就是 request 域中的参数。

我们看到 request 域中没有设置 mytest 和 testvalue 值。但是页面中会显示,因为我们配置了 attributesMap 和 attributes 参数。

如果我们把 viewNames 中的 ”*” 改成 ”index1″。那么就报错了,因为处理视图名的时候 index 匹配不上 index1。

12. InternalResourceViewResolver 类

继承自 UrlBasedViewResolver,以 InternalResourceView 作为视图,若项目中存在“javax.servlet.jsp.jstl.core.Config”该类,那么会以 JstlView 作为视图。重写了 buildView 方法,主要就是为了给 InternalResourceView 视图设置属性。

13. AbstractTemplateViewResolver 类

继承自 UrlBasedViewResolver,重写了 buildView 方法,主要就是构造 AbstractTemplateView 以及为它设置相应的属性。

14. FreeMarkerViewResolver 类

继承自 AbstractTemplateViewResolver,将视图设置为 FreeMarkerView。

15. ModelAndView 对象

顾名思义,带有视图和 Model 属性的一个模型和视图类。

值得注意的是,这个视图属性是一个 Object 类型的数据,可以直接是 View 接口的实现类或者视图名(字符串)。

源码分析

下面我们来分析 SpringMVC 处理视图的源码。

SpringMVC 在处理请求的时候,通过 RequestMappingHandlerMapping 得到 HandlerExecutionChain,然后通过 RequestMappingHandlerAdapter 得到 1 个 ModelAndView 对象,之后通过 processDispatchResult 方法处理。

processDispatchResult 方法如下:

如果配置的 ViewResolver 如下:

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

那么就是使用 InternalResourceViewResolver 来解析视图。

之前分析过,InternalResourceViewResolver 重写了 UrlBasedViewResolver 的 buildView 方法。但是还是会调用 UrlBasedViewResolver 的 buildView 方法。

最终得到 InternalResourceView 或 JstlView 视图。这两个视图的 render 方法本文介绍重要接口及类的时候已分析。

PS:DispathcerServlet 中的 viewResolvers 属性是个集合,如果存在多个 ViewResolver 对象,必定会有优先级的问题,这部分的内容请参考楼主的另一篇博客:

http://www.cnblogs.com/fangjian0423/p/spring-Ordered-interface.html

编码自定义的 ViewResolver

下面,我们就来编写自定义的 ViewResolver。

自定义的 ViewResolver 处理视图名的时候,遇到 “jsp:” 开头的会找 jsp 页面,遇到 “freemarker:” 开头的找 freemarker 页面。

public class CustomViewResolver extends UrlBasedViewResolver {

  public static final String JSP_URL_PREFIX = "jsp:";
  public static final String FTL_URL_PREFIX = "freemarker:";

  private static final boolean jstlPresent = ClassUtils.isPresent("javax.servlet.jsp.jstl.core.Config", CustomViewResolver.class.getClassLoader());

  private Boolean exposePathVariables = false;

  private boolean exposeRequestAttributes = false;

  private boolean allowRequestOverride = false;

  private boolean exposeSessionAttributes = false;

  private boolean allowSessionOverride = false;

  private boolean exposeSpringMacroHelpers = true;

  public CustomViewResolver() {this.setViewClass(FreeMarkerView.class);
  }  

  @Override
  protected AbstractUrlBasedView buildView(String viewName) throws Exception {if(viewName.startsWith(FTL_URL_PREFIX)) {return buildFreemarkerView(viewName.substring(FTL_URL_PREFIX.length()));
    } else if(viewName.startsWith(JSP_URL_PREFIX)) {
        Class viewCls = jstlPresent ? JstlView.class : InternalResourceView.class;
        return buildView(viewCls, viewName.substring(JSP_URL_PREFIX.length()), getPrefix(), ".jsp");
    } else {
        // 默认以 freemarker 处理
        return buildFreemarkerView(viewName);
    }
  }

  private AbstractUrlBasedView build(Class viewClass, String viewName, String prefix, String suffix) {AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
    view.setUrl(prefix + viewName + suffix);
    String contentType = getContentType();
    if (contentType != null) {view.setContentType(contentType);
    }
    view.setRequestContextAttribute(getRequestContextAttribute());
    view.setAttributesMap(getAttributesMap());
    if (this.exposePathVariables != null) {view.setExposePathVariables(exposePathVariables);
    }
    return view;    
  }

  private AbstractUrlBasedView buildFreemarkerView(String viewName) throws Exception {AbstractTemplateView view = (AbstractTemplateView) build(FreeMarkerView.class, viewName, "", getSuffix());
    view.setExposeRequestAttributes(this.exposeRequestAttributes);
    view.setAllowRequestOverride(this.allowRequestOverride);
    view.setExposeSessionAttributes(this.exposeSessionAttributes);
    view.setAllowSessionOverride(this.allowSessionOverride);
    view.setExposeSpringMacroHelpers(this.exposeSpringMacroHelpers);
    return view;      
  }

  //get set 方法省略

}

xml 配置:

<bean class="org.format.demo.support.viewResolver.CustomViewResolver">
  <property name="prefix" value="/WEB-INF/view/"/>
  <property name="suffix" value=".ftl"/>
  <property name="contentType" value="text/html;charset=utf-8"/>
  <property name="exposeRequestAttributes" value="true"/>
  <property name="exposeSessionAttributes" value="true"/>
  <property name="exposeSpringMacroHelpers" value="true"/>
  <property name="requestContextAttribute" value="request"/>
</bean>
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
  <property name="templateLoaderPath" value="/WEB-INF/view/"/>
  <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</prop>
      <prop key="date_format">yyyy-MM-dd</prop>
      <prop key="number_format">#.##</prop>
    </props>
  </property>
</bean>

简单解释一下:CustomViewResolver 解析视图名的时候,判断 “jsp:” 和 “freemarker:” 开头的名字,如果是 “jsp:” 开头的,如果有 JSTL 依赖,构造 JSTLView 视图,否则构造 InternalResourceView 视图。如果是 “freemarker:” 构造 FreemarkerView。在构造视图之前分别会设置一些属性。

xml 配置:配置 prefix 是为了给 jsp 视图用的,freemarker 视图不需要 prefix,因为 FreemarkerView 内部会使用配置的 FreeMarkerConfigurer,并用 FreeMarkerConfigurer 内部的 templateLoaderPath 属性作为前缀,配置的 suffix 是为了让 FreemarkerView 使用,当后缀。

最后附上 Controller 代码:

@Controller
@RequestMapping(value = "/tvrc")
public class TestViewResolverController {@RequestMapping("jsp")
    public ModelAndView jsp(ModelAndView view) {view.setViewName("jsp:trvc/index");
        return view;
    }

    @RequestMapping("/ftl")
    public ModelAndView freemarker(ModelAndView view) {view.setViewName("freemarker:trvc/index");
        return view;
    }

}

视图 /WEB-INF/view/trvc/index.jsp 中的的内容是输出 <h2>This is jsp page</h2>

视图 /WEB-INF/view/trvc/index.ftl 中的的内容是输出 <h2>This is freemarker page</h2>

总结

本文分析了 SpringMVC 中的视图机制,View 和 ViewResolver 这两个接口是视图机制的核心,并分析了几个重要的 View 和 ViewResolver 接口实现类,最终写了一个区别 jsp 和 freemarker 视图的 ViewResolver 实现类,让读者更加理解视图机制。

希望这篇文章能帮助读者了解 SpringMVC 视图机制。

文中难免有错误,希望读者能够指明出来。

退出移动版