0
点赞
收藏
分享

微信扫一扫

SpringMVC运行流程分析之核心流程

关联博客:
​SpringMVC源码分析之策略对象初始化SpringMVC运行流程分析之前置流程SpringMVC运行流程分析之核心流程 前面SpringMVC运行流程分析之前置流程

博文中我们分析了springMVC请求处理的前置流程,本文我们分析其核心流程也就是​​doDispatch(request, response);​​。

这里先放一下时序图展示整体流程:
SpringMVC运行流程分析之核心流程_springmvc核心流程
那么HandlerAdapter核心流程逻辑呢?
SpringMVC运行流程分析之核心流程_sed_02

【1】整体结构

① 源码概览

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
// 处理器执行链对象
HandlerExecutionChain mappedHandler = null;
// 是否multipart请求
boolean multipartRequestParsed = false;

// 异步处理管理器
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
//模型视图对象
ModelAndView mv = null;
// 异常对象
Exception dispatchException = null;

try {
// 如果是multipart request 将会尝试使用multipartResolver解析
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
// 获取处理器适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
// 如果是get且没有修改,直接返回
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 如果某个拦截器前置方法返回false,直接返回
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 核心处理方法
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// 如果是异步请求处理,则这里就返回
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 如果MV不为null,但是没有view,则尝试获取defaultViewName
applyDefaultViewName(processedRequest, mv);
// 处理执行链的后置处理--即拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理最后结果,这里可能会返回到一个页面或者抛出异常,或者返回json等
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 拦截器的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
// 如果是异步请求 且 mappedHandler 不为null,
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
// 如果拦截器是asyncInterceptor类型,则调用afterConcurrentHandlingStarted方法
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
// 如果是multipartRequest,则清理资源 fielItem哦
cleanupMultipart(processedRequest);
}
}
}
}

仅就这段方法来看,其流程如下所示:
SpringMVC运行流程分析之核心流程_springmvc核心流程_03
这里有三个步骤十分关键,分别是第三步​​​mappedHandler = getHandler(processedRequest);​​​获取执行器链,第六步​​mv = ha.handle(processedRequest, response, mappedHandler.getHandler());​​​获取MV以及最终的第十步​​processDispatchResult​​处理最终结果。

第三步是给请求尝试找到一个handler,比如某个controller某个方法,或者说默认的处理器DefaultServletHttpRequestHandler等。然后实例化HandlerExecutionChain 后添加合适的拦截器。

第六步则是核心的处理过程,会进行参数解析、目标方法的反射调用以及最终返回结果的处理。

第十步processDispatchResult处理最终结果通常比如渲染视图或者返回json。如果在前面处理过程中抛出了异常,那么这里也会对异常尝试处理。

② 核心对象HandlerExecutionChain

HandlerExecutionChain ,处理器执行链。其通过遍历HandlerMapping,根据其方法​​HandlerExecutionChain getHandler(HttpServletRequest request)​​获取得到。

通过如下核心属性与构造方法可知,其由handler和HandlerInterceptor组成。handler比如HandlerMethod(或其他handler类)即实际处理请求的目标类(方法)。HandlerInterceptor就是拦截器,在处理流程中分别会触发其preHandle、postHandle以及afterCompletion方法。

HandlerExecutionChain 中的handler可能如下:

实现了org.springframework.web.servlet.function.HandlerFunction,比如ResourceHandlerFunction
实现了org.springframework.web.servlet.mvc.Controller,比如ParameterizableViewController
实现了org.springframework.web.HttpRequestHandler,比如DefaultServletHttpRequestHandler
实现了javax.servlet.Servlet,比如ViewStatusMessagesServlet
实现了org.springframework.web.method.HandlerMethod,比如ServletInvocableHandlerMethod,controller里面很多方法就被包装为了该类型。这个也是最常见的

核心属性和构造方法:

private final Object handler;

private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

private int interceptorIndex = -1;

