Spring MVC视图解析

          对于控制器的目标方法,无论其返回值是String、View、ModelMap或是ModelAndView,SpringMVC都会在内部将它们封装为一个ModelAndView对象进行返回。
      Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP也可是Excell、 JFreeChart等各种表现形式的视图。

1、视图(View)

视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。 为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。
视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。所谓视图是无状态的,是指对于每一个请求,都会创建一个View对象。
 JSP是最常见的视图技术。

2、视图解析器(ViewResolver)和视图(View)

  • springMVC用于处理视图最重要的两个接口是ViewResolverView

所以视图解析器的作用就是通过视图名(处理方法的返回值)生成View对象,所有的视图解析器都必须实现ViewResolver接口。
   SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。程序员可以选择一种视图解析器或混用多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出ServletException异常。
在项目中可以配置InternalResourceViewResolver作为视图解析器,在springmvc.xml中可以做如下配置:

    <!--配置视图解析器-->
    <bean id="viewHandler" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="WEB-INF/pages/"/>
        <property name="suffix"  value=".jsp"/>
    </bean>

3、forward: 和redirect:

一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,会经过视图解析器拼串,但如果返回的字符串中带forward:或redirect:前缀时,SpringMVC会对它们进行特殊处理:将forward: 和redirect: 当成指示符,其后的字符串作为URL 来处理。示例如下:
index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SpringMVC给页面输出数据</title>
</head>
<body>
<center>
    <a href="handler1">handler1</a><br/>
    <a href="handler2">handler2</a><br/>
    <a href="handler3">handler3</a><br/>
    <a href="handler4">handler4</a><br/>
</center>
</body>
</html>

hello.jsp,在当前项目的根路径下,和index.html同级

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Hello</title>
</head>
<body>
<center>
    <h1>这是hello.jsp</h1>
</center>
</body>
</html>

ViewTestController.java

package com.xzy.Contorller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ViewTestController {

    /**
     * handler1把请求转发到hello.jsp页面
     * @return
     */
    @RequestMapping("/handler1")
    public String handler1(){

        System.out.println("handler1");
        return "forward:/hello.jsp";
    }

    /**
     * handler把请求转发给handler1
     * @return
     */
    @RequestMapping("/handler2")
    public String handler2(){
        System.out.println("handler2");
        return "forward:handler1";
    }

    /**
     * 重定向到hello.jsp
     * @return
     */
    @RequestMapping("/handler3")
    public  String handler3(){

        System.out.println("handler3");

        return "redirect:/hello.jsp";
    }


    /**
     * 重定向到handler3
     * @return
     */
    @RequestMapping("/handler4")
    public String handler4(){
        System.out.println("handler4");
        return "redirect:handler3";
    }
}

测试结果:

QQ截图20190807154131.png

QQ截图20190807154027.png

按F12打开开发者工具,可以看到确实两次重定向

4、SpringMVC视图的解析流程(结合源码分析)

  • 源码中把任何返回返回值封装为ModelAndView的实现:
    protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        this.checkRequest(request);
        ModelAndView mav;
        if (this.synchronizeOnSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                Object mutex = WebUtils.getSessionMutex(session);
                synchronized(mutex) {
                  mav = this.invokeHandlerMethod(request, response, handlerMethod);
                }
            } else {
                mav = this.invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
            mav = this.invokeHandlerMethod(request, response, handlerMethod);
        }
        if(!response.containsHeader("Cache-Control")) {
   if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {        this.applyCacheSeconds(response,this.cacheSecondsForSessionAttributeHandlers);
            } else {
                this.prepareResponse(response);
            }
        }
        return mav;
    }

这里以发出了一个GET请求为例:
首先FrameworkServlet类会来处理这个GET请求
doGet

protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
 }

processRequest

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       //省略.....
        try {
            //它本类中的这个方法是个抽象方法,实现这个方法的类是DispatcherServlet
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
           //省略.....var17);
        } finally {
           //省略.....
        }

    }

