Spring MVC 解读——View,ViewResolver

一、概念理解

  •     View ---View接口表示一个响应给用户的视图,例如jsp文件,pdf文件,html文件等,它的定义如下

?
1
2
3
4
5
6
7
8
9
10
11
public  interface  View {
     //HttpServletRequest中的属性名,其值为响应状态码
     String RESPONSE_STATUS_ATTRIBUTE = View. class .getName() +  ".responseStatus" ;
     //HttpServletRequest中的属性名,前一篇文章用到了该变量,它的对应值是请求路径中的变量,及@PathVariable
     //注解的变量
     String PATH_VARIABLES = View. class .getName() +  ".pathVariables" ;
     //该视图的ContentType
     String getContentType();
     //渲染该视图
     void  render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response);
}

   该接口只有两个方法定义,分别表明该视图的ContentType和如何被渲染。Spring中提供了丰富的视图支持,几乎包含所有你想得到的,并且Spring的视图拓展性很好,你可以轻松实现自己的视图。下面是View的一些实现类(不是全部)

  • ViewResolver --- ViewResolver接口定义了如何通过view 名称来解析对应View实例的行为,它的定义相当简单:

?
1
2
3
4
public  interface  ViewResolver {
 
     View resolveViewName(String viewName, Locale locale)  throws  Exception;
}

   该接口只有一个方法,通过view name 解析出View。同样Spring提供了丰富的ViewResolver实现用来解析不同的View:

二、获取ModelAndView

    上一篇文章我们分析了处理器方法如何被调用以及获取了返回值,但是Spring是如何处理返回值并响应给客户呢?这就是这节要分析的,根据返回值解析出对应的视图。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private  ModelAndView invokeHandleMethod(HttpServletRequest request,
             HttpServletResponse response, HandlerMethod handlerMethod)  throws  Exception {
 
         ServletWebRequest webRequest =  new  ServletWebRequest(request, response);
 
         WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
         ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
         ServletInvocableHandlerMethod requestMappingMethod = 
                                     createRequestMappingMethod(handlerMethod, binderFactory);
 
         ModelAndViewContainer mavContainer =  new  ModelAndViewContainer();
         mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
         modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
         mavContainer.setIgnoreDefaultModelOnRedirect( this .ignoreDefaultModelOnRedirect);
 
         AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
         chain.addDelegatingCallable(getAsyncCallable(mavContainer, modelFactory, webRequest));
         chain.setAsyncWebRequest(createAsyncWebRequest(request, response));
         chain.setTaskExecutor( this .taskExecutor);
         //上一篇文章分析到这里,调用了处理器方法并处理了返回值
         requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
 
         if  (chain.isAsyncStarted()) {
             return  null ;
         }
         //这里是根据返回值返回ModelAndView了
         return  getModelAndView(mavContainer, modelFactory, webRequest);
     }

   上面的代码在上一篇文章中已经分析到了invokeAndHandle方法,该方法调用了处理器方法,并处理了返回值,剩下的就是如何将返回值呈现给用户了,我们看getModelAndView的实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private  ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
             ModelFactory modelFactory, NativeWebRequest webRequest)  throws  Exception {
         //主要是同步model属性,并且将BindingResult添加到model中来
         modelFactory.updateModel(webRequest, mavContainer);
         //是否直接处理请求,如@ResponseBody
         if  (mavContainer.isRequestHandled()) {
             return  null ;
         }
         ModelMap model = mavContainer.getModel();
         ModelAndView mav =  new  ModelAndView(mavContainer.getViewName(), model);
         if  (!mavContainer.isViewReference()) {
             mav.setView((View) mavContainer.getView());
         } //如果model是RedirectAttributes,进行flashAttributes的处理
         //即将flashAttribute属性添加到request的Output FlashMap中,以被重定向后的request获取
         if  (model  instanceof  RedirectAttributes) {
             Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
             HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest. class );
             RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
         }
         return  mav;
     }

   上面的代码是根据方法执行完后生成的model和视图名等信息生成ModelAndView对象,该对象维护了一个View和Model的对应关系,以便在View中可以访问Model的属性。

