Java進階篇——springboot2原始碼探究

姬如乀千瀧發表於2023-01-11

1.@EnableAutoConfiguration

除了元註解之外,EnableAutoConfiguration包含了兩大重要部分:

1)@AutoConfigurationPackage註解

該註解只匯入了一個內部類:AutoConfigurationPackages.Registrar.class

類中有兩個方法

從名字上看,registerBeanDefinitions方法註冊了定義好的一些Bean,determineImports方法決定這些要不要匯入

registerBeanDefinitions呼叫了register方法,並傳入了registry引數和一個後設資料名字陣列。registry引數是一個介面,
實際場景中必然是使用的實現類,可以在該方法打斷點debug

發現實際上他的實現類是一個名為DefaultListableBeanFactory,並且可以清晰的看到該類的一些基本屬性的值

可以看到,registry中儲存的是一些關於專案程式的基本配置和bean例項名。比如一些網頁支援元件和springContext以及一些載入器

還有使用者自定義的類,這些類元件中儲存了bean名字、作用域、懶載入等等


再來看另一個引數:new PackageImports(metadata).getPackageNames().toArray(new String[0])

看起來是將後設資料中的包名提取成陣列,我們開啟看看該類在初始化的時候具體幹了什麼,可以看到實際上在初始化時呼叫了一個ClassUtils.getPackageName,
傳入了一個什麼呢,metadata.getclassname,他是什麼呢,打斷點!
,實際上就是啟動類的全名,com.***.application,而這個方法則是將classname的字首提取出來,即提取出我們的包名com.&&&


即這個AutoConfigurationPackages.register方法傳入了我們的一大堆初始bean的名字、配置和總包名com.**,我們研究研究他做了什麼

首先是判斷定義的bean中有沒有Bean,這個bean是類的屬性,存著當前類的全名:org.springframework.boot.autoconfigure.AutoConfigurationPackages

大概想處理的是使用者自定義了AutoConfigurationPackages類的情況。當前是沒有定義的,所以直接走else邏輯

new了一個GenericBeanDefinition,暫且叫他通用的bean定義工具,set一大堆東西。之後呼叫了註冊方法。把工具丟了進去。

這個註冊方法裡大概是將AutoConfigurationPackages類同樣註冊進了定義bean的map中,然後將map中的所有值加入到了這個預設的bean工廠中,之後這個類就走完了。

因此該註解的作用即是將自定義的bean以及一些基本型別、原始元件註冊到bean工廠中

2)AutoConfigurationImportSelector類

該類中點進去映入眼簾的就是selectImports方法。聽起來名字是選擇匯入,也就是該方法決定了要載入什麼依賴元件

而該方法呼叫了並且只呼叫了getAutoConfigurationEntry方法來獲取要載入的元件。

我們來看看這個方法,先是呼叫getAttributes獲取了某個東西,打斷點發現是
這樣就很熟悉了,是EnableAutoConfiguration註解的兩個屬性值,雖然預設值為null。點進方法體發現確實是這樣。

getCandidateConfigurations(annotationMetadata, attributes)方法呼叫了一大串,最終呼叫了這個方法loadSpringFactories,
該方法的大致內容是,從當前所有的依賴包中載入META-INF/目錄下的spring.factories檔案中尋找一些元件。
可以看到他先嚐試從快取中拿,如果為空,則去依賴包中的META-INF/目錄下的spring.factories中載入。


拿到這個元件列表之後,還要進行一層過濾,抽取含有factoryTypeName的元件列表。
這個name即是org.springframework.boot.autoconfigure.EnableAutoConfiguration自動配置註解

再回到getEntry方法,之後對獲取到的元件列表進行去重,然後試圖從列表中拿出排除項,
也就是attributes中獲取到的EnableAutoConfiguration註解的兩個屬性值的內容。很容易理解,屬性值中配置了要排除的內容將在這裡進行排除。

而後呼叫filter方法對列表進行篩選,而篩選使用的是autoConfigurationMetadata這個類,
由此可見,這個類是某種篩選規則,它裡面儲存了501個properties,所以篩選規則可能就是逐一比對,篩選出Metadata中有的元件。

篩選出來的結果即是最終要載入的元件bean。

也就是說,@EnableAutoConfiguration的兩個重要成員,一個決定了要載入預設的哪些元件(使用者自定義bean、數值包裝類、字串類等等)
和配置,另一個決定了要載入哪些外部依賴類,即透過starter等透過pom引入的元件。

2.請求處理

如何知道spring是怎麼、在哪處理請求的呢,有個很簡單的方法,在properties中將日誌級別設定為debug,即dubug=true。而後執行程式,傳送任意一個請求

就會發現spring列印出了關於處理該請求的一些細節,比如處理請求是從初始化DispatcherServlet開始的,
由此可見請求處理最重要的便是DispatcherServlet。而後AbstractHandlerMapping識別到了處理該請求的具體方法。

我們進入到DispatcherServlet中,檢視他的方法。

學過mvc原生web開發的都知道,servlet有兩大重要方法,doGet和doPost。而DispatcherServlet繼承了FrameworkServlet繼承了HttpServletBean,
HttpServletBean繼承了HttpServlet,由此可知,DispatcherServlet也是一個httpServlet。那麼我們就有思路了,從他的do**方法開始探究。

1)doDispatch

從名字看,這個方法似乎是為了做轉發。並且他的引數幾乎和doGet方法是一模一樣的

最開始是初始化了一大堆東西,包括處理非同步請求的非同步管理器以及檢查是否是檔案上傳的請求巴拉巴拉,而後這個getHandler方法,直接獲取到了處理這個請求的具體方法。

它是如何處理的呢,他遍歷了一個handlerMappings的集合,這個mapping裡面儲存的是spring一些專處理對映的類
,比如歡迎頁的對映,以及我們使用requestMapping標註的url。這樣就打通了,他會在requestMappingHandlerMapping中查詢到/hello請求並且找到他對映的方法。
具體查詢呼叫了HandlerMapping的gethandler,由此就和日誌中的對上了,gethandler方法中就是根據url在對映中找方法,先拋開這個細節不看。

拿到這個處理器(方法)後,將其丟入到了HandlerAdapter請求介面卡中,這個在mvc架構中熟悉的身影。
之後並不是立即執行該方法而是先判斷方法的種類,如果是get或者head方法,則執行邏輯。

這個邏輯會衡返回一個-1,也就是寫死的,我不理解為什麼是這樣,在網上搜了Last-Modified,發現這是一種快取機制,
這也就理解了為什麼必須是get方法,而他實現需要實現LastModified介面,我並沒有實現這個,所以spring這個判斷邏輯會衡false。

之後是一個applyPreHandle的方法,點進去就會發現是使用當前spring的攔截器元件對請求進行攔截,

能發現就是一些請求方法攔截器、token攔截器、資源攔截器等等,也就是一些壞的請求會在這條被攔截

之後就是請求介面卡執行自己的處理邏輯了,可以看到他返回的是一個modelAndView型別的例項,那麼就意味著,此時方法已經被執行了。

但是實際上並沒有使用modelAndView作為返回值,而是直接返回的string。所以mv是一個空值,但是可以在響應體中觀察到,
已經有19位元組的東西被寫進了響應體中,頁印證了方法已經被執行。

緊接著是判斷請求是否是非同步請求,如果是非同步請求可能會做First響應之類的處理。

再往下是一個名叫應用預設的檢視名的方法,將請求體中或者預設的檢視名加入model中。

因為如果應用了modelandview,此時才只是一個model,必然要新增對應的view。可以簡單測試一下。我們在處理方法中new一個modelandview物件,設定一個model值並返回。

可以看到,經過此方法之後,我們並沒有設定view值,系統預設將hello當作了view加入model,這個預設值即是請求路徑去掉前面的/。

這個方法之後,又是一個類似於攔截器,有點類似於在方法執行前後各執行一次攔截。攔截器執行完畢後,方法結束。
後面就是一些兜底處理,比如如果是檔案相關之類的請求,要關閉對應的流。如果是非同步請求執行怎樣的邏輯。

2)handle方法

我們知道,介面卡的handle方法裡面執行了方法邏輯,具體是怎麼執行的呢。實際上是呼叫了super的AbstractHandlerMethodAdapter的handle方法。
這個handle方法又會呼叫自己(RequestMappingHandlerAdapter)的handleInternal方法。該方法內對session做了一些處理,
而後呼叫invokeHandlerMethod方法,這個方法內做了很多的處理。比如WebDataBinderFactory binderFactory物件,他是對引數之中的資料格式做轉換的,他裡面初始化了128種物件轉換的方式;

又或者初始化一些引數解析器和返回值處理器:裡面對各種引數和返回值做對應的解析和處理,總之就是把這些丟入到一個mavContainer容器中。

然後呼叫invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0])方法,同時傳入的還有webRequest,
這個就是將請求體和響應體包裝到了一起。這個方法裡面第一行就直接執行了方法,可以看到此時已經有返回值了。

這個方法就比較簡單了:獲取引數、執行方法

①getMethodArgumentValues

該方法最開始是獲取對映方法的引數型別以及引數名

之後做的事大概可以猜到,就是從請求中找出這些引數名對應的引數並且轉化成對應的型別。可以看看具體的

首先定義了一個接收陣列,用來儲存獲取到的引數,而後嘗試從providedArgs中拿對應的引數,但實際上這個引數傳的是空值。

所以執行後面的邏輯,從請求中拿,並且同時傳入了mav容器,這個容器中就有格式轉換器和返回值處理器等。

該方法裡面有兩個重要的構成,getArgumentResolver獲取引數解析器和resolveArgument解析引數。獲取引數解析器方法中,
迴圈目前已經存進ioc容器的解析器元件,和引數進行一一比對,找到可以解析對應引數的解析器。

解析引數方法中,就是獲取引數名容器→獲取方法引數容器→獲取引數名→解析引數。之後會做很多的後續處理,比如格式轉換之類的。

②doInvoke

這個方法比較簡單,利用之前已經存入InvocableHandlerMethod中的反射方法public java.lang.String
com.glodon.controller.HelloController.home(java.lang.String),實際上,該物件例項也是專門儲存方法處理的相關元件的。
獲取到方法後,利用spring反射機制執行該方法。

3)請求響應

前面將方法執行完之後,封裝完modelAndView,在執行完處理後攔截器後,還要執行一個兜底方法對結果進行處理,從名字來看,他是用來處理結果轉發的。

開始是判斷方法執行是否有異常,如果有則走異常處理邏輯。而後如果modelandview不為空,則執行一個render方法render(mv, request, response)

render方法首先從請求中拿到語言標識,並加入到響應體中,而後拿出modelandview中的檢視名,即重定向或者其他檢視。
然後呼叫resolveViewName方法對檢視名進行處理,對當前容器中的所有解析器進行遍歷,哪個可以解析這個檢視名,就直接返回解析結果。

解析器的解析過程,以ContentNegotiatingViewResolver舉例。

這個解析器獲取了請求的Attributes,然後傳入getMediaTypes方法中並呼叫來獲取返回資料型別。

getMediaTypes裡面其實就是一個雙重迴圈匹配,格式化網頁請求→獲取瀏覽器請求頭中的可接受媒體型別→獲取系統可生產的媒體型別→初始化匹配的媒體型別。
然後進行一個雙重迴圈,如果匹配成功,則把對應的媒體型別加入到compatibleMediaTypes中。

排序完成後就是一個set→List,然後進行一個排序,排序的依據就是媒體型別的權重。

瀏覽器在傳送請求的時候會給伺服器一個accept,裡面明確表示了瀏覽器可以接收的返回型別以及他的權重,而這裡的排序就是按照這個權重進行排序的。

該方法返回後,在接著看解析方法,之後呼叫了getCandidateViews獲取候選檢視。

getCandidateViews方法內同樣是個雙層迴圈。外層是除了當前解析器外的其他三個檢視解析器,內層是對匹配到的媒體型別。

透過除錯,發現最終是被InternalResourceViewResolver成功處理,我們只看他的細節。

同樣還是resolveViewname方法,該方法先嚐試去快取中拿,拿不到了才執行createView方法建立檢視。而這個方法就很清晰明瞭了

判斷是重定向還是轉發來生成對應的檢視。最終返回合適的檢視,新增快取巴拉巴拉。

我們測試的剛好是一個重定向檢視,所以返回的結果就是bean為redirect,url為/helloWorld的檢視。

相關文章