springboot源码研究actuator,自定义actuator路径

Posted Leo Han

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot源码研究actuator,自定义actuator路径相关的知识,希望对你有一定的参考价值。

我们知道,当我们在springboot项目中引入了actuator模块之后,可以通过暴露的端口来获取系统相关信息:

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

如获取bean相关信息:
在这里插入图片描述

那么这里是怎么样的逻辑呢 ?
深入源码研究,我们发现,这里所有的起点一个注解上:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Endpoint {
	/**
	 * The id of the endpoint (must follow {@link EndpointId} rules).
	 * @return the id
	 * @see EndpointId
	 */
	String id() default "";

	/**
	 * If the endpoint should be enabled or disabled by default.
	 * @return {@code true} if the endpoint is enabled by default
	 */
	boolean enableByDefault() default true;

}

在acturator中,有如下几个注解是基于Endpoint注解的:

  • WebEndpoint
  • ServletEndpoint
  • ControllerEndpoint 、 RestControllerEndpoint
  • @EndpointExtension 将Endpoint拓展到特定技术下使用,通常不直接使用该注解,而使用它的组合注解,例如:@EndpointWebExtension拓展到Web技术下(例:SpringMVC Spring WebFlux) @EndpointJmxExtension

我们在访问被Endpoint注解修饰的类的时候并不是直接通过Endpoint注解,而是通过其封装的ExposableEndpoint来进行相关细节的封装和访问,其类结构层次如下:
在这里插入图片描述
而Endpoint的封装则在EndpointDiscoverer中进行:

@Override
	public final Collection<E> getEndpoints() {
		if (this.endpoints == null) {
			this.endpoints = discoverEndpoints();
		}
		return this.endpoints;
	}

	private Collection<E> discoverEndpoints() {
		Collection<EndpointBean> endpointBeans = createEndpointBeans();
		addExtensionBeans(endpointBeans);
		return convertToEndpoints(endpointBeans);
	}

	private Collection<EndpointBean> createEndpointBeans() {
		Map<EndpointId, EndpointBean> byId = new LinkedHashMap<>();
		String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
				Endpoint.class);
		for (String beanName : beanNames) {
			if (!ScopedProxyUtils.isScopedTarget(beanName)) {
				EndpointBean endpointBean = createEndpointBean(beanName);
				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();
	}

	private EndpointBean createEndpointBean(String beanName) {
		Object bean = this.applicationContext.getBean(beanName);
		return new EndpointBean(beanName, bean);
	}

	private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
		Map<EndpointId, EndpointBean> byId = endpointBeans.stream()
				.collect(Collectors.toMap(EndpointBean::getId, Function.identity()));
		String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
				EndpointExtension.class);
		for (String beanName : beanNames) {
			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);
		}
	}

	private ExtensionBean createExtensionBean(String beanName) {
		Object bean = this.applicationContext.getBean(beanName);
		return new ExtensionBean(beanName, bean);
	}

	private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) {
		if (isExtensionExposed(endpointBean, extensionBean)) {
			Assert.state(isEndpointExposed(endpointBean) || isEndpointFiltered(endpointBean),
					() -> "Endpoint bean '" + endpointBean.getBeanName() + "' cannot support the extension bean '"
							+ extensionBean.getBeanName() + "'");
			endpointBean.addExtension(extensionBean);
		}
	}

	private Collection<E> convertToEndpoints(Collection<EndpointBean> endpointBeans) {
		Set<E> endpoints = new LinkedHashSet<>();
		for (EndpointBean endpointBean : endpointBeans) {
			if (isEndpointExposed(endpointBean)) {
				endpoints.add(convertToEndpoint(endpointBean));
			}
		}
		return Collections.unmodifiableSet(endpoints);
	}

	private E convertToEndpoint(EndpointBean endpointBean) {
		MultiValueMap<OperationKey, O> indexed = new LinkedMultiValueMap<>();
		EndpointId id = endpointBean.getId();
		addOperations(indexed, id, endpointBean.getBean(), false);
		if (endpointBean.getExtensions().size() > 1) {
			String extensionBeans = endpointBean.getExtensions().stream().map(ExtensionBean::getBeanName)
					.collect(Collectors.joining(", "));
			throw new IllegalStateException("Found multiple extensions for the endpoint bean "
					+ endpointBean.getBeanName() + " (" + extensionBeans + ")");
		}
		for (ExtensionBean extensionBean : endpointBean.getExtensions()) {
			addOperations(indexed, id, extensionBean.getBean(), true);
		}
		assertNoDuplicateOperations(endpointBean, indexed);
		List<O> operations = indexed.values().stream().map(this::getLast).filter(Objects::nonNull)
				.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
		return createEndpoint(endpointBean.getBean(), id, endpointBean.isEnabledByDefault(), operations);
	}

	private void addOperations(MultiValueMap<OperationKey, O> indexed, EndpointId id, Object target,
			boolean replaceLast) {
		Set<OperationKey> replacedLast = new HashSet<>();
		Collection<O> operations = this.operationsFactory.createOperations(id, target);
		for (O operation : operations) {
			OperationKey key = createOperationKey(operation);
			O last = getLast(indexed.get(key));
			if (replaceLast && replacedLast.add(key) && last != null) {
				indexed.get(key).remove(last);
			}
			indexed.add(key, operation);
		}
	}

