spring源码案例分析之健康检查

        今天给大家带来的分析案例是springboot集成的程序健康检测案例,首先是基于springboot1.5.13版本,其次主要分析的包如下图所示。

主要类包

        之所以要分析这块内容,其实还是由于工作上导致的,前段时间,运维想要让我们在程序种加入一个可以访问程序状态的路径,以便于运维检测程序,然后springboot也自带了这个功能,所以我就直接使用了,但是使用的过程种,发现了一个问题,如下图所示。

        显示我的db的状态为unknown,这我就瞬间来精神了,凭啥我得db就是unknown状态,难道不配显示信息吗?当然这是玩笑之话,为啥显示未知状态,肯定还是由程序判断的结果,至于原因,我们接下来具体分析这一块内容,也会顺带分析到整个健康检测的一些核心机制功能实现点。

        至于如何引入spring健康检查,在boot的情况下,下面俩张图估计大家都应该明白了。

主要引入的jar包
yml文件需要配置的点

        怀着好奇心态的我,对引入jar没啥兴趣,但是我有点对这个配置感兴趣,我怀着试试的心态,直接把这个配置给删除了,然后重新访问了/health路径,如下图所示。

        好家伙,还有这么一手,配置不配置依旧还会显示信息,但是显示的信息不一样,于是我们带着疑问进行分析去了。

        首先我们分析下这个配置究竟是干嘛的,根据spring自带的配置提示,如下图所示

org.springframework.boot.actuate.autoconfigure.ManagementServerProperties

我们找到了配置的类在这里,所有在yml中配置的信息都会注入到这个类中。关于配置信息,我们先简单分析这到,后续会有关联点。

        接下来我们分析/health这个路径,大家都知道,既然我能通过http访问这个/health,说明他在spring容器中肯定存在一个控制器,但是我们并没有自己去写这个控制器,由此猜测可能是spring自己注册的,这里就有点小麻烦了,如果我们自己写的话找起来还比较好找,因为直接使用idea搜索或者包都浏览一遍,但是spring自己注册的话,就不可控了,鬼知道他是怎么注册进去的,我们先试着使用idea全局搜索试一下:

我们发现了这个使用点,但是经过排查,发现并不是我们要找的。貌似这样我们又陷入了黑暗,感觉前途一片黑暗,spring源码分析之路宣告封闭,总不能把spring所有类都看一遍找找在哪注册了这个/health,估计看完头发都掉完了。各位莫着急,我这里教大家俩招,保证手到擒来:

第一种方式:

        观看spring启动日志,

会有这么一行数据

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启动大多数情况下会默认输出所有控制器的映射信息,包括对应的handler,上面的信息告诉我们,这个/health对应的控制器为HealthMvcEndpoint,我们一会再去分析这个类。接下来看第二种方式。

第二种方式:

        debug源码,这个需要对springmvc相关源码比较熟悉的人适用,大家都知道,我们spring有一个核心servlet就是dispatcherservlet,所有的映射控制器处理,都要经过他转发,因此我们直接去到这个类。

在第940行打上断点,然后使用postman或者浏览器发起请求,就会自动跳转到这个断点上,至于如何定位到doDispatcher以及这个mappedHandler,详细过程需要结合springmvc部份的源码以及梳理,这里就暂不深究了,有兴趣的小伙伴们可以私信或者留言告诉我,我抽时间可以安排一下,你懂得!

        好了,俩种方式大致上已经告诉小伙伴们了,这里再说一点,貌似还有一种方式可以使用接口请求输出所有映射的详细信息,这里也不深究了,这俩种方式不仅仅限于本篇文章的用途,以后你们如果也想去寻找某个映射或者控制器,都可以使用这俩种方式。还是比较实用的。

        接下来,我们重点去分析这个HealthMvcEndpoint类。

我们可以看到invoke方法上使用的是AcutorGetMapping,本质上还是属于RequestMapping的一种,因此这个invoke方法肯定是我们需要过一遍的,首先我们先简单分析 

