springBootActuator的使用

  • TOC
    {:toc}

springBootActuator的使用

启用actuator功能

​ 这个是Springboot提供的分析软件,他能提供一些指数供我们参考,因为他已经内置在Springboot里面了,要使用它非常简单,我们只需添加如下依赖在项目下。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.3.0</version>
</dependency>

​ 他能提供jmx或者web两种方式的数据暴露方式,我们启用基于web的数据暴露方式。

添加依赖后在配置文件里配置如下

1
2
3
4
5
6
7
8
#默认开启
management.endpoints.enabled-by-default=true
#以web方式暴露所有端点(include * 表示包含全部)
management.endpoints.web.exposure.include=*
#关闭jmx的所有端点(exclude * 表示排除全部)
management.endpoints.jmx.exposure.exclude=*
#health接口包含细节信息
management.endpoint.health.show-details=always

​ 这样运行项目后,我们访问http://127.0.0.1:8080/actuator就能看到一个json,里面包含所有暴露出来的端点地址信息,但是只看json是不利于观察的,下面我们讲解如何配置可视化界面。

配置可视化界面

​ 上面我们已经启用了actuator功能,但是数据不利于观察,于是有人做出了这样的gui项目,只需要将地址配置给gui项目,gui项目帮我们请求地址,将json数据以可视化的方式展示出来。

​ 在上面的项目上添加如下依赖,这是spring-boot-admin项目的客户端。

1
2
3
4
5
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.0</version>
</dependency>

​ 配置文件里配置服务端的地址,假设服务端启动在8888端口

1
spring.boot.admin.client.instance.service-url=http://127.0.0.1:8888

新建一个项目作为服务端,添加如下依赖,这是spring-boot-admin项目的服务端。启动类上添加@EnableAdminServer注解,在8888端口启动。

1
2
3
4
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>

​ 假设服务端启动在8888端口的,在客户端里面配置服务端的地址,这样客户端启动时,会连接服务端,将当前项目的actuator地址告诉服务端,打开http://127.0.0.1:8888就能展示可视化的客户端信息了。

​ 原理是这样的,拥有客户端的项目在启动时,将客户端的地址发送到服务端。服务端提供静态页面,页面上通过js访问服务端,服务端再请求项目,服务端在页面和客户端之间做转发操作。

实现原理源码分析

​ 配置了简单的demo,并简述了他的原理,下面我们深入到代码里面查看他的源码。

客户端原理

​ 找到客户端SpringBootAdminClientAutoConfiguration类,此类在spring.factorise里配置了,启动时会自动执行。我么分析他的源码即可。

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnMissingBean
public RegistrationApplicationListener registrationListener(ClientProperties client,
ApplicationRegistrator registrator) {
RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator);
listener.setAutoRegister(client.isAutoRegistration());
listener.setAutoDeregister(client.isAutoDeregistration());
listener.setRegisterPeriod(client.getPeriod());
return listener;
}

​ 查看此类,其中有方法上标记了@EventListener注解,他在监听到容器启动完成后向服务端注册自己,下面代码里开启的注册的定时任务,10秒钟心跳注册一次。

1
2
3
4
5
6
7
@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)
public void onApplicationReady(ApplicationReadyEvent event) {
if (autoRegister) {
startRegisterTask();
}
}

​ 注册逻辑是写在此方法里的,其中有一个register方法,负责向服务端发送信息。

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnMissingBean
public ApplicationRegistrator registrator(ClientProperties client, ApplicationFactory applicationFactory) {
RestTemplateBuilder builder = new RestTemplateBuilder().setConnectTimeout(client.getConnectTimeout())
.setReadTimeout(client.getReadTimeout());
if (client.getUsername() != null) {
builder = builder.basicAuthentication(client.getUsername(), client.getPassword());
}
return new ApplicationRegistrator(builder.build(), client, applicationFactory);
}

