随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666)
from:Spring 源码分析(四) ——MVC(七)视图呈现 - 水门-kay的个人页面 - OSCHINA - 中文开源技术交流社区
DispatcherServlet 视图设计
前面分析了 Spring MVC 中的 M(Model)和 C(Controller)相关的实现,其中的 M 大致对应 ModelAndView 的生成,而 C 大致对应 DispatcherServlet 和与用户业务逻辑相关的 handler 实现。在 Spring MVC 框架中,DispatcherServlet 起到了非常核心的作用,是整个 MVC 框架的调度枢纽。对应视图呈现功能,它的调用入口同样在 DispatcherServlet 中的 doDispatch 方法中实现。具体来说,它的调用入口是 DispatcherServlet 中的 render 方法。
DispatcherServlet.java
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 从 request 中读取 locale 信息,并设置 response 的 locale 值
Locale locale = this.localeResolver.resolveLocale(request);
response.setLocale(locale);
View view;
// 根据 ModleAndView 中设置的视图名称进行解析,得到对应的视图对象
if (mv.isReference()) {
// 需要对象视图名进行解析
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
// ModelAndView 中有可能已经直接包括了 View 对象,那就可以直接使用
else {
// 直接从 ModelAndView 对象中取得实际的视图对象
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() + "'");
}
}
// 提交视图对象进行展现
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
// 调用 View 实现对数据进行呈现,并通过 HTTPResponse 把视图呈现给 HTTP 客户端
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
// 调用 ViewResolver 进行解析
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
View 接口的设计
下面需要对得到的 View 对象,就行分析。
由此,我们可以看出,Spring MVC 对 常用的视图提供的支持。从这个体系中我们可以看出,Spring MVC 对常用视图的支持,比如 JSP/JSTL 视图、FreeMaker 视图等等。View 的设计其实是非常简单的,只需要实现 Render 接口。
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
JSP 视图的实现
使用 JSP 的页面作为 Web UI,是使用 Java 设计 Web 应用比较常见的选择之一,如果在 JSP 中使用 Jstl(JSP Standard Tag Library)来丰富 JSP 的功能,在 Spring MVC 中就需要使用 JstlView 来作为 View 对象,从而对数据进行视图呈现。而 JstlView 没有实现 render 的方法,而使用的 render 方法是它的基类 AbstractView 中实现的。下面是他的主要时序图:
AbstractView.java
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes);
}
// 这里把所有的相关信息都收集到一个 Map 里
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
// 展现模型数据到视图的调用方法
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
这个基类的 render 方法实现并不困难,而它主要是完成数据的准备工作,比如把所有的数据模型进行整合放到 mergedModel 对象中,而它是一个 HasMap。然后,调用 renderMergedOutputModel()方法。
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 判断需要将哪一个请求的处理器交给 RequestDispatcher
exposeModelAsRequestAttributes(model, request);
// 对数据进行处理
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
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.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
// 转发请求到内部定义好的资源上,比如 JSP 页面,JSP 页面的展现由 Web 容器完成,
// 在这种情况下,View 只是起到转发请求的作用
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(request, response);
}
}
AbstractView.java
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
for (Map.Entry<String, Object> entry : model.entrySet()) {
String modelName = entry.getKey();
Object modelValue = entry.getValue();
if (modelValue != null) {
request.setAttribute(modelName, modelValue);
if (logger.isDebugEnabled()) {
logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() +
"] to request in view with name '" + getBeanName() + "'");
}
}
else {
request.removeAttribute(modelName);
if (logger.isDebugEnabled()) {
logger.debug("Removed model object '" + modelName +
"' from request in view with name '" + getBeanName() + "'");
}
}
}
}
JstlView.java
protected void exposeHelpers(HttpServletRequest request) throws Exception {
if (this.messageSource != null) {
JstlUtils.exposeLocalizationContext(request, this.messageSource);
}
else {
JstlUtils.exposeLocalizationContext(new RequestContext(request, getServletContext()));
}
}
InternalResourceView.java
protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 从 request 中获取 URL 路径
String path = getUrl();
if (this.preventDispatchLoop) {
String uri = request.getRequestURI();
if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
throw new ServletException("Circular view path [" + path + "]: would dispatch back " +
"to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " +
"(Hint: This may be the result of an unspecified view, due to default view name generation.)");
}
}
return path;
}
得到 URL 路径后,使用 RequestDispatcher 把请求转发到这个资源上,就完成了 JSTL 的 JSP 页面展现。
——水门(2016年4月写于杭州)