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,每一个端点都有案例