DispatcherServlet 类
doService方法

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
 protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //省略.....

    //给request域中设置了一些东西
    try {
        //调用doDispatch方法处理
        this.doDispatch(request, response);
    } finally {
       ......
    }

    }

doDispatch方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //省略......
       this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        //省略......
    }

processDispatchResult方法,这个方法就是最终将数据交给页面的方法

   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) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
 mv = ((ModelAndViewDefiningException)exception).getModelAndView();
            } else {
   Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
   //如果自己配置了自定义的HandlerExceptionResolver将会在这个方法里处理
   mv = this.processHandlerException(request, response, handler, exception);
             errorView = mv != null;
            }
        }
        if (mv != null && !mv.wasCleared()) {
              //调用render方法进行视图渲染
             this.render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        } else if (this.logger.isTraceEnabled()) {
            this.logger.trace("No view rendering, null ModelAndView returned.");
        }
      //省略......
    }

DispatcherServlet 类 的render方法并没有继承View接口的render,和View接口的render不是一回事,这个render仅仅是为了命名统一而起的一个名字

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
   //省略......
   //从ModelView中拿到视图名
    String viewName = mv.getViewName();
     View view;
     if (viewName != null) {
          //这一步就是得到一个View对象,resolveViewName的实现看下边
          view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
  if (view == null) {
     throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
    }
 } else {
     view = mv.getView();
     if (view == null) {
       throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
       }
    }

  //省略......
   try {
        //省略......
        //调用了View接口的render方法,这里实际上调用的是视图在渲染时会把Model传入
       view.render(mv.getModelInternal(), request, response);
   } catch (Exception var8) {
      //省略......
   }
}

resolveViewName方法,循环遍历你配置的视图解析器,viewResolvers是进过order排序的,这一步就是ViewResolvers是如何通过视图名产生View对象的关键

protected View resolveViewName(String viewName,Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
    //遍历我们配置的视图解析器
    for (ViewResolver viewResolver : this.viewResolvers) {
       //ViewResolver根据方法的返回值,得到一个View对象,这块又有一个resolveViewName,具体的实现请往下看
       View view = viewResolver.resolveViewName(viewName, locale);
       if (view != null) {
         return view;
       }
    }
     return null;
}

InternalResourceViewResolver继承了AbstractCachingViewResolver,resolveViewName方法首先会判断有没有缓存,要是有缓存,它会先去缓存中通过viewName查找是否有View对象的存在,要是没有,它会通过viewName创建一个新的View对象,并将View对象存入缓存中,这样再次遇到同样的视图名的时候就可以直接在缓存中取出View对象了

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
     //判断有缓存中有没有view对象,有就直接拿来用
     if (!isCache()) {
         return createView(viewName, locale);
     }
     else {
        Object cacheKey = getCacheKey(viewName, locale);
        View view = this.viewAccessCache.get(cacheKey);
        if (view == null) {
          synchronized (this.viewCreationCache) {
              view = this.viewCreationCache.get(cacheKey);
              if (view == null) {
              //根据方法的返回值创建出View对象
               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);
                    if (logger.isTraceEnabled()) {
                    logger.trace("Cached view [" + cacheKey + "]");
                 }
               }
             }
          }
         }
         return (view != UNRESOLVED_VIEW ? view : null);
     }
}

createView的实现细节:

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.canHandle(viewName, locale)) {
         return null;
     } else {
        String forwardUrl;
         //如果方法得到返回值是以redirect:开始的
        if (viewName.startsWith("redirect:")) {
           forwardUrl = viewName.substring("redirect:".length());
           RedirectView view = new RedirectView(forwardUrl,             this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            String[] hosts = this.getRedirectHosts();
            if (hosts != null) {
               view.setHosts(hosts);
             }
             return this.applyLifecycleMethods("redirect:", view);
             //如果方法的返回值是以forward:开始的
          } else if (viewName.startsWith("forward:")) {
            forwardUrl = viewName.substring("forward:".length());
            InternalResourceView view = new InternalResourceView(forwardUrl);
             return this.applyLifecycleMethods("forward:", view);
          } else {
           //其他情况的处理,这里又有一个createView,它调用了父类的createView创建了一个默认的View对象
            return super.createView(viewName, locale);
          }
      }
}

