如何读源码用心总结

首先吐槽下网上的源码分析,总是喜欢把大段大段的源码贴出来凑字数,不指明重点,每一行都写个注释,我自己下载源码看不香吗,来你博客看的目的其实就是想看到是总体思路,而不是源码;然后就是不指明源码版本,虽然有些好的框架总体思路一般都不会大改,但是指明版本会更好一点;更奇葩的,一篇文章前大半部分在写情怀,后半部分在打广告,呵呵。

不要觉得看源码很高级,其实也就那么回事,如果你能把他的东西借鉴过来,或者用他的东西可以用得更底层,这才是真正了解了源码。

一些博客带你看源码,就是方法一层一层往里跳,当前看着挺爽,似乎自己是真懂了,其实卵用没有,这里不举反面教材了,网上到处都是。

那应该怎么看源码呢,个人总结了几点: 入口,脉络,经验,调试

  • 首先,一定要找到入口,要知道从哪个地方开始
  • 然后就是经验 ,经验可以让你从更近的入口开始,如果熟悉并运用过设计模式,则可以根据名称一眼就知道干嘛的
  • 然后就是脉络了,你得知道这个框架的大致思路,你可以大胆猜测,然后小心求证
  • 然后对有一些细枝末节实在是理不清的时候,最先应该借助于网络寻找解答,然后就是自己去一步步调试
  • 一般来说,低版本的框架代码相对简单,可以清晰的知道作者思路,后面包装越来越多,如果是来看源码的话,可能会绕不清楚,所以看源码一般看低版本比较好,但也不能低于一个大版本,大版本有可能会改动比较大

下面用几个实际的例子来说明我的观点:

spring beanFactory

版本 : 5.0.9

虽然现在都是用无 xml 来配置 bean 了,但原理是一样的,个人是从以前的 xml 过来的,所以带你们看的入口是 ClassPathXmlApplicationContext.refresh() ,那我是怎么找到这个入口的呢,这个就是经验,最开始你们找的入口应该是这个,我要启动一个 spring 容器要这样写

ApplicationContext ac = new ClassPathXmlApplicationContext("configLocations.xml");
// 然后找这个构造函数,最终你会找到这个 refresh 方法

入口找到了,接下来就是要知道 spring 的整个脉络 ,spring 主要是用来管理 bean 的,有一个 beanFactory ,在实例化 bean 的时候,因为考虑到懒加载和循环依赖,所以 spring 会先创建 bean 定义的东西 BeanDefinition ,然后再去创建 bean ,创建 bean 的过程中要注入属性,还有我们自己要在构造前和构造后要做一些事情,所以 spring 用 BeanPostProcessor 来解决这个问题;然后像 mybatis 的 Mapper 他是怎么成为 spring bean 的你们有没有想过,它只是一个接口哎,这肯定是用了动态代理技术,说明 spring 允许别人在 bean 实例化之前插入我们自己的 bean 定义,所以整个流程大致长这样

  • 读取 xml 或者扫描拿到 bean 定义
  • BeanFactoryPostProcessor 修改或者添加一些 bean 定义
  • BeanPostProcessor 在 bean 创建过程中填充属性,处理一些与 bean 相关的事情

源码读完后的应用 mybatis Mapper

再具体一点,mybatis 的 Mapper 是如何生成代理类的呢,读者可以看这个类 MapperScannerConfigurer ,它实现了 BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor 的功能就是让使用者可以住容器中注册 BeanDefinition,如果你想看具体实现,找到它的接口 postProcessBeanDefinitionRegistry,看具体要实现的方法,就是实现逻辑;

然后 spring 提供了一个扫描工具 ClassPathBeanDefinitionScanner 可以扫描类路径 ,可以方便的设置 include ,exclude 得到想要的 BeanDefinition ,它会自动将读取到的 BeanDefinition 注入容器,我们可以重写它的 doScan 方法,在注入容器之前对 BeanDefinition 做对应修改,mybatis 就是把接口改成了 FactoryBean ,在 getObject 方法中返回了一个代理类。

像 mybatis 的这个,我们自己也可以也可以造一个轮子:具体可以看我的这个,造了一个 Feign 的轮子

源码读完后的应用 @Autowired

@autowired 是如何工作的呢 ,首先找到入口 AutowiredAnnotationBeanPostProcessor 看名字 BeanPostProcessor 就知道了,是用于 bean 的后置处理,再看它的实现接口,读者应该明白了吧。

源码读完后的应用 @Component

@Component 注解的 bean ,一般来是说单例的,并且在启动时就会被加载进 IOC 容器,那这件事是在哪做的呢

这个注解是在启动时进行扫描的,入口类是 AnnotationConfigApplicationContext ,它会在构造的时候,使用 ClassPathBeanDefinitionScanner 去扫描所有的 @Component,这样扫描出来默认是单例的,可以看 AbstractBeanDefinition 定义的 scope 是没有被修改的,默认值就是单例。

