源码分析-视图解析器与视图
# 360.源码分析-视图解析器与视图
之前我们简单讲了下 Thymeleaf 开发的基本过程,例如返回页面,转发请求,遍历数据等;本文就来讲讲视图解析的原理。
# doDispatch
我们还是从 DispatcherServlet
类的 doDispatch
方法说起,打个断点:
我们先来测试登录的请求,也打个断点
然后我们以 debug 的方式启动。之前我们已经讲过方法参数的处理过程了,这里就不再赘述。
启动项目后,我们打开登录页 localhost: 9999 (opens new window),此时后台会执行到断点处,我们先放行,因为我们测的是 login 请求,注意,可能会有多个请求,例如一些静态资源的请求,
然后输入用户名和密码,点击登录,此时会执行到 login 请求(可以通过查看变量确定是否 login 请求):
然后我们执行到 handle 方法,并步入进去:
再次步入:
再次步入,然后里面的 invokeHandlerMethod
,就是执行我们的目标方法
invokeHandlerMethod
方法里会初始化参数解析器,返回值解析器等;该方法执行完后,就会调用我们自己的 controller 方法:
然后我们继续放行,等到方法执行完后,就会有返回值:
下一步,就是寻找返回值的 handler,然后处理;我们步入进去:
然后可以看到其会调用一个 selectHandler 方法,用来选择哪个 handler 来处理返回值;执行后,可以看到会选择 ViewNameMethodReturnValueHandler
选择的原理也很简单,遍历每个 Handler,调用 supportsReturnType 方法;
对于 ViewNameMethodReturnValueHandler
而言,会判断是否返回为空,或者返回字符串:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
2
3
4
5
其处理返回值的方法:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址
然后调用 isRedirectViewName
方法,是否重定向(其实就是看字符串是否以 redirect 开头):
protected boolean isRedirectViewName(String viewName) {
return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
}
2
3
接下来我们放行,直到 getModelAndView
方法:
变量 mavContainer,是 ModelAndViewContainer 类型的,里面有所有的方法参数,例如 User 对象。然后会封装一个 ModelAndView 对象
然后会判断是否重定向的请求,是则会将重定向的数据,重新放入:
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
return mav;
2
3
4
5
6
7
8
最后返回 mav,也就是一个 ModelAndView 对象
# processDispatchResult
我们一点点步出,就会来到派发结果的代码:processDispatchResult
方法,它来决定页面如何响应
该方法会做一些初始化,以及判断 mavContainer 是否为空,不是则调用 render 方法进行页面渲染:
render 方法首先会判断是否有(Locale
),暂时不用管
下一步就是解析视图名:
View
是一个接口,定义了render
方法 ,定义了渲染逻辑;其可以从方法参数中获取 model,然后通过 response 对象写入数据。
那怎么得到 view 对象呢?通过 resolveViewName
方法:
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到还是遍历所有的视图解析器,然后调用视图解析器的方法,看看能否获取到 view 对象,能则返回。默认有 5 个视图解析器:
第一个是内容协商的解析器。我们执行代码,步入进去:
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;
}
}
//.......
2
3
4
5
6
7
8
9
10
11
12
首先是一些初始化,然后获取到所有能用的 View 对象(getCandidateViews 方法),例如获取到了有 2 个 RedirectView:
然后根据内容协商,获取到最佳匹配的那个 view 对象(getBestView 方法),并返回
那 getCandidateViews 方法里,是如何获取 view 对象的呢?其实也是通过遍历循环:
其实,viewResolver 里已经包含了刚刚说的那 5 个解析器,因此我们第一个内容协商解析器,就可以返回 View 对象了。
在 viewResolver 中的 resolveViewName 方法中,步入进去是一个抽象方法,调用 createView 方法:
然后由具体的实现类,来调用 create 方法,例如 Thymeleaf 的视图解析器,就会判断是否 redirect 请求,是则返回一个 RedirectView 对象:
得到 view 对象后,就回调用其 render 方法:之前我们说过,render 就是定义了渲染的方法的
我们步入进去,可以看到其最后调用了 renderMergedOutputModel
方法:
其第一步是拿到路径,然后重定向(sendRedirect 方法):
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String targetUrl = createTargetUrl(model, request);
targetUrl = updateTargetUrl(targetUrl, model, request, response);
// Save flash attributes
RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);
// Redirect
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
2
3
4
5
6
7
8
9
10
11
12
13
最后,调用的是我们 Servlet 原生 API 中,最原始的 Servlet:
# Thymeleaf 的 render 方法
刚刚我们讲了下转发请求的时候,是如何执行代码的;那么 Thymeleaf 也是差不多的过程,例如我们可以看看 ThymeleafView 的方法,其就是渲染页面用的:
public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
renderFragment(this.markupSelectors, model, request, response);
}
2
3
可以看到其调用了 renderFragment
方法,该方法代码很多,这里列举一些关键的,例如这是放数据的:
这个是判断是否有 ::
,例如我们之前用的 th:replace
:
最后,最重要的是 viewTemplateEngine.process
方法,其就是渲染 HTML 文件的了:
其最后就是通过输出流,拼接数据,然后返回给浏览器:
# 小结
一句话:视图解析器,根据返回的不同规则(例如返回值是 Redirect 开始,返回值是字符串,返回值以 forward 开始....)得到视图(View 对象),然后通过视图来渲染数据,返回给浏览器
后续我们还可以自定义视图解析器,自定义视图;例如我们上一篇博客是将数据渲染成一个表格;但如果我们想要直接返回一个 Excel 表格,也是可以的,自定义视图并实现 render 方法即可。
视图解析原理流程:
目标方法处理的过程中,所有数据(包括数据和视图地址)都会被放在 ModelAndViewContainer 里
方法的参数如果是一个自定义类型对象(从请求参数中确定的,例如 User 对象),也会把它重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
processDispatchResult 处理派发结果(页面改如何响应)。
render(mv, request, response) 进行页面渲染逻辑,根据方法的 String 返回值得到 View 对象【View 对象定义了页面的渲染逻辑】
所有的视图解析器尝试是否能根据当前返回值得到 View 对象
得到了 redirect:/main.html --> Thymeleaf new RedirectView()
ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的 render 进行页面渲染工作
RedirectView 如何渲染【重定向到一个页面】:首先获取目标 url 地址,然后调用原生的 ServletAPI:
response.sendRedirect(encodedURL);