SpringMVC 视图渲染过程解析

基本概念

所有的 MVC 框架都有一套它自己的解析视图的机制,springmvc 也不例外,它使用ViewResolver 进行视图解析,让用户在浏览器中渲染模型。

Springmvc 处理视图最重要的两个接口是 ViewResolver 和 View :

ViewResolver 接口在视图名称和真正的视图之间提供映射,它是一种开箱即用的技术,能够解析 JSP、Velocity 模板和 XSLT 等多种视图:

public interface ViewResolver {
    // 关键 --> 根据 vieName 返回 View 视图
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

View 接口则处理请求将真正的视图呈现给用户:

public interface View {
 
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
 
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";
 
    String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
 
    // 取得网页文件的类型和编码
    String getContentType();
 
    // 关键 --> 渲染页面
    void render(Map<string,> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
 
}</string,>

视图解析在 springmvc 中配置如下(以 InternalResourceViewResolver 为例 ):

bean :表示指定的 ViewResolver;

viewClass : 表示要解析的视图类型,如 JSP 等;

prefix/suffix : 表示路径前缀/后缀,假设 viewname 为 hello,则完整的路径为:“/WEB-INF/page/hello.jsp

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

1.过程

找到 DispatcherServlet 的 doDispatch 方法,该方法负责实现 springmvc 的请求转发过程,该过程最后一步就是处理请求结果,并进行视图渲染。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
 
    // ...
 
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
 
        // ...
 
        // 调用控制器(controller)匹配请求路径的处理方法,并返回 mv
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
 
        // ...
 
        // 关键 --> 处理请求结果(同时在这里根据 mv 进行视图渲染)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // ...
    }
    catch (Error err) {
        // ...
    }
    finally {
        // ...
    }
}

再来看 processDispatchResult 这个方法,该方法负责处理请求结果,并进行视图渲染。

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, 
    HandlerExecutionChain mappedHandler,ModelAndView mv, Exception exception) throws Exception {
 
    boolean errorView = false;
 
    // 异常处理...
 
    if (mv != null && !mv.wasCleared()) {
 
        // 渲染页面
        render(mv, request, response);
 
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    } else {
 
        // 日志输出...
    }
 
    // ...
}

接着来看 render 方法,该方法负责视图渲染的过程。

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
 
    // 根据请求决定回复消息的 locale
    Locale locale = this.localeResolver.resolveLocale(request);
    response.setLocale(locale);
 
    View view;
 
    // 判断 mv 是否字符串
    if (mv.isReference()) {
 
        // 关键 -> 解析视图名返回 view,为空则抛出异常
        view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
        if (view == null) {
            // 抛出异常...
        }
 
    } else {
 
        // 若 mv 不是字符串直接取得视图,为空则抛出异常
        view = mv.getView();
        if (view == null) {
            // 抛出异常...
        }
    }
 
    // 日志输出...
 
 
    try {
        // 关键 -> 由 View 进行真正的视图渲染
        view.render(mv.getModelInternal(), request, response);
    } catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex);
        }
        throw ex;
    }
}

最后来看看 resolveViewName,该方法在 mv 属于字符串类型时会被调用。

protected View resolveViewName(String viewName, Map<string, object=""> model, Locale locale,HttpServletRequest request) throws Exception {
    // 遍历 Ioc 容器中定义的 ViewResolver 
    for (ViewResolver viewResolver : this.viewResolvers) {
 
        // 关键 -> 调用 它的 resolveViewName 方法
        View view = viewResolver.resolveViewName(viewName, locale);
 
        // 不为空,则返回 view
        if (view != null) {
            return view;
        }
    }
 
    return null;
}</string,>

2.总结

分析完 springmvc 视图解析过程,再来做下总结:

请求转发过程中(doDispatch)最后进行请求结果的处理,同时会进行视图渲染(processDispatchResult);

处理完请求结果,调用 redner 方法进行视图渲染;

在渲染过程中,判断 mv 是否属于字符串类型,是的话调用 resolveViewName 方法(该方法会遍历 Ioc 容器中 ViewResolver ,并调用它的 resolveViewName )取得视图名,否则直接取得视图名。最后再调用 View 的 render 方法进行真正的视图渲染。