三、RedirectAttributes    

    上面的代码还有一个对RedirectAttributes的处理,这里我们来分析下是个什么回事?我们知道request中的属性只能在request范围内访问到,一旦执行重定向,重定向后的request并访问不到前面设置的属性了,虽然放到Session中可以在不同的request中共享这些属性,但是有时候放到Session中显得没有必要,毕竟很多属性只需要在“某次操作”中有用(重定向操作对用户来说其实是一次操作,因为重定向是浏览器执行的,对用户透明的。

    因此为了解决这个问题,Spring引入了RedirectAttributes概念,即添加到RedirectAttributes中的属性,在重定向后依旧可以获取到,并且获取到以后,这些属性就会失效,新的request便无法获取了,这样就方便了开发者,同样也节省了内错占用。

    那Spring是怎么实现的呢?这里牵扯到了FlashMap这一概念,Spring会默认为每一个请求添加两个FlashMap属性,一个是InputFlashMap,另一个是OutputFlashMap,其中InputFlashMap便包含了上一个请求在重定向到该请求前设置的属性值,也就是上一个请求的OutputFlashMap,看下面的图方便理解:

    下面是DispatcherServlet中doService中的代码片段,在调用doDispatch前便设置了InputFlashmap和OutputFlashMap:

?
1
2
3
4
5
6
7
8
9
10
//尝试获取该request的InputFlashMap
FlashMap inputFlashMap =  this .flashMapManager.retrieveAndUpdate(request, response);
         if  (inputFlashMap !=  null ) {
             request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, 
                                                 Collections.unmodifiableMap(inputFlashMap));
         }
//设置该请求的OutputFlashMap
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE,  new  FlashMap());
//设置该请求的FlashMapManager,用来管理InputFlashMap和OutputFlashMap
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE,  this .flashMapManager);

四、视图解析

    了解了FlashMap的概念我们继续往下看,前面我们已经获取到了请求的ModelAndView对象,这时invokeHandleMethod执行完毕将控制权交给了doDispatch,我们看怎么处理ModelAndView:

?
1
2
3
4
5
6
7
8
9
10
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if  (asyncChain.isAsyncStarted()) { ///异步调用,暂不关心
         mappedHandler.applyPostHandleAsyncStarted(processedRequest, response);
         return ;
} //如果ModelAndView中没有设置视图名,则设置默认视图(大致是prefix/请求路径/suffix)
applyDefaultViewName(request, mv);
//执行拦截器的后处理器
mappedHandler.applyPostHandle(processedRequest, response, mv);
//处理分派结果,响应用户
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

   重点就在最后一行,我们继续追踪:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private  void  processDispatchResult(HttpServletRequest request, HttpServletResponse response,
             HandlerExecutionChain mappedHandler, ModelAndView mv, Exception 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 );
             }
         }
         // 如果返回View需要渲染?
         if  (mv !=  null  && !mv.wasCleared()) {
             //惊醒视图的渲染,我们主题
             render(mv, request, response);
             if  (errorView) {
                 WebUtils.clearErrorRequestAttributes(request);
             }
         }
         else  {
         }
         //调用拦截器的afterComplete
         if  (mappedHandler !=  null ) {
             mappedHandler.triggerAfterCompletion(request, response,  null );
         }
     }

   上面的代码我们着重看render方法是怎样实现的,这是我们今天的主题啊:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected  void  render(ModelAndView mv,HttpServletRequest request,HttpServletResponse response){
         // 确定当前请求的Locale,并设置Response
         Locale locale =  this .localeResolver.resolveLocale(request);
         response.setLocale(locale);
 
         View view; //ModelAndView中的View还只是名称,需要解析成View对象
         if  (mv.isReference()) {
             view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
             if  (view ==  null ) {
                 throw  new  ServletException(
                         "Could not resolve view with name '" );
             }
         }
         else  { //直接获取视图对象
             view = mv.getView();
             if  (view ==  null ) {
                 throw  new  ServletException( "ModelAndView ["  + mv +  "] " );
             }
         }
         //委托视图对象进行渲染
         view.render(mv.getModelInternal(), request, response);
     }

   上面的代码涉及了两个重要步骤,视图名的解析和视图的渲染,这一小节我们来讲解视图名的解析,也就是ViewResolver了:

?
1
2
3
4
5
6
7
8
9
10
protected  View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
             HttpServletRequest request)  throws  Exception {
         for  (ViewResolver viewResolver :  this .viewResolvers) {
             View view = viewResolver.resolveViewName(viewName, locale);
             if  (view !=  null ) {
                 return  view;
             }
         }
         return  null ;
     }

    我们查看resolveViewName方法,发现其中有一个viewResolvers实例变量,如果你看过前面的几篇文章,你获取会记得handlerMappings, handlerAdapters等变量,不错他们是一伙的,都是在DispatcherServlet初始化时完成设置的,并且我们可以在配置文件中定义我们自己的HandleMappings, HandlerAdapters,ViewResolvers等(这里不讲解怎样设置了),但是如果我们不设置的话Spring也会为我们设置一些默认值:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
org.springframework.web.servlet.HandlerMapping =
                 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
                 org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
 
org.springframework.web.servlet.HandlerAdapter=
                 org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
                 org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
                 org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
 
org.springframework.web.servlet.HandlerExceptionResolver=
     org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
     org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
     org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
 
org.springframework.web.servlet.RequestToViewNameTranslator=
                 org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
 
org.springframework.web.servlet.ViewResolver=
                             org.springframework.web.servlet.view.InternalResourceViewResolver
 
org.springframework.web.servlet.FlashMapManager=
                     org.springframework.web.servlet.support.SessionFlashMapManager

   上面代码片段来自Spring MVC包中的DispatcherServlet.properties属性文件中,这里Spring为我们默认设置了诸多处理器,解析器等,可以看出在我们不进行ViewResolver设置的情况下,默认实现是InternalResourceViewResolver。由第一节的ViewResolver继承层次图我们知道,InternalResourceViewResolver继承自UrlBasedViewResolver, 而UrlBasedViewResolver继承自AbstractCachingViewResolver,其实这就是Spring的聪明之处,为了提高性能,Spring中充斥着缓存策略,这不,在试图解析中也使用了缓存。这样只需在第一次解析时完成整个的视图创建工作,后续的请求只需从缓存中索取即可了。

    这里的InternalResourceViewResolver主要是用来支持Jsp文件的,换句话说,如果你的系统中只用到了jsp文件而没有模板引擎等框架,这个ViewResolver就够你用了,你也就无需在配置文件中多此一举的写上该ViewResolver了。下面我们就来看它的实现吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  View resolveViewName(String viewName, Locale locale)  throws  Exception {
         //如果没有被缓存呢,只能创建了
         if  (!isCache()) {
             return  createView(viewName, locale);
         }
         else  { //检索缓存中的视图对象
             Object cacheKey = getCacheKey(viewName, locale);
             synchronized  ( this .viewCache) {
                 View view =  this .viewCache.get(cacheKey);
                 if  (view ==  null  && (! this .cacheUnresolved 
                                                 || ! this .viewCache.containsKey(cacheKey))) {
                     // Ask the subclass to create the View object.
                     view = createView(viewName, locale);
                     if  (view !=  null  ||  this .cacheUnresolved) {
                         this .viewCache.put(cacheKey, view);
                     }
                 }
                 return  view;
             }
         }
     }

   方法很简单,我们接着看是怎样创建视图的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected  View createView(String viewName, Locale locale)  throws  Exception {
         // 当前ViewResolver无法解析该视图名,返回null
         if  (!canHandle(viewName, locale)) {
             return  null ;
         }
         // view名称以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);
         }
         // view名称以forward:开头,即转发视图解析
         if  (viewName.startsWith(FORWARD_URL_PREFIX)) {
             String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
             return  new  InternalResourceView(forwardUrl);
         }
         // 正常情况下,让父类创建吧
         return  super .createView(viewName, locale);
     }

   创建视图时,Spring会检查视图名,有三种情况redirect视图,forward视图,普通视图,进行了不同处理。对于redirect视图,spring获取redirectURL并创建了RedirectView对象,然后执行了一下bean实例的生命周期方法,没什么实质性东西,我们不关心。对于转发视图,创建了InternalResourceView对象,上面说的这两种对象的渲染过程我们过会会降到的。这里大家先记住。第三种情况呢,又交给了父类处理,我们继续看看吧:

?
1
2
3
4
5
6
7
8
9
protected  View createView(String viewName, Locale locale)  throws  Exception {
         return  loadView(viewName, locale);
}
@Override
protected  View loadView(String viewName, Locale locale)  throws  Exception {
         AbstractUrlBasedView view = buildView(viewName);
         View result = applyLifecycleMethods(viewName, view);
         return  (view.checkResource(locale) ? result :  null );
}

   父类的createView方法又委托给了loadView,而loadView是抽象的由子类实现,好吧,我只能说这个地方真饶。我们继续看loadView中有一个buildView方法,看着不错哦:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected  AbstractUrlBasedView buildView(String viewName)  throws  Exception {
         //根据ViewClass实例化该Class
         AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils
                                                             .instantiateClass(getViewClass());
         //设置视图的url,prefix/viewName/suffix
         view.setUrl(getPrefix() + viewName + getSuffix());
         String contentType = getContentType();
         if  (contentType !=  null ) { //设置ContentType
             view.setContentType(contentType);
         } //设置请求上下文属性
         view.setRequestContextAttribute(getRequestContextAttribute());
         view.setAttributesMap(getAttributesMap());
         if  ( this .exposePathVariables !=  null ) { //设置是否暴露PathVariable
             view.setExposePathVariables(exposePathVariables);
         }
         return  view;
}

   上面的代码又出来个ViewClass, prefix,suffix,他们又是个什么东西呢?其实我们知道在配置InternalResourceViewResolver时可以指定一个viewClass,prefix,suffix,没错,就是他们,先说prefix,suffix,我们看到了它会分别添加到viewName的前后,组成视图的URL。那个viewClass呢就是视图的class对象类型了。我们看InternalResourceViewResolver的构造器:

?
1
2
3
4
5
6
7
public  InternalResourceViewResolver() {
         Class viewClass = requiredViewClass();
         if  (viewClass.equals(InternalResourceView. class ) && jstlPresent) {
             viewClass = JstlView. class ;
         }
         setViewClass(viewClass);
}

   会发现在我们没有指定的情况下默认是JstlView哦。根据第一季中的图片我们可以知道它继承自InternalResourceView。到此为止呢我们的视图对象已经创建完毕。

    我们这里只解析了Spring默认情况下的InternalResourceViewResolver的解析过程,默认情况下解析的视图类型是JstlView。如果是Redirect的话则是RedirectView。

