第五篇:视图

第五篇:视图

官方文档

SpringMVC

View Technologies

View Resolution

本章学习的,就是 SpringMVC 的请求处理流程中的第④⑤⑥⑦步,我们在源码中都能找到对应的步骤

总结

Spring MVC 定义了 ViewResolverView 接口,使您可以在浏览器中渲染 Model,而无需绑定到特定的视图技术。ViewResolver 提供了视图名称和实际视图之间的映射。View 处理移交给特定视图技术之前的数据准备工作。

ViewResolver 的实现类

重点关注

Thymeleaf

Documentation - Thymeleaf

Tutorial: Thymeleaf 3.0 + Spring

Tutorial: Thymeleaf 2.1 + Spring

各种视图的实践

SpringMVC 中的视图是 View 接口,视图的作用渲染数据,将模型 Model 中的数据展示给用户

SpringMVC 视图的种类很多,默认有转发视图 InternalResourceView 和重定向视图 RedirectView,还有各种视图技术的视图,比如 FreeMarkerViewThymeleafView,而这些视图,都有对应的 ViewResolver

如果引入 Jstl 的 jar,则转发视图则为 JstlView,实际上 JstlView 继承 InternalResourceView,也还是 InternalResourceView

ThymeleafView

控制器

@Controller
@RequestMapping("/View")
public class ViewController {


    @RequestMapping("/thymeleafView")
    public String thymeleafView() {
        return "thymeleafView";
    }

}

视图 thymeleafView

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>thymeleafView</title>
</head>
<body>
<h1>thymeleafView</h1>
</body>
</html>

测试方法

@SpringJUnitWebConfig(locations = "classpath:SpringMVC.xml")
class ViewControllerTest {

    private MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void thymeleafView() throws Exception {
        mockMvc.perform(get("/View/thymeleafView")).andExpect(view().name("thymeleafView")).andDo(print());
    }
}

日志

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /View/thymeleafView
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = xyz.xiashuo.springmvcview.ViewController
           Method = xyz.xiashuo.springmvcview.ViewController#thymeleafView()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = thymeleafView
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>thymeleafView</title>
</head>
<body>
<h1>thymeleafView</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

简单的源码分析

解析视图的时候,从 DispatcherServletdoDispatch 往下调用栈如下,一直到 ThymeleafViewResolver(继承自 AbstractCachingViewResolver)

DispatcherServletdoDispatchmv = ha.handle(processedRequest, response, mappedHandler.getHandler());

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null;

    ......

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

        try {

            ......

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

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

            ......

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

            // Actually invoke the handler.
            // 调用控制器方法,返回 ModelAndView
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

            // 如果视图名为空,则需要根据请求解析出默认的视图名
            applyDefaultViewName(processedRequest, mv);

            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }

        // 后续的视图解析
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            ......
        }
    }
}

拿到控制器返回的 ModelAndView 对象,然后调用 processDispatchResult 方法进行处理,processDispatchResult 调用 DispatcherServlet 的 render 方法渲染 ModelAndView 对象,

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    ......

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        // 调用 render 方法渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        // mv 为null,表示不需要进行视图渲染,
        // 比如`@ResponseBody`注解修饰的控制器方法的返回值,其为空的原因是因为在其对应的返回值处理器RequestResponseBodyMethodProcessor中将mavContainer的 requestHandled这个标志位设置为true
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    ......

    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