可以整个视图机制都围绕着 ViewResolver 和 View 两个接口展开,简单流程如下:

doDispatch - processDispatchResult - redner - resolveViewName - ViewResolver.resolveViewName - View.render

 

源码分析(ViewResolver)

分析完 springmvc 的视图解析机制,再来看看 ViewResovler 这个接口。上面提到过,它的主要作用是根据【视图名(ViewName)返回视图(View)】。

首先来看它的继承关系:

这里写图片描述

1.AbstractCachingViewResolver

通过上面的继承关系图可以知道该类直接实现了 ViewResolver 接口,是带有缓存功能的基础实现抽象类

重点来看 resolveViewName 这个方法:

// 成员变量
 
public static final int DEFAULT_CACHE_LIMIT = 1024
 
private final Map<object, view=""> viewAccessCache = new ConcurrentHashMap<object, view="">(DEFAULT_CACHE_LIMIT);
 
private final Map<object, view=""> viewCreationCache = new LinkedHashMap<object, view="">(DEFAULT_CACHE_LIMIT, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<object, view=""> eldest) {
        if (size() > getCacheLimit()) {
            viewAccessCache.remove(eldest.getKey());
            return true;
        } else {
            return false;
        }
    }
};
 
// 方法
 
public View resolveViewName(String viewName, Locale locale) throws Exception {
 
    // 缓存功能是否打开,只要 cacheLimit(默认为 1024) 大于 0 就代表缓存功能默认打开
    if (!isCache()) {
 
        // 若缓存关闭,则创建视图
        return createView(viewName, locale);
    } else {
 
        // 返回 viewName + "_" + locale
        Object cacheKey = getCacheKey(viewName, locale);
 
        // 从 viewAccessCache 取得 view
        View view = this.viewAccessCache.get(cacheKey);
 
 
        if (view == null) {
            synchronized (this.viewCreationCache) {
 
                // 从 viewCreationCache 取得 view
                view = this.viewCreationCache.get(cacheKey);
 
                if (view == null) {
 
                    // 为空,创建视图
                    view = createView(viewName, locale);
 
                    // 仍然为空,则标记为【不可解析视图】
                    if (view == null && this.cacheUnresolved) {
                        view = UNRESOLVED_VIEW;
                    }
 
                    if (view != null) {
                        // 添加进缓存
                        this.viewAccessCache.put(cacheKey, view);
                        this.viewCreationCache.put(cacheKey, view);
 
                        // 日志输出...
                    }
                }
            }
        }
 
        return ( view != UNRESOLVED_VIEW ? view : null );
    }
}</object,></object,></object,></object,></object,>

再来看看它的 createView 方法,该方法负责视图的创建。

protected View createView(String viewName, Locale locale) throws Exception {
    return loadView(viewName, locale);
}
 
// 抽象方法,留给子类做具体实现
protected abstract View loadView(String viewName, Locale locale) throws Exception;

观察代码,可以该类的视图解析过程如下:

缓存关闭,则创建视图;

缓存开启,先后从 viewCreationCache,viewCreationCache 这个两个缓存集合里查找视图,仍然为空,则创建视图,并分别添加进这两个缓存集合里。

 

2.UrlBasedViewResolver

继承自 AbstractCachingViewResolver 抽象类、并实现 Ordered 接口的类,是 ViewResolver 接口简单的实现类,该类重写了 createView,loadView 这两个方法。

首先来看它的 createView 方法

protected View createView(String viewName, Locale locale) throws Exception {
 
    // 是否支持该视图的名的处理
    if (!canHandle(viewName, locale)) {
        // 返回 null 进入下一个视图解析器
        return null;
    }
 
    // 检查是否以 "redirect" 开头
    if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
        String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
        RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
        return applyLifecycleMethods(viewName, view);
    }
 
    // 检查是否以 "forward" 开头
    if (viewName.startsWith(FORWARD_URL_PREFIX)) {
        String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
        return new InternalResourceView(forwardUrl);
    }
 
    // 其他情况由父类处理,其实就是调用该类的 loadView 方法
    return super.createView(viewName, locale);
}

