springmvc的php视图,SpringMVC 视图渲染过程解析

基本概念

所有的 MVC 框架都有一套它自己的解析视图的机制,springmvc 也不例外,它使用ViewResolver 进行视图解析,让用户在浏览器中渲染模型。

Springmvc 处理视图最重要的两个接口是 ViewResolver 和 View :

ViewResolver 接口在视图名称和真正的视图之间提供映射,它是一种开箱即用的技术,能够解析 JSP、Velocity 模板和 XSLT 等多种视图:

public interface ViewResolver {

// 关键 --> 根据 vieName 返回 View 视图

View resolveViewName(String viewName, Locale locale) throws Exception;

}

View 接口则处理请求将真正的视图呈现给用户:

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 model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}

视图解析在 springmvc 中配置如下(以 InternalResourceViewResolver 为例 ):

bean :表示指定的 ViewResolver;

viewClass : 表示要解析的视图类型,如 JSP 等;

prefix/suffix : 表示路径前缀/后缀,假设 viewname 为 hello,则完整的路径为:“/WEB-INF/page/hello.jsp”

1.过程

找到 DispatcherServlet 的 doDispatch 方法,该方法负责实现 springmvc 的请求转发过程,该过程最后一步就是处理请求结果,并进行视图渲染。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

// ...

try {

ModelAndView mv = null;

Exception dispatchException = null;

// ...

// 调用控制器(controller)匹配请求路径的处理方法,并返回 mv

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// ...

// 关键 --> 处理请求结果(同时在这里根据 mv 进行视图渲染)

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

}

catch (Exception ex) {

// ...

}

catch (Error err) {

// ...

}

finally {

// ...

}

}

再来看 processDispatchResult 这个方法,该方法负责处理请求结果,并进行视图渲染。

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,

HandlerExecutionChain mappedHandler,ModelAndView mv, Exception exception) throws Exception {

boolean errorView = false;

// 异常处理...

if (mv != null && !mv.wasCleared()) {

// 渲染页面

render(mv, request, response);

if (errorView) {

WebUtils.clearErrorRequestAttributes(request);

}

} else {

// 日志输出...

}

// ...

}

接着来看 render 方法,该方法负责视图渲染的过程。

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

// 根据请求决定回复消息的 locale

Locale locale = this.localeResolver.resolveLocale(request);

response.setLocale(locale);

View view;

// 判断 mv 是否字符串

if (mv.isReference()) {

// 关键 -> 解析视图名返回 view,为空则抛出异常

view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);

if (view == null) {

// 抛出异常...

}

} else {

// 若 mv 不是字符串直接取得视图,为空则抛出异常

view = mv.getView();

if (view == null) {

// 抛出异常...

}

}

// 日志输出...

try {

// 关键 -> 由 View 进行真正的视图渲染

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;

}

}

最后来看看 resolveViewName,该方法在 mv 属于字符串类型时会被调用。

protected View resolveViewName(String viewName, Map model, Locale locale,HttpServletRequest request) throws Exception {

// 遍历 Ioc 容器中定义的 ViewResolver

for (ViewResolver viewResolver : this.viewResolvers) {

// 关键 -> 调用 它的 resolveViewName 方法

View view = viewResolver.resolveViewName(viewName, locale);

// 不为空,则返回 view

if (view != null) {

return view;

}

}

return null;

}

2.总结

分析完 springmvc 视图解析过程,再来做下总结:

请求转发过程中(doDispatch)最后进行请求结果的处理,同时会进行视图渲染(processDispatchResult);

处理完请求结果,调用 redner 方法进行视图渲染;

在渲染过程中,判断 mv 是否属于字符串类型,是的话调用 resolveViewName 方法(该方法会遍历 Ioc 容器中 ViewResolver ,并调用它的 resolveViewName )取得视图名,否则直接取得视图名。最后再调用 View 的 render 方法进行真正的视图渲染。

可以整个视图机制都围绕着 ViewResolver 和 View 两个接口展开,简单流程如下:

doDispatch - processDispatchResult - redner - resolveViewName - ViewResolver.resolveViewName - View.render

源码分析(ViewResolver)

分析完 springmvc 的视图解析机制,再来看看 ViewResovler 这个接口。上面提到过,它的主要作用是根据【视图名(ViewName)返回视图(View)】。

首先来看它的继承关系:

0589ea7ff316b3176e1abb73ac87966a.png

1.AbstractCachingViewResolver

通过上面的继承关系图可以知道该类直接实现了 ViewResolver 接口,是带有缓存功能的基础实现抽象类。

重点来看 resolveViewName 这个方法:

// 成员变量

public static final int DEFAULT_CACHE_LIMIT = 1024

private final Map viewAccessCache = new ConcurrentHashMap(DEFAULT_CACHE_LIMIT);

private final Map viewCreationCache = new LinkedHashMap(DEFAULT_CACHE_LIMIT, 0.75f, true) {

@Override

protected boolean removeEldestEntry(Map.Entry eldest) {

if (size() > getCacheLimit()) {

viewAccessCache.remove(eldest.getKey());

return true;

} else {

return false;

}

}

};

// 方法

public View resolveViewName(String viewName, Locale locale) throws Exception {

// 缓存功能是否打开,只要 cacheLimit(默认为 1024) 大于 0 就代表缓存功能默认打开

if (!isCache()) {

// 若缓存关闭,则创建视图

return createView(viewName, locale);

} else {

// 返回 viewName + "_" + locale

Object cacheKey = getCacheKey(viewName, locale);

// 从 viewAccessCache 取得 view

View view = this.viewAccessCache.get(cacheKey);

if (view == null) {

synchronized (this.viewCreationCache) {

// 从 viewCreationCache 取得 view

view = this.viewCreationCache.get(cacheKey);

if (view == null) {

// 为空,创建视图

view = createView(viewName, locale);

// 仍然为空,则标记为【不可解析视图】

if (view == null && this.cacheUnresolved) {

view = UNRESOLVED_VIEW;

}

if (view != null) {

// 添加进缓存

this.viewAccessCache.put(cacheKey, view);

this.viewCreationCache.put(cacheKey, view);

// 日志输出...

}

}

}

}

return ( view != UNRESOLVED_VIEW ? view : null );

}

}

再来看看它的 createView 方法,该方法负责视图的创建。

protected View createView(String viewName, Locale locale) throws Exception {

return loadView(viewName, locale);

}

// 抽象方法,留给子类做具体实现

protected abstract View loadView(String viewName, Locale locale) throws Exception;

观察代码,可以该类的视图解析过程如下:

缓存关闭,则创建视图;

缓存开启,先后从 viewCreationCache,viewCreationCache 这个两个缓存集合里查找视图,仍然为空,则创建视图,并分别添加进这两个缓存集合里。

2.UrlBasedViewResolver

继承自 AbstractCachingViewResolver 抽象类、并实现 Ordered 接口的类,是 ViewResolver 接口简单的实现类,该类重写了 createView,loadView 这两个方法。

首先来看它的 createView 方法

protected View createView(String viewName, Locale locale) throws Exception {

// 是否支持该视图的名的处理

if (!canHandle(viewName, locale)) {

// 返回 null 进入下一个视图解析器

return null;

}

// 检查是否以 "redirect" 开头

if (viewName.startsWith(REDIRECT_URL_PREFIX)) {

String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());

RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());

return applyLifecycleMethods(viewName, view);

}

// 检查是否以 "forward" 开头

if (viewName.startsWith(FORWARD_URL_PREFIX)) {

String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());

return new InternalResourceView(forwardUrl);

}

// 其他情况由父类处理,其实就是调用该类的 loadView 方法

return super.createView(viewName, locale);

}

再来看看它 的 loadView 方法,如果视图名不包含 redirect,forward 时,调用父类的createView 的时会调用到它。

protected View loadView(String viewName, Locale locale) throws Exception {

// 关键 -> 根据 ViewName 取得 View

AbstractUrlBasedView view = buildView(viewName);

View result = applyLifecycleMethods(viewName, view);

return ( view.checkResource(locale) ? result : null );

}

protected AbstractUrlBasedView buildView(String viewName) throws Exception {

// 实例化视图类(View)

AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());

// 根据【前缀+视图名+后缀】生成 url,并添加进 View

view.setUrl(getPrefix() + viewName + getSuffix());

// 取得网页的类型和编码 ,并添加进 View

String contentType = getContentType();

if (contentType != null) {

view.setContentType(contentType);

}

// 设置 View 的其他相关属性

view.setRequestContextAttribute(getRequestContextAttribute());

