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
management.endpoints.web.exposure.include=*
management.endpoints.jmx.exposure.exclude=*
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-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.class
,AdminServerWebConfiguration.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;
public AdminServerInstanceWebClientConfiguration(ObjectProvider<InstanceWebClientCustomizer> customizers, WebClient.Builder webClient) { this.instanceWebClientBuilder = InstanceWebClient.builder(webClient); customizers.orderedStream().forEach((customizer) -> customizer.customize(this.instanceWebClientBuilder)); }
@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
也是可以的