​ 这是在servlet环境下的处理逻辑,可看淡它使用RestTemplate向服务端发送客户端的信息,如果是webflux环境下,会使用webclient发送信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected boolean register(Application self, String adminUrl, boolean firstAttempt) {
try {
ResponseEntity<Map<String, Object>> response = template.exchange(adminUrl, HttpMethod.POST,
new HttpEntity<>(self, HTTP_HEADERS), RESPONSE_TYPE);

if (response.getStatusCode().is2xxSuccessful()) {
if (registeredId.compareAndSet(null, response.getBody().get("id").toString())) {
LOGGER.info("Application registered itself as {}", response.getBody().get("id").toString());
} else {
LOGGER.debug("Application refreshed itself as {}", response.getBody().get("id").toString());
}
return true;
} else {
if (firstAttempt) {
LOGGER.warn(
"Application failed to registered itself as {}. Response: {}. Further attempts are logged on DEBUG level",
self, response.toString());
} else {
LOGGER.debug("Application failed to registered itself as {}. Response: {}", self,
response.toString());
}
}
} catch (Exception ex) {
if (firstAttempt) {
LOGGER.warn(
"Failed to register application as {} at spring-boot-admin ({}): {}. Further attempts are logged on DEBUG level",
self, client.getAdminUrl(), ex.getMessage());
} else {
LOGGER.debug("Failed to register application as {} at spring-boot-admin ({}): {}", self,
client.getAdminUrl(), ex.getMessage());
}

}
return false;
}

​ 那么到底向服务端发送的是什么内容呢?,查看restTemplate发送请求的代码,他发送了Application序列化的结果。下面是一个ApplicationFactory,他的createApplication会创建一个Application的实例,该实例将包含服务端需要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class ServletConfiguration {
@Bean
@ConditionalOnMissingBean
public ApplicationFactory applicationFactory(InstanceProperties instance,
ManagementServerProperties management,
ServerProperties server,
ServletContext servletContext,
PathMappedEndpoints pathMappedEndpoints,
WebEndpointProperties webEndpoint,
MetadataContributor metadataContributor,
DispatcherServletPath dispatcherServletPath) {
return new ServletApplicationFactory(instance,
management,
server,
servletContext,
pathMappedEndpoints,
webEndpoint,
metadataContributor,
dispatcherServletPath
);
}
}

​ 这就是Application内含有的字段。这些字段如果不自己配置,都可以从程序里自动获取到。

1
2
3
4
5
6
public class Application {
private final String name;
private final String managementUrl;
private final String healthUrl;
private final String serviceUrl;
private final Map<String, String> metadata;

总结

​ 这就是客户端逻辑,他先是使用监听器监听程序启动,再收集Application类字段上的那些信息,根据容器的不同选择resttemplate 或者 webclient,使用Schedult线程池,每10秒向服务器发送本程序的信息。

​ 我们只需要配置服务端的地址,剩下的数据都能自动收集到。并且客户端的代码很少,就十几个类。功能也比较简单。

服务端原理

​ 引入下面依赖后,共有三个依赖项目被引入。

1
2
3
4
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>

分别是

  • spring-boot-admin-server-ui

  • spring-boot-admin-server-cloud

