Spring MVC 视图解析流程
2022-09-18 21:00:20

环境配置

这里直接使用 Spring Boot 和 Thymeleaf 快速搭建一个环境

编写 Controller

@Controller
public class UrlController {

    @GetMapping(value = {"/", "index"})
    public String indexController(Model model) {
        model.addAttribute("name", "Zh0um1");
        return "index";
    }
}

resource/templates 目录下创建 index.html 作为模板

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Index Page</title>
</head>
<body>
<p>Hello Thymeleaf</p>
<div th:text="${name}"></div>
</body>
</html>

页面效果

Pasted-image-20220918211121.png

视图解析过程分析

我们知道 Spring Boot 的请求会经过 DispatcherServlet#doDispatch 被分发到对应的 Handler 中进行处理,所以直接在这个方法中下断点进行分析

封装 ModelAndView

在获取到对应的 handler 后通过 ha.handle 方法得到了 ModeAndView 对象

Pasted-image-20220918214704.png

一路跟进该方法直到 RequestMappingHandlerAdapter#handleInternal 方法中,可以看到 ModelAndView 对象是通过 invokeHandlerMethod 方法获取到的

Pasted-image-20220918215303.png

一路跟进到 ServletInvocableHandlerMethod#invokeAndHandle 方法中

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    setResponseStatus(webRequest);

    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    }
    else if (StringUtils.hasText(getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }

    mavContainer.setRequestHandled(false);
    Assert.state(this.returnValueHandlers != null, "No return value handlers");
    try {
        this.returnValueHandlers.handleReturnValue(
                returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    catch (Exception ex) {
        if (logger.isTraceEnabled()) {
            logger.trace(formatErrorForReturnValue(returnValue), ex);
        }
        throw ex;
    }
}

通过 InvocableHandlerMethod#invokeForRequest 方法获取到请求的返回值

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

通过 doInvoke 方法反射调用对应的 Controller,获取到方法的返回值 returnValue

值得一提的是这里的 InvocableHandlerMethod 对象示例是在 doDispatch 方法中获取到的 handler 对象的的包装类(应该是这么叫的吧),也就是有些文章说的通过 invokeForRequest 函数,根据用户提供的 url,调用相关的 Controller

随后根据 returnValue 是否为 null 来寻找视图,也就是模板文件,这里返回了 index,所以会通过 handleReturnValue 方法来寻找视图

Pasted-image-20220918235304.png

handleReturnValue 中,通过 selectHandler 方法遍历所有的 returnValueHandlers 来获取合适的 handler

Pasted-image-20220918235929.png

随后在 handler.handleReturnValue() 根据返回值来确定视图名称

Pasted-image-20220919000450.png

invokeAndHandle 方法完成后来到 RequestMappingHandlerAdapter#getModelAndView 方法中

Pasted-image-20220919001831.png

mavContainer 对象中取出视图名称和数据封装成 ModelAndView 对象返回出去,自此就完成了 ModelAndView 的封装

简单概括一下就是通过 url 匹配到对应的 Controller,反射调用 Controller 获得返回值,根据返回值类型寻找合适的视图名称,最后把视图名称和数据封装成 ModelAndView 对象

获取视图

拿到 ModelAndView 对象进入 DispatcherServlet#processDispatchResult 方法

Pasted-image-20220920103933.png

DispatcherServlet#render 方法中获取到视图解析器并完成最后的渲染

可以看到当 ModelAndView 对象中的视图名称不为 null 时,通过 DispatcherServlet#resolveViewName 方法获取视图解析器

Pasted-image-20220920104200.png

获取视图的逻辑很简单,就是通过遍历所有的视图解析器,通过 resolveViewName 方法来解析视图名称对应的视图,解析成功就返回该视图

Pasted-image-20220920104848.png

这里获取到的 ThymeleafView 视图是从 ContentNegotiatingViewResolver 中返回的,而不是 ThymeleafViewResolver,可以跟进 ContentNegotiatingViewResolver#resolveViewName 看看具体实现

public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }

这里主要就是两个方法 getCandidateViewsgetBestView

getCandidateViews 方法中,遍历所有的 ViewResolver,将解析成功的视图放到视图候选列表中

Pasted-image-20220920105926.png

然后通过 getBestView 方法从候选列表中拿到最适配的视图,该方法会优先匹配存在重定向操作的视图,如果不存在重定向操作的视图则根据请求头中的 Accept 字段的值与 candidateViews 的相关顺序,并判断是否兼容来返回最适配的 View

Pasted-image-20220920110323.png

渲染视图

DispatcherServlet#render 中获取到视图后会调用视图对应的 render 方法

这里会调用 ThymeleafView#render 方法,最终会调用到 ThymeleafView#renderFragment 方法

renderFragment 方法中会将之前获得的视图名称作为模板名,获取到模板渲染引擎后,解析模板中的表达式,最后返回到浏览器中

关键代码如下

final String viewTemplateName = getTemplateName();
final ISpringTemplateEngine viewTemplateEngine = getTemplateEngine();

final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

final FragmentExpression fragmentExpression;
try {
    // By parsing it as a standard expression, we might profit from the expression cache
    fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
} catch (final TemplateProcessingException e) {
    throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}