view.setAttributesMap(getAttributesMap());

Boolean exposePathVariables = getExposePathVariables();

if (exposePathVariables != null) {

view.setExposePathVariables(exposePathVariables);

}

Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();

if (exposeContextBeansAsAttributes != null) {

view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);

}

String [ ] exposedContextBeanNames = getExposedContextBeanNames();

if (exposedContextBeanNames != null) {

view.setExposedContextBeanNames(exposedContextBeanNames);

}

return view;

}

3.InternalResourceViewResolver

继承自 UrlBasedViewResolver ,以 InternalResourceView 作为视图,若项目中存在“javax.servlet.jsp.jstl.core.Config”该类,那么会以 JstlView 作为视图。

重写了 buildView 方法,主要就是为了给 InternalResourceView 视图设置属性。

源码分析(View)

View 负责视图的真正渲染。来看它的继承关系(部分显示):

1b93582d608425566a78c86a1925238c.png

1.AbstractView

View接口的基础实现抽象类,重点来看 render 方法:

public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {

// 日志记录...

// 将属性合并到 map 中去

Map mergedModel = createMergedOutputModel(model, request, response);

// 判断是否是下载资源,并设置请求头(为了解决 IE 下 https 下载请求的 bug)

prepareResponse(request, response);

// 抽象方法,留给子类实现

renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

}

createMergedOutputModel 方法:

protected Map createMergedOutputModel(Map model, HttpServletRequest request,HttpServletResponse response) {

@SuppressWarnings("unchecked")

Map pathVars = (this.exposePathVariables ?(Map) request.getAttribute(View.PATH_VARIABLES) : null);

// 把 exposePathVariables,staticAttributes,modelMap,以及 requestContextAttribute 的属性都添加进新的 map

int size = this.staticAttributes.size();

size += (model != null ? model.size() : 0);

size += (pathVars != null ? pathVars.size() : 0);

Map mergedModel = new LinkedHashMap(size);

mergedModel.putAll(this.staticAttributes);

if (pathVars != null) {

mergedModel.putAll(pathVars);

}

if (model != null) {

mergedModel.putAll(model);

}

// Expose RequestContext?

if (this.requestContextAttribute != null) {

mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));

}

return mergedModel;

}

观察代码可以发现该类的主要工作是将各种属性都添加进 map 中,便于属性的传递,管理。

2.AbstractUrlBasedView

继承自 AbstractView 抽象类,增加了1个类型为 String 的 url 参数。

3.InternalResourceView

继承自 AbstractUrlBasedView 抽象类的类,该类重写了renderMergedOutputModel 这个方法:

protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {

// 将 map 的值添加 reqeust 的 attribute

exposeModelAsRequestAttributes(model, request);

// Expose helpers as request attributes, if any.

exposeHelpers(request);

// 验证要渲染的页面地址

String dispatcherPath = prepareForRendering(request, response);

// 为指定的页面建立 RequestDispatcher

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!");

}

// 判断是 request 是 include 方法还是 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.

if (logger.isDebugEnabled()) {

logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");

}

rd.forward(request, response);

}

}

实例探究

利用 ContentNegotiatingViewResolver 集成多种视图

首先来看配置文件(这里集成 vm,jsp,json,xml 这四种视图显示)

Velocity 属性文件配置

input.encoding=UTF-8

output.encoding=UTF-8

contentType=text/html;charset=UTF-8

resource.loader=webapp

webapp.resource.loader.class=org.apache.velocity.tools.view.WebappResourceLoader

webapp.resource.loader.path=/WEB-INF/velocity/

测试

// 先用 velocity 视图解析(order = 0),然后再用 jsp 视图解析

https://localhost:8080/demo/hello

// 返回 json 视图

https://localhost:8080/demo/hello.json

// 返回 xml 视图

https://localhost:8080/demo/hello.xml

注意:

这里利用路径扩展名(favorPathExtension)区分 MIME 类型,以此确定要返回的视图类型。也可以利用参数(默认为 fomart)来区分。

VelocityViewResolver(velocity 视图解析)的 order 优先级必须比InternalResourceViewResolver(jsp 视图解析器)高。因为InternalResourceViewResolver 不管路径下是否存在指定 jsp 文件都会返回 View(视图),这样会导致 VelocityViewResolver 不生效。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值