  • spring-boot-admin-server

spring-boot-admin-server-ui

​ 首先查看spring-boot-admin-server-ui项目,他在项目的META-INF文件夹下放置了页面的静态文件,并在代码里配置了UiController来返回这些静态页面。页面路径相关的配置由AdminServerUiProperties类配置,比较简单。

spring-boot-admin-server-cloud

​ 发现他配置了InstanceDiscoveryListener类,这里面对服务发现的事件进行监听,比如监听到InstanceRegisteredEvent事件时,调用discoveryClient读取注册中心,这些事件和注册中心都是在spring-cloud-commons包里定义的。如果使用注册中心,admin-server就能从注册中心拉取客户端信息,即使客户端不添加admin-starter-client也是可以的,这个稍后讲。

​ 下面这个方法就是就是发现逻辑,使用discoveryClient发现服务,进行转换后存储在InstanceRegistry里面。

1
2
3
4
5
6
7
8
9
10
protected void discover() {
log.debug("Discovering new instances from DiscoveryClient");
Flux.fromIterable(discoveryClient.getServices())
.filter(this::shouldRegisterService)
.flatMapIterable(discoveryClient::getInstances)
.flatMap(this::registerInstance)
.collect(Collectors.toSet())
.flatMap(this::removeStaleInstances)
.subscribe(v -> { }, ex -> log.error("Unexpected error.", ex));
}

spring-boot-admin-server

首先spring.factories文件里面引入了 AdminServerAutoConfiguration配置类

1
2
3
4
5
6
7
@ImportAutoConfiguration({ AdminServerInstanceWebClientConfiguration.class, AdminServerWebConfiguration.class })
public class AdminServerAutoConfiguration {
.
.
.
}

同时这个类又引入了AdminServerInstanceWebClientConfiguration.classAdminServerWebConfiguration.class两个类。

AdminServerInstanceWebClientConfiguration配置类

这个类里面主要定义和配置了InstanceWebClient.Builder,这是一个包装了WebClient的客户端,使用它来获取客户端信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration(proxyBeanMethods = false)
@Lazy(false)
public class AdminServerInstanceWebClientConfiguration {

private final InstanceWebClient.Builder instanceWebClientBuilder;

//构造方法里,传入了WebClient和InstanceWebClientCustomizer,webclient.build就是springboot自动配置的
public AdminServerInstanceWebClientConfiguration(ObjectProvider<InstanceWebClientCustomizer> customizers,
WebClient.Builder webClient) {
this.instanceWebClientBuilder = InstanceWebClient.builder(webClient);
customizers.orderedStream().forEach((customizer) -> customizer.customize(this.instanceWebClientBuilder));
}

//这里在返回InstanceWebClient时进行了clone操作,同时也会可用一份WebClient,所以对此webclient的改动不会反应到全局webclient上
@Bean
@ConditionalOnMissingBean
@Scope("prototype")
public InstanceWebClient.Builder instanceWebClientBuilder() {
return this.instanceWebClientBuilder.clone();
}

​ 此配置类执行完成后,我们就拥有了一个封装好的Webclient供我们使用,并且对此类的所有配置都不会影响全局Webclient配置。

​ 下面这个类可以为请求添加Basic验证的请求头,如果客户端需要Basic验证,并且配置了密码,就能验证访问。

1
2
3
4
5
6
7
8
9
10
@Configuration(proxyBeanMethods = false)
protected static class HttpHeadersProviderConfiguration {

@Bean
@ConditionalOnMissingBean
public BasicAuthHttpHeaderProvider basicAuthHttpHeadersProvider() {
return new BasicAuthHttpHeaderProvider();
}

}
AdminServerWebConfiguration

此类配置了一些Controller。如下面这个方法用来接收客户端的注册。

1
2
3
4
5
6
7
8
9
10
11
@PostMapping(path = "/instances", consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResponseEntity<Map<String, InstanceId>>> register(@RequestBody Registration registration,
UriComponentsBuilder builder) {
Registration withSource = Registration.copyOf(registration).source("http-api").build();
LOGGER.debug("Register instance {}", withSource);
return registry.register(withSource).map((id) -> {
URI location = builder.replacePath("/instances/{id}").buildAndExpand(id).toUri();
return ResponseEntity.created(location).body(Collections.singletonMap("id", id));
});
}

AdminServerAutoConfiguration

​ 上面通过controller可以接受用户注册,通过Discover能从注册中心发现服务,那么将这些信息存储在哪里呢?

由此类里定义的三个组件完成的,分别是

  • InstanceRegistry 客户端实例的注册器,处理注册操作
  • InstanceIdGenerator 客户端id生成
  • SnapshottingInstanceRepository 上面的注册器实际上将信息存储在这个类里面,相当于是注册器的数据库,默认存储在内存里。

服务发现和密码保护

​ 客户端Actuator相关接口一般是要加密的,如果采用HttpBasic方式加密,可以将密码配置到服务端,或者配置到注册中心的元数据里面,默认admin-server会从元数据里面获取如下的帐号密码。

1
2
3
4
5

private static final String[] USERNAME_KEYS = { "user.name", "user-name", "username" };

private static final String[] PASSWORD_KEYS = { "user.password", "user-password", "userpassword" };

​ eureka注册中心将数据存储到元数据里面的方法是下面这样,这样服务端就不用配置客户端的密码了,能直接从注册中心里获取到。

1
2
eureka.instance.metadata-map.user-name=abc
eureka.instance.metadata-map.user-password=abc

总结

​ 以上就是admin-server端的源码分析,他能使用服务发现,或者接口的方式得到客户端的信息,并存储起来,如果管理网页被打开,就会进行请求转发,转发到对应客户端的地址上。如果不存在注册中心,他就自己维持心跳,维持状态,否则它可以直接从注册中心内获取这些信息。关于密码保护,如果使用注册中心的话,可以将密码存储到元数据里,他尝试从元数据里按照上面三个名字获取账号密码。

总结

​ 以上就是Actuator可视化的过程,强烈推荐将admin-server单独部署为一个项目,将所有项目部署到一个注册中心里,这样其他项目不使用admin-client也是可以的


springBootActuator的使用
https://www.huangchaoyu.com/344497220.html
作者
hcy
发布于
2020年8月25日
更新于
2024年8月17日
许可协议