render 方法中先调用 DispatcherServlet 的 resolveViewName 方法使用 ModelAndView 对象中的视图名解析出 View 对象,然后再用 ModelAndView 中保存的 Model 来调用 View 对象的 render 方法渲染视图,将视图内容渲染到 HTTP 响应中。(我们使用的是 ThymeleafViewResolver,所以这里的 View 对象就是 ThymeleafView)

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 != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    View view;
    String viewName = mv.getViewName();
    if (viewName != null) {
        // We need to resolve the view name.
        // 解析视图名
        view = resolveViewName(viewName, 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.isTraceEnabled()) {
        logger.trace("Rendering view [" + view + "] ");
    }
    try {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // 使用Model渲染视图
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

DispatcherServlet 的 resolveViewName 方法也很简单,使用容器中注册的多个 ViewResolver 来依次解析视图名称,只要解析成功就返回,所以如果系统中配置了多个视图解析器,其顺序也很重要

因为我们只注册了一个 ThymeleafViewResolver,所以这里 viewResolvers 属性只有一个元素。

@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
        Locale locale, HttpServletRequest request) throws Exception {

    if (this.viewResolvers != null) {
        // 使用容器中注册的的多个ViewResolver来依次解析视图名称,
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                // 只要解析成功就返回,所以如果系统中配置了多个视图解析器,其顺序也很重要
                return view;
            }
        }
    }
    return null;
}

ThymeleafViewResolver 源码

Thymeleaf 本身相关的 API 和用法,请看《Thymeleaf 模板引擎.md

Thymeleaf 和 SpringBoot Web 的整合请看《Thymeleaf 整合 SpringBoot.md

resolveViewName,根据视图名称和本地化信息解析视图。注意,为了增加加载速度,这里有缓存,viewAccessCache,第二次访问同样的视图名的时候,就会从缓存中加载。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    if (!isCache()) {
        // 未开启缓存直接调用 createView 方法创建View对象
        // 一般都是开启了缓存的
        return createView(viewName, locale);
    }
    else {
        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) {
                    // Ask the subclass to create the View object.
                    // 缓存里实在是找不到,才调用 createView 方法创建View对象
                    view = createView(viewName, locale);
                    if (view == null && this.cacheUnresolved) {
                        view = UNRESOLVED_VIEW;
                    }
                    if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
                        this.viewAccessCache.put(cacheKey, view);
                        this.viewCreationCache.put(cacheKey, view);
                    }
                }
            }
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace(formatKey(cacheKey) + "served from cache");
            }
        }
        return (view != UNRESOLVED_VIEW ? view : null);
    }
}

看创建 View 对象的方法,从 createView 方法可以看出,根据视图名称的前缀调用不同的视图,是在 ThymeleafViewResolver 内部,根据视图名称创建 View 对象的时候处理的,这应该是视图解析解本身必须支持的。

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    } else {
        String forwardUrl;
        if (viewName.startsWith("redirect:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("redirect:".length(), viewName.length());
            RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, "redirect:");
        } else if (viewName.startsWith("forward:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("forward:".length(), viewName.length());
            return new InternalResourceView(forwardUrl);
        } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
            return this.loadView(viewName, locale);
        }
    }
}

只有不带 redirect: 或者 forward: 前缀,才会构建 ThymeleafView,通过调用 loadView 构建 ThymeleafView