可以看到,这里主要就是扫描当前bean中有没有被Endpoint修饰的类,另外查看有没没有EndpointExtension修饰的类,但是其必须有对应的被Endpoint修饰的类,这里实际上创建的就是EndpointBean
在创建EndpointBean 的时候,还有一步重要的操作是创建Operation,其主要逻辑就是,扫描被Endpoint修饰的类的方法,如果方法上有

private static final Map<OperationType, Class<? extends Annotation>> OPERATION_TYPES;

	static {
		Map<OperationType, Class<? extends Annotation>> operationTypes = new EnumMap<>(OperationType.class);
		operationTypes.put(OperationType.READ, ReadOperation.class);
		operationTypes.put(OperationType.WRITE, WriteOperation.class);
		operationTypes.put(OperationType.DELETE, DeleteOperation.class);
		OPERATION_TYPES = Collections.unmodifiableMap(operationTypes);
	}

注解,则会创建对应的Operation,而最后实际创建则再其对应的子类中实现:
在这里插入图片描述
我们这里以WebEndpointDiscoverer为例进行说明:

protected WebOperation createOperation(EndpointId endpointId, DiscoveredOperationMethod operationMethod,
			OperationInvoker invoker) {
		String rootPath = PathMapper.getRootPath(this.endpointPathMappers, endpointId);
		WebOperationRequestPredicate requestPredicate = this.requestPredicateFactory.getRequestPredicate(endpointId,
				rootPath, operationMethod);
		return new DiscoveredWebOperation(endpointId, operationMethod, invoker, requestPredicate);
	}
	//PathMapper.java
static String getRootPath(List<PathMapper> pathMappers, EndpointId endpointId) {
		Assert.notNull(endpointId, "EndpointId must not be null");
		if (pathMappers != null) {
			for (PathMapper mapper : pathMappers) {
				String path = mapper.getRootPath(endpointId);
				if (StringUtils.hasText(path)) {
					return path;
				}
			}
		}
		return endpointId.toString();
	}

WebEndpointDiscoverer则是在WebEndpointAutoConfiguration进行初始化的:

@Configuration
@ConditionalOnWebApplication
@AutoConfigureAfter(EndpointAutoConfiguration.class)
@EnableConfigurationProperties(WebEndpointProperties.class)
public class WebEndpointAutoConfiguration {

	private static final List<String> MEDIA_TYPES = Arrays.asList(ActuatorMediaType.V2_JSON, "application/json");

	private final ApplicationContext applicationContext;

	private final WebEndpointProperties properties;

	public WebEndpointAutoConfiguration(ApplicationContext applicationContext, WebEndpointProperties properties) {
		this.applicationContext = applicationContext;
		this.properties = properties;
	}

