如何讀源碼用心總結

首先吐槽下網上的源碼分析,總是喜歡把大段大段的源碼貼出來湊字數,不指明重點,每一行都寫個註釋,我自己下載源碼看不香嗎,來你博客看的目的其實就是想看到是總體思路,而不是源碼;然後就是不指明源碼版本,雖然有些好的框架總體思路一般都不會大改,但是指明版本會更好一點;更奇葩的,一篇文章前大半部分在寫情懷,後半部分在打廣告,呵呵。

不要覺得看源碼很高級,其實也就那麼回事,如果你能把他的東西借鑑過來,或者用他的東西可以用得更底層,這纔是真正瞭解了源碼。

一些博客帶你看源碼,就是方法一層一層往裏跳,當前看着挺爽,似乎自己是真懂了,其實卵用沒有,這裏不舉反面教材了,網上到處都是。

那應該怎麼看源碼呢,個人總結了幾點: 入口,脈絡,經驗,調試

  • 首先,一定要找到入口,要知道從哪個地方開始
  • 然後就是經驗 ,經驗可以讓你從更近的入口開始,如果熟悉並運用過設計模式,則可以根據名稱一眼就知道幹嘛的
  • 然後就是脈絡了,你得知道這個框架的大致思路,你可以大膽猜測,然後小心求證
  • 然後對有一些細枝末節實在是理不清的時候,最先應該藉助於網絡尋找解答,然後就是自己去一步步調試
  • 一般來說,低版本的框架代碼相對簡單,可以清晰的知道作者思路,後面包裝越來越多,如果是來看源碼的話,可能會繞不清楚,所以看源碼一般看低版本比較好,但也不能低於一個大版本,大版本有可能會改動比較大

下面用幾個實際的例子來說明我的觀點:

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

我的博客文章大綱

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章