springmvc进阶(1):视图解析过程和自定义视图

本篇首先结合源码讲述一下SpringMVC如何解析、渲染视图并转发返回结果对象。

最后讲述如何自定义视图。


视图实现类

  • SpringMVC借助视图解析器得到最终的视图对象,最终的视图对象可能是JSP或者Excel等等。
  • 对于最终采用何种视图对象对模型数据进行渲染,处理器并不关心。处理器的工作重点聚焦在生产模型数据的工作上,从而实现MVC的充分解耦。
  • 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户;

为了实现视图模型和具体实现技术的解耦,Spring在org.springframwork.web.servlet包中定义了一个高度抽象的View接口
这里写图片描述

视图对象由视图解析器负责实例化,由于他们是无状态的,所以不存在线程安全的问题。

常见的视图实现类

  • InternalResourceView
  • JstlView 继承自 InternalResourceView

这里写图片描述

上面都是官方给我们提供的常用的视图实现类,我们也可以自定义一些视图实现类,这个在最后说。


视图解析器

  • SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring mvc上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。
  • 每一种解析策略对应一个具体的视图解析器实现类。
  • 视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。
  • 所有的视图解析器都必须实现ViewResolver接口。

这里写图片描述
常见的视图解析器实现类
这里写图片描述

  • 可以选择一种或多种视图解析器,可以通过其order属性指定解析器的优先顺序,order越小优先级越高。
<!-- 配置页面解析器 -->
<bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
    <property name="order" value="100"></property>
</bean>
  • SpringMVC会按照视图解析器顺序的优先次序进行解析,直到返回视图对象。若无,则抛出ServletException异常。

上面基本概念也介绍的差不多了,下面开始结合源码看看视图解析的流程:

public class DispatcherServlet extends FrameworkServlet {
    //省略前面的代码
    ...
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 1. 获取ModelAndView
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(request, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            //2. 处理转发结果
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Error err) {
            triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }
    //省略后面的代码
    ...
}

整个流程的控制是由DispatcherServlet 统一控制的。
在doDispatch方法中进行了这些处理:
1. 获取ModelAndView
请求处理方法执行完成后,最终返回一个ModelAndView对象。对于那些返回String,view或者ModelAndView等类型的处理方法,SpringMVC也会在内部将他们装配成一个ModelAndView对象。
这里写图片描述

可以看到ModelAndView含有两个主要属性:
view : 存放处理方法返回的页面名称
model : map类型,存放处理方法中要返回给前端的键值对数据。

扫描二维码关注公众号,回复: 1212119 查看本文章

获取MV方法源码如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
        //之前的代码省略

    private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
            ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

        modelFactory.updateModel(webRequest, mavContainer);
        if (mavContainer.isRequestHandled()) {
            return null;
        }

        /**
         * model和view信息放在mavContainer中
         * mavContainer主要是通过反射执行处理方法获取返回值和model值
         */
        ModelMap model = mavContainer.getModel();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }
        if (model instanceof RedirectAttributes) {
            Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
            HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
        return mav;
    }
//后面的代码省略
}
  • model和view信息放在mavContainer中
  • mavContainer主要是通过反射执行处理方法获取返回值和model
  • mavContainer有两个方法getModel()和getViewName()获取model和viewName,然后将之放入ModelAndView 中。

2.处理转发结果
在转发结果处理方法中,主要是进行视图的渲染。

public class DispatcherServlet extends FrameworkServlet {
        ...
        //之前的代码省略
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

        boolean errorView = false;

        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }

        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
            //在这里进行视图渲染方法的调用
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                        "': assuming HandlerAdapter completed request handling");
            }
        }

        if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Concurrent handling started during a forward
            return;
        }

        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }
    //后面的代码省略
    ...
}

这个方法主要是判断是渲染返回正常视图还是渲染返回异常视图,关于异常处理可查看之前的博文。

视图渲染方法:

public class DispatcherServlet extends FrameworkServlet {
        ...
        //之前的代码省略
    protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // Determine locale for request and apply it to the response.
        Locale locale = this.localeResolver.resolveLocale(request);
        response.setLocale(locale);