!getDelegate().isEnabled())

private static final String ENDPOINTS_ENABLED_PROPERTY = "endpoints.enabled";

我们并没有配置上面这个变量,他也没有默认的属性值在容器中,因此这个值肯定不存在的,所以在第73行判断条件为false,默认返回return true。所以不会走if里面的语句。

进入getHealth方法中,发现会调用getCurrentHealth,在这个方法中,sprng做了一个缓存机制,把得到的cachedHealth缓存了起来,并且有一个时间过期机制。第一次调用的时候这个cachedHealth肯定是null,因此我们需要分析getDelegate().invoke()方法,getDeleagte()方法属于超类中的方法,会返回一个泛型的delegate对象,我们简单看下超类的结构

有很多子类都实现了这个超类,分别提供不同的功能,我们这次研究的HealthMvcEndpoint就属于其中之一,并且这个超类中还有一个泛型变量delegate,这个也是在类实例化阶段需要填充的,这个我们稍后在分析,这个泛型变量具体类型基于子类的实现方式,我们看到子类HealthMvcEndpoint中明确了泛型为HealthEndpoint类型,接下来接续分析delegate.invoke()方法

可以看到healthIndicator是在构造函数中进行初始化的,老样子,继续走主流程,稍后在分析这块如何初始化的,这个类名是CompositeHealthIndicators,看类名就是综合健康检查的意思,一目了然。

继续分析health()方法

我们可以看到这个indicators是由一个map组成,value存的是所有spring集成的第三方中间件的健康检查的控件类,有redis,db,mail,config等等,然后for循环这个indicators,在healths中会放入各个中间件的一些健康信息,最后调用healthAggregator进行聚合处理。我们先简单看一个redis的健康检查控件

org.springframework.boot.actuate.health.RedisHealthIndicator

我们进入到这个类中

这个类比较简单,就一个核心方法doHealthCheck,了解spring源码的人都应该清楚,像这种方法名一看就是被调用的,而且绝大多数是在本类,但我们这个本类没有其他方法,而且这个方法是重写的,因此我们去超类中看看结构。

我们看到了超类中定义了一个抽象方法,并且在health()方法中进行调用,这个health方法就是上面CompositeHealthIndicators类中进行重写的health方法中进行for循环调用的地方。当调用到RedisHealthIndicator的health方法的时候,会默认调用超类的health方法,然后通过重写的方法调用到子类的doHealthCheck,这是典型的模板方法设计模式。其实设计模式也挺实用的,虽然我也不是很清楚每种场景的设计模式。

我们重点分析doHealthCheck,首先方法通过redisConnectionFactory获取一个redisconnection,如果获取到了则说明redis状态一且正常,且可以获取redis一些版本以及其他信息,这里大家看到connection做了类型判断,判断是集群模式还是单机模式,不同的类型走不同的处理逻辑,我们这边不研究这个了。这里需要注意的一个点是,我们并没看到down的处理逻辑,而且我们应该了解如果connection获取不到,肯定会报socket连接异常,但是这里异常虽然try了,并没有catch,因此异常肯定会往上层代码抛出,我们去看超类的处理

try {

doHealthCheck(builder);

}

catch (Exception ex) {

this.logger.warn("Health check failed", ex);

builder.down(ex);

}

一且都很清晰明了了,这里不但会打印日志,还会将这个中间件标记为down状态。

redis的其实并没有什么难度,当我准备分析db的时候,也觉得大致一样,但是真正分析的时候还是有点不同的,还是比较有趣的,我们带着db为啥是unknown的状态的疑问,接下来我们重点分析db的健康检查原理。

分析前我们先看上面一张图,我们会发现这个linkedhashmap中的db的value明显和别的不一样,上面我们已经分析过了redis的健康检查控件就是RedisHealthIndicator,但是这个db的却是CompositeHealthIndicator,再看下面这张图

org.springframework.boot.actuate.health.DataSourceHealthIndicator

明明是有db的专属控件的,为什么这里health方法中却不是呢,通过查看DataSourceHealthIndicator在哪被初始化,如下图所示进行研究

我们看上面这个很重要的类,org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration

顾名思义,这个类是基本所有spring集成中间件健康检查的自动装配类。这时候有人会问我,你怎么找到这个类的呢,总不能一个个去看吧,其实很简单,我们把鼠标放在DataSourceHealthIndicator类名上,使用idea的find usages(alt+f7)就可以知道这个类在哪里被调用、使用、初始化等等。

我们还是继续分析这个HealthIndicatorAutoConfiguration,它由众多静态内部类组成,基本每个静态内部类都是一个中间件的健康检查装配类,我们看上面关于db的装配类,在重点分析第222行@bean注解的方法之前,我们先看下第193行这个db静态内部类的构造方法,因为这个有个属性的初始化跟后面的分析有关,构造方法中,它初始化了俩个属性如下:

this.dataSources = filterDataSources(dataSources.getIfAvailable());

this.metadataProviders = metadataProviders.getIfAvailable();

我们主要看第一个,dataSources的初始化,调用了filterDataSources方法,传参是

dataSources.getIfAvailable(),类型是ObjectProvider<Map<String, DataSource>> dataSources

关于这种ObjectProvider类型的参数,其实是spring独特的一个注入方式,我们这里不深究了,以后有机会在讲,这个变量的主要作用就是spring容器初始化这个构造函数的时候,会把容器中所有dataSource的对象注入到这个容器中,key就是dataSource的beanName,value就是dataSource。

因此我们看filterDataSources方法,正常程序中一般都会有dataSource的对象,所以第202行不会成立,继续往下看,第206,207行判断这个map的value是否是AbstractRoutingDataSource的子类,关于

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

有的小伙伴可能不太了解,这个是spring多数据源的一个超类。如果你的项目中,需要使用多数据源,那么这个类你必然会用到,而我这个程序中也正好是使用了多数据源,因此当程序走到这一步的时候,不会走if分支,直接返回一个空的数据源map给类的私有属性。

说完了上面类的构造方法,我们继续看被@bean注解的dbHealthIndicator方法,接触过springboot的都知道,所有含有@bean的方法,只要被spring容器扫描到,那么在容器初始化阶段的时候,就会去解析这个bean注入到容器中,这个方法会调用createHealthIndicator方法,这个方法是来自于超类

这个类一共就俩个方法,并且是重载的,一个接受map参数返回HealthIndicator,一个接受泛型变量返回泛型对象。

第二个方法比较复杂,因为在dbHealthIndicator中调用的createHealthIndicator传的参数是一个map,所以我们这里只需看第一个,首先是if条件,上文我们也知道了,多数据源的dataSource是不会放入到map中的,因此在这里不会走if,直接new一个CompositeHealthIndicator,参数是HealthAggregator,且下面的for循环也没有任何意义,直接会跳过,返回这个composite,这样我们db的健康检查控件就初始化完毕了,不太像我们之前redis分析的那样,简单明了,这里貌似弯弯绕绕比较多,对于这个CompositeHealthIndicator,我们貌似有点眼熟,上文貌似是在HealthEndpoint中有这个,为啥db也是这个呢

我们再来看下这个类,其实这里spring相当于刷了一个小聪明,它复用了这个类,在无法正常初始化中间件的控件的时候,就像上面db的datasource为空的时候,他就默认初始化一个CompositeHealthIndicator,我们看这个health方法,和之前分析这个health不同,我们这里indicators为空,所以不会走for循环,所以我们看下第70行方法,传入的是一个空的healths,

这里aggregate方法里面处理了这个healths变量,37行因为空的map不会走,所以他会调用抽象方法,aggregateStatus方法,入参是一个空的集合

