环境配置
这里直接使用 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>
页面效果

视图解析过程分析
我们知道 Spring Boot 的请求会经过 DispatcherServlet#doDispatch 被分发到对应的 Handler 中进行处理,所以直接在这个方法中下断点进行分析
封装 ModelAndView
在获取到对应的 handler 后通过 ha.handle 方法得到了 ModeAndView 对象

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

一路跟进到 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 方法来寻找视图

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

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

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

从 mavContainer 对象中取出视图名称和数据封装成 ModelAndView 对象返回出去,自此就完成了 ModelAndView 的封装
简单概括一下就是通过 url 匹配到对应的 Controller,反射调用 Controller 获得返回值,根据返回值类型寻找合适的视图名称,最后把视图名称和数据封装成 ModelAndView 对象
获取视图
拿到 ModelAndView 对象进入 DispatcherServlet#processDispatchResult 方法

在 DispatcherServlet#render 方法中获取到视图解析器并完成最后的渲染
可以看到当 ModelAndView 对象中的视图名称不为 null 时,通过 DispatcherServlet#resolveViewName 方法获取视图解析器

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

这里获取到的 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;
}
}
这里主要就是两个方法 getCandidateViews 和 getBestView
在 getCandidateViews 方法中,遍历所有的 ViewResolver,将解析成功的视图放到视图候选列表中

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

渲染视图
在 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 + "'");
}