Spring源码分析二十五 :Actuator 浅析①

一、前言

本文是笔者阅读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 的功能,自定义的过程也很简单:

  1. 使用 @Endpoint 注解标注当前类作为一个端点注册,指定id为访问路径
  2. 通过 @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 引入的类很多,我们这里不在贴出所有的类,下面我们看其中的几个关键类

  1. EndpointAutoConfiguration :Actuator 的 起始配置类。
  2. WebEndpointAutoConfiguration :Web 环境下的关键配置类,引入了很多核心类
  3. 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默认情况下,由端点生成和使用的媒体类型
WebEndpointDiscovererweb 端点发现者,发现被@Endpoint 注解修饰的类作为端点。下文详细介绍
ControllerEndpointDiscoverercontroller 端点发现者,发现被 @ControllerEndpoint 或 @RestControllerEndpoint 修饰的l类作为端点,下文详细介绍
PathMappedEndpoints保存了所有 Endpoint 信息 以及访问基础路径
IncludeExcludeEndpointFilter<ExposableWebEndpoint>WEB 下的过滤器,默认只开启 /info 和 /health,可以通过配置控制哪些路径的端点开启或者关闭
IncludeExcludeEndpointFilter<ExposableControllerEndpoint>controller 下的过滤器,可以通过配置控制哪些路径的端点开启或者关闭
ServletEndpointDiscovererservlet 上下文加载的 Servlet 端点发现者。

这里我们注意 WebEndpointAutoConfiguration 中注入了三个端点发现器 :WebEndpointDiscovererControllerEndpointDiscovererServletEndpointDiscoverer 。这三个类都是 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 的逻辑可以分为如下三个步骤 :

  1. 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<>();
    	}
    
  2. 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;
    
  3. 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);
	}

下面我们主要来看下面两个方法:

  1. 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
    

    具体分析如下:

    1. EndpointDiscoverer#isFilterMatch(endpointBean.getFilter(), endpointBean) :这一步的目的是判断在当前端点发现器下是否应当暴露。如对于被@JmxEndpoint 修饰的类,如果当前端点发现器是 WebEndpointDiscoverer,则不应暴露,此时@JmxEndpoint 注解上的 @FilteredEndpoint 指定 过滤器 JmxEndpointFilter,JmxEndpointFilter就会判断当前端点发现器是否为 JmxEndpointDiscoverer ,不为则认为不应暴露,随即EndpointFilter#match 返回false,取消当前 Endpoint的暴露。

    2. EndpointDiscoverer#isEndpointFiltered(endpointBean) :这一步的目的是确定当前端点发现器是否允许暴露当前Endpoint,我们可以通过配置文件中来指定暴露哪些端点或者排除哪些端点,满足配置条件的端点才可以暴露,其对应的 Filter 为 IncludeExcludeEndpointFilter 。需要注意的是,EndpointDiscoverer#filter 可能存在多个,需要全部满足才可以暴露。

    3. 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);
      	}
      
  2. 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);
    	}
    

我们按照注释的顺序来说 :

  1. 创建 Operation : 这里会创建 Operation 实体类,即被 @ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法封装成 Operation 实体类。
  2. 创建 扩展类的 Operation :解析扩展类的同样创建扩展类的 被@ReadOperation、@WriteOperation、@DeleteOperation 注解修饰的方法封装成 Operation 实体类,并替换端点的Operation。
  3. 创建 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 总结

我们来总结一下本文的内容:

  1. EndpointDiscoverer#getEndpoints 会找出容器中所有被 @Endpoint 注解修饰的类,并为其创建 EndpointBean 类。
  2. EndpointDiscoverer#addExtensionBeans 方法会找出容器中被 @EndpointExtension 注解修饰的类,并为其创建 ExtensionBean,随后ExtensionBean会绑定对应的EndpointBean 类中。
  3. 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
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值