/**
* Create a new HandlerExecutionChain.
* @param handler the handler object to execute
*/
public HandlerExecutionChain(Object handler) {
this(handler, (HandlerInterceptor[]) null);
}

③ 核心对象ModelAndView

模型视图对象,模型这里理解为一个存放数据的map,视图view通常需要一个​​ViewResolver​​解析处理。视图解析器会获取到model然后渲染view最后返回给前台一个解析后的页面。

我们下面看下其核心属性和构造方法。

// 视图实例或者视图名称
@Nullable
private Object view;

// 存放数据 map结构
@Nullable
private ModelMap model;

// 响应状态码 可为空
@Nullable
private HttpStatus status;

// 指示是否已经调用clear方法进行清理
private boolean cleared = false;

// 空构造方法
public ModelAndView() {
}

// 设置viewName
public ModelAndView(String viewName) {
this.view = viewName;
}

// 设置view
public ModelAndView(View view) {
this.view = view;
}


// 设置viewName和model
public ModelAndView(String viewName, @Nullable Map<String, ?> model) {
this.view = viewName;
if (model != null) {
getModelMap().addAllAttributes(model);
}
}

// 设置view和model
public ModelAndView(View view, @Nullable Map<String, ?> model) {
this.view = view;
if (model != null) {
getModelMap().addAllAttributes(model);
}
}
// 设置viewName status
public ModelAndView(String viewName, HttpStatus status) {
this.view = viewName;
this.status = status;
}

// 完整实例
public ModelAndView(@Nullable String viewName, @Nullable Map<String, ?> model, @Nullable HttpStatus status) {
this.view = viewName;
if (model != null) {
getModelMap().addAllAttributes(model);
}
this.status = status;
}


// 设置viewName 然后向model中添加属性 modelName = modelObject
public ModelAndView(String viewName, String modelName, Object modelObject) {
this.view = viewName;
addObject(modelName, modelObject);
}

// 设置view然后向model中添加属性 modelName = modelObject
public ModelAndView(View view, String modelName, Object modelObject) {
this.view = view;
addObject(modelName, modelObject);
}

另外,关于HandlerMapping组件、HandlerAdapter组件可以参考下面博文,这里不再赘述。
​SpringMVC常见组件之HandlerMapping分析​、SpringMVC常见组件之HandlerAdapter分析

【2】方法说明

① checkMultipart

源码如下所示,其核心是判断当前请求是否为​​multipart request​​​并尝试使用​​multipartResolver​​进行解析。通常这一步应用于文件上传场景。

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}

② getHandler(processedRequest)

获取处理器执行链对象​​HandlerExecutionChain​​​。这里本质是遍历容器中的​​HandlerMapping​​ ,调用其getHandler方法获取HandlerExecutionChain 。

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

关于​​mapping.getHandler(request)​​​具体逻辑参考博文SpringMVC常见组件之HandlerMapping分析。

③ getHandlerAdapter(mappedHandler.getHandler())

同②一样,这里会遍历容器中的​​handlerAdapters​​​,找到一个支持当前handler的适配器。如果找不到,将会抛出​​ServletException​​异常。

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

④ 拦截器的方法

这里指的是​​HandlerExecutionChain.applyPreHandle、HandlerExecutionChain.applyPostHandle​​​以及​​triggerAfterCompletion(有两个哦)​​。

① 前置处理与后置处理

applyPreHandle与applyPostHandle本质只是遍历interceptorList,调用其preHandle方法。

需要注意的是在applyPreHandle中,如果某个拦截器的preHandle方法返回false,则会触发triggerAfterCompletion方法。applyPostHandle则不会触发triggerAfterCompletion方法。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
// 这里第三个参数为null
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}

②triggerAfterCompletion

首先是​​DispatcherServlet​​​的​​triggerAfterCompletion​​​,其调用位置如下图所示(会将异常跑出去):
SpringMVC运行流程分析之核心流程_mvc_04
方法如下所示,其会调用​​​mappedHandler(HandlerExecutionChain)​​​的​​triggerAfterCompletion​​方法,然后抛出异常。

