一、前言
本文是笔者阅读Spring源码的记录文章,由于本人技术水平有限,在文章中难免出现错误,如有发现,感谢各位指正。在阅读过程中也创建了一些衍生文章,衍生文章的意义是因为自己在看源码的过程中,部分知识点并不了解或者对某些知识点产生了兴趣,所以为了更好的阅读源码,所以开设了衍生篇的文章来更好的对这些知识点进行进一步的学习。
全集目录:Spring源码分析:全集整理
在 Springboot 中, 端点可以通过 JMX 方式使用, 也可以使用 Http 方式使用,本文着重介绍与 Http 方式的使用和实现。
关于Actuator 更详细的介绍和基础使用可详参: Spring boot——Actuator 详解
关于 JMX 的详细介绍可详参 : JMX 入门(一)基础操作
Spring Boot Actuator 的关键特性是在应用程序里提供众多 Web 接口,通过它们了解应用程序运行时的内部状况。Actuator 提供了 13 个接口,可以分为三大类:配置接口、度量接口和其它接口,具体如下表所示。
HTTP 方法 | 路径 | 描述 |
---|---|---|
GET | /autoconfig | 提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过 |
GET | /configprops | 描述配置属性(包含默认值)如何注入Bean |
GET | /beans | 描述应用程序上下文里全部的Bean,以及它们的关系 |
GET | /dump | 获取线程活动的快照 |
GET | /env | 获取全部环境属性 |
GET | /env/{name} | 根据名称获取特定的环境属性值 |
GET | /health | 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供 |
GET | /info | 获取应用程序的定制信息,这些信息由info打头的属性提供 |
GET | /mappings | 描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系 |
GET | /metrics | 报告各种应用程序度量信息,比如内存用量和HTTP请求计数 |
GET | /metrics/{name} | 报告指定名称的应用程序度量值 |
POST | /shutdown | 关闭应用程序,要求endpoints.shutdown.enabled设置为true |
GET | /trace | 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等) |
二、使用介绍
1. 简单使用
actuator 的使用需要引入相应的依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
需要注意 ,默认情况下 jmx是所有端点都暴露了, 而 http 方式只有 info 和 health 能用,我们可以通过如下配置来选择是否暴露其他端点
属性 | 意义 | 默认值 |
---|---|---|
management.endpoints.jmx.exposure.exclude | 暴露排除某些端点 | |
management.endpoints.jmx.exposure.include | 暴露某些端点 | * |
management.endpoints.web.exposure.exclude | 暴露排除某些端点 | |
management.endpoints.web.exposure.include | 暴露某些端点 | info, health |
如:在 Springboot 中我们可以通过如下配置开发所有端点或指定端点:
management:
endpoints:
web:
exposure:
# 指定开放的端点路径, * 代表全部
include: "*"
endpoint:
beans:
# 是否启用
enabled: true
当 服务启动后,我们可以通过 http://{ip}:{port}/{context-path}/actuator
来获取 actuator 的接口。(默认情况下,端点的访问根路径是 actuator,我们可以通过 management.endpoints.base-path
配置来配置访问路径)。
如下,我们调用 beans 接口来获取容器中的Bean 信息:
Springboot 提供了很多 Endpoint ,如有需要可以参考 : Spring boot——Actuator 详解,本文不再叙述。
2 基础注解
下面我们来介绍一下 Springboot 提供的一些注解 。
2.1.1 @Endpoint
@Endpoint
标注当前类作为一个端点类来处理。@Endpoint
会通过 Web 和 Jmx 两种方式暴露端点,如果我们想单独暴露 Web 场景或者 Jmx 场景,可以使用其子注解 @WebEndpoint
和 @JmxEndpoint
来进行单独暴露。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Endpoint {
// 端点 id,作为访问路径
String id() default "";
// 是否启用,默认启用
boolean enableByDefault() default true;
}
下面我们以 @WebEndpoint
注解为例,我们可以看到相较于 @Endpoint 来说 @WebEndpoint多了一个 @FilteredEndpoint 注解, @FilteredEndpoint 作为 端点过滤器注解,可在@Endpoint上使用以实现隐式过滤的注释,通常用作技术特定端点注释的元注释。
简单来说 :当前端点是否会暴露其中一个条件就是满足 @FilteredEndpoint 指定的过滤器的过滤条件。WebEndpointFilter 的作用则是校验当前端点发现器是否是 WebEndpointDiscoverer,如果是则当前端点可以考虑暴露(还有其他校验条件),否则当前端点不进行暴露,从而满足了 `@WebEndpoint 注解暴露在Web 环境下的功能。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 继承 @Endpoint 注解
@Endpoint
// 指定 WebEndpointFilter过滤器
@FilteredEndpoint(WebEndpointFilter.class)
public @interface WebEndpoint {
// 端点 id,作为访问路径
@AliasFor(annotation = Endpoint.class)
String id();
// 是否启用,默认启用
@AliasFor(annotation = Endpoint.class)
boolean enableByDefault() default true;
}
2.1.2 @XxxOperation
@XxxOperation 指的是如下三个注解,该注解适用于方法上,标注方法的功能和访问方式,如下:
@ReadOperation
:标识该方法为读操作,使用 GET 方式访问@WriteOperation
:标识该方法为写操作,使用 POST 方式访问@DeleteOperation
:标识该方法为删除操作,使用 DELETE 方式访问
额外的 还存在 @Selector
注解,用于标识方法入参,如下方法:
// 我们可以通过使用 delete 方式调用 http://localhost:8080/actuator/demo/{i} 访问该接口
@DeleteOperation
public Integer delete(@Selector Integer i) {
return num;
}
2.1.2 @EndpointExtension
@EndpointExtension 可以作为已有端点的扩展,允许将额外的技术特定operations添加到现有端点。一个端点只能存在一个扩展,并且当端点扩展类和原始端点中都存在同一个方法时,端点扩展里的方法会覆盖原始端点的方法调用。同样的 @EndpointExtension 也存在两个子注解 @EndpointWebExtension 和 @EndpointJmxExtension 分别应用于 Web 环境和 Jmx 环境的扩展,这里不再赘述。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EndpointExtension {
// 端点过滤器,指定过滤哪些方法
Class<? extends EndpointFilter<?>> filter();
// 作用于哪个端点
Class<?> endpoint() default Void.class;
}
3. 自定义 Endpoint
介绍完上面的注解,我们便可以通过 Springboot 提供的 @Endpoint 注解来完成自定义 Endpoint 的功能,自定义的过程也很简单:
- 使用
@Endpoint
注解标注当前类作为一个端点注册,指定id为访问路径 - 通过
@ReadOperation
、@WriteOperation
、@DeleteOperation
注解来标注暴露的方法,通过@Selector
注解来指定参数。
如下定义一个简单的Endpoint :
@Component
@Endpoint(id = "demo")
public class DemoEndpoint {
private int num = 0;
// GET请求 http://localhost:8080/actuator/demo
@ReadOperation
public int read() {
return num;
}
// POST 请求 http://localhost:8080/actuator/demo/99
@WriteOperation
public int write(@Selector int i) {
num += i;
return num;
}
// DELETE请求 http://localhost:8080/actuator/demo/99
@DeleteOperation
public int delete(@Selector int i) {
num -= 1;
return num;
}
}
上面我们可以看到自定义一个 Endpoint,需要使用 @Endpoint(id = "demo")
来指定一个类,其中id为这个 Endpoint 的id,可以简单认为即是暴露后的http访问路径。在需要暴露的方法上还需要加上如下三个注解:
- @ReadOperation :标识该方法为读操作,使用 GET 方式访问
- @WriteOperation :标识该方法为写操作,使用 POST 方式访问
- @DeleteOperation :标识该方法为删除操作,使用 DELETE 方式访问
除此之外我们可以定义EndpointExtension 来扩展操作,如下:
@Component
@EndpointWebExtension(endpoint = DemoEndpoint.class)
// 可以指定过滤器,即待扩展的端点需要满足 类型是 DemoEndpoint 并且满足 DemoEndpointFilter 的过滤条件。
//@EndpointExtension(filter = DemoEndpointFilter.class, endpoint = DemoEndpoint.class)
public class DemoEndpointExtension {
private int num = 0;
@ReadOperation
public int read() {
System.out.println("DemoEndpointExtension.read");
return num;
}
@WriteOperation
public int write(@Selector int i) {
System.out.println("DemoEndpointExtension.write");
num += i;
return num;
}
@DeleteOperation
public int delete(@Selector int i) {
System.out.println("DemoEndpointExtension.delete");
num -= 1;
return num;
}
}
上面我们介绍了 Actuator 的基础功能和使用,下面我们来看看在 Springboot 中 Actuator 是如何实现的。
三、Endpoint 自动引入
我们在使用 Actuator 功能时需要先引入 spring-boot-starter-actuator
依赖包,其中会引入 spring-boot-actuator-autoconfigure
,而 spring-boot-actuator-autoconfigure
的利用Springboot 自动装配的特性引在 spring.factories
中引入了很多类完成Actuator 功能的启用 。
spring.factories
引入的类很多,我们这里不在贴出所有的类,下面我们看其中的几个关键类
- EndpointAutoConfiguration :Actuator 的 起始配置类。
- WebEndpointAutoConfiguration :Web 环境下的关键配置类,引入了很多核心类
- EndpointDiscoverer :抽象类,是所有端点发现类的父类,完成了端点发现的功能,核心功能实现类。
1. EndpointAutoConfiguration
EndpointAutoConfiguration 看名字也可以知道是一个配置类,具体如下:
@Configuration(proxyBeanMethods = false)
public class EndpointAutoConfiguration {
// 参数类型映射转换,会对返回的参数类型做转换,
@Bean
@ConditionalOnMissingBean
public ParameterValueMapper endpointOperationParameterMapper(
@EndpointConverter ObjectProvider<Converter<?, ?>> converters,
@EndpointConverter ObjectProvider<GenericConverter> genericConverters) {
ConversionService conversionService = createConversionService(
converters.orderedStream().collect(Collectors.toList()),
genericConverters.orderedStream().collect(Collectors.toList()));
return new ConversionServiceParameterValueMapper(conversionService);
}
private ConversionService createConversionService(List<Converter<?, ?>> converters,
List<GenericConverter> genericConverters) {
if (genericConverters.isEmpty() && converters.isEmpty()) {
return ApplicationConversionService.getSharedInstance();
}
ApplicationConversionService conversionService = new ApplicationConversionService();
converters.forEach(conversionService::addConverter);
genericConverters.forEach(conversionService::addConverter);
return conversionService;
}
// 提供结果缓存的支持
@Bean
@ConditionalOnMissingBean
public CachingOperationInvokerAdvisor endpointCachingOperationInvokerAdvisor(Environment environment) {
return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment));
}
}
EndpointAutoConfiguration 向 Spring 容器中注入了两个类 :
- ParameterValueMapper :参数类型映射。会将参数类型转换为所需的类型。比如我们请求的参数 是 String 类型的 “123”, 但是 Endpoint 的入参确实 Integer 类型,这时就会通过该类来对参数进行类型转换,从 String转换 Integer。
- CachingOperationInvokerAdvisor :看名字可以知道,这是一个缓存顾问,用于缓存 Operation。
这两个类并不涉及主流程,所以我们这里不再展开分析。
2. WebEndpointAutoConfiguration
WebEndpointAutoConfiguration 也是一个配置类,其中引入了 实现Actuator 功能的关键类,这里直接说明具体类的左右,不再贴出具体引入代码。
引入的Bean | 描述 |
---|---|
PathMapper | 配置参数映射。对应 management.endpoints.web.path-mapping 参数 。 |
EndpointMediaTypes | 默认情况下,由端点生成和使用的媒体类型 |
WebEndpointDiscoverer | web 端点发现者,发现被@Endpoint 注解修饰的类作为端点。下文详细介绍 |
ControllerEndpointDiscoverer | controller 端点发现者,发现被 @ControllerEndpoint 或 @RestControllerEndpoint 修饰的l类作为端点,下文详细介绍 |
PathMappedEndpoints | 保存了所有 Endpoint 信息 以及访问基础路径 |
IncludeExcludeEndpointFilter<ExposableWebEndpoint> | WEB 下的过滤器,默认只开启 /info 和 /health,可以通过配置控制哪些路径的端点开启或者关闭 |
IncludeExcludeEndpointFilter<ExposableControllerEndpoint> | controller 下的过滤器,可以通过配置控制哪些路径的端点开启或者关闭 |
ServletEndpointDiscoverer | servlet 上下文加载的 Servlet 端点发现者。 |
这里我们注意 WebEndpointAutoConfiguration 中注入了三个端点发现器 :WebEndpointDiscoverer 、ControllerEndpointDiscoverer 、ServletEndpointDiscoverer 。这三个类都是 EndpointDiscoverer 的子类,我们在下面的代码分析中会一一提到这些类,这里不做过多赘述。
下面我们来看看端点发现者的具体实现。
四、EndpointDiscoverer
EndpointDiscoverer 是一个抽象类,是所有端点发现器的父类,借由其提供的一些方法,可以发现容器中的端点。
Springboot 默认提供了下面四种实现器,均继承于 EndpointDiscoverer,各自具备不同的端点发现规则和处理方式。
- JmxEndpointDiscoverer : Jmx 端点发现器
- ServletEndpointDiscoverer : Servlet 环境下的的端点发现器
- WebEndpointDiscoverer :Web 的端点发现器。 存在子类 CloudFoundryWebEndpointDiscoverer
- ControllerEndpointDiscoverer :Controller 的端点发现器
对于每一个端点发现器,其内部都存在一个 endpoints集合用来保存符合自身发现条件端点。而这个集合是通过 调用EndpointDiscoverer#getEndpoints 方法来发现的,如下:
private volatile Collection<E> endpoints;
.....
@Override
public final Collection<E> getEndpoints() {
if (this.endpoints == null) {
// 发现所有 Endpoint 并保存到 endpoints 中
this.endpoints = discoverEndpoints();
}
return this.endpoints;
}
private Collection<E> discoverEndpoints() {
// 1. 获取所有 EndpointBean
Collection<EndpointBean> endpointBeans = createEndpointBeans();
// 2. 添加扩展 Bean
addExtensionBeans(endpointBeans);
// 3. 转换为指定类型的 Endpoint
return convertToEndpoints(endpointBeans);
}
EndpointDiscoverer#getEndpoints 方法作为功能核心方法,会发现所有端点:如果当前发现器端点集合没有初始化,则通过 discoverEndpoints()
来发现端点。
这里 EndpointDiscoverer#discoverEndpoints 的逻辑可以分为如下三个步骤 :
-
createEndpointBeans() :从容器中获取所有被 @Endpoint 注解修饰的bean,并封装为 EndpointBean 返回。EndpointBean 是 EndpointDiscoverer 的内部类,包含的下面几个属性:
private static class EndpointBean { // bean 的名称 private final String beanName; // bean 的类型 private final Class<?> beanType; // bean的供应商,通过此属性可以从容器中获取到bean 实例,是一种懒加载的方式 private final Supplier<Object> beanSupplier; // endpoint id ,即 @Endpoint 的id 属性 private final EndpointId id; // 是否启用,同理是 @Endpoint 的一个属性 private boolean enabledByDefault; // 当前类上 @FilteredEndpoint 注解指定的 filter private final Class<?> filter; // @EndpointExtension 注解指定的值 private Set<ExtensionBean> extensions = new LinkedHashSet<>(); }
-
addExtensionBeans(endpointBeans) :从 容器中获取所有被 @EndpointExtension注解修饰的bean,并封装成 ExtensionBean,随后判断是否可以作为当前 EndpointBean的扩展,如果可以则将 ExtensionBean 添加到 EndpointBean 中作为其扩展的一部分,相同的方法会被ExtensionBean 覆盖 。当端点方法被调用时,如果 ExtensionBean 存在该方法则直接调用 ExtensionBean的方法,如果不存在 则调用 Endpoint 的方法,同时 EndpointDiscoverer 可以自由新增方法也可以调用。ExtensionBean 也是 EndpointDiscoverer 内部类,其属性基本与 EndpointBean 相同。
private static class ExtensionBean { // bean 名称 private final String beanName; // bean 类型 private final Class<?> beanType; // bean 供应商,可以获取bean private final Supplier<Object> beanSupplier; // @EndpointExtension 注解上的 endpoint 指定的 Endpoint 的id private final EndpointId endpointId; // @EndpointExtension 注解上的 filter private final Class<?> filter;
-
convertToEndpoints(endpointBeans) : 将转换为指定类型的 Endpoint。对于不同的端点发现器,其 EndpointBean 类型并不相同,如JmxEndpointDiscoverer 对应 ExposableJmxEndpoint, ControllerEndpointDiscoverer 对应 ExposableControllerEndpoint、WebEndpointDiscoverer 对应 ExposableWebEndpoint 等。而在 EndpointDiscoverer 中统一生成 都是 EndpointDiscoverer#EndpointBean 。在这一步则是转换为各个 端点发现器 需要的泛化类型 E 。
下面我们来看这三步的具体实现:
1. EndpointDiscoverer#createEndpointBeans
EndpointDiscoverer#createEndpointBeans 的实现如下:
private Collection<EndpointBean> createEndpointBeans() {
Map<EndpointId, EndpointBean> byId = new LinkedHashMap<>();
// 从容器中获取被 @Endpoint 注解修饰的类的beanName
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
Endpoint.class);
for (String beanName : beanNames) {
if (!ScopedProxyUtils.isScopedTarget(beanName)) {
// 根据 beanName 创建 EndpointBean
EndpointBean endpointBean = createEndpointBean(beanName);
// 保存到 Map 中
EndpointBean previous = byId.putIfAbsent(endpointBean.getId(), endpointBean);
Assert.state(previous == null, () -> "Found two endpoints with the id '" + endpointBean.getId() + "': '"
+ endpointBean.getBeanName() + "' and '" + previous.getBeanName() + "'");
}
}
// 返回
return byId.values();
}
// 根据 beanName 创建 EndpointBean
private EndpointBean createEndpointBean(String beanName) {
// 获取 beanName 对应的 bean 的类型
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName, false));
// bean的供应商 :可以从容器中获取 bean
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
// 封装为 EndpointBean 返回
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
}
2. EndpointDiscoverer#addExtensionBeans
和 @Endpoint 相同的是, @EndpointExtension同样 存在子注解 @EndpointWebExtension 和 @EndpointJmxExtension,分别用于 Web环境和 Jmx 环境。下面我们来看 EndpointDiscoverer#addExtensionBeans 的具体实现 :
private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
Map<EndpointId, EndpointBean> byId = endpointBeans.stream()
.collect(Collectors.toMap(EndpointBean::getId, Function.identity()));
// 获取容器中所有被 EndpointExtension 注解修饰的类 的beanName
// EndpointExtension 有三个子注解 EndpointCloudFoundryExtension、EndpointJmxExtension、EndpointWebExtension
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
EndpointExtension.class);
for (String beanName : beanNames) {
// 创建扩展bean
ExtensionBean extensionBean = createExtensionBean(beanName);
EndpointBean endpointBean = byId.get(extensionBean.getEndpointId());
Assert.state(endpointBean != null, () -> ("Invalid extension '" + extensionBean.getBeanName()
+ "': no endpoint found with id '" + extensionBean.getEndpointId() + "'"));
addExtensionBean(endpointBean, extensionBean);
}
}
// 创建扩展bean
private ExtensionBean createExtensionBean(String beanName) {
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName));
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
}
// 添加扩展bean
private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) {
// 是否支持扩展 : extensionBean的过滤器是否匹配 && 是否是支持的扩展类型
if (isExtensionExposed(endpointBean, extensionBean)) {
Assert.state(isEndpointExposed(endpointBean) || isEndpointFiltered(endpointBean),
() -> "Endpoint bean '" + endpointBean.getBeanName() + "' cannot support the extension bean '"
+ extensionBean.getBeanName() + "'");
// 添加到 endpointBean 的扩展属性中
endpointBean.addExtension(extensionBean);
}
}
这里我们注意到,是否将 ExtensionBean 添加到 EndpointBean 中作为扩展的判断是通过 isExtensionExposed 方法完成,下面我们来看下其判断逻辑:
// 判断当前扩展是否适用于当前 endpointBean
private boolean isExtensionExposed(EndpointBean endpointBean, ExtensionBean extensionBean) {
// 判断是否适用于当前 endPointBean,需要满足下面两个条件
// 1. ExtensionBean 的 filter.match(endpointBean) = true 。而ExtensionBean#filter属性来源是 @EndpointExtension 注解指定的 EndpointFilter。
// 2. isExtensionTypeExposed 成立 ( EndpointDiscoverer 默认返回true,确定是否应该公开扩展 bean。子类可以重写此方法以提供额外的逻辑)
return isFilterMatch(extensionBean.getFilter(), endpointBean)
&& isExtensionTypeExposed(extensionBean.getBeanType());
}
这里我们从 ExtensionBean 的构造函数中来看看 ExtensionBean#filter 从何而来:同样 ExtensionBean 是 EndpointDiscoverer 的内部类,其构造函数如下,可以看到 ExtensionBean#filter 是 @EndpointExtension 注解指定的 EndpointFilter。
ExtensionBean(Environment environment, String beanName, Class<?> beanType, Supplier<Object> beanSupplier) {
this.beanName = beanName;
this.beanType = beanType;
this.beanSupplier = beanSupplier;
MergedAnnotation<EndpointExtension> extensionAnnotation = MergedAnnotations
.from(beanType, SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class);
Class<?> endpointType = extensionAnnotation.getClass("endpoint");
MergedAnnotation<Endpoint> endpointAnnotation = MergedAnnotations
.from(endpointType, SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
Assert.state(endpointAnnotation.isPresent(),
() -> "Extension " + endpointType.getName() + " does not specify an endpoint");
this.endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
this.filter = extensionAnnotation.getClass("filter");
}
3. EndpointDiscoverer#convertToEndpoints
EndpointDiscoverer#convertToEndpoints 的实现如下:
private Collection<E> convertToEndpoints(Collection<EndpointBean> endpointBeans) {
Set<E> endpoints = new LinkedHashSet<>();
for (EndpointBean endpointBean : endpointBeans) {
// 1. 是否是可暴露的端点
if (isEndpointExposed(endpointBean)) {
// 2. 进行类型转换
endpoints.add(convertToEndpoint(endpointBean));
}
}
// 不可修改set 返回
return Collections.unmodifiableSet(endpoints);
}
下面我们主要来看下面两个方法:
-
EndpointDiscoverer#isEndpointExposed : 判断当前端点是否应该暴露 :满足 Endpoint 上
@FilteredEndpoint
指定的Filter 规则 && 满足当前 EndpointDiscoverer 的 filter 规则 && 在当前 EndpointDiscoverer 中 Endpoint 是否应该公布。// 判断当前端点是否需要暴露 private boolean isEndpointExposed(EndpointBean endpointBean) { // 过滤器是否匹配当前 EndpointBean return isFilterMatch(endpointBean.getFilter(), endpointBean) && // EndpointDiscoverer#filters 匹配当前 endpointBean !isEndpointFiltered(endpointBean) // 确定是否应该公开端点 bean。子类可以重写此方法以提供额外的逻辑 && isEndpointTypeExposed(endpointBean.getBeanType()); }
我们来看具体的三个判断条件,可以得到isEndpointExposed 成立的条件是 :
EndpointBean 必须被 EndpointBean#filters 过滤通过 (判读当前端点发现器是否匹配) && EndpointBean 必须被 EndpointDiscoverer#filters 过滤通过 (满足暴露路径配置) && isEndpointTypeExposed(endpointBean.getBeanType()) 返回 true
具体分析如下:
-
EndpointDiscoverer#isFilterMatch(endpointBean.getFilter(), endpointBean) :这一步的目的是判断在当前端点发现器下是否应当暴露。如对于被@JmxEndpoint 修饰的类,如果当前端点发现器是 WebEndpointDiscoverer,则不应暴露,此时@JmxEndpoint 注解上的 @FilteredEndpoint 指定 过滤器 JmxEndpointFilter,JmxEndpointFilter就会判断当前端点发现器是否为 JmxEndpointDiscoverer ,不为则认为不应暴露,随即EndpointFilter#match 返回false,取消当前 Endpoint的暴露。
-
EndpointDiscoverer#isEndpointFiltered(endpointBean) :这一步的目的是确定当前端点发现器是否允许暴露当前Endpoint,我们可以通过配置文件中来指定暴露哪些端点或者排除哪些端点,满足配置条件的端点才可以暴露,其对应的 Filter 为 IncludeExcludeEndpointFilter 。需要注意的是,EndpointDiscoverer#filter 可能存在多个,需要全部满足才可以暴露。
-
EndpointDiscoverer#isEndpointTypeExposed(endpointBean.getBeanType()) :默认返回true,确定是否应该公开端点 bean。子类可以重写此方法以提供额外的逻辑。
如 ControllerEndpointDiscoverer#isEndpointTypeExposed 重写下:@Override protected boolean isEndpointTypeExposed(Class<?> beanType) { MergedAnnotations annotations = MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS); // 当前 endpoint 的 bean 必须被 @ControllerEndpoint 或 @RestControllerEndpoint 修饰 return annotations.isPresent(ControllerEndpoint.class) || annotations.isPresent(RestControllerEndpoint.class); }
-
-
EndpointDiscoverer#convertToEndpoint : EndpointBean 转换为指定的泛型 E 类型。每个端点发现器的对端点的处理都不相同,这里则是交由每个端点发现器来转换为自己需要的类型。其实现如下:
// 转换为指定的泛型 E 类型 private E convertToEndpoint(EndpointBean endpointBean) { MultiValueMap<OperationKey, O> indexed = new LinkedMultiValueMap<>(); EndpointId id = endpointBean.getId(); // 1. 创建 Operation addOperations(indexed, id, endpointBean.getBean(), false); // Extensions 校验,至多只能有一个 if (endpointBean.getExtensions().size() > 1) { ... 抛出异常 } // 2. 创建 扩展类的 Operation for (ExtensionBean extensionBean : endpointBean.getExtensions()) { // 这里会替换最新的 Operation。 addOperations(indexed, id, extensionBean.getBean(), true); } // Operation 重复校验 assertNoDuplicateOperations(endpointBean, indexed); // 获取 indexed.values() 中最后的一个operations List<O> operations = indexed.values().stream().map(this::getLast).filter(Objects::nonNull) .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); // 3. 创建 Endpoint,抽象类,由子类实现,基本都是简单封装,篇幅所限,不再赘述 return createEndpoint(endpointBean.getBean(), id, endpointBean.isEnabledByDefault(), operations); }
我们按照注释的顺序来说 :
- 创建 Operation : 这里会创建 Operation 实体类,即被 @ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法封装成 Operation 实体类。
- 创建 扩展类的 Operation :解析扩展类的同样创建扩展类的 被@ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法封装成 Operation 实体类,并替换端点的Operation。
- 创建 Endpoint :创建 Endpoint 实体类,简单封装。
下面我们具体来看其中关键的方法。
3.1 EndpointDiscoverer#addOperations
EndpointDiscoverer#addOperations 创建了 Operation 实例并添加到了 indexed 中,其实现如下:
// 泛型 O extends Operation
private void addOperations(MultiValueMap<OperationKey, O> indexed, EndpointId id, Object target,
boolean replaceLast) {
Set<OperationKey> replacedLast = new HashSet<>();
// 1. 通过operationsFactory 获取 operations
Collection<O> operations = this.operationsFactory.createOperations(id, target);
for (O operation : operations) {
// 2. 创建 OperationKey ,由子类实现,需要注意对于 ControllerEndpointDiscoverer、ServletEndpointDiscoverer 来说该方法默认是抛出异常的。也即是说对于 Controller 和 Servlet 方式来说是不需要通过 Operation 的方式来暴露端点的。
OperationKey key = createOperationKey(operation);
// 3. 获取OperationKey 对应的最新的 O
O last = getLast(indexed.get(key));
// 如果 replaceLast = true (替换最后一个) 则进行替换,否则追加。在解析扩展类的Operation 时候会进行替换
// 需要注意的是,这里的替换是 Operation 级别,而非 Endpoint 级别。
if (replaceLast && replacedLast.add(key) && last != null) {
indexed.get(key).remove(last);
}
indexed.add(key, operation);
}
}
上面得到关键在于 this.operationsFactory.createOperations(id, target) ,该方法创建 Operation 操作类,来执行具体端点操作。
下面我们具体来看:
-
this.operationsFactory.createOperations 的实现 :operationsFactory 的类型是 DiscoveredOperationsFactory ,其中 DiscoveredOperationsFactory是EndpointDiscoverer 的内部类。 EndpointDiscoverer#operationsFactory 的初始化是在 EndpointDiscoverer 构造函数中调用 EndpointDiscoverer#getOperationsFactory 实现的,如下:
private DiscoveredOperationsFactory<O> getOperationsFactory(ParameterValueMapper parameterValueMapper, Collection<OperationInvokerAdvisor> invokerAdvisors) { return new DiscoveredOperationsFactory<O>(parameterValueMapper, invokerAdvisors) { // 重写 createOperation 方法,调用 EndpointDiscoverer#createOperation 方法来处理 // EndpointDiscoverer#createOperation 供子类实现。 @Override protected O createOperation(EndpointId endpointId, DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { // 由子类实现具体方法 return EndpointDiscoverer.this.createOperation(endpointId, operationMethod, invoker); } }; }
所以
this.operationsFactory.createOperations(id, target)
会调用 DiscoveredOperationsFactory#createOperation(EndpointId , Object),如下:// 1. 遍历Endpoint bean 的所有方法 Collection<O> createOperations(EndpointId id, Object target) { return MethodIntrospector .selectMethods(target.getClass(), (MetadataLookup<O>) (method) -> createOperation(id, target, method)) .values(); } // 2. 将每个方法转换为 Operation(可能为空) private O createOperation(EndpointId endpointId, Object target, Method method) { // 遍历 OPERATION_TYPES return OPERATION_TYPES.entrySet().stream() // 继续调用 createOperation .map((entry) -> createOperation(endpointId, target, method, entry.getKey(), entry.getValue())) .filter(Objects::nonNull).findFirst().orElse(null); } // 3. operationType 和 annotationType 是 OPERATION_TYPES 的key 和value private O createOperation(EndpointId endpointId, Object target, Method method, OperationType operationType, Class<? extends Annotation> annotationType) { // 寻找包含 annotationType 注解的方法 MergedAnnotation<?> annotation = MergedAnnotations.from(method).get(annotationType); if (!annotation.isPresent()) { // 没找到返回 空 return null; } // 包装找到的方法 DiscoveredOperationMethod operationMethod = new DiscoveredOperationMethod(method, operationType, annotation.asAnnotationAttributes()); // 包装成 invoke OperationInvoker invoker = new ReflectiveOperationInvoker(target, operationMethod, this.parameterValueMapper); // 通过 OperationInvokerAdvisor 接口 进行代理增强 invoker = applyAdvisors(endpointId, operationMethod, invoker); // 创建 Operation,这里调用的是 EndpointDiscoverer.operationsFactory#createOperations 方法 return createOperation(endpointId, operationMethod, invoker); }
其中 OPERATION_TYPES 的值是在 DiscoveredOperationsFactory 静态代码块中初始化,如下:
// DiscoveredOperationsFactory 部分代码如下: static { Map<OperationType, Class<? extends Annotation>> operationTypes = new EnumMap<>(OperationType.class); // 这里初始化了 @ReadOperation、@WriteOperation、@DeleteOperation 的处理 operationTypes.put(OperationType.READ, ReadOperation.class); operationTypes.put(OperationType.WRITE, WriteOperation.class); operationTypes.put(OperationType.DELETE, DeleteOperation.class); // 初始化 OPERATION_TYPES OPERATION_TYPES = Collections.unmodifiableMap(operationTypes); }
这里我们可以知道 :这里的逻辑是 遍历 Endpoint 代表的 bean 的所有方法,从中找到被 @ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法,并封装成 Operation 返回。
而具体创建Operation 的逻辑在 EndpointDiscoverer#createOperation 中,该方法交由子类实现,几个子类实现相近,下面以 WebEndpointDiscoverer 为例,其实现如下:
// WebEndpointDiscoverer#createOperation @Override protected WebOperation createOperation(EndpointId endpointId, DiscoveredOperationMethod operationMethod, OperationInvoker invoker) { // 这里的 this.endpointPathMappers 即是 MappingWebEndpointPathMapper,在WebEndpointDiscoverer 构造函数中初始化 // 获取 management.endpoints.web.base-path 的配置路径 String rootPath = PathMapper.getRootPath(this.endpointPathMappers, endpointId); // requestPredicateFactory 在构造函数中借由 EndpointMediaTypes 生成 // 创建web操作的请求谓词 WebOperationRequestPredicate requestPredicate = this.requestPredicateFactory.getRequestPredicate(rootPath, operationMethod); // 封装成 DiscoveredWebOperation 返回 return new DiscoveredWebOperation(endpointId, operationMethod, invoker, requestPredicate); }
3.2 EndpointDiscoverer#createEndpoint
EndpointDiscoverer#createEndpoint 交由各个子类实现,不同的端点发现器创建类型不同,其创建过程比较简单,篇幅所限,不再赘述。
- ControllerEndpointDiscoverer 实际返回类型 DiscoveredControllerEndpoint 。
- JmxEndpointDiscoverer 实际返回类型 DiscoveredJmxEndpoint
- ServletEndpointDiscoverer 实际返回类型 DiscoveredServletEndpoint
- WebEndpointDiscoverer 实际返回类型 DiscoveredWebEndpoint
4 总结
我们来总结一下本文的内容:
- EndpointDiscoverer#getEndpoints 会找出容器中所有被 @Endpoint 注解修饰的类,并为其创建 EndpointBean 类。
- EndpointDiscoverer#addExtensionBeans 方法会找出容器中被 @EndpointExtension 注解修饰的类,并为其创建 ExtensionBean,随后ExtensionBean会绑定对应的EndpointBean 类中。
- EndpointDiscoverer#convertToEndpoints 会将EndpointBean 转换为各个端点发现器需要的类型。其中会遍历 Endpoint 的方法,找到被 @ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法并封装成 Operation,保存到 EndpointBean 中。
可以发现,本文只了解了 Endpoint 的创建过程,至于如何调用以及可以通过http 来调用本文并未涉及,篇幅所限,请移步 :Spring源码分析二十六 :Actuator 浅析②
以上:内容部分参考
https://www.freesion.com/article/8628704493/
https://www.jianshu.com/p/af9738634a21
https://blog.csdn.net/zhangweiocp/article/details/115767931
https://blog.csdn.net/sparrowxin/article/details/110962804
https://blog.csdn.net/z69183787/article/details/110633486
https://blog.csdn.net/qq_41907991/article/details/105123387
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正