spring boot Actuator原理详解之启动

2023-11-02

本文基于spring boot 2.2.0 release版本。

从本文开始,我将连续几篇文章介绍Actuator的原理,本文是原理的第一篇文章,介绍Actuator是如何启动的。
我们知道访问Actuator可以通过JMX、web等渠道,本文以web为例介绍。

一、WebMvcEndpointHandlerMapping

在actuator-autoconfigure包下面为web访问提供了多个自动配置类,从目录名上可以看出来,当以servlet模式启动容器时,会使用类WebMvcEndpointManagementContextConfiguration做自动配置。

reactive和jersey模式启动的场景不做介绍

在这里插入图片描述
在该类中会自动创建WebMvcEndpointHandlerMapping对象,并将该对象注册到spring容器中。WebMvcEndpointHandlerMapping便是启动整个Actuator的最关键类。下面来看一下它的创建过程。

	@Bean
	@ConditionalOnMissingBean
	public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
			ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier,
			EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
			WebEndpointProperties webEndpointProperties, Environment environment) {
		List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
		Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
		//将所有的端点功能放到集合中,spring搜寻端点的过程后面做介绍
		allEndpoints.addAll(webEndpoints);
		allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
		allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
		//获取Actuator的url路径,可以通过management.endpoints.web.basePath修改
		String basePath = webEndpointProperties.getBasePath();
		EndpointMapping endpointMapping = new EndpointMapping(basePath);
		boolean shouldRegisterLinksMapping = StringUtils.hasText(basePath)
				|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT);
		//创建WebMvcEndpointHandlerMapping对象
		return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
				corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
				shouldRegisterLinksMapping);
	}

在webEndpointServletHandlerMapping()方法中首先查找出spring容器中所有的ExposableWebEndpoint对象,这些对象就是端点对象,然后根据这些对象创建WebMvcEndpointHandlerMapping。
WebMvcEndpointHandlerMapping实现了InitializingBean接口,因此创建完对象后,接着调用afterPropertiesSet()方法做初始化。

	public void afterPropertiesSet() {
		initHandlerMethods();
	}
	protected void initHandlerMethods() {
		//遍历所有的端点
		for (ExposableWebEndpoint endpoint : this.endpoints) {
			//遍历端点对象中的每个操作对象
			//每个端点功能,比如info、beans,当通过web访问这些功能时,都是由操作对象处理请求
			for (WebOperation operation : endpoint.getOperations()) {
				//将每个操作对象注册为RequestMappingInfo,类似于@RequestMapping
				registerMappingForOperation(endpoint, operation);
			}
		}
		if (this.shouldRegisterLinksMapping) {
			//注册links,用于处理“/actuator”请求
			registerLinksMapping();
		}
	}
	private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
		WebOperationRequestPredicate predicate = operation.getRequestPredicate();
		//path是部分url路径,可以简单的认为是端点名称
		String path = predicate.getPath();
		String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
		if (matchAllRemainingPathSegmentsVariable != null) {
			//做通配符替换,表示该url可以匹配所有的路径
			path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", "**");
		}
		ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
				new ServletWebOperationAdapter(operation));
	    //注册RequestMappingInfo对象,注册了之后,它就可以处理http请求了
	    //可以处理的URL请求由path指定,处理该请求的对象是OperationHandler对象,
	    //处理请求的方法是由handleMethod指定,handleMethod是一个Method对象,
	    //表示的是OperationHandler对象的handle()方法
		registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation),
				this.handleMethod);
	}
	//注册links,用于处理“/actuator”请求
	//原理与registerMappingForOperation类似
	private void registerLinksMapping() {
		PatternsRequestCondition patterns = patternsRequestConditionForPattern("");
		RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(RequestMethod.GET);
		ProducesRequestCondition produces = new ProducesRequestCondition(this.endpointMediaTypes.getProduced()
				.toArray(StringUtils.toStringArray(this.endpointMediaTypes.getProduced())));
		RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, null, produces, null);
		LinksHandler linksHandler = getLinksHandler();
		//注册RequestMappingInfo对象
		//处理请求的是WebMvcLinksHandler对象的links()方法
		registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links",
				HttpServletRequest.class, HttpServletResponse.class));
	}
	//创建RequestMappingInfo,类似于@RequestMapping
	private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
		PatternsRequestCondition patterns = patternsRequestConditionForPattern(path);
		RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
				RequestMethod.valueOf(predicate.getHttpMethod().name()));
		ConsumesRequestCondition consumes = new ConsumesRequestCondition(
				StringUtils.toStringArray(predicate.getConsumes()));
		ProducesRequestCondition produces = new ProducesRequestCondition(
				StringUtils.toStringArray(predicate.getProduces()));
		return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null);
	}

