环境配置
这里直接使用 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 + "'");
}