protected View loadView(String viewName, Locale locale) throws Exception {
    AutowireCapableBeanFactory beanFactory = this.getApplicationContext().getAutowireCapableBeanFactory();
    boolean viewBeanExists = beanFactory.containsBean(viewName);
    Class<?> viewBeanType = viewBeanExists ? beanFactory.getType(viewName) : null;
    AbstractThymeleafView view;
    if (viewBeanExists && viewBeanType != null && AbstractThymeleafView.class.isAssignableFrom(viewBeanType)) {
        BeanDefinition viewBeanDefinition = beanFactory instanceof ConfigurableListableBeanFactory ? ((ConfigurableListableBeanFactory)beanFactory).getBeanDefinition(viewName) : null;
        if (viewBeanDefinition != null && viewBeanDefinition.isPrototype()) {
            view = (AbstractThymeleafView)beanFactory.getBean(viewName);
        } else {
            AbstractThymeleafView viewInstance = (AbstractThymeleafView)BeanUtils.instantiateClass(this.getViewClass());
            view = (AbstractThymeleafView)beanFactory.configureBean(viewInstance, viewName);
        }
    } else {
        AbstractThymeleafView viewInstance = (AbstractThymeleafView)BeanUtils.instantiateClass(this.getViewClass());
        if (viewBeanExists && viewBeanType == null) {
            beanFactory.autowireBeanProperties(viewInstance, 0, false);
            beanFactory.applyBeanPropertyValues(viewInstance, viewName);
            view = (AbstractThymeleafView)beanFactory.initializeBean(viewInstance, viewName);
        } else {
            beanFactory.autowireBeanProperties(viewInstance, 0, false);
            view = (AbstractThymeleafView)beanFactory.initializeBean(viewInstance, viewName);
        }
    }

    view.setTemplateEngine(this.getTemplateEngine());
    view.setStaticVariables(this.getStaticVariables());
    if (view.getTemplateName() == null) {
        view.setTemplateName(viewName);
    }

    if (!view.isForceContentTypeSet()) {
        view.setForceContentType(this.getForceContentType());
    }

    if (!view.isContentTypeSet() && this.getContentType() != null) {
        view.setContentType(this.getContentType());
    }

    if (view.getLocale() == null && locale != null) {
        view.setLocale(locale);
    }

    if (view.getCharacterEncoding() == null && this.getCharacterEncoding() != null) {
        view.setCharacterEncoding(this.getCharacterEncoding());
    }

    if (!view.isProducePartialOutputWhileProcessingSet()) {
        view.setProducePartialOutputWhileProcessing(this.getProducePartialOutputWhileProcessing());
    }

    return view;
}

DispatcherServlet 的 render 方法在获取 View 对象后,会调用 View 对象的 render 方法渲染视图,我们使用的是 ThymeleafViewResolver,所以这里的 View 对象就是 ThymeleafView,现在我们来看看 ThymeleafView 的 render 方法,调用的是 renderFragment 方法。

 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    this.renderFragment(this.markupSelectors, model, request, response);
}

其中 renderFragment 方法,简单粗暴,直接写入 HttpServletResponse 中

protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        ......

        // 在这里,把模板中的信息,写入响应中 
        boolean producePartialOutputWhileProcessing = this.getProducePartialOutputWhileProcessing();
        Writer templateWriter = producePartialOutputWhileProcessing ? response.getWriter() : new FastStringWriter(1024);
        // 执行完此语句之后,model中的所有属性,同步到request域中
        viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter);
        if (!producePartialOutputWhileProcessing) {
            response.getWriter().write(templateWriter.toString());
            response.getWriter().flush();
        }

    }
}

这里有一点需要注意,传入 renderFragment 方法的参数中,model 和 request 是不相干的,但是执行完

viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter);

之后,model 中的所有属性,同步到 request 域之中。这样在页面中就可以通过访问 request 域拿到 Model 中的值,非常方便。

此外,当视图名称为 forward: 开头的时候,createView 方法返回的就是 InternalResourceView 了,这里也会有将 model 中的所有属性同步到 request 域之中的操作。请看下一小节 转发视图 中的 简单的源码分析 小节。

转发视图

当控制器方法中所设置的视图名称以 forward: 为前缀时,创建 InternalResourceView 视图,此时会将前缀 forward: 去掉,剩余部分作为最终路径通过转发的方式实现跳转,然后再进入转发路径对应的控制器进行下一步的处理。

SpringMVC 中默认的转发视图的实现是 InternalResourceView

控制器

@RequestMapping("/thymeleafView")
public String thymeleafView() {
    return "thymeleafView";
}

@RequestMapping("/forward")
public String forward(Model model) {
    model.addAttribute("hello", "xiashuo.xyz");
    return "forward:/View/thymeleafView";
}

测试类,无法测试转发之后的结果,是 Spring 故意不支持的,也有可能是我没找到写法,直接写 HTML 测试吧