在afterPropertiesSet()方法里面遍历了所有的端点对象的操作对象WebOperation,并根据WebOperation和path创建RequestMappingInfo,其中path指定了可以处理的URL请求路径,比如“/info”,“/beans”,WebOperation用于处理URL请求。
注册了RequestMappingInfo之后,便可以处理web请求了。当通过web访问指定的端点时,spring根据URL路径找到对应的RequestMappingInfo,RequestMappingInfo将请求转发给适配器ServletWebOperationAdapter,适配器内部再去调用WebOperation处理该请求。
到这里,WebMvcEndpointHandlerMapping的启动过程便结束了。上面留了一个问题是spring如何收集所有的端点对象?

二、spring收集端点对象原理

下面介绍一下spring收集端点对象原理。
spring提供了端点发现类EndpointDiscoverer,它的作用是从spring容器中查找符合要求的端点对象。EndpointDiscoverer是一个抽象类,spring提供了五个实现类:

发现类 作用
JmxEndpointDiscoverer 检索@Endpoint注解的类,将端点转换为DiscoveredJmxEndpoint,然后以JMX形式对外提供端点功能。
ServletEndpointDiscoverer 检索@ServletEndpoint注解的类,这种类以servlet的形式对外提供端点功能,缺点是性能低。
WebEndpointDiscoverer 检索@Endpoint注解的类,被@Endpoint注解的类就是端点,将端点转换为DiscoveredWebEndpoint,然后以web形式对外提供端点功能。
CloudFoundryWebEndpointDiscoverer 是WebEndpointDiscoverer的子类,在Cloud Foundry环境下使用。
ControllerEndpointDiscoverer 检索@ControllerEndpoint或者@RestControllerEndpoint注解的类,这种类由spring mvc或者spring webflux处理,然后对外提供端点功能。

EndpointDiscoverer首先检索所有@Endpoint注解的端点对象,然后调用一些过滤方法对这些对象进行过滤:

	private boolean isEndpointExposed(EndpointBean endpointBean) {
		return isFilterMatch(endpointBean.getFilter(), endpointBean) && !isEndpointFiltered(endpointBean)
				&& isEndpointExposed(endpointBean.getBean());
	}
  • isFilterMatch():检查这些对象是否含有EndpointDiscoverer子类要求的注解,如果没有则过滤掉;有些端点对象还有@FilteredEndpoint注解,该注解里面指定了过滤器EndpointFilter,isFilterMatch()实例化EndpointFilter对象,然后使用该对象检查端点对象,如果检查不通过,则过滤掉;
  • isEndpointFiltered():当使用web提供端点功能时,spring提供了ExposeExcludePropertyEndpointFilter过滤器,该过滤器可以根据配置决定是否过滤掉端点对象;
  • isEndpointExposed():在isFilterMatch()中已经调用过该方法,作用是检查端点对象是否含有EndpointDiscoverer子类要求的注解。

我们来看一下ExposeExcludePropertyEndpointFilter过滤器的创建。当在web环境下使用Actuator时,spring会执行下面的方法创建对象。

	@Bean
	public ExposeExcludePropertyEndpointFilter<ExposableWebEndpoint> webExposeExcludePropertyEndpointFilter() {
		WebEndpointProperties.Exposure exposure = this.properties.getExposure();
		return new ExposeExcludePropertyEndpointFilter<>(ExposableWebEndpoint.class, exposure.getInclude(),
				exposure.getExclude(), "info", "health");
	}

ExposeExcludePropertyEndpointFilter创建时会读取配置
management.endpoints.web.exposure.exclude/management.endpoints.web.exposure.include的值,然后还指定了"info", "health"为两个默认暴露的端点。

端点对象如果通过了isEndpointExposed()的检查,那么意味着该端点可以对外暴露了。但是在暴露前,还有一些工作需要做。这些工作主要是两方面:一是创建操作对象Operation,二是创建ExposableEndpoint对象。这两个类是不是有点眼熟,对了,在第一小节里面提到过它们,Operation是最终处理请求的对象,在web环境中,Operation的实现类是DiscoveredWebOperation(它还有一个父接口是WebOperation),每个DiscoveredWebOperation对象都对应了一个http请求路径,当收到请求后,它调用端点对应的中对应的方法处理该请求;ExposableEndpoint用于封装端点对象,除此之外,它还封装了端点发现类、操作对象Operation、请求路径等,凡是被它封装的端点对象,意味着端点对象可以处理外部请求。