五、视图渲染

    视图解析出来了,下面就是要将视图渲染给用户显示了。这里我们依旧只讲解默认的JstlView的渲染过程,当然还有RedirectView的。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public  void  render(Map<String, ?> model, HttpServletRequest request, 
                                                 HttpServletResponse response)  throws  Exception {
         
         Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
 
         prepareResponse(request, response);
         renderMergedOutputModel(mergedModel, request, response);
}
protected  Map<String, Object> createMergedOutputModel(Map<String, ?> model, 
                             HttpServletRequest request, HttpServletResponse response) {
         @SuppressWarnings ( "unchecked" )
         //如果需要保留PathVariable
         Map<String, Object> pathVars =  this .exposePathVariables ?
             (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) :  null ;
 
         //联合动态和静态属性
         int  size =  this .staticAttributes.size();
         size += (model !=  null ) ? model.size() :  0 ;
         size += (pathVars !=  null ) ? pathVars.size() :  0 ;
         Map<String, Object> mergedModel =  new  HashMap<String, Object>(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;
}

   上面代码是AbstractView中的方法,也就是所有视图都会执行的操作,就是将静态属性和动态生成的属性合并,我们重点看

renderMergedOutputModel方法,子类会覆盖该方法,实现不同的逻辑。我们来看JstlView和RedirectView的实现,首先JstlView :

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected  void  renderMergedOutputModel(
             Map<String, Object> model, HttpServletRequest request,HttpServletResponse response){
 
         //确定执行请求转发的request对象
         HttpServletRequest requestToExpose = getRequestToExpose(request);
         //将model中的属性暴露为请求属性表中
         exposeModelAsRequestAttributes(model, requestToExpose);
         //暴露MessageResource
         exposeHelpers(requestToExpose);
         //确定转发的路径,也就是View的URL,但会检查是否会进入死循环,即跟当前请求同一个路径
         String dispatcherPath = prepareForRendering(requestToExpose, response);
         //生成RequestDispatcher对象
         RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
         if  (rd ==  null ) {
             throw  new  ServletException( "Could not get RequestDispatcher for ["  + getUrl() + "]" );
         }
         //include操作
         if  (useInclude(requestToExpose, response)) {
             response.setContentType(getContentType());
             rd.include(requestToExpose, response);
         }
         else  {
             //执行转发,暴露属性到转发请求中
             exposeForwardRequestAttributes(requestToExpose);
             rd.forward(requestToExpose, response);
         }
}

  方法看着很长其实思路比较简单,主要就是调用了RequestDispatcher的include 或forward的方法,将请求转发到指定URL。JstlView的视图渲染相对简单,我们来看RedirectView的渲染:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected  void  renderMergedOutputModel(
             Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
             throws  IOException {
         //获取重定向的路径,也就是前面生成RedirectView时设置的URL,但会进行相对路径的处理
         String targetUrl = createTargetUrl(model, request);
         //调用用户注册的RequestDataValueProcessor的process方法,通常用不到,不管
         targetUrl = updateTargetUrl(targetUrl, model, request, response);
         //哈哈,这里就是我们上面讲到的FlashMap的处理啦,是怎样实现的呢?
         //我们知道前面将RedirectAttributes的属性都设置到了当前请求的OutputFlashMap中了,这里再取出来。
         //设置flashMap的目标请求路径,用来比对下次请求的路径,如果匹配,将其中的属性设置到请求属性表中
         FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
         if  (!CollectionUtils.isEmpty(flashMap)) {
             UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
             flashMap.setTargetRequestPath(uriComponents.getPath());
             flashMap.addTargetRequestParams(uriComponents.getQueryParams());
         }
         //将flashMap交由FlashMapManager管理。
         FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
         flashMapManager.saveOutputFlashMap(flashMap, request, response);
         //返回结果,设置响应头304.
         sendRedirect(request, response, targetUrl.toString(),  this .http10Compatible);
}

   到此为止,我们的试图解析,渲染过程就完全分析完了,获取到目前为止有点晕,其实好好思考下,Spring在视图解析,和渲染这块给了我们足够的拓展空间。

六、总结

    Spring对视图的支持相当完善,默认的JSP不用说,PDF,Excel, 等,还包括主流的模板引擎,像FreeMarker, Tiles等,可以参考第一张图片。当然你完全也可以实现自己的View,以及ViewResolver,来解析自定义的视图。不过应该没多大必要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值