private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {

if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}

​HandlerExecutionChain​​​的​​triggerAfterCompletion​​​方法如下所示,本质是遍历​​interceptorList​​​调用每一个拦截器的​​afterCompletion​​方法。

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}

从这里也可以看出​​HandlerExecutionChain​​​虽然维护了​​handler​​​与​​interceptorList​​​,但是本身没有业务逻辑,具体业务逻辑处理都委派给了​​HandlerInterceptor​​。

⑤ 默认视图名称处理

如果​​ha.handle(processedRequest, response, mappedHandler.getHandler())​​返回的MV不是null,但是却没有view。那么这里会尝试根据request获取一个默认viewName。

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
mv.setViewName(defaultViewName);
}
}
}

// 获取默认视图名称方法如下
@Nullable
protected String getDefaultViewName(HttpServletRequest request) throws Exception {
return (this.viewNameTranslator != null ? this.viewNameTranslator.getViewName(request) : null);
}

这里根据请求获取默认视图名称步骤如下:

  • ①获取请求路径 lookupPath
  • ② 如果是以​​/​​​ 开头,则去掉头部的​​/​​;
  • ③ 如果是以​​/​​​ 结尾,则去掉尾部的​​/​​;
  • ④ 去掉文件拓展名​​e.g. "mypath/myfile.txt" -> "mypath/myfile".​
  • ⑤ 如果SLASH不等于separator,则进行替换​​path = StringUtils.replace(path, SLASH, this.separator);​​​,默认情况下​​SLASH==separator==/​

假设这里请求是​​http://localhost:8080/testNoView​​​,那么获取的lookupPath为 ​​/testNoView​​​,得到的默认视图名称是testNoView。如果目标位置不存在这个文件,则会抛出如下异常:
SpringMVC运行流程分析之核心流程_mvc_05

【3】结果处理

这里指的是​​processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);​​。

① processDispatchResult

代码如下所示,分为这样几步:异常处理、视图处理、异步请求判断以及最后的triggerAfterCompletion。

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;
// 异常处理
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 视图处理
if (mv != null && !mv.wasCleared()) {
//核心步骤--视图渲染
render(mv, request, response);
if (errorView) { //如果是错误视图,清理request中error相关属性
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
// 异步请求判断
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
// triggerAfterCompletion 处理 这里不再赘述
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

② 异常处理

如下所示是doDispatch的核心逻辑 。

//doDispatch的核心逻辑
DispatcherServlet#doDispatch
- processedRequest = checkMultipart(request);
- mappedHandler = getHandler(processedRequest);
- HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
- mappedHandler.applyPreHandle(processedRequest, response)
- mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- applyDefaultViewName(processedRequest, mv);
- mappedHandler.applyPostHandle(processedRequest, response, mv);
- processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
- triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
-

如果我们的业务抛出了异常,那么是在什么位置呢?

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- AbstractHandlerMethodAdapter#handle
- RequestMappingHandlerAdapter#handleInternal
- mav = invokeHandlerMethod(request, response, handlerMethod);
- RequestMappingHandlerAdapter#invokeHandlerMethod
// 假设这里抛出异常--通常就是我们的业务异常
- invocableMethod.invokeAndHandle(webRequest, mavContainer);
- return getModelAndView(mavContainer, modelFactory, webRequest);

如下图所示,当执行​​invokeAndHandle​​​反射调用我们的目标方法时,假设业务抛出了异常,那么这里会一路抛出到​​DispatcherServlet.doDispatch​​​中,交给​​processDispatchResult​​​去处理。无论​​RequestMappingHandlerAdapter​​​还是​​ServletInvocableHandlerMethod​​​都没有对这里的异常进行拦截。
SpringMVC运行流程分析之核心流程_sed_06

如下代码所示,如果异常是​​ModelAndViewDefiningException​​​,则直接获取其​​ModelAndView​​​属性。否则获取handler,然后根据​​processHandlerException(request, response, handler, exception)​​尝试获取MV。

​processHandlerException​​​放入如下所示,首先从request移除属性​​HandlerMapping.class.getName() + ".producibleMediaTypes"​​​;然后尝试用​​HandlerExceptionResolver​​ 处理异常获取到exMV。获取到exMV后如果其不为null,那么将会是否有数据,是否有view;最后暴露错误属性信息给request。

@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {

// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

// 遍历异常解析器处理异常,核心步骤
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
// 判断model是否为空
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
// 获取默认视图名称
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
// 暴露错误相关属性信息给request
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}

throw ex;
}

如果我们使用​​@ControllerAdvice​​​和​​@ExceptionHandler(value = {Exception.class})​​​来捕捉异常进行全局异常处理。那么上面​​exMv = resolver.resolveException(request, response, handler, ex);​​​我们会调用异常解析器去处理异常,得到的exMV呢是一个空的ModelAndView。
SpringMVC运行流程分析之核心流程_mvc_07

关于异常解析器处理异常逻辑参考博文SpringMVC常见组件之HandlerExceptionResolver分析​。这里我们看下​​WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());​​方法。