然后找到 AbstractApplicationContext.finishBeanFactoryInitialization() ,看其注释就是实例化所有非延迟加载的单例,看这方法最后一行 beanFactory.preInstantiateSingletons() ,在这个里面会把所有的单例 bean 调用 getBean() 方法,相当于实例化;

其它疑问

可能读者还有疑问 , BeanPostProcessor 是什么时候被调起来的呢 , 这个入口在 BeanFactory 的 getBean 过程中, spring 只在在 getBean 才会去初始化 bean ,所以入口在 BeanFactory ,更准确的说在 DefaultListableBeanFactory.doGetBean()

springmvc

springmvc 如果要看的话,首先要知道它本身是基于 servlet 的,也就是主入口是 DispatcherServlet

然后它有几大组件 HandlerMappingHandlerAdapterViewResolver

那 handlerMapping 用的哪一个呢,如果使用的是 springboot (本分析基于 2.0.5) 那个入口可以从 spring.factories 开始看,里面有一个 WebMvcAutoConfiguration 写清楚了 handlermapping 使用的是 RequestMappingHandlerMapping,handlerAdapter 使用的是 RequestMappingHandlerAdapter,viewResolver 使用的是 InternalResourceViewResolver ,然后我们看下这几个类,做为入口类

RequestMappingHandlerMapping

这个类主要用于注解 RequestMapping 的路径映射

它的父类实现了 InitializingBean ,我们可以直接看它的 afterPropertiesSet 方法 ,在里面有这样一句代码

// isHandler 的意思跟进去看就知道是 Controller 或有 RequestMapping 注解的类
if (beanType != null && isHandler(beanType)) {
    detectHandlerMethods(beanName);
}

然后去检查它的所有处理方法 ,最后注册到 Map 中

RequestMappingHandlerAdapter

handlerAdapter 主要在处理请求的时候起作用,看名字知道是适配器模式,它把 HandlerMethod 转换成 ModelAndView ,这个入口可以看 DispatcherServlet.doDispatch() 方法

InternalResourceViewResolver

用于视图解析,控制层返回一个视图名,它负责解析成对应视图,现在应用大部分都是用 @RestController ,即返回 json 的形式,这个一般很少用了。

RefreshScope 的源码分析

springcloud 版本: Greenwich.SR5

一直挺好奇, refreshScope 标记的类为什么能够刷新 ,最开始我猜测应该是在对数据变更时,获取到 spring 容器中的 bean 然后对 bean 的属性进行刷新,但后面我发现不是这样的。

首先一惯的套路,看 spring.factories 文件 spring-cloud-context 包 ,有一个 RefreshAutoConfiguration

首先,RefreshScope 标记的 bean 是在 GenericScope 中进行注册的,它实现了 BeanDefinitionRegistryPostProcessor

然后 ,RefreshScope 标记的 bean 是在 GenericScope 进行实例化的,在接到 ContextRefreshedEvent 事件时,调用 start 方法,进行 getBean() 实例化,所在在 容器中存在两个 bean 定义 ,一个是在最开始扫描的时候拿到的 bean 定义,名字为类名小写,一个是这里注册的 bean 定义名字为 scopeTarget.类名小写,如果这时用 beanFactory.getBeansOfType() 方法就会有两个同类型的 bean ,在一些地方使用可能会有问题

然后属性刷新是怎么做的呢,入口在 RefreshEventListener ,调用了 ContextRefresher 的 refresh() 方法,当有 RefreshEvent 事件过来的时候,可以看到当刷新的时候,是把所有 scope bean 全部销毁,然后另外再启动一个容器重新加载环境变量,当再次用到 scopeBean 的时候,又会重新创建 bean

所以 nacos 客户端那边如果收到有数据更新,肯定是发了一个 RefreshEvent

nacos 源码分析

接上面的 @ScopeRefresh ,nacos 使用长轮询来动态加载配置,具体现象是这样的

但服务端无数据更新时,客户端发起的请求服务端会先阻塞住,等 29.5 秒再响应 ,但是当有配置更新时,服务端会立马响应。

具体是怎么实现的呢:客户端发起一个请求时,服务端使用到了 servlet3 的 AsyncRequest ,将请求加到一个队列中,然后监听 LocalDataChangeEvent 事件,当发生这个事件时,使用 AsyncRequest 进行响应,当定时器到了 29.5 秒时,也给客户端进行响应,这时客户端需要重新发起请求数据的连接,再次轮询。

在 nacos 服务端修改数据就只要做一件事 ,发生 LocalDataChangeEvent 事件即可。

我模拟了下 nacos 的长轮询,项目地址: https://gitee.com/sanri/example/tree/master/testwebsocket/src/main/java/com/sanri/test/web/service 入口是 AsyncController

友情链接

我的一个工具 sanri-tools , 可以做 kafka 监控(主题,消费组,分区,反序列化) , redis 数据查看(可以反序列化字段信息看到真实数据),数据表管理,代码生成

sanri-tools

我的博客文章大纲

猜你喜欢

转载自blog.csdn.net/sanri1993/article/details/106960620
今日推荐