SpringMVC源码-SpringMVC源码请求执行流程及重点方法doDispatch讲解

一、开始请求

在浏览器访问http://localhost:8080/spring_mymvc/userlist这个接口,是个get请求。
FrameworkServlet类的service方法会被请求到:
调用路径如下:

service:945, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:890, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1789, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

在这里插入图片描述
可以看到该请求时get请求,走下面分支处理。进入super.service(request, response);发现方法跟丢了。。。。。。。
因为源码时在tomcat里的
在这里插入图片描述
super.service(request, response);Ctrl加鼠标左键点进去发现,进入到HttpServlet类的service方法:
在这里插入图片描述
会进入到FrameworkServlet的doGet方法,然后走processRequest(request, response);最后执行doService(request, response);方法
FrameworkServlet #doGet 执行路径:

doGet:960, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:945, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:890, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1789, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

可以看到无论git还是post请求最终都会执行processRequest(request, response);方法
在这里插入图片描述
因为不管是什么请求方式除了参数的处理不一样 其他的都有相似之处 所以都执行一个最终的公共方法。
processRequest

/**处理此请求,发布一个事件,而不管结果如何。<p>实际的事件处理是由抽象的{@link doService}模板方法执行的。* Process this request, publishing an event regardless of the outcome.* <p>The actual event handling is performed by the abstract* {@link #doService} template method.*/protected final void processRequest(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 记录当前时间,用于计算处理请求花费的时间long startTime = System.currentTimeMillis();// 记录异常,用于保存处理请求过程中发送的异常Throwable failureCause = null;// 获取LocaleContextHolder中原来保存的LocaleContext(保存的本地化信息)  事务中就是这样 先把最开始的保存 把新的放进去 用完新的再把最开始保存的恢复回去LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();// 获取当前请求的LocaleContextLocaleContext localeContext = buildLocaleContext(request);// 获取RequestContextHolder总原来保存的RequestAttribute(管理request和session的属性) spring事务处理的时候 有类似的操作 获取当前的 保存起来 后续恢复RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();// 获取当前请求的ServletRequestAttributeServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);// 获取异步管理器WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());// 将当前请求的LocaleContext和ServletRequestAttribute设置到LocaleContextHolder和RequestContextHolderinitContextHolders(request, localeContext, requestAttributes);try {// 执行真正的逻辑doService(request, response);}catch (ServletException | IOException ex) {// 记录抛出的异常failureCause = ex;throw ex;}catch (Throwable ex) {// 记录抛出的异常failureCause = ex;throw new NestedServletException("Request processing failed", ex);}finally {// 恢复原来的LocaleContext和ServletRequestAttributes到LocaleContextHolder和RequestContextHolder中resetContextHolders(request, previousLocaleContext, previousAttributes);if (requestAttributes != null) {requestAttributes.requestCompleted();}// 如果日志级别为debug,则打印请求日志logResult(request, response, failureCause, asyncManager);// 发布ServletRequestHandledEvent请求处理完成事件publishRequestHandledEvent(request, response, startTime, failureCause);}}

执行真正的逻辑
doService(request, response);

/**公开dispatcherservlet特定的请求属性,并委托给{@link doDispatch}进行实际的调度。* Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}* for the actual dispatching.*/@Overrideprotected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {// 如果日志级别为 DEBUG,则打印请求日志logRequest(request);// Keep a snapshot of the request attributes in case of an include,// to be able to restore the original attributes after the include.// 当include请求时对request的Attribute做快照备份Map<String, Object> attributesSnapshot = null;if (WebUtils.isIncludeRequest(request)) {attributesSnapshot = new HashMap<>();Enumeration<?> attrNames = request.getAttributeNames();while (attrNames.hasMoreElements()) {String attrName = (String) attrNames.nextElement();if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {attributesSnapshot.put(attrName, request.getAttribute(attrName));}}}// Make framework objects available to handlers and view objects.// 设置Spring框架中的常用对象到request属性中,这四个属性会在handler和view中使用request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());//重定向的时候方便参数的传递// FlashMap的相关配置,主要用于Redirect转发时参数的传递,此处有一个应用场景:如果post请求是提交表单,提交完之后redirect到一个显示订单的页面,// 此时需要知道一些订单的信息,但redirect本身没有提交参数的功能,如果想传递参数,那么就必须要写到url,而url有长度的限制同时还容易对外暴露,此时// 可以使用flashMap来传递参数,if (this.flashMapManager != null) {FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);if (inputFlashMap != null) {request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));}request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);}try {// 执行请求的分发doDispatch(request, response);}finally {// 异步处理相关if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Restore the original attribute snapshot, in case of an include.// 还原request快照的属性if (attributesSnapshot != null) {restoreAttributesAfterInclude(request, attributesSnapshot);}}}}

执行请求的分发
doDispatch(request, response);

/*** 处理实际的分发到处理器中* 内层是捕获在对请求进行处理的过程中抛出的异常,在处理异常的时候会设置到dispatcherException变量,然后在processorDispatcherResult方法中进行处理* 外层是处理渲染页面时抛出的异常,主要是处理processDispatchResult方法抛出的异常** Process the actual dispatching to the handler.* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters* to find the first that supports the handler class.* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers* themselves to decide which methods are acceptable.* @param request current HTTP request* @param response current HTTP response* @throws Exception in case of any kind of processing failure*/protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 实际处理时所用的request,如果不是上传请求,则直接使用接收到的request,否则封装成上传类型的requestHttpServletRequest processedRequest = request;// 处理请求的处理器链(包含处理器和对应的interceptor)HandlerExecutionChain mappedHandler = null;// 是不是上传请求的标志boolean multipartRequestParsed = false;// 获取异步管理器WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);//这里划为两个try ,第一个是处理请求后端,第二个是处理数据渲染。分别处理异常了try {// 封装model和view的容器ModelAndView mv = null;// 处理请求过程中抛出的异常,但是不包含渲染过程中抛出的异常Exception dispatchException = null;try {// 检测请求是否为上传请求,如果是则通过multipartResolver将其封装成MultipartHttpServletRequest对象processedRequest = checkMultipart(request);// 设置上传请求的标志multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.// 获得请求对应的HandlerExecutionChain对象(HandlerMethod和HandlerInterceptor拦截器们)mappedHandler = getHandler(processedRequest);//请求对应的是哪个controller//  如果获取不到,则根据配置抛出异常或返回404错误if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.// 获得当前handler对应的HandlerAdapter对象 controller或者控制器有多种不同的实现方式 为了方便后续过程中调用 使用适配器模式来 解决HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.// 处理GET、HEAD请求的Last-Modified,当浏览器第一次跟服务器请求资源时,服务器会在返回的请求头里包含一个last_modified的属性,// 代表资源最后时什么时候修改的,在浏览器以后发送请求的时候,会同时发送之前接收到的Last_modified.服务器接收到带last_modified的请求后,// 会跟实际资源的最后修改时间做对比,如果过期了返回新的资源,否则直接返回304表示未过期,直接使用之前缓存的结果即可String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {// 获取请求中服务器端最后被修改时间long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}// 执行响应的Interceptor的preHandler// 注意:该方法如果有一个拦截器的前置处理返回false,则开始倒序触发所有的拦截器的 已完成处理if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler.// 真正的调用handler方法,也就是执行对应的方法,并返回视图mv = ha.handle(processedRequest, response, mappedHandler.getHandler());// 如果需要异步处理,直接返回if (asyncManager.isConcurrentHandlingStarted()) {return;}// 当view为空时,根据request设置默认的viewapplyDefaultViewName(processedRequest, mv);// 执行响应的interceptor的postHandler方法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);}// 处理返回结果,包括处理异常、渲染页面、触发Interceptor的afterCompletionprocessDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {// 已完成处理 拦截器triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {// 完成处理激活触发器triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {// 判断是否执行异步请求if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.// 删除上传请求的资源if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}

重要的方法:

  • 检测请求是否为上传请求,如果是则通过multipartResolver将其封装成MultipartHttpServletRequest对象
    processedRequest = checkMultipart(request);
  • 获得请求对应的HandlerExecutionChain对象(HandlerMethod和HandlerInterceptor拦截器们)
    mappedHandler = getHandler(processedRequest);//请求对应的是哪个controller
  • 获得当前handler对应的HandlerAdapter对象 controller或者控制器有多种不同的实现方式 为了方便后续过程中调用 使用适配器模式来 解决
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  • 处理GET、HEAD请求的Last-Modified,当浏览器第一次跟服务器请求资源时,服务器会在返回的请求头里包含一个last_modified的属性,
    // 代表资源最后时什么时候修改的,在浏览器以后发送请求的时候,会同时发送之前接收到的Last_modified.服务器接收到带last_modified的请求后,
    // 会跟实际资源的最后修改时间做对比,如果过期了返回新的资源,否则直接返回304表示未过期,直接使用之前缓存的结果即可
    lastModified
  • 执行响应的Interceptor的preHandler
    // 注意:该方法如果有一个拦截器的前置处理返回false,则开始倒序触发所有的拦截器的 已完成处理
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
  • 真正的调用handler方法,也就是执行对应的方法,并返回视图
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  • 执行响应的interceptor的postHandler方法
    mappedHandler.applyPostHandle(processedRequest, response, mv);
  • 处理返回结果,包括处理异常、渲染页面、触发Interceptor的afterCompletion
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

SpringMVC的九大内置组件:
在这里插入图片描述

doDispatch方法的执行流程:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/435364.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

??实验——完全使用Ansible部署多台服务器的服务

文章目录 需求“NTP时间同步服务”“Zabix”“ELK”gitlab、Jenkis集群架构自动上线代码两台Web服务器部署同一Web应用WeCenter&#xff0c;且两台服务器的用户上传的数据目录挂载到共享存储服务器中&#xff0c;总数据保存在一台数据库服务器中使用sersync简单实现两台共享存储…

「系列投研|01」建立自己的移动比特币银行——赛道概况

ZJUBCA 09/28/2024 01&#xff5c;BTCfi赛道概况 BTCFi&#xff1a;建立自己的移动比特币银行 ——从Lending到Staking的全面解读 作者&#xff1a; Freya & Knight& Ausdin from ZJUBCA Elaine & Youyu from Satoshi Lab 关键词 BTCFi&#xff0c;稳定币&#xff…

4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1

目录 前序工作 1. 服务器项目名和本地一致 2. pycharm连接服务器 3. 本地项目对应到服务器项目 4. 简单测试一下同步效果 同步成功 前序工作 在同步到服务器之前&#xff0c;得确保已经完成以下几个前置步骤&#xff1a; 1. 租一个云服务器&#xff0c;可参考&#xff1a…

使用transformers中的pipeline调用huggingface中模型过程中可能遇到的问题和修改建议

使用transformers中的pipeline调用huggingface中模型过程 前言管线使用中的问题和解决huggingface的连接问题使用huggingface示例修改源继续使用pipeline No module named keras.engine 前言 HuggingFace有一个巨大的模型库&#xff0c;其中包括很多的比较成熟的经典模型&…

利用ChatGPT实现的生成式人工智能自动化控制系统

一、引言 随着信息化与智能化时代的到来&#xff0c;人工智能&#xff08;AI&#xff09;技术迅猛发展&#xff0c;正在深刻地重塑各行业的运营模式。在这一背景下&#xff0c;生成式人工智能&#xff08;Generative AI&#xff09;以其卓越的创造力和广泛的应用潜力&#xff…

Webpack 特性探讨:CDN、分包、Tree Shaking 与热更新

文章目录 前言包准备CDN 集成代码分包Tree Shaking原理实现条件&#xff1a;解决 treeShaking 无效方案&#xff1a;示例代码&#xff1a; 热更新&#xff08;HMR&#xff09; 前言 Webpack 作为现代前端开发中的核心构建工具&#xff0c;提供了丰富的特性来帮助开发者优化和打…

java基础(4)类和对象

目录 1.前言 2.正文 2.1类的定义与使用 2.1.1类的定义 2.1.2类的实例化 2.1.3this引用 2.1.3.1 访问当前对象的成员变量 2.1.3.2调用当前对象的成员方法 2.1.3.3构造函数中的 this 2.1.3.4归纳this 2.2封装 2.2.1封装的定义 2.2.2访问修饰符 2.3static 2.3.1sta…

初学Vue

文章目录 简介特点 初识Vue模板语法两大类插值语法指令语法 两种数据绑定方式单项绑定&#xff08;v-bind&#xff09;双向绑定&#xff08;v-model&#xff09; 数据代理事件处理基本使用事件修饰符 键盘事件计算属性 computed 简介 一套用于构建用户界面的渐进式JavaScript框…

【面试题】软件测试实习(含答案)

软件测试实习常见面试题&#xff0c;主要是功能测试相关的基础问题 目录 一、软件测试基础 1、介绍一下你最近的项目&#xff0c;以及工作职责 2、软件项目的测试流程? 3、黑盒测试与白盒测试的区别? 4、黑盒测试常见的设计方法?怎么理解等价类方法和边界值方法 1&…

服务器几核几G几M是什么意思?如何选择?

服务器几核几G几M是什么意思&#xff1f;我们建站、搭建网络平台都要用到云服务器&#xff0c;不管在腾讯云、阿里云还是别的云服务平台选购&#xff0c;都会接触到服务器配置。云服务器就是把物理服务器&#xff08;俗称“母鸡”&#xff09;&#xff0c;用虚拟机技术虚拟出多…

FreeRTOS学习笔记一——FreeRTOS介绍

RTOS学习笔记&#xff0c;主要参考正点原子教程 目录 FreeRTOS特点任务调度方式抢占式调度时间片调度 任务状态状态转换任务列表 FreeRTOS特点 实现多个任务功能划分延时函数实现任务调度高优先级抢占低优先级每个任务都有自己的栈空间 注意&#xff1a; 中断可以打断任意任务…

设计模式之享元(Flyweight)模式

前言 面向对象很好地解决了 “抽象” 的问题&#xff0c;但是不可避免的要付出一定的代价。对于通常情况来讲&#xff0c;面向对象的成本大都可以忽略不计。但是某些情况&#xff0c;面向对象所带来的成本必须谨慎处理 具体需要自己根据需求去评估 定义 “对象性能” 模式。运用…

探索基于知识图谱和 ChatGPT 结合制造服务推荐前沿

0.概述 论文地址&#xff1a;https://arxiv.org/abs/2404.06571 本研究探讨了制造系统集成商如何构建知识图谱来识别新的制造合作伙伴&#xff0c;并通过供应链多样化来降低风险。它提出了一种使用制造服务知识图谱&#xff08;MSKG&#xff09;提高 ChatGPT 响应准确性和完整…

探索顶级低代码开发平台,实现创新

文章盘点ZohoCreator、OutSystems等10款顶尖低代码开发平台&#xff0c;各平台以快速开发、集成、数据安全等为主要特点&#xff0c;适用于不同企业需求&#xff0c;助力数字化转型。 一、Zoho Creator Zoho Creator 是一个低代码开发平台&#xff0c;它简化了应用开发中的复杂…

小程序视频编辑SDK解决方案,轻量化视频制作解决方案

面对小程序、网页、HTML5等多样化平台&#xff0c;如何轻松实现视频编辑的轻量化与高效化&#xff0c;成为了众多开发者和内容创作者共同面临的挑战。正是洞察到这一市场需求&#xff0c;美摄科技推出了其领先的小程序视频编辑SDK解决方案&#xff0c;为创意插上翅膀&#xff0…

2024前端技术发展概况

当前前端技术呈现出多方面的发展态势&#xff0c;以下是详细介绍&#xff1a; 前端框架不断演进&#xff1a; React&#xff1a;作为流行的前端框架之一&#xff0c;React 依旧保持着强大的生命力。它具有高效的虚拟 DOM 机制&#xff0c;能够快速更新和渲染页面&#xff0c;极…

pdf页面尺寸裁减

1、编辑pdf 2、点击裁减页面&#xff0c;并在空白区域双击裁减 3、输入裁减数据&#xff1a;

【易上手快捷开发新框架技术】nicegui标签组件lable用法庖丁解牛深度解读和示例源代码IDE运行和调试通过截图为证

传奇开心果微博文系列 序言一、标签组件lable最基本用法示例1.在网页上显示出 Hello World 的标签示例2. 使用 style 参数改变标签样式示例 二、标签组件lable更多用法示例1. 添加按钮动态修改标签文字2. 点击按钮动态改变标签内容、颜色、大小和粗细示例代码3. 添加开关组件动…

Angular基础学习(入门 --> 入坑)

目录 一、Angular 环境搭建 二、创建Angular新项目 三、数据绑定 四、ngFor循环、ngIf、ngSwitch、[ngClass]、[ngStyle]、管道、事件、双向数据绑定--MVVM 五、DOM 操作 &#xff08;ViewChild&#xff09; 六、组件通讯 七、生命周期 八、Rxjs 异步数据流 九、Http …

关于vue2+uniapp+uview+vuex 私募基金项目小程序总结

1.关于权限不同tabbar处理 uniapp 实现不同用户展示不同的tabbar(底部导航栏)_uniapp tabbar-CSDN博客 但是里面还有两个问题 一个是role应该被本地存储并且初始化 第二个问题是假设我有3个角色 每个角色每个tabbar不一样的&#xff0c;点击tabbar时候会导致错乱 第三个问题…