public static void exposeErrorRequestAttributes(HttpServletRequest request, Throwable ex,
@Nullable String servletName) {

exposeRequestAttributeIfNotPresent(request, ERROR_STATUS_CODE_ATTRIBUTE, HttpServletResponse.SC_OK);
exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_TYPE_ATTRIBUTE, ex.getClass());
exposeRequestAttributeIfNotPresent(request, ERROR_MESSAGE_ATTRIBUTE, ex.getMessage());
exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_ATTRIBUTE, ex);
exposeRequestAttributeIfNotPresent(request, ERROR_REQUEST_URI_ATTRIBUTE, request.getRequestURI());
if (servletName != null) {
exposeRequestAttributeIfNotPresent(request, ERROR_SERVLET_NAME_ATTRIBUTE, servletName);
}
}

错误属性key如下所示(都是​​public static final​​):

String EXCEPTION_ATTRIBUTE = DispatcherServlet.class.getName() + ".EXCEPTION";

String ERROR_STATUS_CODE_ATTRIBUTE = "javax.servlet.error.status_code";
String ERROR_EXCEPTION_TYPE_ATTRIBUTE = "javax.servlet.error.exception_type";
String ERROR_MESSAGE_ATTRIBUTE = "javax.servlet.error.message";
String ERROR_EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception";
String ERROR_REQUEST_URI_ATTRIBUTE = "javax.servlet.error.request_uri";
String ERROR_SERVLET_NAME_ATTRIBUTE = "javax.servlet.error.servlet_name";

③视图处理

视图处理这里指的是​​render(mv, request, response);​​。ModelAndView里面包含了数据和视图信息,那么这一部分通常主要逻辑就是数据渲染,给浏览器一个解析好的页面。

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 尝试解析获取得到locale信息
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
//为响应设置locale信息
response.setLocale(locale);

View view;
// 获取视图名称
String viewName = mv.getViewName();
// 如果视图名称不为null,则解析得到View实例
if (viewName != null) {
//核心步骤哦,解析viewName,会遍历视图解析器处理
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// 如果不存在viewName则尝试获取view实例,也不存在,则抛出异常
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() + "'");
}
}

// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
// 如果MV中有http status ,则给response设置status状态码
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// 视图渲染,核心步骤,调用具体的View实例的render方法
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}

这里有两步需要注意:​​resolveViewName(viewName, mv.getModelInternal(), locale, request);​​​解析视图名称得到具体的View以及​​view.render(mv.getModelInternal(), request, response);​​调用具体的View实例的render方法进行视图渲染。

如下所示​​resolveViewName​​​方法解析视图名称得到实际的View实例,核心是遍历容器中的​​viewResolvers​​​调用其​​resolveViewName​​方法得到View实例。

@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;
}


举报

相关推荐

0 条评论