	@Bean
	public PathMapper webEndpointPathMapper() {
		return new MappingWebEndpointPathMapper(this.properties.getPathMapping());
	}
	@Bean
	@ConditionalOnMissingBean(WebEndpointsSupplier.class)
	public WebEndpointDiscoverer webEndpointDiscoverer(ParameterValueMapper parameterValueMapper,
			EndpointMediaTypes endpointMediaTypes, ObjectProvider<PathMapper> endpointPathMappers,
			ObjectProvider<OperationInvokerAdvisor> invokerAdvisors,
			ObjectProvider<EndpointFilter<ExposableWebEndpoint>> filters) {
		return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper, endpointMediaTypes,
				endpointPathMappers.orderedStream().collect(Collectors.toList()),
				invokerAdvisors.orderedStream().collect(Collectors.toList()),
				filters.orderedStream().collect(Collectors.toList()));
	}
	......
}

WebEndpointProperties则对应我们在项目中的配置:

management:
  endpoints:
    web:
      exposure:
        include: "*"  #需要开放才能通过接口请求刷新

可以看到,如果默认情况下我们不配置任何相关路径,那么Endpoint的默认路径就是其endpointId, 如果我们想要更改某个Endpoint的路径,则按照WebEndpointProperties ->pathMapping ,按照 endpointId和需要的路径更改即可,

@ConfigurationProperties(prefix = "management.endpoints.web")
public class WebEndpointProperties {

	private final Exposure exposure = new Exposure();

	/**
	 * Base path for Web endpoints. Relative to server.servlet.context-path or
	 * management.server.servlet.context-path if management.server.port is configured.
	 */
	private String basePath = "/actuator";

	/**
	 * Mapping between endpoint IDs and the path that should expose them.
	 * `这里记录的endpintID和其对应的路径,如果没有在这里指定,则默认采取路由路径是endponitId`
	 */
	private final Map<String, String> pathMapping = new LinkedHashMap<>();

	public Exposure getExposure() {
		return this.exposure;
	}

	public String getBasePath() {
		return this.basePath;
	}

	public void setBasePath(String basePath) {
		Assert.isTrue(basePath.isEmpty() || basePath.startsWith("/"), "Base path must start with '/' or be empty");
		this.basePath = cleanBasePath(basePath);
	}

	private String cleanBasePath(String basePath) {
		if (StringUtils.hasText(basePath) && basePath.endsWith("/")) {
			return basePath.substring(0, basePath.length() - 1);
		}
		return basePath;
	}

	public Map<String, String> getPathMapping() {
		return this.pathMapping;
	}
	public static class Exposure {

		/**
		 * Endpoint IDs that should be included or '*' for all.
		 */
		private Set<String> include = new LinkedHashSet<>();

		/**
		 * Endpoint IDs that should be excluded or '*' for all.
		 */
		private Set<String> exclude = new LinkedHashSet<>();
		...........
	}

}

例如调整配置如下:

management:
  endpoints:
    web:
      exposure:
        include: "*"  #需要开放才能通过接口请求刷新
      path-mapping:
        beans: beansPath

在这里插入图片描述

可以看到,我们可以自己自定义actuator的端点的路由信息。

到这里,我们就把一个Endpoint的信息解析并封装,那么是怎么暴露请求的呢 ?
还是以WebEndpoint为例说明。
在这里插入图片描述
将Endpoint暴露对应的请求在WebMvcEndpointHandlerMapping中,类结构层次如下:
在这里插入图片描述
我们看到其父类AbstractHandlerMethodMapping实现了InitializingBean接口,其重写afterPropertiesSet方法如下:

	public void afterPropertiesSet() {
		initHandlerMethods();
	}

AbstractHandlerMethodMapping中:

//AbstractHandlerMethodMapping.java
protected void initHandlerMethods() {
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

而在子类AbstractWebMvcEndpointHandlerMapping重写了该方法如下:

// AbstractWebMvcEndpointHandlerMapping.java
protected void initHandlerMethods() {
		for (ExposableWebEndpoint endpoint : this.endpoints) {
			for (WebOperation operation : endpoint.getOperations

以上是关于springboot源码研究actuator,自定义actuator路径的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot2.x系列教程(七十一)Spring Boot Actuator,每一个端点都有案例

源码分析:SpringBoot健康检查

spring boot 源码解析52-actuate中MVCEndPoint解析

聊聊springboot2的httptrace

springboot 动态日志管理(actuator)

springboot04_springboot特性之Actuator