
在公司里带过几次新人每次讲到SpringMVC的执行流程我最喜欢问一个问题你往Controller上写了个RequestMapping浏览器请求过来Spring到底是怎么找到这个方法又是怎么把参数塞进去的大部分人都能背出DispatcherServlet、HandlerMapping、HandlerAdapter这几个名字再多问一句“Adapter为什么不能省”就支支吾吾了。这篇就把这个执行链路彻底说透重点拆穿两个核心组件HandlerMapping和HandlerAdapter。不管你是刚接触SpringMVC准备面试还是写了两年CRUD想搞明白拦截器为什么有时候不生效这篇都值得看下去。1. SpringMVC执行流程从一次请求进入后的完整九步说起1.1 用餐厅点餐类比理解整体流程想快速在脑子里建立SpringMVC执行流程的框架我建议用“去餐厅吃饭”来类比。你走进一家餐厅门口有个领位员这就是DispatcherServlet——所有请求的第一站。领位员不自己炒菜他要根据你点的菜名找对应的厨师。找厨师这件事就是HandlerMapping干的活。厨师把菜炒好服务员端上来这个“把厨师手里做好的菜变成你面前能吃的菜”的过程就是HandlerAdapter要解决的问题。一次完整的SpringMVC请求走完的链路大概是九步请求到达DispatcherServlet前端控制器接收所有请求。DispatcherServlet调用HandlerMapping根据URL找到对应的Handler处理器通常就是我们写的Controller方法。HandlerMapping返回一条HandlerExecutionChain里面不光有Handler本身还有一堆拦截器Interceptor。DispatcherServlet拿到Handler之后并不直接调用而是寻找能处理这个Handler的HandlerAdapter。HandlerAdapter调用Handler真正的业务方法。调用之前参数解析器会把HTTP请求里的参数转换成方法形参调用之后返回值处理器会把方法返回值包装成ModelAndView。DispatcherServlet拿到ModelAndView之后如果里面有View就交给ViewResolver解析渲染。渲染完成把响应写回浏览器。这个流程你闭上眼睛能默写出来才说明你真正把SpringMVC的骨架记牢了。可以这么说HandlerMapping解决的是“请求去哪儿”的问题HandlerAdapter解决的是“处理器怎么被调用”的问题而DispatcherServlet只是中间调度的人不干具体的活。1.2 DispatcherServlet核心入口源码速览光背流程不够我建议你直接打开源码看一眼。SpringMVC的启动入口是DispatcherServlet整个请求处理的核心方法叫doDispatch。源码位置在org.springframework.web.servlet.DispatcherServlet你搜一下就能看到。我来摘一段核心代码建议你逐行读一遍protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest request; HandlerExecutionChain mappedHandler null; boolean multipartRequestParsed false; // 1. 如果是文件上传请求先解析成MultipartHttpServletRequest processedRequest checkMultipart(request); multipartRequestParsed (processedRequest ! request); // 2. 通过HandlerMapping找到对应的处理器执行链重点 mappedHandler getHandler(processedRequest); if (mappedHandler null) { noHandlerFound(processedRequest, response); return; } // 3. 通过HandlerAdapter找到能处理当前Handler的适配器重点 HandlerAdapter ha getHandlerAdapter(mappedHandler.getHandler()); // 4. 执行拦截器的preHandle方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 5. HandlerAdapter真正调用Controller方法 ModelAndView mv ha.handle(processedRequest, response, mappedHandler.getHandler()); // 6. 执行拦截器的postHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv); }第一步和第二步之间还有一点细节比如getHandler内部是遍历容器里所有的HandlerMapping逐个尝试谁先返回非null结果就用谁的。这也是为什么你可以同时配置注解式Controller、XML配置的Controller甚至自定义的HandlerMapping它们之间并不冲突各管各的URL。1.3 为什么设计成“映射器适配器”两段式设计模式里有个基本原则叫“单一职责”SpringMVC把一句“找处理器并调用”拆成两个组件背后有非常现实的原因。HandlerMapping只负责匹配它的产出只是一个Handler对象可能是Controller方法也可能是HttpRequestHandler甚至是一个普通的Servlet。而HandlerAdapter专门负责“怎么调用”。假如没有适配器DispatcherServlet就要写一大堆if (handler instanceof Controller)之类的判断每增加一种处理器类型就得改一遍DispatcherServlet这违反了开闭原则。有了适配器之后DispatcherServlet只需要面向HandlerAdapter接口编程每种Handler类型配一个适配器实现扩展新处理器类型时DispatcherServlet一行代码都不用动。我经常跟新人说这两个组件合起来就像手机充电器。HandlerMapping是告诉你“这个设备支持哪种充电协议”HandlerAdapter就是那个插头不管你是Type-C、Lightning还是Micro-USB充电头都能给你输出合适的电流。DispatcherServlet是充电口背后的电路板它根本不关心你是哪种插头反正往里插就完事了。2. HandlerMapping把URL变成Handler的“路由表”2.1 HandlerMapping接口设计与初始化时机先看一眼接口定义它非常简洁public interface HandlerMapping { HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }一个接口只有一个方法输入一个HttpServletRequest输出一个HandlerExecutionChain。HandlerExecutionChain不是一个孤单的Handler对象它内部维护了一个ListObject interceptorList和一个HandlerInterceptor[] interceptors数组也就是说HandlerMapping在找到处理方法的同时还会把匹配URL的拦截器一并组装进链里。这也是为什么很多人疑惑“拦截器在哪里生效” — 答案就在这里拦截器是HandlerMapping根据请求路径从注册表里捞出来挂到执行链上的。HandlerMapping的初始化时机也很关键。在SpringMVC容器启动阶段DispatcherServlet的initHandlerMappings方法会从当前容器里找所有类型为HandlerMapping的Bean一个都不落注册到自己的handlerMappings列表里。默认情况下RequestMappingHandlerMapping会被自动注册而且它的order是0排在所有HandlerMapping的最前面优先级最高。2.2 默认的RequestMappingHandlerMapping工作方式在所有HandlerMapping实现里我们日常打交道最多的就是RequestMappingHandlerMapping。它专门处理注解式的RequestMapping包括GetMapping、PostMapping这些衍生注解。它的匹配过程说穿了就是维护一张路由注册表。在Spring容器初始化时它会扫描所有Bean挨个检查类上有没有Controller或者RequestMapping方法上有没有RequestMapping相关的注解有的话就把方法封装成一个HandlerMethod对象把URL规则和这个BeanName、Method对象一起存起来。请求进来时它拿请求的路径和已有的URL规则做匹配命中之后就返回对应的HandlerMethod再组装上匹配到的拦截器。这里有个细节值得说RequestMappingHandlerMapping默认只处理Controller标注的类这跟组件扫描的Component不同。如果你把Controller类上的Controller写成了Component很多时候界面功能依然正常因为Component确实也会被扫描成Bean但映射逻辑不会失灵原因在于RequestMappingHandlerMapping判断的是isHandler(Class? beanType)它要求类上有Controller或RequestMapping注解。你写Component并不会被识别为handler于是这个Bean虽然存在但它上面的RequestMapping方法不会被注册请求就会全部404。这种问题是我见过最隐性的一类错误因为项目能启动很多功能正常只有一个接口找不到。2.3 其他HandlerMappingBeanNameUrl和SimpleUrlHandlerMapping除了默认的注解式映射SpringMVC在历史版本里还留下了两个经典实现BeanNameUrlHandlerMapping和SimpleUrlHandlerMapping。BeanNameUrlHandlerMapping的逻辑很有意思它把Bean的name当成URL使用。比如你在配置里定义一个Beanname是/helloclass是某个继承了AbstractController的处理器那么这个Bean就能直接响应/hello这个请求。这种写法在早期Spring项目中常见现在基本被注解式取代了但你在老项目中偶尔还是会碰到。SimpleUrlHandlerMapping则是通过配置把URL路径映射到容器里已有的某个Handler Bean。它的配置方式大概是这样的bean classorg.springframework.web.servlet.handler.SimpleUrlHandlerMapping property namemappings props prop key/user/listuserController/prop /props /property /bean它比BeanNameUrl灵活的地方在于URL和Bean的名字没有任何绑定关系你想怎么映射都行。理解了这些不同实现你回头看doDispatch里的getHandler方法它遍历的是DispatcherServlet持有的handlerMappings列表每个HandlerMapping都有自己的order属性数字越小优先级越高。一个请求如果被RequestMappingHandlerMapping先接住了就不会再去问后面的HandlerMapping。这也是为什么你可以在一个项目里混用注解式Controller和古老的Controller接口实现类两者依然能同时工作。2.4 自定义HandlerMapping的扩展思路如果你有特殊需求比如根据请求头或者特定业务字段做路由完全可以自己实现一个HandlerMapping。我提供一个最小实现思路三步搞定。第一步实现HandlerMapping接口。第二步在getHandler方法里写你的匹配逻辑匹配成功就返回一个HandlerExecutionChain失败返回null。第三步把这个自定义Bean注册到Spring容器里并设置一个合适的order值让它在默认映射之后或者之前生效。Component public class CustomHandlerMapping implements HandlerMapping { Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { String path request.getRequestURI(); if (/custom/route.equals(path)) { Object handler new CustomHandler(); return new HandlerExecutionChain(handler); } return null; } }这种自定义HandlerMapping在你只需要处理少量特殊路径时很实用而且因为你返回null不会影响后续的默认HandlerMapping安全的很。不过我还是建议正常情况下能用注解解决的问题不要自己造轮子自定义HandlerMapping更适合做框架级扩展比如集成某个第三方协议时。3. HandlerAdapter处理器调用的“万能充电头”3.1 HandlerMapping找到Handler之后发生了什么HandlerMapping把Handler找出来了但它只是一个普通对象DispatcherServlet并不知道怎么调用它。你可能觉得Controller方法不就是反射调用一下的事情吗没那么简单。你想想一个RequestMapping方法参数列表五花八门有RequestParam、PathVariable、RequestBody返回类型又有String、ModelAndView、ResponseEntity、void这好几种。直接反射调用的话参数解析逻辑和返回值处理逻辑全都要DispatcherServlet自己写代码就会爆炸。所以设计了HandlerAdapter。它的接口定义非常短public interface HandlerAdapter { boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; }supports方法用来判断当前Adapter能不能处理这个Handler能处理就返回true。handle方法内部才是真正的处理器调用。DispatcherServlet在getHandlerAdapter方法里会遍历所有HandlerAdapter调supports方法逐个问“这活儿你能干吗”第一个回答能干的就被选中执行后续的调用。3.2 三种经典HandlerAdapter的用途对比SpringMVC内置了三种常用的HandlerAdapter它们的适配对象完全不同我整理了一个表格方便你对照着看适配器适配对象典型场景RequestMappingHandlerAdapterHandlerMethod即注解式RequestMapping方法日常开发90%以上的接口HttpRequestHandlerAdapterHttpRequestHandler对HttpServletRequest/HttpServletResponse直接操作静态资源处理、RMI、JAX-RS服务转发SimpleControllerHandlerAdapterController接口实现类旧版org.springframework.web.servlet.mvc.Controller老项目、遗留ControllerSimpleControllerHandlerAdapter适配的是老式的Controller接口这种接口要求实现类重写handleRequest(HttpServletRequest, HttpServletResponse)方法返回值是一个ModelAndView。在Spring 2.5之前的时代这是主流的写法现在基本被Controller注解式取代了但框架依然保留了向后兼容的能力。而HttpRequestHandlerAdapter适配的是HttpRequestHandler接口它跟Controller接口的区别是它连ModelAndView都不返回完全面向原生Servlet处理。最常见的静态资源处理ResourceHttpRequestHandler就是一个例子它直接把文件流写回响应。3.3 RequestMappingHandlerAdapter的威力参数解析与返回值处理日常开发中你接触最多的肯定是RequestMappingHandlerAdapter它支撑了一套完整的参数解析体系。要知道一个Controller方法的参数是怎么被填上的你得认识一个关键概念HandlerMethodArgumentResolver参数解析器。RequestMappingHandlerAdapter内部维护着一个参数解析器列表对方法里的每个参数都会挨个调用解析器的supportsParameter方法找到支持的解析器后再调用它的resolveArgument方法从HttpServletRequest里把值解析出来。我们熟知的RequestParam、PathVariable、RequestBody、RequestHeader等等背后都有对应的解析器处理。我给你列几个常见的解析器和它们对应的注解遇到参数注入问题时你可以反查是不是走错了解析器RequestParamMethodArgumentResolver处理RequestParam标注的参数以及简单类型的无注解参数。PathVariableMethodArgumentResolver处理PathVariable标注的参数从URL模板变量里取值。RequestBodyMethodArgumentResolver处理RequestBody标注的参数通常会配合HttpMessageConverter把JSON字符串转成Java对象。RequestHeaderMethodArgumentResolver处理RequestHeader标注的参数从请求头里取值。ServletModelAttributeMethodProcessor处理ModelAttribute标注的参数常用于表单对象绑定。你没有感觉RequestBody能直接转成User对象靠的就是MappingJackson2HttpMessageConverter里的ObjectMapper。如果某天你发现RequestBody接收到的对象某些字段是null第一步不是怀疑代码逻辑而是排查你有没有引入Jackson依赖、字段名跟JSON里是否对得上以及有没有默认构造函数。返回值处理也是同样一套逻辑HandlerMethodReturnValueHandler负责把方法返回值变成ModelAndView。比如ResponseBody注解就是通过RequestResponseBodyMethodProcessor处理的它把方法返回值交给HttpMessageConverter直接写入响应体。而ViewNameMethodReturnValueHandler处理返回String类型且方法上没有ResponseBody的情况此时字符串被当作视图名处理。3.4 HandlerAdapter和HandlerMapping如何被装配在一起看到这里你可能有个疑问HandlerMapping和HandlerAdapter这些组件是在哪里被注册的呢如果是Spring Boot项目WebMvcAutoConfiguration会自动配置一个RequestMappingHandlerMapping和一个RequestMappingHandlerAdapter它们的Bean名称分别是requestMappingHandlerMapping和requestMappingHandlerAdapter。如果你手动声明了同名的Bean就会覆盖自动配置。如果是传统SSM项目用XML配置你可能需要在spring-mvc.xml里显式配置mvc:annotation-driven /这个标签背后做的事就是注册这两个核心组件同时还会注册DefaultAnnotationHandlerMapping旧版本或者RequestMappingHandlerMapping、ConfigurableWebBindingInitializer等一堆配套设施。我遇到过有人为了省事把mvc:annotation-driven /去掉自己写了个bean classorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping /来替代结果Controller里凡是带了ResponseBody的接口全部返回404或者直接报错。原因很简单光有HandlerMapping不够没有对应的HandlerAdapter做参数解析和返回值处理注解功能几乎全部瘫痪。所以提醒一句除非你真的清楚自己在干什么否则不要轻易替换这些核心组件。4. 源码实操断点跟踪一次完整的请求执行4.1 在IDEA里准备好断点和观察点理论讲再多不如自己动手看一次请求在源码里是怎么走的。我强烈建议你按我这个方法在IDEA里操作一遍十分钟就能建立非常深刻的肌肉记忆。第一步创建一个最简单的SpringMVC项目写一个接口RestController public class DemoController { GetMapping(/hello) public MapString, String hello(RequestParam(name) String name) { MapString, String result new HashMap(); result.put(msg, hello name); return result; } }第二步在DispatcherServlet#doDispatch方法上打一个断点并且确保断点条件设置成request.getRequestURI().contains(/hello)这样你在调试时不会被静态资源请求干扰。第三步启动项目浏览器访问/hello?nameandy断点命中后开始逐步观察几个关键变量。4.2 从doDispatch到handle的链路观察清单我先给你一个观察清单你照着这个顺序基本就能把上面的所有概念串起来。先看this.handlerMappings。这是一个List里面应该有RequestMappingHandlerMapping等实例。继续往下执行到mappedHandler getHandler(processedRequest)这行执行完之后mappedHandler.getHandler()返回的对象类型是HandlerMethod它持有bean实例、method实例、bean类型等信息你可以展开bean字段看看会发现它指的就是DemoController。这说明HandlerMapping已经精准定位到了目标方法。再看getHandlerAdapter(mappedHandler.getHandler())这个方法执行完返回的适配器类型应该是RequestMappingHandlerAdapter。它就是后续调用ha.handle()的主角。继续进入ha.handle你会看到RequestMappingHandlerAdapter内部会先调invokeHandlerMethod在这个方法里会创建ServletInvocableHandlerMethod执行invokeAndHandle。你继续跟下去会看到一个叫InvocableHandlerMethod#getMethodArgumentValues的方法这个地方就是参数解析器的用武之地。IDEA左侧的变量面板里你可以看到resolvers列表里面每个元素就是一个HandlerMethodArgumentResolver它会遍历这些解析器找到支持RequestParam的那一个把HTTP请求里的nameandy解析出来赋值给方法形参。参数解析完之后你会回到doDispatch此时ModelAndView里已经带着返回值数据后续根据返回值有没有ResponseBody决定是渲染视图还是直接写出JSON。4.3 拦截器在源码里的三个钩子位置在看源码的时候你一定会碰到拦截器相关的三个方法它们分布在doDispatch的各个阶段我们借助源码来加深理解。第一个钩子是mappedHandler.applyPreHandle(processedRequest, response)在Handler方法调用之前执行。这里会按顺序执行所有拦截器的preHandle方法只要任何一个返回false请求就会中断后面的Controller方法根本不会执行。第二个钩子是mappedHandler.applyPostHandle(processedRequest, response, mv)在Handler方法调用完毕之后执行。注意它执行的时候视图还没渲染也就是说你在postHandle里还能修改ModelAndView。第三个钩子不在doDispatch的主流程里在processDispatchResult处理完结果后会调用mappedHandler.triggerAfterCompletion(request, response, exception)。无论请求是否抛异常只要applyPreHandle成功执行过这个方法一定会执行。所以如果你在拦截器里做了资源的打开操作记得在afterCompletion里释放这个位置就是最合适的。我调试过很多次代码之后最大的感受是很多面试题里问“拦截器和Filter有什么区别”看一遍源码你自然就懂了。Filter是基于Servlet容器层面的请求还没到DispatcherServlet就被拦截拦截器是基于SpringMVC层面的它挂在HandlerMapping返回的HandlerExecutionChain上在适配器调用Handler的前后生效。5. 执行流程相关的高频问题与排坑实录5.1 常见异常和问题速查表根据我自己和带过的实习生排查过的经验执行流程这块稍微“想当然”一点就会踩坑。我整理了下面这张表格覆盖了最常见的几种问题现象和排查思路常见现象可能原因排查方向接口404项目能正常启动Controller没加Controller或RequestMappingHandlerMapping没注册请求路径和配置不一致先看日志里有没有RequestMappingHandlerMapping的注册记录再确认类注解和路径接口405提示Request method not supportedURL匹配但HTTP方法不匹配比如只写了GetMapping却发POST请求确认表单提交方式和接口注解是否一致参数全是null或根本进不了方法参数解析器没生效常见于没加RequestBody却传JSON对象缺默认构造器检查RequestMappingHandlerAdapter是否存在、是否有默认构造函数返回的JSON多了莫名其妙的字段返回值处理器和MessageConverter的行为比如把String类型直接按ResponseBody输出看方法上有没有ResponseBody确认视图解析和消息转换的边界拦截器不生效拦截器类没注册到HandlerMapping的执行链里路径配置含通配符但没匹配上检查addInterceptors注册的位置和你拦截的路径规则这里我必须单独展开一个典型的拦截器失效场景。SpringMVC拦截器的注册方式是这样的Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /static/**); } }如果你用的是传统的XML配置那对应的是mvc:interceptors mvc:interceptor mvc:mapping path/**/ bean classcom.example.MyInterceptor/ /mvc:interceptor /mvc:interceptors最容易出问题的点在于如果你同时启用了自定义的WebMvcConfigurer和EnableWebMvc又对HandlerMapping做了定制可能会导致顺序错乱。5.2 一个典型的“接口明明存在却404”排查过程我给你还原一个真实案例。有次同事找我说新加的接口/order/detail访问一直404但其它接口都正常。我第一反应不是去看Controller代码而是先看启动日志。SpringMVC在启动阶段会打印出所有注册的URL映射在日志里搜“order”结果发现根本没有这条映射记录。这说明RequestMappingHandlerMapping压根没把这个方法注册进去。再看Controller类发现类上确实加了RestController方法上写了GetMapping(/order/detail)注释也没问题。那问题在哪呢我又查了一下类上是否被RequestMapping标注发现这个类把注解写成了RequestMapping(/order)。这看起来没问题啊结果仔细一看路径里多了个空格实际请求的是/order /detail。问题一下就明朗了URL路径里带了空格浏览器发请求时空格被编码成了%20和注册路径当然不匹配。排这种问题最直接的方法就是打开控制台日志或者调试接口看看doDispatch里拿到的request URI是什么再去DispatcherServlet里找到getHandler执行的逻辑一目了然。5.3 “No adapter for handler”和参数解析失败的深层原因有时候你在控制台会看到类似No adapter for handler [xxx]的报错。这句话翻译过来就是所有HandlerAdapter里没有一个supports方法返回true。最典型的场景是你把一个自定义的Handler对象塞进了HandlerExecutionChain但你没有为它配置对应的HandlerAdapter。我见过有人为了处理某种特殊请求在拦截器里直接new了一个HandlerExecutionChain(handler)返回结果DispatcherServlet拿着这个handler去找适配器三种内置适配器都不认就报了No adapter for handler。解决办法就是给它配一个实现了HandlerAdapter接口的适配器让它能处理这种自定义handler。参数解析失败则是另一类问题。我举一个例子很多人踩过方法参数是User对象但没有加任何注解请求发的是JSON体。这种情况下SpringMVC不会把JSON解析成User对象它可能会走ServletModelAttributeMethodProcessor尝试从表单参数里绑定属性结果所有的属性都绑定不上得到的User对象全是默认值。这个问题的本质就是对参数解析器机制不熟悉。别小看这个点它能直接区分你是“背过流程”还是“真懂执行原理”。6. 一些必须知道的避坑心得6.1 核心源码不要只背结论要会验证我见过很多简历上写“熟悉SpringMVC执行流程”的人面试时候确实能一字不差地背出九大步骤但一让他说RequestMappingHandlerAdapter里的参数解析器列表有哪些或者让他从一个报错反推是哪个环节出了问题就露馅了。如果你也想摆脱“只会背”的状态我的建议很朴素拉一个SpringMVC项目打断点从doDispatch开始一步步走完整个hello接口然后把断点撤销再走一遍RequestBody的接口看看执行路径有什么不同。这两趟走下来你对执行流程的理解就会上一个台阶。6.2 排查问题时的倒推思维从实战角度说遇到接口问题时不要一上来就翻Controller代码。建立一个倒推的排查顺序会高效得多先确认请求有没有到DispatcherServlet这一步可以看日志或者断点如果没到问题在Filter或容器层面。再看getHandler有没有返回HandlerExecutionChain如果返回null说明HandlerMapping没匹配上。再看getHandlerAdapter能不能找到Adapter找不到就报No adapter。最后再进handle方法看参数解析和返回值处理。用这个顺序我从没遇到过排查半天还定位不到的问题。这套倒推法也是理解整个执行流程最有价值的地方因为你把每个关键环节都“钉死”在了源码的某个方法上而不是靠缝缝补补试错。6.3 面试题角度三句话讲清楚HandlerMapping和HandlerAdapter如果你正准备面试可以用这三句话给自己压轴第一句HandlerMapping是路由表负责根据URL找到执行链并挂上拦截器。第二句HandlerAdapter是适配层负责让不同类型的Handler都能被统一调用同时管理参数解析和返回值处理。第三句DispatcherServlet通过遍历和supports方法把两者串联起来自己只做调度不碰具体逻辑。这三句话能说出来面试官基本就认可你是理解了这个机制的。最后再分享一个小技巧如果你真想把SpringMVC的脉络刻在脑子里别只看SpringMVC顺手看一下SpringBoot的DispatcherServletAutoConfiguration你就能明白自动配置是怎么把这些组件注入容器的。说句实话搞懂执行流程之后很多以前“莫名其妙”的Bug都会变成一眼能看穿的小问题。这大概就是读源码最实在的回报。