@Test
void forward() throws Exception {
    //注意,无法测试转发之后的结果,是Spring故意不支持的,主要是没有过滤连FilterChain。要是想测试转发目的URL,那就再写一个测试直接测这个URL即可。
    //https://github.com/spring-projects/spring-framework/issues/18914
    // 懒得研究了
    //get("/View/forward").buildRequest(servletContext).getRequestDispatcher("forward:/View/thymeleafView").forward();
    mockMvc.perform(get("/View/forward")).andExpect(view().name("forward:/View/thymeleafView")).andDo(print());
}

html 测试

<a th:href="@{/View/forward}">转发</a>

请求路径是对应源控制器,但是视图,是目的控制器返回的视图

简单的源码分析

其实前半段跟 ThymeleafView 小节的简单源码分析是一样的,不一样的是,在 ThymeleafViewResolvercreateView 方法中,视图名称因为有前缀 forward:,就会走不同的分支,最终返回的 View 的实际类型就是 InternalResourceView

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    } else {
        String forwardUrl;
        if (viewName.startsWith("redirect:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("redirect:".length(), viewName.length());
            RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, "redirect:");
        } else if (viewName.startsWith("forward:")) {
            // 这个时候进入的就是这个分支,最终返回的View的实际类型就是InternalResourceView
            vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("forward:".length(), viewName.length());
            return new InternalResourceView(forwardUrl);
        } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
            return this.loadView(viewName, locale);
        }
    }
}

