SpringBoot的大名这里就不多废话了,作为用来简化新 Spring 应用的初始搭建以及开发过程,使得Spring焕发第二春的开发框架。其所遵循的CoC原则让Spring诟病良久的繁杂配置得到极大改善。而本文主要关注的是其提供的开箱即食的Actuator功能。
在平时使用Spring的过程中,我们偶尔会遇到如下情况:
凡此种种情况发生的时候,都会让人异常烦躁,尤其是如果类似情况发生在线上的时候,真是有一种叫天天不应叫地地不灵的绝望感。
而在SpringBoot中,就专门针对以上痛点提供了一个专门的Starter来解决。而且借助SpringBoot的AutoConfig等特性,达到了开箱即食的效果,如果没有额外的需求,只需要在Maven中引入相应的Starter依赖即可获得这些功能。
最后,笔者阅读的SpringBoot版本为1.5.7.RELEASE,看官请注意甄别。
本文主要关注于底层实现细节,因此这里只给出简单的配置,缩小关注范围,聚焦兴趣点。更详尽的配置可以参见底部给出的链接。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
到此位置,默认的配置工作就算是结束了。启动项目之后直接访问 127.0.0.1:8080/beans
即可得到JSON格式的,当前Spring容器下包含的所有的bean的相关信息。
接下来就让我们看看SpringBoot是如何实现这一功能的。
首先明确的是,这些信息其实在没有actuator之前是已经存在了的,actuator只是将其以一种规范,有序的方式暴露给了外界,所以我们的关注点也就是这一块。
EndpointAutoConfiguration
类我们首先找到了EndpointAutoConfiguration
类,该类将负责将我们BeansEndpoint
(提供/beans
请求的响应结果),EnvironmentEndpoint
(提供/env
请求的响应结果)等实例注册到Spring容器中。
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;
}
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);
}
}
}
EndpointHandlerMapping
类对于上面EndpointWebMvcManagementContextConfiguration
类中,通过方法endpointHandlerMapping()
注册到Spring容器中的实例为EndpointHandlerMapping
类型,关于该类型,观察其继承链:
我们注意到EndpointHandlerMapping
类中代码量极少,绝大部分逻辑都位于其基类AbstractEndpointHandlerMapping
中(该类也是直接继承自spring-webmvc.jar中的RequestMappingHandlerMapping
),因此我们将关注点转义到该类上来:
首先,对于SpringMVC源码有所了解的读者应该非常熟悉上图中的大部分类型,对于注册到Spring容器中的EndpointHandlerMapping
实例,将在SpringMVC的核心类DispatcherServlet
的initHandlerMappings()
中被收集,正式开始准备响应相应的前端请求。
其次,我们发现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中为每个Endpoint配置的path
参数,正是在这里被读取应用的。
纵观AbstractEndpointHandlerMapping.afterPropertiesSet()
实现,代码量很少,而且绝大部分都是回调基类的方法,因此我们有必要看下实际的MvcEndpoint
接口实现类EndpointMvcAdapter
。
EndpointMvcAdapter
EndpointMvcAdapter
的适配作用就是将BeansEndpoint适配到MVC模式下, 也正是因为有了这个的封装, 才会无需额外的配置就能直接得到相应的请求响应。
// EndpointMvcAdapter.invoke()
// 该方法将在用户请求相应的地址时候被回调, 例如 127.0.0.1:8080/beans
// 本方法的覆写主要是为了和SpringMVC作兼容
@Override
// 我们请求时候的返回数据都是JSON格式的, 原因就在这个@ActuatorGetMapping注解
@ActuatorGetMapping
@ResponseBody
public Object invoke() {
return super.invoke();
}
BeansEndpoint
类讨论完了SpringBoot-Actuator为了实现系统状态监控功能的引入的一些关键类,接下来让我们回归Endpoint本身,这里我们就以上面一再被提到的BeansEndpoint
为例,其他诸如响应/mappings
的RequestMappingEndpoint
等就由读者自行阅读了。
// 可以通过配置前缀 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。
在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内部源码的实现来做,这里就不重复了。
最后,我们尝试总结一下:
EndpointAutoConfiguration
类负责注册内置的Endpoint到容器中。EndpointWebMvcManagementContextConfiguration
类则会注册EndpointHandlerMapping
实例到Spring容器中,该EndpointHandlerMapping
类型是通过继承自SpringMVC中的RequestMappingHandlerMapping
类来参与到SpringMVC的请求响应中的。EndpointHandlerMapping
实例在构建时候会读取上面注册到容器中的Endpoint实例以及MvcEndpoint
实例,将其负责的请求和相应的响应逻辑注册到EndpointHandlerMapping
实例自身的响应清单中, 这样就实现了完整的前端请求到后端响应的逻辑链。