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