然后在 DispatcherServletrender 方法的后半段的 view.render(mv.getModelInternal(), request, response); 中的 View 的实际类型就变成了 InternalResourceView

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 != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    View view;
    String viewName = mv.getViewName();
    if (viewName != null) {
        // We need to resolve the view name.
        view = resolveViewName(viewName, 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.isTraceEnabled()) {
        logger.trace("Rendering view [" + view + "] ");
    }
    try {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // 这个时候view的实际类型就是InternalResourceView
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

InternalResourceView 的 render 方法实现继承自 AbstractViewInternalResourceView 继承了 AbstractView)中的 render 方法,此 render 方法调用 renderMergedOutputModel 来转发请求,

@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (logger.isDebugEnabled()) {
        logger.debug("View " + formatViewName() + ", model " + (model != null ? model : Collections.emptyMap()) + (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
    }
    // 将 Model中的值、静态属性、还有路径遍历全部放到一个map里 ,即 mergedModel
    Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
    prepareResponse(request, response);
    // 将 最终的 mergedModel 用于视图渲染
    renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

InternalResourceView 重写了这个方法,自己指定了特定的逻辑。最终,我们来看 InternalResourceViewrenderMergedOutputModel 方法。

@Override
protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

    // Expose the model object as request attributes.
    // 将 model 中的属性,一个一个地设置到了请求域中
    exposeModelAsRequestAttributes(model, request);

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

    // Determine the path for the request dispatcher.
    String dispatcherPath = prepareForRendering(request, response);

    // Obtain a RequestDispatcher for the target resource (typically a JSP).
    // 获取转发对象
    // 开始看到熟悉的API了
    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.
    // 什么时候调用include方法,什么时候调用forward方法呢?
    // 要么 alwaysInclude 标志位设置为 true,或者在源Servlet/控制器中已经提交了response 才会使用include,
    // 否则一直用forward
    if (useInclude(request, response)) {
        response.setContentType(getContentType());
        if (logger.isDebugEnabled()) {
            logger.debug("Including [" + getUrl() + "]");
        }
        // 请求包含
        rd.include(request, response);
    }

    else {
        // Note: The forwarded resource is supposed to determine the content type itself.
        if (logger.isDebugEnabled()) {
            logger.debug("Forwarding to [" + getUrl() + "]");
        }
        // 请求转发
        rd.forward(request, response);
    }
}

我们可以看到 renderMergedOutputModel 方法一上来,就调用 exposeModelAsRequestAttributes,将传入的 Model 中的值全部同步到了 request 域中

然后,确定转发路径,最后,进行转发。

实际上,视图转发最终都是委托给原生的 ServletAPIRequestDispatcherforwardinclude 方法。

请求转发 request.getRequestDispatcher("").forward (request, response); 和请求包含 request.getRequestDispatcher("").include(request, response);,请求对象就不用讨论了,不管是请求转发还是请求包含,请求对象一直都是同一个,响应对象虽然也是同一个,他们的区别在于对响应的处理方式上,由于 forward() 方法先清空用于存放响应正文数据的缓冲区,因此 servlet 源组件生成的响应结果不会被发送到客户端,只有目标组件生成的结果才会被发送到客户端,所以对源组件叫“留头不留体”,目标组件为“留体不留头”, 如果源组件在进行请求转发之前,已经提交了响应结果(例如调用了 flush 或 close() 方法),那么 forward() 方法会抛出 IllegalStateException。为了避免该异常,不应该在源组件中提交响应结果,所以叫留体抛异常。include() 与 forward() 相比,源组件与被 include 的目标组件的输出数据都会被添加到响应结果中,在目标组件中对响应状态代码或者响应头所做的修改都会被忽略,所以对源组件来说是“留头又留体”,对目标组件为“留体不留头”。

请求转发中对响应消息头的设置都是不保留的吗?感觉我得写个代码跑一跑上面的结论。

成功转发之后,将再次进入 DispatcherServletdoDispatch 方法,开始对转发链接的请求的处理流程。

重定向视图

当控制器方法中所设置的视图名称以 "redirect:" 为前缀时,创建 RedirectView 视图,此时会将前缀 "redirect:" 去掉,剩余部分作为最终路径通过重定向的方式实现访问,如果这个重定向的地址在当前应用上下文以内,会再进入重定向路径对应的控制器进行下一步的处理。

SpringMVC 中默认的转发视图的实现是 RedirectView

转发和重定向的区别:

控制器

@RequestMapping("/redirect")
public String redirect() {
    return "redirect:/View/thymeleafView";
}

html 中进行访问

<a th:href="@{/View/redirect}">重定向</a>

访问结果,请求路径是对应重定向目的控制器,视图也是重定向目的控制器返回的视图

简单的源码分析

其实前半段跟 ThymeleafView 小节的简单源码分析是一样的,不一样的是,在 ThymeleafViewResolvercreateView 方法中,视图名称因为有前缀 redirect:,就会走不同的分支,最终返回的 View 的实际类型就是 RedirectView

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
        return null;
    } else {
        String forwardUrl;
        if (viewName.startsWith("redirect:")) {
            // 这个时候进入的就是这个分支,最终返回的View的实际类型就是 RedirectView
            vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("redirect:".length(), viewName.length());
            RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, "redirect:");
        } else if (viewName.startsWith("forward:")) {
            vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
            forwardUrl = viewName.substring("forward:".length(), viewName.length());
            return new InternalResourceView(forwardUrl);
        } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
            return this.loadView(viewName, locale);
        }
    }
}