再来看看它 的 loadView 方法,如果视图名不包含 redirect,forward 时,调用父类的createView 的时会调用到它。

protected View loadView(String viewName, Locale locale) throws Exception {
    // 关键 -> 根据 ViewName 取得 View
    AbstractUrlBasedView view = buildView(viewName);
 
    View result = applyLifecycleMethods(viewName, view);
 
    return ( view.checkResource(locale) ? result : null );
}
 
 
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
 
    // 实例化视图类(View) 
    AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
 
    // 根据【前缀+视图名+后缀】生成 url,并添加进 View
    view.setUrl(getPrefix() + viewName + getSuffix());
 
    // 取得网页的类型和编码 ,并添加进 View
    String contentType = getContentType();
    if (contentType != null) {
        view.setContentType(contentType);
    }
 
    // 设置 View 的其他相关属性
 
    view.setRequestContextAttribute(getRequestContextAttribute());
    view.setAttributesMap(getAttributesMap());
 
    Boolean exposePathVariables = getExposePathVariables();
    if (exposePathVariables != null) {
        view.setExposePathVariables(exposePathVariables);
    }
    Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
    if (exposeContextBeansAsAttributes != null) {
        view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
    }
    String [ ] exposedContextBeanNames = getExposedContextBeanNames();
    if (exposedContextBeanNames != null) {
        view.setExposedContextBeanNames(exposedContextBeanNames);
    }
 
    return view;
}

 

3.InternalResourceViewResolver

继承自 UrlBasedViewResolver ,以 InternalResourceView 作为视图,若项目中存在“javax.servlet.jsp.jstl.core.Config”该类,那么会以 JstlView 作为视图。

重写了 buildView 方法,主要就是为了给 InternalResourceView 视图设置属性。

源码分析(View)

View 负责视图的真正渲染。来看它的继承关系(部分显示):

201608200934081308.png

1.AbstractView

View接口的基础实现抽象类,重点来看 render 方法:

public void render(Map<string,> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
 
    // 日志记录...
 
    // 将属性合并到 map 中去
    Map<string, object=""> mergedModel = createMergedOutputModel(model, request, response);
 
    // 判断是否是下载资源,并设置请求头(为了解决 IE 下 https 下载请求的 bug)
    prepareResponse(request, response);
 
    // 抽象方法,留给子类实现
    renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}</string,></string,>

createMergedOutputModel 方法:

protected Map<string, object=""> createMergedOutputModel(Map<string,> model, HttpServletRequest request,HttpServletResponse response) {
 
    @SuppressWarnings("unchecked")
    Map<string, object=""> pathVars = (this.exposePathVariables ?(Map<string, object="">) request.getAttribute(View.PATH_VARIABLES) : null);
 
    // 把 exposePathVariables,staticAttributes,modelMap,以及 requestContextAttribute 的属性都添加进新的 map
    int size = this.staticAttributes.size();
    size += (model != null ? model.size() : 0);
    size += (pathVars != null ? pathVars.size() : 0);
 
    Map<string, object=""> mergedModel = new LinkedHashMap<string, object="">(size);
 
    mergedModel.putAll(this.staticAttributes);
 
    if (pathVars != null) {
        mergedModel.putAll(pathVars);
    }
 
    if (model != null) {
        mergedModel.putAll(model);
    }
 
    // Expose RequestContext?
    if (this.requestContextAttribute != null) {
        mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
    }
 
    return mergedModel;
}</string,></string,></string,></string,></string,></string,>

观察代码可以发现该类的主要工作是将各种属性都添加进 map 中,便于属性的传递,管理。

2.AbstractUrlBasedView

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

3.InternalResourceView

继承自 AbstractUrlBasedView 抽象类的类,该类重写了renderMergedOutputModel 这个方法:

protected void renderMergedOutputModel(Map<string, object=""> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
 
    // 将 map 的值添加 reqeust 的 attribute
    exposeModelAsRequestAttributes(model, request);
 
    // Expose helpers as request attributes, if any.
    exposeHelpers(request);
 
    // 验证要渲染的页面地址
    String dispatcherPath = prepareForRendering(request, response);
 
    // 为指定的页面建立 RequestDispatcher 
    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!");
    }
 
 
    // 判断是 request 是 include 方法还是 forward 方法
    if (useInclude(request, response)) {
 
        response.setContentType(getContentType());
 
        if (logger.isDebugEnabled()) {
            logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
        }
 
        rd.include(request, response);
 
    }else {
        // Note: The forwarded resource is supposed to determine the content
        // type itself.
        if (logger.isDebugEnabled()) {
            logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
        }
 
        rd.forward(request, response);
    }
}

实例探究

利用 ContentNegotiatingViewResolver 集成多种视图

首先来看配置文件(这里集成 vm,jsp,json,xml 这四种视图显示)

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <!-- 内容协商管理器 -->
    <property name="contentNegotiationManager" ref="contentNegotiationManager">
    <!-- 视图解析器  -->
    <property name="viewResolvers">
        <list>
            <!--Veocity 视图解析器  -->
            <bean class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
                <property name="order" value="0">
                <property name="cache" value="false">
                <property name="suffix" value=".vm">
            </property></property></property></bean>
            <!--JSP 视图解析器  -->
            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="viewClass" value="org.springframework.web.servlet.view.JstlView">
                <property name="prefix" value="/WEB-INF/page/">
                <property name="suffix" value=".jsp"></property>
            </property></property></bean>
        </list>
    </property>
 
    <!-- 默认视图 -->
    <property name="defaultViews">
        <list>
            <!-- json 视图解析 -->
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
            <!-- xml 视图解析  -->
            <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
                <property name="marshaller">
                    <bean class="org.springframework.oxm.xstream.XStreamMarshaller">
                </bean></property>
            </bean>
        </bean></list>
    </property>
</property></bean>
 
<bean class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean" id="contentNegotiationManager">
    <!-- 根据请求参数或拓展名映射到相应的MIME类型 -->
    <property name="mediaTypes"><map>
            <entry key="json" value="application/json">
            <entry key="xml" value="application/xml">
        </entry></entry></map>
    </property>
    <!-- 设置默认的MIME类型,如果没有指定拓展名或请求参数,则使用此默认MIME类型解析视图 -->
    <property name="defaultContentType" value="text/html"> 
    <!-- 是否不适用请求头确定MIME类型 -->
    <property name="ignoreAcceptHeader" value="true"> 
    <!-- 是否根据路径拓展名确定MIME类型 -->
    <property name="favorPathExtension" value="true"> 
     <!-- 是否使用参数来确定MIME类型 -->
    <property name="favorParameter" value="false"> 
</property></property></property></property></bean>
 
<!-- Veocity 模版配置 -->
<bean class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
    <property name="configLocation" value="/WEB-INF/velocity.properties">
</property></bean>

Velocity 属性文件配置

input.encoding=UTF-8
output.encoding=UTF-8
contentType=text/html;charset=UTF-8 
resource.loader=webapp
webapp.resource.loader.class=org.apache.velocity.tools.view.WebappResourceLoader
webapp.resource.loader.path=/WEB-INF/velocity/

测试

// 先用 velocity 视图解析(order = 0),然后再用 jsp 视图解析
https://localhost:8080/demo/hello
 
// 返回 json 视图
https://localhost:8080/demo/hello.json
 
// 返回 xml 视图
https://localhost:8080/demo/hello.xml

注意:

这里利用路径扩展名(favorPathExtension)区分 MIME 类型,以此确定要返回的视图类型。也可以利用参数(默认为 fomart)来区分。

VelocityViewResolver(velocity 视图解析)的 order 优先级必须比InternalResourceViewResolver(jsp 视图解析器)高。因为InternalResourceViewResolver 不管路径下是否存在指定 jsp 文件都会返回 View(视图),这样会导致 VelocityViewResolver 不生效。

转载于:https://my.oschina.net/zjllovecode/blog/1543872

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值