Spring MVC学习教程之视图深入解析

前言

在RequestMappingHandlerAdapter对request进行了适配,并且调用了目标handler之后,其会返回一个ModelAndView对象,该对象中主要封装了两个属性:view和model。其中view可以是字符串类型也可以是View类型,如果是字符串类型,则表示逻辑视图名,如果是View类型,则其即为我们要转换的目标view;这里model是一个Map类型的对象,其保存了渲染视图所需要的属性。本文主要讲解Spring是如何通过用户配置的ViewResolver来对视图进行解析,并且声称页面进行渲染的。

首先我们来看一个比较典型的ViewResolver配置:

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

这里配置的ViewResolver是InternalResourceViewResolver,其主要有两个属性:prefix和suffix。在进行视图解析时,如果ModelAndView中的view是字符串类型的,那么要解析的视图存储位置就通过“prefix + (String)view + suffix”的格式生成要解析的文件路径,并且将其封装为一个View对象,最后通过View对象来渲染具体的视图。前面讲到,ModelAndView中view也可以是View类型的,如果其是View类型的,那么这里就可以跳过第一步,直接使用其提供的View对象进行视图解析了。

由上面的讲解可以看出,对于视图的解析可以分为两个步骤:①解析逻辑视图名;②渲染视图。对应于这两步,Spring也抽象了两个接口:ViewResolver和View,这两个接口的声明分别如下:

public interface ViewResolver {
 // 通过逻辑视图名和用户地区信息生成View对象
 View resolveViewName(String viewName, Locale locale) throws Exception;
}
public interface View {
 // 获取返回值的contentType
 default String getContentType() {
 return null;
 }

 // 通过用户提供的模型数据与视图信息渲染视图
 void render(@Nullable Map<String, ?> model, HttpServletRequest request,
  HttpServletResponse response) throws Exception;
}

从上面两个接口的声明可以看出,ViewResolver的作用主要在于通过用户提供的逻辑视图名根据一定的策略生成一个View对象,而View接口则负责根据视图信息和需要填充的模型数据进行视图的渲染。这里我们首先看InternalResourceViewResolver是如何解析视图名的,如下是其具体实现方式:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
 // 判断当前ViewResolver是否设置了需要对需要解析的视图进行缓存,如果不需要缓存,
 // 则每次请求时都会重新解析生成视图对象
 if (!isCache()) {
  // 根据视图名称和用户地区信息创建View对象
  return createView(viewName, locale);
 } else {
  // 如果可以对视图进行缓存,则首先获取缓存使用的key,然后从缓存中获取该key,如果没有取到,
  // 则对其进行加锁,再次获取,如果还是没有取到,则创建一个新的View,并且对其进行缓存。
  // 这里使用的是双检查法来判断缓存中是否存在对应的逻辑视图。
  Object cacheKey = getCacheKey(viewName, locale);
  View view = this.viewAccessCache.get(cacheKey);
  if (view == null) {
   synchronized (this.viewCreationCache) {
    view = this.viewCreationCache.get(cacheKey);
    if (view == null) {
     view = createView(viewName, locale);
     // 这里cacheUnresolved指的是是否缓存默认的空视图,UNRESOLVED_VIEW是
     // 一个没有任何内容的View
     if (view == null && this.cacheUnresolved) {
      view = UNRESOLVED_VIEW;
     }
     if (view != null) {
      this.viewAccessCache.put(cacheKey, view);
      this.viewCreationCache.put(cacheKey, view);
      if (logger.isTraceEnabled()) {
       logger.trace("Cached view [" + cacheKey + "]");
      }
     }
    }
   }
  }
  return (view != UNRESOLVED_VIEW ? view : null);
 }
}

上面代码中,InternalResourceViewResolver主要是判断了当前是否配置了需要缓存生成的View对象,如果需要缓存,则从缓存中取,如果没有配置,则每次请求时都会重新生成新的View对象。这里我们继续看其是如何创建视图的:

@Override
protected View loadView(String viewName, Locale locale) throws Exception {
 // 使用逻辑视图名按照指定规则生成View对象
 AbstractUrlBasedView view = buildView(viewName);
 // 应用声明周期函数,也就是调用View对象的初始化函数和Spring用于切入bean创建的
 // Processor和Aware函数
 View result = applyLifecycleMethods(viewName, view);
 // 检查view的准确性,这里默认始终返回true
 return (view.checkResource(locale) ? result : null);
}

// 这里buildView()方法主要是根据逻辑视图名生成一个View对象
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
 // 对于InternalResourceViewResolver而言,其返回的View对象的
 // 具体类型是InternalResourceView
 Class<?> viewClass = getViewClass();
 Assert.state(viewClass != null, "No view class");

 // 使用反射生成InternalResourceView对象实例
 AbstractUrlBasedView view = (AbstractUrlBasedView)
  BeanUtils.instantiateClass(viewClass);
 // 这里可以看出,InternalResourceViewResolver获取目标视图的方式就是将用户返回的
 // viewName与prefix和suffix进行拼接,以供View对象直接读取
 view.setUrl(getPrefix() + viewName + getSuffix());

 // 设置View的contentType属性
 String contentType = getContentType();
 if (contentType != null) {
  view.setContentType(contentType);
 }

 // 设置contextAttribute和attributeMap等属性
 view.setRequestContextAttribute(getRequestContextAttribute());
 view.setAttributesMap(getAttributesMap());

 // 这了pathVariables表示request请求url中的属性,这里主要是设置是否将这些属性暴露到视图中
 Boolean exposePathVariables = getExposePathVariables();
 if (exposePathVariables != null) {
  view.setExposePathVariables(exposePathVariables);
 }

 // 这里设置的是是否将Spring的bean暴露在视图中,以供给前端调用
 Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
 if (exposeContextBeansAsAttributes != null) {
  view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
 }

 // 设置需要暴露给前端页面的bean名称
 String[] exposedContextBeanNames = getExposedContextBeanNames();
 if (exposedContextBeanNames != null) {
  view.setExposedContextBeanNames(exposedContextBeanNames);
 }

 return view;
}

protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
 ApplicationContext context = getApplicationContext();
 if (context != null) {
  // 对生成的View对象应用初始化方法,主要包括InitializingBean.afterProperties()和一些
  // Processor,Aware方法
  Object initialized = context.getAutowireCapableBeanFactory()
   .initializeBean(view, viewName);
  if (initialized instanceof View) {
   return (View) initialized;
  }
 }
 return view;
}

从上面对于视图名称的解析,可以看出,其主要做了四部分工作:①实例化View对象;②设置目标视图地址;③初始化视图的一些基本属性,如需要暴露的bean对象;④调用View对象的初始化方法对其进行初始化。从这里的生成View对象的过程也可以看出,ViewResolver生成的View对象只是保存了目标view的地址,而对其加载和渲染的过程主要是委托给了View对象进行的。下面我们就来看一下InternalResourceView是如何结合具体的model来渲染视图的:

@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
  HttpServletResponse response) throws Exception {

 if (logger.isTraceEnabled()) {
  logger.trace("Rendering view with name '" + this.beanName + "' with model "
   + model + " and static attributes " + this.staticAttributes);
 }

 // 这里主要是将request中pathVariable,staticAttribute与用户返回的model属性
 // 合并为一个Map对象,以供给后面对视图的渲染使用
 Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
 // 判断当前View对象的类型是否为文件下载类型,如果是文件下载类型,则设置response的
 // Pragma和Cache-Control等属性值
 prepareResponse(request, response);
 // 通过合并的model数据以及视图地址进行视图的渲染
 renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

这里对于视图的渲染主要分为了三步:①合并用户返回的model数据和request中的pathVariable与staticAttribute等数据;②判断当前是否为文件下载类型的视图解析,如果是,则设置Pragma和Cache-Control等header;③通过合并的模型数据和request请求对视图进行渲染。这里我们主要看一下renderMergedOutputModel()方法是如何对视图进行渲染的:

@Override
protected void renderMergedOutputModel(Map<String, Object> model,
  HttpServletRequest request, HttpServletResponse response) throws Exception {
 // 这里主要是对model进行遍历,将其key和value设置到request中,当做request的
 // 一个属性供给页面调用
 exposeModelAsRequestAttributes(model, request);
 // 提供的一个hook方法,默认是空实现,用于用户进行request属性的自定义使用
 exposeHelpers(request);

 // 检查当前是否存在循环类型的视图名称解析,主要是根据相对路径进行判断视图名是无法解析的
 String dispatcherPath = prepareForRendering(request, response);

 // 获取当前request的RequestDispatcher对象,该对象有两个方法:include()和forward(),
 // 用于对当前的request进行转发,其实也就是将当前的request转发到另一个url,这里的另一个
 // url就是要解析的视图地址,也就是说进行视图解析的时候请求的对于文件的解析实际上相当于
 // 构造了另一个(文件)请求,在该请求中对文件内容进行渲染,从而得到最终的文件。这里的
 // include()方法表示将目标文件引入到当前文件中,与jsp中的include标签作用相同;
 // forward()请求则表示将当前请求转发到另一个请求中,也就是目标文件路径,这种转发并不会
 // 改变用户浏览器地址栏的请求地址。
 RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
 if (rd == null) {
  throw new ServletException("Could not get RequestDispatcher for [" + getUrl()
   + "]: Check that the corresponding file exists within your web "
   + "application archive!");
 }

 // 判断当前是否为include请求,如果是,则调用RequestDispatcher.include()方法进行文件引入
 if (useInclude(request, response)) {
  response.setContentType(getContentType());
  if (logger.isDebugEnabled()) {
   logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '"
    + getBeanName() + "'");
  }
  rd.include(request, response);
 } else {
  if (logger.isDebugEnabled()) {
   logger.debug("Forwarding to resource [" + getUrl()
    + "] in InternalResourceView '" + getBeanName() + "'");
  }
  // 如果当前不是include()请求,则直接使用forward请求将当前请求转发到目标文件路径中,
  // 从而渲染该视图
  rd.forward(request, response);
 }
}

上述代码就是进行视图渲染的核心逻辑,上述逻辑主要分为两个步骤:①将需要在页面渲染使用的model数据设置到request中;②按照当前请求的方式(include或forward)来将当前请求转发到目标文件中,从而达到目标文件的渲染。从这里可以看出,实际上对于Spring而言,其对页面的渲染并不是在其原始的request中完成的。