然后在 DispatcherServletrender 方法的后半段的 view.render(mv.getModelInternal(), request, response); 中的 View 的实际类型就变成了 RedirectView

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 != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    View view;
    String viewName = mv.getViewName();
    if (viewName != null) {
        // We need to resolve the view name.
        view = resolveViewName(viewName, 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.isTraceEnabled()) {
        logger.trace("Rendering view [" + view + "] ");
    }
    try {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // 这个时候view的实际类型就是 RedirectView
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

RedirectView 的 render 方法实现继承自 AbstractView(RedirectView 继承了 AbstractView)中的 render 方法,前面提到过,InternalResourceView 也继承了 AbstractView 此 render 方法,AbstractView 中的这个 render 方法调用子类对 renderMergedOutputModel 方法的重写实现来执行特定的逻辑,来重定向请求。现在,我们来看 RedirectViewrenderMergedOutputModel 方法。RedirectViewrenderMergedOutputModel 方法最后委托为 sendRedirect 来执行重定向。然后我们又看到了熟悉的 API

protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
        String targetUrl, boolean http10Compatible) throws IOException {

    String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
    // 这个参数是为了判断要不要兼容 http1.0 的客户端
    //在ThymeleafViewResolver的createView方法创建RedirectView的时候指定,传入ThymeleafViewResolve的redirectHttp10Compatible字段,默认为true。
    if (http10Compatible) {
        HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
        if (this.statusCode != null) {
            response.setStatus(this.statusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else if (attributeStatusCode != null) {
            response.setStatus(attributeStatusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else {
            // Send status code 302 by default.
            response.sendRedirect(encodedURL);
        }
    }
    else {
        HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
        response.setStatus(statusCode.value());
        response.setHeader("Location", encodedURL);
    }
}

实际上,视图重定向最终都是委托给原生的 ServletAPIHttpServletResponsesendRedirect 方法。

http10Compatible 参数的意义

设置是否与 HTTP 1.0 客户端保持兼容。在默认实现中,此配置为 true,这将在任何情况下强制执行 HTTP 状态码 302,即委托给 HttpServletResponse.sendRedirect 进行重定向。如果此配置为 false,将发送 HTTP 状态代码 303,这是 HTTP 1.1 客户机的正确代码,但 HTTP 1.0 客户机不能理解。许多 HTTP 1.1 客户机将 302 视为 303,没有任何区别。然而,一些客户端在 POST 请求后重定向时依赖 303; 在这种情况下需要将此配置设置为 false

这个参数,实际上是在 ThymeleafViewResolver 的 createView 方法创建 RedirectView 的时候指定的,传入 ThymeleafViewResolve 的 redirectHttp10Compatible 字段,默认为 true。

成功重定向之后,如果重定向的目的地还是在此应用上下文内,将再次进入 DispatcherServletdoDispatch 方法,开始对重定向链接的请求的处理流程

视图控制器 <mvc:view-controller>

当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用 <view-controller> 标签进行表示

<!--
    path:设置处理的请求地址。路径属性有点类似于@RequestMapping注解中的路径
    view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/" view-name="index"/>

注意:当 SpringMVC 中设置任何一个 view-controller 时,其他控制器中的请求映射将全部失效,此时需要在 SpringMVC 的核心配置文件中设置开启 mvc 注解驱动的标签:<mvc:annotation-driven />

<mvc:view-controller> 实际上是引入了 DefaultRequestToViewNameTranslator。

基于 Java 配置的 <mvc:view-controller> 可查看《SpringMVC- 第十篇:基于注解配置 SpringMVC》的 addViewControllers 方法

将 JSP 作为视图的视图解析器 - InternalResourceViewResolver

InternalResourceViewResolver 是一个方便的视图解析器,UrlBasedViewResolver 的子类,支持 InternalResourceView(即 servlet 和 jsp) 和其子类,如 JstlView。

这个解析器生成的所有视图的视图类都可以通过 setViewClass 指定。详见 UrlBasedViewResolver 的 javadoc。默认值为 InternalResourceView,如果有 JSTL API,则为 JstlView。

顺便说一句,将 JSP 文件作为视图放在 WEB-INF 下是一个很好的实践,可以隐藏它们,使它们不被直接访问 (例如通过手动输入 URL)。只有控制器才能访问它们。

注意: 当使用多个 viewresolver 对视图进行链式解析时(DispatcherServletresolveViewName 方法),InternalResourceViewResolver 总是需要放在最后,因为它将尝试解析任何视图名,无论底层资源是否实际存在。

简单了解,有需要再学习。尚硅谷的简单入门教程

默认视图解析

在在 DispatcherServletdoDispatch 中,控制器方法返回了 ModelAndView 之后,调用 processDispatchResult 进行渲染之前,中间有一步操作,applyDefaultViewName(processedRequest, mv);,用于处理控制器返回的 ModelAndView 对象不包含视图(视图为空)的情况。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null;

    ......

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

        try {

            ......

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

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

            ......

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

            // Actually invoke the handler.
            // 调用控制器方法,返回 ModelAndView
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

            // 控制器方法返回了`ModelAndView`之后,调用`processDispatchResult`进行渲染之前,需要进行判断
            // 如果视图名为空,则需要根据请求解析出默认的视图名
            applyDefaultViewName(processedRequest, mv);

            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            ......

        }

        // 后续的视图解析
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    }
    catch (Exception ex) {
        ......

    }
    catch (Throwable err) {
        ......

    }
    finally {
        ......

    }
}

applyDefaultViewName 方法的功能很简单,如果控制器方法返回的视图中不包含视图名称,我们就给它一个默认的,因为视图解析器拿不到视图名将无法解析视图,所以必须有视图名。

默认的视图名不是某一个固定的视图,而是是通过默认的策略,生成一个请求对应的视图名

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
    if (mv != null && !mv.hasView()) {
        // ModelAndView 中不包含视图名,视图解析器将无法解析,所以必须有视图名, 没办法,只能给一个默认的
        String defaultViewName = getDefaultViewName(request);
        if (defaultViewName != null) {
            mv.setViewName(defaultViewName);
        }
    }
}

getDefaultViewName 方法很简单,RequestToViewNameTranslator 的 getViewName 方法根据请求返回一个默认的视图名称,有一点跟其他的多策略处理模式不一样,这里的 RequestToViewNameTranslator 是单个 RequestToViewNameTranslator 实例,而不是一个 List<RequestToViewNameTranslator>

protected String getDefaultViewName(HttpServletRequest request) throws Exception {
    return (this.viewNameTranslator != null ? this.viewNameTranslator.getViewName(request) : null);
}

这个方法在 DispatcherServletprocessHandlerException 方法也有调用。

RequestToViewNameTranslator 的默认实现是 DefaultRequestToViewNameTranslator ,会直接把请求的 URI 去掉应用上下文部分,去掉请求的拓展后缀,返回中间那一段,

DefaultRequestToViewNameTranslator 的例子:

http://localhost:8080/gamecast/display.html » display

http://localhost:8080/gamecast/displayShoppingCart.html » displayShoppingCart

http://localhost:8080/gamecast/admin/index.html » admin/index

DefaultRequestToViewNameTranslator 又是啥时候注册到容器中的呢?

DispatcherServletinitStrategies 的 initRequestToViewNameTranslator 方法,

protected void initStrategies(ApplicationContext context) {
    // 文件上传解析器
    initMultipartResolver(context);
    // 本地信息解析器,用于国际化
    initLocaleResolver(context);
    initThemeResolver(context);
    // 处理器映射
    initHandlerMappings(context);
    // 处理器适配
    initHandlerAdapters(context);
    // 处理器异常解析策略
    initHandlerExceptionResolvers(context);
    // 从请求中获取视图名的策略
    initRequestToViewNameTranslator(context);
    // 视图解析器解析策略
    initViewResolvers(context);
    initFlashMapManager(context);
}

initRequestToViewNameTranslator 方法中,会直接在 bean 容器中查找名称为 viewNameTranslator 的 bean(对,你没看错,就是直接通过 BeanID 去找),

实际上,在通过 XML 加载 SpringMVC 的时候,具体地说是在处理 <mvc:default-servlet-handler/> 或者 <mvc:annotation-driven> 标签的时候,都会将 DefaultRequestToViewNameTranslator 注册到 IOC 容器中,名字就是 viewNameTranslator,详情请看《SpringMVC- 第九篇:基于 XML 配置 SpringMVC》,

通过注解配置 SpringMVC 的时候也有类似的操作。

如果没有配置 SpringMVC 的时候没有注入相应的 bean,就调用 DispatcherServletgetDefaultStrategy 方法查找,此方法用于获取单个默认策略,此外,还有一个 getDefaultStrategies 方法,用于获取多个默认策略,getDefaultStrategy和getDefaultStrategies 是获取默认策略的通用方法,在 DispatcherServletinitStrategies 加载不同场景下的策略的时候,均通过这两个方法来加载,这两个方法的实际执行也很简单,就是从默认的 properties 文件中加载,文件名为 DispatcherServlet.properties,路径为 org\springframework\web\servlet\DispatcherServlet.properties,跟 DispatcherServlet 处于同一个包下面,内容如下

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
    org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
    org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

我们不仅能看到 RequestToViewNameTranslator 的默认策略实现,还能看到 HandlerExceptionResolver、ViewResolver 的默认策略实现。

知道了原理,那定制起来也就简单了,以后有需要再定制。TODO

实践

控制器方法

@Controller
@RequestMapping("/DefaultView")
public class DefaultViewNameController {

    @RequestMapping("/blankView")
    public String testBlank() {
        return "";
    }

    @RequestMapping("/nullView")
    public void testNull() {
    }

}

测试类方法

@SpringJUnitWebConfig(locations = "classpath:SpringMVC.xml")
class DefaultViewNameControllerTest {

    private MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void testBlank() throws Exception {
        mockMvc.perform(get("/DefaultView/blankView")).andDo(print());
    }

    @Test
    void testNull() throws Exception {
        mockMvc.perform(get("/DefaultView/nullView")).andDo(print());
    }
}

分析操作

先运行 testBlank,会发现直接报错,表示找不到 .html

Caused by: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "ServletContext resource [/WEB-INF/templates/.html]")

说明这个时候,系统依然会把空字符串当作是常规的视图名来进行渲染,这个时候在 /WEB-INF/templates 下新建名字为 .html 的视图模板即可,再测试就不会报错了。

妈的,没想到这也行,手动狗头。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Blank View</title>
</head>
<body>
<h1>空视图名对应的视图</h1>
</body>
</html>

再运行 testNull 方法,依然是直接报错,不过报错信息是,找不到 /WEB-INF/templates/DefaultView/nullView.html

Caused by: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "ServletContext resource [/WEB-INF/templates/DefaultView/nullView.html]")