        View view;
        if (mv.isReference()) {
            // We need to resolve the view name.
            //1. 通过视图解析器获取到具体的视图
            view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
            if (view == null) {
                throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                        "' in servlet with name '" + getServletName() + "'");
            }
        }
        else {
            // No need to lookup: the ModelAndView object contains the actual View object.
            view = mv.getView();
            if (view == null) {
                throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                        "View object in servlet with name '" + getServletName() + "'");
            }
        }

        // Delegate to the View object for rendering.
        if (logger.isDebugEnabled()) {
            logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
        }
        try {
           //2. 这里真正开始渲染视图
            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;
        }
    }
    //后面的代码省略
    ...
}

视图对象解析方法:

public class DispatcherServlet extends FrameworkServlet {
        ...
        //之前的代码省略
    //视图解析
    protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
            HttpServletRequest request) throws Exception {
        //遍历viewResolvers
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                return view;
            }
        }
        return null;
    }
    //后面的代码省略
    ...
}
  • 遍历viewResolvers,然后一个一个根据viewName去解析视图,如果这个视图存在就返回,如果都找不到则返回null.
  • viewResolvers是去spring容器中通过ViewResolver类查找配置的bean

这里我配置了一个InternalResourceViewResolver:

<!-- 配置页面解析器 -->
<bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
    <property name="order" value="100"></property>
</bean>

看一下返回的view对象:
这里写图片描述

  • 可以看到拿到了name和转发的URL。
  • 为何这个view是JstlView而不是XML配置的InternalResourceViewResolver默认对应的InternlResourceView呢?
    因为JSP页面使用了JSTL标签!SpringMVC会自动使用InternlResourceView的子类 — JstlView

拿到了视图之后,开始进行视图渲染:

public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {
    ...
    @Override
    public void render(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);
        }
        //这里准备数据
        Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
        prepareResponse(request, response);
        //这里进行真正的视图渲染
        renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
    }
    ...
}

这里是先准备数据,再进行数据和页面结合的处理。

然后进入renderMergedOutputModel方法看看:

public class InternalResourceView extends AbstractUrlBasedView {
    ...
    @Override
    protected void renderMergedOutputModel(
            Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        // Expose the model object as request attributes.
        //1. 这里将model数据全部放入当前request中,暴露给页面
        exposeModelAsRequestAttributes(model, request);

        // Expose helpers as request attributes, if any.
        exposeHelpers(request);

        // Determine the path for the request dispatcher.
         //2. 拿到转发的视图路径
        String dispatcherPath = prepareForRendering(request, response);

        // Obtain a RequestDispatcher for the target resource (typically a JSP).
        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!");
        }

        // If already included or response already committed, perform include, else 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() + "'");
            }
            //3. 转发
            rd.forward(request, response);
        }
    }
    ...
}

上面代码中:

  • exposeModelAsRequestAttributes(model, request);
    执行的方法主要是讲Model中的数据放入request中,这样在转发后页面上从request域中获取的数据就可以显示了。

    获取转发器:

RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);

进行转发:rd.forward(request, response);

之后的事情,JSP解析等等,就是服务器的事情了


自定义视图

定义一个View实现类HelloView

package com.springmvc.common;

import java.util.Date;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.View;
@Component
public class HelloView implements View {

    @Override
    public String getContentType() {
        return "text/html";
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        //这里写具体的显示操作
        response.getWriter().print("hello view...,time:"+new Date());

    }

}

必须将之注册为bean

<context:component-scan base-package="com.springmvc"/>

配置相应的视图解析器

<!-- 配置 BeanNameViewResolver 页面解析器,解析自定义视图 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
    <property name="order" value="100"></property>
</bean>

配置处理器

@RequestMapping("/helloView")
public String helloView(){
    System.out.println("helloView...");
    return "helloView";
}

测试

<a href="helloView">helloView</a>

这里写图片描述

猜你喜欢

转载自blog.csdn.net/abc997995674/article/details/80484415
今日推荐