Spring MVC 解读——View,ViewResolver

    上一篇文章(1)(2)分析了Spring是如何调用和执行控制器方法,以及处理返回结果的,现在我们就分析下Spring如何解析返回的结果生成响应的视图。

一、概念理解

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

    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实例的行为,它的定义相当简单:

    public interface ViewResolver {

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

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

二、获取ModelAndView

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

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

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

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

   上面的代码是根据方法执行完后生成的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//尝试获取该request的InputFlashMap 2FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); 3        if (inputFlashMap != null) { 4            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE,  5                                                Collections.unmodifiableMap(inputFlashMap)); 6        } 7//设置该请求的OutputFlashMap 8request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); 9//设置该请求的FlashMapManager,用来管理InputFlashMap和OutputFlashMap 10request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);

四、视图解析

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

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

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

1private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, 2            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) { 3        boolean errorView = false; 4        //出现异常,进行异常处理,暂不关心 5        if (exception != null) { 6            if (exception instanceof ModelAndViewDefiningException) { 7                logger.debug("ModelAndViewDefiningException encountered", exception); 8                mv = ((ModelAndViewDefiningException) exception).getModelAndView(); 9            } 10            else { 11                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); 12                mv = processHandlerException(request, response, handler, exception); 13                errorView = (mv != null); 14            } 15        } 16        // 如果返回View需要渲染? 17        if (mv != null && !mv.wasCleared()) { 18            //惊醒视图的渲染,我们主题 19            render(mv, request, response); 20            if (errorView) { 21                WebUtils.clearErrorRequestAttributes(request); 22            } 23        } 24        else { 25        } 26        //调用拦截器的afterComplete 27        if (mappedHandler != null) { 28            mappedHandler.triggerAfterCompletion(request, response, null); 29        } 30    }

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

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

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

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

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

1org.springframework.web.servlet.HandlerMapping = 2                org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ 3                org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping 4 5org.springframework.web.servlet.HandlerAdapter= 6                org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ 7                org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ 8                org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter 9 10org.springframework.web.servlet.HandlerExceptionResolver= 11    org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\ 12    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ 13    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver 14 15org.springframework.web.servlet.RequestToViewNameTranslator= 16                org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator 17 18org.springframework.web.servlet.ViewResolver= 19                            org.springframework.web.servlet.view.InternalResourceViewResolver 20 21org.springframework.web.servlet.FlashMapManager= 22                    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了。下面我们就来看它的实现吧:

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

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

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

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

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

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

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

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

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

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

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

五、视图渲染

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

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

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

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

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

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

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

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

六、总结

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

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java将前端的json数组字符串转换为列表

记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang