SpringBoot源码研究之Actuator

SpringBoot的大名这里就不多废话了,作为用来简化新 Spring 应用的初始搭建以及开发过程,使得Spring焕发第二春的开发框架。其所遵循的CoC原则让Spring诟病良久的繁杂配置得到极大改善。而本文主要关注的是其提供的开箱即食的Actuator功能。

1. 概述

在平时使用Spring的过程中,我们偶尔会遇到如下情况:

  1. 这个请求的前端映射地址明明配置了,怎么就是报404?
  2. 这个配置信息我明明写到了配置文件里面,怎么就是读取不到?
  3. 明明指示了扫描路径,怎么在容器里就是找不到相应的实例?

凡此种种情况发生的时候,都会让人异常烦躁,尤其是如果类似情况发生在线上的时候,真是有一种叫天天不应叫地地不灵的绝望感。

而在SpringBoot中,就专门针对以上痛点提供了一个专门的Starter来解决。而且借助SpringBoot的AutoConfig等特性,达到了开箱即食的效果,如果没有额外的需求,只需要在Maven中引入相应的Starter依赖即可获得这些功能。

最后,笔者阅读的SpringBoot版本为1.5.7.RELEASE,看官请注意甄别。

2. 配置

本文主要关注于底层实现细节,因此这里只给出简单的配置,缩小关注范围,聚焦兴趣点。更详尽的配置可以参见底部给出的链接。


<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-actuatorartifactId>
dependency>

到此位置,默认的配置工作就算是结束了。启动项目之后直接访问 127.0.0.1:8080/beans 即可得到JSON格式的,当前Spring容器下包含的所有的bean的相关信息。

接下来就让我们看看SpringBoot是如何实现这一功能的。

3 底层细节

首先明确的是,这些信息其实在没有actuator之前是已经存在了的,actuator只是将其以一种规范,有序的方式暴露给了外界,所以我们的关注点也就是这一块。

3.1 EndpointAutoConfiguration

我们首先找到了EndpointAutoConfiguration类,该类将负责将我们BeansEndpoint(提供/beans请求的响应结果),EnvironmentEndpoint(提供/env请求的响应结果)等实例注册到Spring容器中。

3.2 EndpointWebMvcManagementContextConfiguration

上面的EndpointAutoConfiguration只是负责将相应的逻辑处理Bean注册到了容器中,但针对响应SpringMVC的请求的这部分工作,还是在EndpointWebMvcManagementContextConfiguration类中完成的。

关于该类,我们主要关注其endpointHandlerMapping()方法:

// EndpointWebMvcManagementContextConfiguration.endpointHandlerMapping()
@Bean
@ConditionalOnMissingBean
public EndpointHandlerMapping endpointHandlerMapping() {
	// 将收集MvcEndpoint实现类的工作交给专门的MvcEndpoints类来完成, 下方将有专门的讲解
	// 而且需要注意的是mvcEndpoints()方法是一个定义Bean的方法, 所以其是参与Spring Bean的声明周期的.
	Set<MvcEndpoint> endpoints = mvcEndpoints().getEndpoints();
	// 跨域问题, 默认允许
	CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties);
	EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints,
			corsConfiguration);
	//  我们在属性文件中配置的 management.context-path=/monitor 正是在这里起作用
	mapping.setPrefix(this.managementServerProperties.getContextPath());
	// 权限相关
	MvcEndpointSecurityInterceptor securityInterceptor = new MvcEndpointSecurityInterceptor(
			this.managementServerProperties.getSecurity().isEnabled(),
			this.managementServerProperties.getSecurity().getRoles());
	mapping.setSecurityInterceptor(securityInterceptor);
	// 预留的扩展, 默认为空
	for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) {
		customizer.customize(mapping);
	}
	return mapping;
}
3.3 MvcEndpoints

对于MvcEndpoints类,我们发现其实现了非常出名的Spring接口——InitializingBean

// MvcEndpoints.afterPropertiesSet()
@Override
public void afterPropertiesSet() throws Exception {
	// 从Spring容器中取出MvcEndpoint实现类, 此过程中将触发相应的实例和装配依赖等操作, 默认的实现类如下: 
	// [org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint@25d0b918, org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint@d3cce46, org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint@4b09d1c3, org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint@76ffc17c, org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint@40538370, org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint@699d96bc, org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint@5d08976a]
	Collection<MvcEndpoint> existing = BeanFactoryUtils
			.beansOfTypeIncludingAncestors(this.applicationContext, MvcEndpoint.class)
			.values();
	this.endpoints.addAll(existing);
	this.customTypes = findEndpointClasses(existing);
	// 从Spring容器中取出Endpoint实现类, 默认实现如下(其中就有我们上面提到的BeansEndpoint和EnvironmentEndpoint):
	// [org.springframework.boot.actuate.endpoint.RequestMappingEndpoint@760a2b6e, org.springframework.boot.actuate.endpoint.EnvironmentEndpoint@63c84d31, org.springframework.boot.actuate.endpoint.HealthEndpoint@37cf91d8, org.springframework.boot.actuate.endpoint.BeansEndpoint@14201a90, org.springframework.boot.actuate.endpoint.InfoEndpoint@1308ef19, org.springframework.boot.actuate.endpoint.LoggersEndpoint@76134b9b, org.springframework.boot.actuate.endpoint.MetricsEndpoint@48af5f38, org.springframework.boot.actuate.endpoint.TraceEndpoint@5b1ff8cd, org.springframework.boot.actuate.endpoint.DumpEndpoint@6cee903a, org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint@1f3c5308, org.springframework.boot.actuate.endpoint.ShutdownEndpoint@66b31d46, org.springframework.boot.actuate.endpoint.ConfigurationPropertiesReportEndpoint@62de73eb]
	@SuppressWarnings("rawtypes")
	Collection<Endpoint> delegates = BeanFactoryUtils
			.beansOfTypeIncludingAncestors(this.applicationContext, Endpoint.class)
			.values();
	for (Endpoint<?> endpoint : delegates) {
		if (isGenericEndpoint(endpoint.getClass()) && endpoint.isEnabled()) {
			// 将找到的Endpoint实现类, 挨个使用EndpointMvcAdapter进行适配, 主要目的是为了将`Endpoint`适配为`MvcEndpoint`,以响应WEB环境下的请求。
			// EndpointMvcAdapter, 看名字就知道该类实现的是设计模式中的适配器模式.
			EndpointMvcAdapter adapter = new EndpointMvcAdapter(endpoint);
			String path = determinePath(endpoint,
					this.applicationContext.getEnvironment());
			if (path != null) {
				adapter.setPath(path);
			}
			this.endpoints.add(adapter);
		}
	}
}
3.4 EndpointHandlerMapping

对于上面EndpointWebMvcManagementContextConfiguration类中,通过方法endpointHandlerMapping()注册到Spring容器中的实例为EndpointHandlerMapping类型,关于该类型,观察其继承链:
SpringBoot源码研究之Actuator_第1张图片
我们注意到EndpointHandlerMapping类中代码量极少,绝大部分逻辑都位于其基类AbstractEndpointHandlerMapping中(该类也是直接继承自spring-webmvc.jar中的RequestMappingHandlerMapping),因此我们将关注点转义到该类上来:

  • 首先,对于SpringMVC源码有所了解的读者应该非常熟悉上图中的大部分类型,对于注册到Spring容器中的EndpointHandlerMapping实例,将在SpringMVC的核心类DispatcherServletinitHandlerMappings()中被收集,正式开始准备响应相应的前端请求。

  • 其次,我们发现AbstractEndpointHandlerMapping覆写了基类实现的afterPropertiesSet()方法(真实来源是非常熟悉的InitializingBean接口)

    // AbstractEndpointHandlerMapping.afterPropertiesSet()
    @Override
    public void afterPropertiesSet() {
    	// 先回调基类的实现
    	super.afterPropertiesSet();
    	// 加入自定义逻辑
    	if (!this.disabled) {
    		for (MvcEndpoint endpoint : this.endpoints) {
    			// 回调基类方法, 将我们之前注册的, 并适配过的Endpoint实例注册到SpringMVC的前端请求响应的映射表(AbstractHandlerMethodMapping.MappingRegistry类中的urlLookup字段)中
    			//上面提到的BeansEndpoint实例在这里的实际类型为EndpointMvcAdapter
    			detectHandlerMethods(endpoint);
    		}
    	}
    }
    