以下都是解析视图名的实现细节,感兴趣的可以看一下。

父类AbstractCachingViewResolver类的createView实现细节:

protected View createView(String viewName, Locale locale) throws Exception {
   return loadView(viewName, locale);
}

InternalResourceViewResolver继承了UrlBasedViewResolver
UrlBasedViewResolver类中loadView方法的实现:

protected View loadView(String viewName, Locale locale) throws Exception {
        AbstractUrlBasedView view = buildView(viewName);
        View result = applyLifecycleMethods(viewName, view);
        return (view.checkResource(locale) ? result : null);
    }

UrlBasedViewResolver的buildView方法会获取一个View对象,这个对象会将视图以什么格式呈现给用户,例如如果是jsp显示呈现给用户的话,那这个view对象就是JstlView,默认的是JstlView。在这个方法中我们看到了getPrefix() + viewName + getSuffix()这样一段代码,这就是对视图路径的一个拼接了,getPrefix()方法获取前缀,也就是我们在配置文件中配置的<property name="prefix" value="/WEB-INF/PAGE/"/>的value中的值了,getSuffix()方法就是获取后缀值了,也就是我们在配置文件中配置的<property name="suffix" value=".jsp"/>的value中的值。这样就将将视图的物理路径找到了,并赋值到View的URL属性中去。

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
   Class<?> viewClass = this.getViewClass();
   Assert.state(viewClass != null, "No view class");
   AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(viewClass);
   view.setUrl(this.getPrefix() + viewName + this.getSuffix());
   String contentType = this.getContentType();
   if (contentType != null) {
      view.setContentType(contentType);
    }

    view.setRequestContextAttribute(this.getRequestContextAttribute());
    view.setAttributesMap(this.getAttributesMap());
    Boolean exposePathVariables = this.getExposePathVariables();
    if (exposePathVariables != null) {
        view.setExposePathVariables(exposePathVariables);
    }

   Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes();
    if (exposeContextBeansAsAttributes != null) {
            view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
     }
    String[] exposedContextBeanNames = this.getExposedContextBeanNames();
    if (exposedContextBeanNames != null) {
        view.setExposedContextBeanNames(exposedContextBeanNames);
     }
    return view;
}

就这样我们得到了一个View对象,这个视图的name就是逻辑视图名,因为当将View对象放在缓存的时候,我们可以通过逻辑视图名在缓存中找出View对象。我们在获取到View对象的时候,我们还要将View进行渲染,并呈现给用户。

View是个接口,AbstractView实现了render方法:

public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
     if (this.logger.isDebugEnabled()) {
         this.logger.debug("View " + this.formatViewName() + ", model " + (model != null ? model : Collections.emptyMap()) + (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
      }
   //主要是将一些属性填充到Map中
   Map<String, Object> mergedModel = this.createMergedOutputModel(model, request, response);
    //对response头进行了一些属性设置
    this.prepareResponse(request, response);
    //渲染给页面输出的所有model数据
    this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);
}

最后一行的renderMergedOutputModel方法由AbstractView的孙子类InternalResourceView实现InternalResourceView的renderMergedOutputModel方法帮我们获取到视图的物理路径,然后将这段路径传给RequestDispatcher对象,再调用RequestDispatcher的forward方法将页面呈现给用户,这样就走完了视图的解析了。

@Override
protected void renderMergedOutputModel(
     Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

     // Expose the model object as request attributes.
     exposeModelAsRequestAttributes(model, request);
    // Expose helpers as request attributes, if any.
    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.
          if (logger.isDebugEnabled()) {
            logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
          }
             //对请求进行转发,至此结束了视图解析解析过程
           rd.forward(request, response);
     }
}

最后一句话总结:
视图解析器只是为了得到视图对象;视图对象才是真正的转发(将模型数据发在request域中数据)或重定向到页面(视图对象才是真正的渲染视图)。


留言区

还能输入500个字符