本文首先讲解了Spring进行视图渲染所需要的两大组件ViewResolver和View的关系,然后以InternalResourceViewResolver和InternalResourceView为例讲解Spring底层是如何解析一个view,并且渲染该View的。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 自定义spring mvc的json视图实现思路解析

    场景 我们团队现在面临着多端数据接口对接的问题,为了解决这个问题我们定义了接口对接的规范, 前端(安卓,Ios,web前端)和后端进行了数据的格式规范的讨论,确定了json的数据格式: { "code":"200", "data":{"":""}, "message":"处理成功" } { "code":"300", "

  • 浅谈SpringMVC之视图解析器(ViewResolver)

    SpringMVC中的视图解析器的主要作用就是将逻辑视图转换成用户可以看到的物理视图. 当用户对SpringMVC应用程序发起请求时,这些请求都会被SpringMVC的DispatcherServlet处理,通过处理器找到最为合适的HandlerMapping定义的请求映射中最为合适的映射,然后通过HandlerMapping找到相对应的Handler,然后再通过相对应的HandlerAdapter处理该Handler.返回结果是一个ModelAndView对象,当该ModelAndView对象

  • Spring MVC学习教程之视图深入解析

    前言 在RequestMappingHandlerAdapter对request进行了适配,并且调用了目标handler之后,其会返回一个ModelAndView对象,该对象中主要封装了两个属性:view和model.其中view可以是字符串类型也可以是View类型,如果是字符串类型,则表示逻辑视图名,如果是View类型,则其即为我们要转换的目标view:这里model是一个Map类型的对象,其保存了渲染视图所需要的属性.本文主要讲解Spring是如何通过用户配置的ViewResolver来对视

  • Spring MVC处理方法返回值过程解析

    这篇文章主要介绍了Spring MVC处理方法返回值过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 对于Spring MVC处理方法支持支持一系列的返回方式: (1)ModelAndView (2)Model (3)ModelMap (4)Map (5)View (6)String (7)Void (8)Object 一,ModelAndView @RequestMapping("/threadRequest*") publi

  • Spring MVC学习之DispatcherServlet请求处理详析

    前言 要深入理解spring mvc的工作流程,就需要先了解spring mvc的架构: 从上图可以看到 前端控制器DispatcherServlet在其中起着主导作用,理解了DispatcherServlet 就完全可以说弄清楚了spring mvc. DispatcherServlet作为Spring用于处理web请求注册的唯一一个Servlet,所有的请求都是经由DispatcherServlet进行分发处理的.本文主要讲解DispatcherServlet是如何对请求进行分发,处理,并且

  • Spring MVC学习笔记之json格式的输入和输出

    Spring mvc处理json需要使用jackson的类库,因此为支持json格式的输入输出需要先修改pom.xml增加jackson包的引用 <!-- json --> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-lgpl</artifactId> <version>1.8.1</version>

  • Spring MVC 前端控制器 (DispatcherServlet)处理流程解析

    目录 Spring MVC 请求处理流程 DispatcherServlet 源码分析 Spring MVC 中的一些核心类 Spring MVC 请求处理流程 用户发起请求,到 DispatcherServlet; 然后到 HandlerMapping 返回处理器链(包含拦截器和具体处理的 Handler); 调用处理器链的适配器 HandlerAdapter 来处理: 执行具体的方法,比如 @RequestMapper修饰的逻辑处理方法: 返回结果的视图解析器: 最后进行视图解析和渲染返回结

  • Eclipse使用maven搭建spring mvc图文教程

    本文为大家介绍了Eclipse使用maven搭建spring mvc的详细步骤,供大家参考,具体内容如下 1. 环境配置 a). Java 1.7 b). Eclipse luna c). Maven3.2.5 d). Spring 4.1.4 2. 创建maven工程 a). 打开eclipse,file->new->project->Maven->Maven Project b). 下一步 c). 选择创建的工程为webapp,下一步 d). 填写项目的group id和art

  • Spring MVC 学习 之 - URL参数传递详解

    在学习 Spring Mvc 过程中,有必要来先了解几个关键参数: @Controller: 在类上注解,则此类将编程一个控制器,在项目启动 Spring 将自动扫描此类,并进行对应URL路由映射. @Controller public class UserAction{ } @RequestMapping 指定URL映射路径,如果在控制器上配置 RequestMapping  ,具体请求方法也配置路径则映射的路径为两者路径的叠加 常用映射如:RequestMapping("url.html&q

  • Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

    0 摘要 本文从源码层面简单讲解SpringMVC的处理器映射环节,也就是查找Controller详细过程 1 SpringMVC请求流程 Controller查找在上图中对应的步骤1至2的过程 SpringMVC详细运行流程图 2 SpringMVC初始化过程 2.1 先认识两个类 1.RequestMappingInfo 封装RequestMapping注解 包含HTTP请求头的相关信息 一个实例对应一个RequestMapping注解 2.HandlerMethod 封装Controlle

  • Spring mvc JSON数据交换格式原理解析

    什么是JSON JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛. 采用完全独立于编程语言的文本格式来存储和表示数据. 简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言. 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率. 在 JavaScript 语言中,一切都是对象.因此,任何JavaScript 支持的类型都可以通过 JSON 来表示,例如字符串.数字.对象.数组等.看看他的要求和

  • ASP.NET Core MVC学习教程之路由(Routing)

    前言 ASP.NET Core MVC 路由是建立在ASP.NET Core 路由的,一项强大的URL映射组件,它可以构建具有理解和搜索网址的应用程序.这使得我们可以自定义应用程序的URL命名形式,使得它在搜索引擎优化(SEO)和链接生成中运行良好,而不用关心Web服务器上的文件是怎么组织的.我们可以方便的使用路由模板语法定义路由,路由模板语法支持路由值约束,默认值和可选值. 基于约束的路由允许全局定义应用支持的URL格式,以及这些格式是怎样各自在给定的控制器中映射到指定的操作方法(Action

随机推荐