SpringBoot源码研究之Actuator_第2张图片
以上堆栈图中我们也可以看到,SpringBoot中为每个Endpoint配置的path参数,正是在这里被读取应用的。

纵观AbstractEndpointHandlerMapping.afterPropertiesSet()实现,代码量很少,而且绝大部分都是回调基类的方法,因此我们有必要看下实际的MvcEndpoint接口实现类EndpointMvcAdapter

3.5 EndpointMvcAdapter

EndpointMvcAdapter的适配作用就是将BeansEndpoint适配到MVC模式下, 也正是因为有了这个的封装, 才会无需额外的配置就能直接得到相应的请求响应。

// EndpointMvcAdapter.invoke()
// 该方法将在用户请求相应的地址时候被回调, 例如 127.0.0.1:8080/beans
// 本方法的覆写主要是为了和SpringMVC作兼容
@Override
// 我们请求时候的返回数据都是JSON格式的, 原因就在这个@ActuatorGetMapping注解
@ActuatorGetMapping
@ResponseBody
public Object invoke() {
	return super.invoke();
}
3.6 BeansEndpoint

讨论完了SpringBoot-Actuator为了实现系统状态监控功能的引入的一些关键类,接下来让我们回归Endpoint本身,这里我们就以上面一再被提到的BeansEndpoint为例,其他诸如响应/mappingsRequestMappingEndpoint等就由读者自行阅读了。

// 可以通过配置前缀 endpoints.beans.xx 来配置相关属性
@ConfigurationProperties(prefix = "endpoints.beans")
// 基类的泛型参数为invoke()方法的返回值类型
public class BeansEndpoint extends AbstractEndpoint<List<Object>>
		implements ApplicationContextAware {

	...	
		
	public BeansEndpoint() {
		// 这个字符就是URL访问地址
		// 也可以通过配置 endpoints.beans.id=fulizhe
		super("beans");
	}

	...
	
	// 前端请求到来时, 该方法将被回调
	@Override
	public List<Object> invoke() {
		return this.parser.parseList(this.liveBeansView.getSnapshotAsJson());
	}

	...
}

纵观以上代码,SpringBoot的AbstractEndpoint基类已经作了最大的努力,子类只需要关注自身逻辑即可。接下来就让我们尝试自定义一个Endpoint。

4. 自定义Endpoint

在SpringBoot下,如果遵循其最佳实践,自定义扩展一般都是非常方便的。我们定义如下的一个类:

@ConfigurationProperties(prefix = "endpoints.calc")
@Component
public class CalcEndpoint extends AbstractEndpoint<Object> {

	public RecalcEndpoint() {
		super("calc");
	}

	@Override
	public Object invoke() {
		return Collections.singletonMap("name", "fulizhe");
	}
}

我们只需要保证该类存放于SpringBoot的扫描路径下,剩下的工作将由SpringBoot来完成。测试地址为:http://localhost:8080/calc

本示例只是讲解了自定义Actuator下前后端如何交互,对于Spring容器的读取等操作读者可以参考SpringBoot内部源码的实现来做,这里就不重复了。

5. 总结

最后,我们尝试总结一下:

  1. EndpointAutoConfiguration类负责注册内置的Endpoint到容器中。
  2. EndpointWebMvcManagementContextConfiguration类则会注册EndpointHandlerMapping实例到Spring容器中,该EndpointHandlerMapping类型是通过继承自SpringMVC中的RequestMappingHandlerMapping类来参与到SpringMVC的请求响应中的。
  3. 并且该EndpointHandlerMapping实例在构建时候会读取上面注册到容器中的Endpoint实例以及MvcEndpoint实例,将其负责的请求和相应的响应逻辑注册到EndpointHandlerMapping实例自身的响应清单中, 这样就实现了完整的前端请求到后端响应的逻辑链。

6. Links

  1. 使用Spring Boot Actuator监控应用小结
  2. 一览Spring Bean 定义和映射全貌的神器

你可能感兴趣的:(SpringBoot)