????????今天給大家?guī)淼姆治霭咐莝pringboot集成的程序健康檢測案例氧腰,首先是基于springboot1.5.13版本诅福,其次主要分析的包如下圖所示泛范。
????????之所以要分析這塊內(nèi)容,其實(shí)還是由于工作上導(dǎo)致的谍夭,前段時間黑滴,運(yùn)維想要讓我們在程序種加入一個可以訪問程序狀態(tài)的路徑,以便于運(yùn)維檢測程序慧库,然后springboot也自帶了這個功能,所以我就直接使用了馋嗜,但是使用的過程種齐板,發(fā)現(xiàn)了一個問題,如下圖所示。
????????顯示我的db的狀態(tài)為unknown甘磨,這我就瞬間來精神了橡羞,憑啥我得db就是unknown狀態(tài),難道不配顯示信息嗎济舆?當(dāng)然這是玩笑之話卿泽,為啥顯示未知狀態(tài),肯定還是由程序判斷的結(jié)果滋觉,至于原因签夭,我們接下來具體分析這一塊內(nèi)容,也會順帶分析到整個健康檢測的一些核心機(jī)制功能實(shí)現(xiàn)點(diǎn)椎侠。
? ? ? ? 至于如何引入spring健康檢查第租,在boot的情況下,下面?zhèn)z張圖估計大家都應(yīng)該明白了我纪。
? ? ? ? 懷著好奇心態(tài)的我慎宾,對引入jar沒啥興趣,但是我有點(diǎn)對這個配置感興趣浅悉,我懷著試試的心態(tài)趟据,直接把這個配置給刪除了,然后重新訪問了/health路徑术健,如下圖所示汹碱。
? ? ? ? 好家伙,還有這么一手苛坚,配置不配置依舊還會顯示信息比被,但是顯示的信息不一樣,于是我們帶著疑問進(jìn)行分析去了泼舱。
? ? ? ? 首先我們分析下這個配置究竟是干嘛的等缀,根據(jù)spring自帶的配置提示,如下圖所示
我們找到了配置的類在這里娇昙,所有在yml中配置的信息都會注入到這個類中尺迂。關(guān)于配置信息,我們先簡單分析這到冒掌,后續(xù)會有關(guān)聯(lián)點(diǎn)噪裕。
????????接下來我們分析/health這個路徑,大家都知道股毫,既然我能通過http訪問這個/health膳音,說明他在spring容器中肯定存在一個控制器,但是我們并沒有自己去寫這個控制器铃诬,由此猜測可能是spring自己注冊的祭陷,這里就有點(diǎn)小麻煩了苍凛,如果我們自己寫的話找起來還比較好找,因?yàn)橹苯邮褂胕dea搜索或者包都瀏覽一遍兵志,但是spring自己注冊的話醇蝴,就不可控了,鬼知道他是怎么注冊進(jìn)去的想罕,我們先試著使用idea全局搜索試一下:
我們發(fā)現(xiàn)了這個使用點(diǎn)悠栓,但是經(jīng)過排查,發(fā)現(xiàn)并不是我們要找的按价。貌似這樣我們又陷入了黑暗惭适,感覺前途一片黑暗,spring源碼分析之路宣告封閉俘枫,總不能把spring所有類都看一遍找找在哪注冊了這個/health腥沽,估計看完頭發(fā)都掉完了。各位莫著急鸠蚪,我這里教大家倆招今阳,保證手到擒來:
第一種方式:
? ? ? ? 觀看spring啟動日志,
會有這么一行數(shù)據(jù)
2021-01-15 09:59:29 [main] [org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry:543] - Mapped "{[/health || /health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)
到這里估計大家都看明白了茅信,spring啟動大多數(shù)情況下會默認(rèn)輸出所有控制器的映射信息盾舌,包括對應(yīng)的handler,上面的信息告訴我們蘸鲸,這個/health對應(yīng)的控制器為HealthMvcEndpoint妖谴,我們一會再去分析這個類。接下來看第二種方式酌摇。
第二種方式:
? ? ? ? debug源碼膝舅,這個需要對springmvc相關(guān)源碼比較熟悉的人適用,大家都知道窑多,我們spring有一個核心servlet就是dispatcherservlet仍稀,所有的映射控制器處理,都要經(jīng)過他轉(zhuǎn)發(fā)埂息,因此我們直接去到這個類技潘。
在第940行打上斷點(diǎn),然后使用postman或者瀏覽器發(fā)起請求千康,就會自動跳轉(zhuǎn)到這個斷點(diǎn)上享幽,至于如何定位到doDispatcher以及這個mappedHandler,詳細(xì)過程需要結(jié)合springmvc部份的源碼以及梳理拾弃,這里就暫不深究了值桩,有興趣的小伙伴們可以私信或者留言告訴我,我抽時間可以安排一下豪椿,你懂得奔坟!
? ? ? ? 好了斯入,倆種方式大致上已經(jīng)告訴小伙伴們了,這里再說一點(diǎn)蛀蜜,貌似還有一種方式可以使用接口請求輸出所有映射的詳細(xì)信息,這里也不深究了增蹭,這倆種方式不僅僅限于本篇文章的用途滴某,以后你們?nèi)绻蚕肴ふ夷硞€映射或者控制器,都可以使用這倆種方式滋迈。還是比較實(shí)用的霎奢。
? ? ? ? 接下來,我們重點(diǎn)去分析這個HealthMvcEndpoint類饼灿。
我們可以看到invoke方法上使用的是AcutorGetMapping幕侠,本質(zhì)上還是屬于RequestMapping的一種,因此這個invoke方法肯定是我們需要過一遍的碍彭,首先我們先簡單分析?
!getDelegate().isEnabled())
private static final String ENDPOINTS_ENABLED_PROPERTY = "endpoints.enabled";
我們并沒有配置上面這個變量晤硕,他也沒有默認(rèn)的屬性值在容器中,因此這個值肯定不存在的庇忌,所以在第73行判斷條件為false舞箍,默認(rèn)返回return true。所以不會走if里面的語句皆疹。
進(jìn)入getHealth方法中疏橄,發(fā)現(xiàn)會調(diào)用getCurrentHealth,在這個方法中略就,sprng做了一個緩存機(jī)制捎迫,把得到的cachedHealth緩存了起來,并且有一個時間過期機(jī)制表牢。第一次調(diào)用的時候這個cachedHealth肯定是null窄绒,因此我們需要分析getDelegate().invoke()方法,getDeleagte()方法屬于超類中的方法初茶,會返回一個泛型的delegate對象颗祝,我們簡單看下超類的結(jié)構(gòu)
有很多子類都實(shí)現(xiàn)了這個超類,分別提供不同的功能恼布,我們這次研究的HealthMvcEndpoint就屬于其中之一螺戳,并且這個超類中還有一個泛型變量delegate,這個也是在類實(shí)例化階段需要填充的折汞,這個我們稍后在分析倔幼,這個泛型變量具體類型基于子類的實(shí)現(xiàn)方式,我們看到子類HealthMvcEndpoint中明確了泛型為HealthEndpoint類型爽待,接下來接續(xù)分析delegate.invoke()方法
可以看到healthIndicator是在構(gòu)造函數(shù)中進(jìn)行初始化的损同,老樣子翩腐,繼續(xù)走主流程,稍后在分析這塊如何初始化的膏燃,這個類名是CompositeHealthIndicators茂卦,看類名就是綜合健康檢查的意思,一目了然组哩。
繼續(xù)分析health()方法
我們可以看到這個indicators是由一個map組成等龙,value存的是所有spring集成的第三方中間件的健康檢查的控件類,有redis伶贰,db蛛砰,mail,config等等黍衙,然后for循環(huán)這個indicators泥畅,在healths中會放入各個中間件的一些健康信息,最后調(diào)用healthAggregator進(jìn)行聚合處理琅翻。我們先簡單看一個redis的健康檢查控件
org.springframework.boot.actuate.health.RedisHealthIndicator
我們進(jìn)入到這個類中
這個類比較簡單位仁,就一個核心方法doHealthCheck,了解spring源碼的人都應(yīng)該清楚方椎,像這種方法名一看就是被調(diào)用的障癌,而且絕大多數(shù)是在本類,但我們這個本類沒有其他方法辩尊,而且這個方法是重寫的泻蚊,因此我們?nèi)コ愔锌纯唇Y(jié)構(gòu)骚亿。
我們看到了超類中定義了一個抽象方法,并且在health()方法中進(jìn)行調(diào)用,這個health方法就是上面CompositeHealthIndicators類中進(jìn)行重寫的health方法中進(jìn)行for循環(huán)調(diào)用的地方误算。當(dāng)調(diào)用到RedisHealthIndicator的health方法的時候嚎花,會默認(rèn)調(diào)用超類的health方法撩嚼,然后通過重寫的方法調(diào)用到子類的doHealthCheck捆憎,這是典型的模板方法設(shè)計模式。其實(shí)設(shè)計模式也挺實(shí)用的迟隅,雖然我也不是很清楚每種場景的設(shè)計模式但骨。
我們重點(diǎn)分析doHealthCheck,首先方法通過redisConnectionFactory獲取一個redisconnection智袭,如果獲取到了則說明redis狀態(tài)一且正常奔缠,且可以獲取redis一些版本以及其他信息,這里大家看到connection做了類型判斷吼野,判斷是集群模式還是單機(jī)模式校哎,不同的類型走不同的處理邏輯,我們這邊不研究這個了。這里需要注意的一個點(diǎn)是闷哆,我們并沒看到down的處理邏輯腰奋,而且我們應(yīng)該了解如果connection獲取不到,肯定會報socket連接異常抱怔,但是這里異常雖然try了劣坊,并沒有catch,因此異城簦肯定會往上層代碼拋出讼稚,我們?nèi)タ闯惖奶幚?/p>
try {
doHealthCheck(builder);
}
catch (Exception ex) {
this.logger.warn("Health check failed", ex);
builder.down(ex);
}
一且都很清晰明了了,這里不但會打印日志绕沈,還會將這個中間件標(biāo)記為down狀態(tài)。
redis的其實(shí)并沒有什么難度帮寻,當(dāng)我準(zhǔn)備分析db的時候乍狐,也覺得大致一樣,但是真正分析的時候還是有點(diǎn)不同的固逗,還是比較有趣的浅蚪,我們帶著db為啥是unknown的狀態(tài)的疑問,接下來我們重點(diǎn)分析db的健康檢查原理烫罩。
分析前我們先看上面一張圖惜傲,我們會發(fā)現(xiàn)這個linkedhashmap中的db的value明顯和別的不一樣,上面我們已經(jīng)分析過了redis的健康檢查控件就是RedisHealthIndicator贝攒,但是這個db的卻是CompositeHealthIndicator盗誊,再看下面這張圖
org.springframework.boot.actuate.health.DataSourceHealthIndicator
明明是有db的專屬控件的,為什么這里health方法中卻不是呢隘弊,通過查看DataSourceHealthIndicator在哪被初始化哈踱,如下圖所示進(jìn)行研究
我們看上面這個很重要的類,org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration
顧名思義梨熙,這個類是基本所有spring集成中間件健康檢查的自動裝配類开镣。這時候有人會問我,你怎么找到這個類的呢咽扇,總不能一個個去看吧邪财,其實(shí)很簡單,我們把鼠標(biāo)放在DataSourceHealthIndicator類名上质欲,使用idea的find usages(alt+f7)就可以知道這個類在哪里被調(diào)用树埠、使用、初始化等等嘶伟。
我們還是繼續(xù)分析這個HealthIndicatorAutoConfiguration弥奸,它由眾多靜態(tài)內(nèi)部類組成,基本每個靜態(tài)內(nèi)部類都是一個中間件的健康檢查裝配類奋早,我們看上面關(guān)于db的裝配類盛霎,在重點(diǎn)分析第222行@bean注解的方法之前赠橙,我們先看下第193行這個db靜態(tài)內(nèi)部類的構(gòu)造方法,因?yàn)檫@個有個屬性的初始化跟后面的分析有關(guān)愤炸,構(gòu)造方法中期揪,它初始化了倆個屬性如下:
this.dataSources = filterDataSources(dataSources.getIfAvailable());
this.metadataProviders = metadataProviders.getIfAvailable();
我們主要看第一個,dataSources的初始化规个,調(diào)用了filterDataSources方法凤薛,傳參是
dataSources.getIfAvailable(),類型是ObjectProvider<Map<String, DataSource>> dataSources
關(guān)于這種ObjectProvider類型的參數(shù)诞仓,其實(shí)是spring獨(dú)特的一個注入方式缤苫,我們這里不深究了,以后有機(jī)會在講墅拭,這個變量的主要作用就是spring容器初始化這個構(gòu)造函數(shù)的時候活玲,會把容器中所有dataSource的對象注入到這個容器中,key就是dataSource的beanName谍婉,value就是dataSource舒憾。
因此我們看filterDataSources方法,正常程序中一般都會有dataSource的對象穗熬,所以第202行不會成立镀迂,繼續(xù)往下看,第206唤蔗,207行判斷這個map的value是否是AbstractRoutingDataSource的子類探遵,關(guān)于
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
有的小伙伴可能不太了解,這個是spring多數(shù)據(jù)源的一個超類妓柜。如果你的項目中别凤,需要使用多數(shù)據(jù)源,那么這個類你必然會用到领虹,而我這個程序中也正好是使用了多數(shù)據(jù)源规哪,因此當(dāng)程序走到這一步的時候,不會走if分支塌衰,直接返回一個空的數(shù)據(jù)源map給類的私有屬性诉稍。
說完了上面類的構(gòu)造方法,我們繼續(xù)看被@bean注解的dbHealthIndicator方法最疆,接觸過springboot的都知道杯巨,所有含有@bean的方法,只要被spring容器掃描到努酸,那么在容器初始化階段的時候服爷,就會去解析這個bean注入到容器中,這個方法會調(diào)用createHealthIndicator方法,這個方法是來自于超類
這個類一共就倆個方法仍源,并且是重載的心褐,一個接受map參數(shù)返回HealthIndicator,一個接受泛型變量返回泛型對象笼踩。
第二個方法比較復(fù)雜逗爹,因?yàn)樵赿bHealthIndicator中調(diào)用的createHealthIndicator傳的參數(shù)是一個map,所以我們這里只需看第一個嚎于,首先是if條件掘而,上文我們也知道了,多數(shù)據(jù)源的dataSource是不會放入到map中的于购,因此在這里不會走if袍睡,直接new一個CompositeHealthIndicator,參數(shù)是HealthAggregator肋僧,且下面的for循環(huán)也沒有任何意義斑胜,直接會跳過,返回這個composite色瘩,這樣我們db的健康檢查控件就初始化完畢了,不太像我們之前redis分析的那樣逸寓,簡單明了居兆,這里貌似彎彎繞繞比較多,對于這個CompositeHealthIndicator竹伸,我們貌似有點(diǎn)眼熟泥栖,上文貌似是在HealthEndpoint中有這個,為啥db也是這個呢
我們再來看下這個類勋篓,其實(shí)這里spring相當(dāng)于刷了一個小聰明吧享,它復(fù)用了這個類,在無法正常初始化中間件的控件的時候譬嚣,就像上面db的datasource為空的時候钢颂,他就默認(rèn)初始化一個CompositeHealthIndicator,我們看這個health方法拜银,和之前分析這個health不同殊鞭,我們這里indicators為空,所以不會走for循環(huán)尼桶,所以我們看下第70行方法操灿,傳入的是一個空的healths,
這里aggregate方法里面處理了這個healths變量泵督,37行因?yàn)榭盏膍ap不會走趾盐,所以他會調(diào)用抽象方法,aggregateStatus方法,入?yún)⑹且粋€空的集合
這個抽象類默認(rèn)就一個子類救鲤,這個子類重寫了aggregateStatus久窟,首先因?yàn)槿雲(yún)⑹强占希缘谝粋€for循環(huán)不會走蜒简,到if語句的時候瘸羡,因?yàn)檫@個方法內(nèi)部變量依舊是空集合,所以條件成立搓茬,返回狀態(tài)為unknown狀態(tài)犹赖。所以這就解釋了為啥我程序中db狀態(tài)為啥是unknown狀態(tài)。到此為止我們梳理下整個的調(diào)用鏈路:
首先是HealthMvcEndpoint調(diào)用了invoke方法卷仑,而invoke最終調(diào)用了HealthEndpoint中的invoke方法峻村,然后在invoke方法中調(diào)用healthIndicator變量進(jìn)行for循環(huán)所有中間件健康檢查控件類的health方法,如果能正常初始化的控件就會正常顯示狀態(tài)up或者down(如redis)锡凝,如果不能正常初始化的則會默認(rèn)賦值一個控件粘昨,如多數(shù)據(jù)源情況下的db控件,則絕大多數(shù)都會返回unknown狀態(tài)窜锯。
文章到這里张肾,開始的問題已經(jīng)分析出了因果,我們在分析問題的時候還留下了幾個其他疑問锚扎,分別如下:
1吞瞪、ManagementServerProperties類的配置究竟有何作用?(為什么配置了就會顯示更多信息驾孔,不配就只顯示一個狀態(tài))
2芍秆、HealthMvcEndpoint類中delegate是如何初始化的?
3翠勉、HealthEndpoint中的healthIndicator是如何初始化的妖啥?
我們一個個來分析,首先第一個問題:
我們看到Health類中包含了status以及details对碌,status就是狀態(tài)荆虱,而details是各個中間件的詳細(xì)信息,就像一開始文章所示的請求返回信息一樣朽们,包含redis的版本信息克伊,磁盤信息等。上面getHealth方法中有個exposeHealthDetails方法华坦,如果這個方法返回true則是返回詳細(xì)信息愿吹,如果false,則看下面的構(gòu)造只返回status惜姐。因此我們需要看下這個方法犁跪,
這個方法首先先判斷this.secure,如果是false直接返回true椿息,如果是true則走下面。/health要想返回詳細(xì)信息這里一定得是false坷衍,我們看下這個secure是在那里初始化得寝优。
首先是在構(gòu)造函數(shù)中被初始化的,我們接下來看構(gòu)造函數(shù)被誰調(diào)用
這里注意看構(gòu)造函數(shù)的參數(shù)其中是由一個managementServerProperties.getSecurity().isEnabled()傳入的枫耳,我們點(diǎn)進(jìn)去看下
原來我們在yml中配置的屬性乏矾,最終都會注入到這個類中,并且在HealthMvcEndpoint類初始化的時候一并傳入過去迁杨,然后處理相關(guān)邏輯的時候會使用到這些屬性钻心。這就解釋了我們第一個問題。
第二個問題铅协,HealthMvcEndpoint類中delegate是如何初始化的捷沸?
我們看到healthMvcEndpoint構(gòu)造函數(shù)中傳入這個delegate,經(jīng)過排查狐史,發(fā)現(xiàn)在@bean方法中進(jìn)行初始化的痒给,
再通過上面這張圖我們很清晰就能明白,這個delegate本身也會作為一個bean放入到容器中骏全,然后作為構(gòu)造參數(shù)注入到別的類中進(jìn)行調(diào)用苍柏。因此第二個問題就分析完了。
第三個問題姜贡,HealthEndpoint中的healthIndicator是如何初始化的试吁?
我們看上面圖示,第56行new了一個默認(rèn)的CompositeHealthIndicator鲁豪,然后經(jīng)過一個for循環(huán)處理潘悼,填充了一下healthIndicator的內(nèi)部變量indicators律秃,因此我們的重點(diǎn)是這個構(gòu)造函數(shù)的healthIndicators變量爬橡。
可以看到這個構(gòu)造函數(shù)參數(shù)是由this的一個變量傳遞的,經(jīng)過上面的分析棒动,這里不是空糙申,所以肯定是有值的,
這個this.healthIndicators是由ObjectProvider類調(diào)用getIfAvailable方法得到來的船惨,這個方法我們上面分析過柜裸,其實(shí)這是spring常用的一種注入方式,結(jié)果就是能夠到所有的HealIndicator的bean的map對象粱锐,key為beanname疙挺,value為bean,這里也不深究了怜浅,如果有想了解這塊知識的可以私信留言告訴我铐然,我到時候整理講解一下蔬崩。
因此關(guān)于前面遺留的三個問題我也間接的回答完了,你們也可以自己去嘗試分析一下搀暑,看一下是否如上所說沥阳。這一期的案例分析就說到這里了。
這篇文章還是讓我花了不少時間去書寫和思考的自点,如果有喜歡的小伙伴一定要點(diǎn)擊收藏點(diǎn)贊哦桐罕,你們的贊揚(yáng)是我繼續(xù)的動力,哈哈桂敛,客套話了功炮。想關(guān)于這篇文章討論的可以在下方留言,我看到了隨時會回復(fù)的埠啃。
寫在文后:
關(guān)于下篇文章死宣,我準(zhǔn)備寫一篇關(guān)于springboot初始化相關(guān)的文章,主要還是針對于問題而去分析的碴开,我這里可以先拋出問題毅该,留給各位去思考:
如果spring中有一個bean,我們自己也去定義了潦牛,為什么springboot會默認(rèn)先初始化我們的類眶掌?
關(guān)于這個問題的討論也可放在下方去留言。巴碗。朴爬。。橡淆。召噩。。