这个抽象类默认就一个子类,这个子类重写了aggregateStatus,首先因为入参是空集合,所以第一个for循环不会走,到if语句的时候,因为这个方法内部变量依旧是空集合,所以条件成立,返回状态为unknown状态。所以这就解释了为啥我程序中db状态为啥是unknown状态。到此为止我们梳理下整个的调用链路:

首先是HealthMvcEndpoint调用了invoke方法,而invoke最终调用了HealthEndpoint中的invoke方法,然后在invoke方法中调用healthIndicator变量进行for循环所有中间件健康检查控件类的health方法,如果能正常初始化的控件就会正常显示状态up或者down(如redis),如果不能正常初始化的则会默认赋值一个控件,如多数据源情况下的db控件,则绝大多数都会返回unknown状态。

文章到这里,开始的问题已经分析出了因果,我们在分析问题的时候还留下了几个其他疑问,分别如下:

1、ManagementServerProperties类的配置究竟有何作用?(为什么配置了就会显示更多信息,不配就只显示一个状态)

2、HealthMvcEndpoint类中delegate是如何初始化的?

3、HealthEndpoint中的healthIndicator是如何初始化的?

我们一个个来分析,首先第一个问题:

我们看到Health类中包含了status以及details,status就是状态,而details是各个中间件的详细信息,就像一开始文章所示的请求返回信息一样,包含redis的版本信息,磁盘信息等。上面getHealth方法中有个exposeHealthDetails方法,如果这个方法返回true则是返回详细信息,如果false,则看下面的构造只返回status。因此我们需要看下这个方法,

这个方法首先先判断this.secure,如果是false直接返回true,如果是true则走下面。/health要想返回详细信息这里一定得是false,我们看下这个secure是在那里初始化得。

首先是在构造函数中被初始化的,我们接下来看构造函数被谁调用

这里注意看构造函数的参数其中是由一个managementServerProperties.getSecurity().isEnabled()传入的,我们点进去看下

原来我们在yml中配置的属性,最终都会注入到这个类中,并且在HealthMvcEndpoint类初始化的时候一并传入过去,然后处理相关逻辑的时候会使用到这些属性。这就解释了我们第一个问题。

第二个问题,HealthMvcEndpoint类中delegate是如何初始化的?

我们看到healthMvcEndpoint构造函数中传入这个delegate,经过排查,发现在@bean方法中进行初始化的,

再通过上面这张图我们很清晰就能明白,这个delegate本身也会作为一个bean放入到容器中,然后作为构造参数注入到别的类中进行调用。因此第二个问题就分析完了。

第三个问题,HealthEndpoint中的healthIndicator是如何初始化的?

我们看上面图示,第56行new了一个默认的CompositeHealthIndicator,然后经过一个for循环处理,填充了一下healthIndicator的内部变量indicators,因此我们的重点是这个构造函数的healthIndicators变量。

可以看到这个构造函数参数是由this的一个变量传递的,经过上面的分析,这里不是空,所以肯定是有值的,

这个this.healthIndicators是由ObjectProvider类调用getIfAvailable方法得到来的,这个方法我们上面分析过,其实这是spring常用的一种注入方式,结果就是能够到所有的HealIndicator的bean的map对象,key为beanname,value为bean,这里也不深究了,如果有想了解这块知识的可以私信留言告诉我,我到时候整理讲解一下。

因此关于前面遗留的三个问题我也间接的回答完了,你们也可以自己去尝试分析一下,看一下是否如上所说。这一期的案例分析就说到这里了。

这篇文章还是让我花了不少时间去书写和思考的,如果有喜欢的小伙伴一定要点击收藏点赞哦,你们的赞扬是我继续的动力,哈哈,客套话了。想关于这篇文章讨论的可以在下方留言,我看到了随时会回复的。

写在文后:

关于下篇文章,我准备写一篇关于springboot初始化相关的文章,主要还是针对于问题而去分析的,我这里可以先抛出问题,留给各位去思考:

如果spring中有一个bean,我们自己也去定义了,为什么springboot会默认先初始化我们的类?

关于这个问题的讨论也可放在下方去留言。。。。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容