1、创建操作对象Operation

Operation是一个接口,它负责根据外部请求调用端点对象中的处理方法,并将处理结果返回给客户端。在不同的场景下,该接口有不同的子接口,比如在JMX环境下,子接口是JmxOperation,web环境下,子接口是WebOperation。每个子接口都一个实现类,WebOperation的实现类是DiscoveredWebOperation。DiscoveredWebOperation的继承关系图如下,其他的类没有展示:
在这里插入图片描述

下面介绍一下web环境下,DiscoveredWebOperation对象是如何创建的。
spring首先解析端点对象的各个方法,检查方法上是否有以下三个注解:

	//注解@ReadOperation表示当前方法是只读的,对应http请求中的GET方法
	//注解@WriteOperation表示当前方法会修改数据,,对应http请求中的POST方法
	//注解@DeleteOperation表示当前方法会删除数据,比如删除缓存数据,对应http请求中的DELETE方法
	//spring mvc会根据http请求的方法映射到对应注解的方法
	operationTypes.put(OperationType.READ, ReadOperation.class);
	operationTypes.put(OperationType.WRITE, WriteOperation.class);
	operationTypes.put(OperationType.DELETE, DeleteOperation.class);

如果某个方法有上面三个注解中的一个,那么便根据Method对象、端点对象调用WebEndpointDiscoverer.createOperation()方法创建DiscoveredWebOperation。

	@Override
	//endpointId表示端点名称,同时也对应的http路径
	//operationMethod表示被注解的方法,持有一个Method对象
	//invoker调用器对象,持有端点对象和operationMethod对象
	protected WebOperation createOperation(EndpointId endpointId, DiscoveredOperationMethod operationMethod,
			OperationInvoker invoker) {
		String rootPath = PathMapper.getRootPath(this.endpointPathMappers, endpointId);//rootPath既表示端点名称,也表示请求路径
		//requestPredicate用于初始化RequestMappingInfo,它可以限定WebOperation处理哪些请求
		WebOperationRequestPredicate requestPredicate = this.requestPredicateFactory.getRequestPredicate(rootPath,
				operationMethod);
		return new DiscoveredWebOperation(endpointId, operationMethod, invoker, requestPredicate);
	}

2、创建ExposableEndpoint

ExposableEndpoint表示可以对外暴露的端点对象,如果一个端点想要接受外部请求,必须被ExposableEndpoint封装。
ExposableEndpoint只有一个实现类:DiscoveredWebEndpoint。
创建完操作对象Operation后,接下来针对每个端点对象就要创建ExposableEndpoint对象。
在web环境下,调用WebEndpointDiscoverer.createEndpoint()方法创建DiscoveredWebEndpoint对象。

	@Override
	//endpointBean表示端点对象,比如BeansEndPoint
	//enabledByDefault是@Endpoint注解enableByDefault属性的值
	//operations就是DiscoveredWebOperation对象的集合
	protected ExposableWebEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
			Collection<WebOperation> operations) {
		String rootPath = PathMapper.getRootPath(this.endpointPathMappers, id);
		return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath, enabledByDefault, operations);
	}

创建完所有的ExposableEndpoint对象之后,spring将这些对象组装成集合对象,该集合对象也是WebEndpointDiscoverer.endpoints属性。下面回到本文最开始的地方,webEndpointServletHandlerMapping方法的入参webEndpointsSupplier便是WebEndpointDiscoverer对象,我们看到代码中通过该对象获得了所有的ExposableWebEndpoint,之后便是遍历每个端点对象的操作对象,并根据操作对象创建RequestMappingInfo。这样当发起http请求时,便可以将请求转发给操作对象,操作对象根据方法信息就可以调用端点对象中的方法了。

Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();

三、总结

下面对上述流程做一个简单的总结:

  1. 扫描所有@Endpoint注解的类,这些类都是Endpoint;
  2. 使用过滤器对Endpoint对象进行过滤,没有被过滤掉的才可以进入下一步;
  3. 读取Endpoint对象的每个方法,判断是否有@ReadOperation、@WriteOperation、@DeleteOperation三个注解,如果有,则针对每个被注解的方法创建操作对象Operation;
  4. 根据操作对象、Endpoint对象、Endpoint名创建为RequestMappingInfo,并将其注册到spring mvc中;
  5. 注册成功之后,Endpoint对象便可以对外提供服务。

参考文章

https://blog.csdn.net/andy_zhang2007/article/details/90438116

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

spring boot Actuator原理详解之启动 的相关文章

随机推荐