说明这个时候,默认的视图解析策略生效了,当前的请求的路径去掉应用上下文路径,去掉 URL 后缀,剩下的就是 /DefaultView/nullViewDefaultRequestToViewNameTranslator 将其作为根据路径解析出来的视图名进行解析。去相应的路径下去查找视图模板文件,找到了就正常渲染,没找到,就报错,我们在 /WEB-INF/templates/ 下新建路径 DefaultView,创建名为 nullView 视图文件之后,请求就可以正常解析了

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Null View</title>
</head>
<body>
<h1>null视图名对应的视图</h1>
</body>
</html>

总结

返回空字符串,不会触发默认视图解析,返回 null 才是会触发默认视图解析

在控制器方法返回一个写死的视图名,其实不是一个明智的方法,尤其是当对应的视图包含在很多层路径下的时候,要么你自定义视图解析方法,要么在写视图名称的时候就得把这多层路径带上,写起来很麻烦,这个时候,默认的视图解析就很有用了,我们可以直接在存放视图模板文件的根路径下建立跟请求路径对应的文件夹结构,这样,视图解析的时候就可以自动根据请求路径,去查找对应的视图,这样就很方便了,同时,如果 RequestToViewNameTranslator 的默认实现 DefaultRequestToViewNameTranslator 不能满足我们的要求,我们还可以手动实现 RequestToViewNameTranslator 来自定义这个过程。

On this page
第五篇:视图
  • 官方文档
    1. SpringMVC
      1. 总结
    2. Thymeleaf
  • 各种视图的实践
  • ThymeleafView
    1. 简单的源码分析
    2. ThymeleafViewResolver 源码
  • 转发视图
    1. 简单的源码分析
  • 重定向视图
    1. 简单的源码分析
  • 视图控制器
  • 将 JSP 作为视图的视图解析器 - InternalResourceViewResolver
  • 默认视图解析
    1. 实践
      1. 分析操作
    2. 总结