spring components actuator (2): code analyst

如何使用不再赘述,解释几个感兴趣的关键实现吧:

1 Endpoint如何实现的?

先看下如何实现一个自定义的Endpoint:

@Component
@Endpoint(id = "myendpoint")
public class MyEndpoint {

public final static class Student {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

@ReadOperation
public Student getData() {
Student student = new Student();
student.setName("fujian");
return student;
}
}

然后修改配置application.yml:

[Java]
management:
metrics:
enable:
kafka: true

endpoints:
web:
exposure:
include: myendpoint
[/Java]
然后就可以访问了:http://localhost:8080/actuator/myendpoint

步骤1: 扫描Endpoint,组装成EndpointBean集合

执行的是:org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer#discoverEndpoints

private Collection discoverEndpoints() {
Collection endpointBeans = createEndpointBeans();
addExtensionBeans(endpointBeans);
return convertToEndpoints(endpointBeans);
}

private Collection createEndpointBeans() {
Map byId = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
Endpoint.class);
//最终调用的是:org.springframework.beans.factory.ListableBeanFactory#getBeanNamesForAnnotation
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();
}

步骤2: 封装EndpointBean到ServletEndpointRegistrar:这里注意下,它是一种ServletContextInitializer:

执行的是:org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration.WebMvcServletEndpointManagementContextConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DispatcherServlet.class)
public static class WebMvcServletEndpointManagementContextConfiguration {
//注意这个返回类型的定义:public class ServletEndpointRegistrar implements ServletContextInitializer
@Bean
public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties,
ServletEndpointsSupplier servletEndpointsSupplier, DispatcherServletPath dispatcherServletPath) {
return new ServletEndpointRegistrar(dispatcherServletPath.getRelativePath(properties.getBasePath()),
servletEndpointsSupplier.getEndpoints());
}

}

步骤3:ServletContextInitializer就是启动(Tomcat等容器)成功之后会回调它的onStartup方法。

执行的是:org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar#onStartup

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.servletEndpoints.forEach((servletEndpoint) -> register(servletContext, servletEndpoint));
}

private void register(ServletContext servletContext, ExposableServletEndpoint endpoint) {
String name = endpoint.getEndpointId().toLowerCaseString() + “-actuator-endpoint”;
String path = this.basePath + “/” + endpoint.getRootPath();
String urlMapping = path.endsWith(“/”) ? path + “*” : path + “/*”;
EndpointServlet endpointServlet = endpoint.getEndpointServlet();
//关键点
Dynamic registration = servletContext.addServlet(name, endpointServlet.getServlet());
registration.addMapping(urlMapping);
registration.setInitParameters(endpointServlet.getInitParameters());
registration.setLoadOnStartup(endpointServlet.getLoadOnStartup());
logger.info(“Registered ‘” + path + “‘ to ” + name);
}

记录到此,基本应该清楚了,最核心点就是在于扫描功能+ServletContextInitializer的作用。

这里可以自己写一个测试下:
@Component
public class DemoServletContextInitializer implements ServletContextInitializer {

public void onStartup(ServletContext servletContext) throws ServletException{
System.out.println(“this is my demo servlet context initializer” );
}

}

启动程序后,就有输出了。

2 某项服务的Health check是如何开启和关闭的?

先看下如何开启或者关闭?
management:
health:
db:
enabled: true
redis:
enabled: true
cassandra:
enabled: true

以redis为例:不是enable了,就意味着一定会检查,原因参考:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@ConditionalOnBean(RedisConnectionFactory.class) //必须有Redis的Jar依赖
@ConditionalOnEnabledHealthIndicator(“redis”)
@AutoConfigureAfter({ RedisAutoConfiguration.class, RedisReactiveHealthContributorAutoConfiguration.class })
public class RedisHealthContributorAutoConfiguration
extends CompositeHealthContributorConfiguration {

@Bean
@ConditionalOnMissingBean(name = { “redisHealthIndicator”, “redisHealthContributor” })
public HealthContributor redisHealthContributor(Map redisConnectionFactories) {
return createContributor(redisConnectionFactories);
}

}

再来看下,management.health.redis怎么开启和关闭的?关键点在于上面的:@ConditionalOnEnabledHealthIndicator(“redis”)

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnEnabledHealthIndicatorCondition.class)
public @interface ConditionalOnEnabledHealthIndicator {

/**
* The name of the health indicator.
* @return the name of the health indicator
*/
String value();

}

其中Conditional表明了条件OnEnabledHealthIndicatorCondition,看下这个条件:

class OnEnabledHealthIndicatorCondition extends OnEndpointElementCondition {

OnEnabledHealthIndicatorCondition() {
super(“management.health.”, ConditionalOnEnabledHealthIndicator.class);
}

}

继续看父类的一段实现:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
//annotationType就是ConditionalOnEnabledHealthIndicator
AnnotationAttributes annotationAttributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(this.annotationType.getName()));
String endpointName = annotationAttributes.getString(“value”);
//value就是redis
ConditionOutcome outcome = getEndpointOutcome(context, endpointName);
if (outcome != null) {
return outcome;
}
return getDefaultEndpointsOutcome(context);
}

OnEnabledHealthIndicatorCondition指明了判断条件时,可以依据注解”ConditionalOnEnabledHealthIndicator.class”来获取开关项(value),那么上述的metadata是什么?其实就是基类SpringBootCondition中的AnnotatedTypeMetadata:

public abstract class SpringBootCondition implements Condition {

@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
//……

}

顾名思义,其实就是Conditional标记的类上标记的所有注解信息,所以RedisHealthContributorAutoConfiguration上标记的ConditionalOnEnabledHealthIndicator,自然也是其中一个,所以能拿到它的value:redis,结合前缀”management.health.”,就这样对应上了。

这里也写一个简化版案例:

@Configuration
public class BeanConfig {

private final static class DemoCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// style one:
MergedAnnotations annotations = metadata.getAnnotations();
MergedAnnotation annotation = annotations.get(MyAnnotation.class);
System.out.println(annotation.getString(“value”));

// style two:
//MyAnnotation.class.getName(): com.example.actuatorlearning.BeanConfig$MyAnnotation
Map myAnnotation = metadata.getAnnotationAttributes(MyAnnotation.class.getName());
System.out.println(myAnnotation.get(“value”));
return false;
}
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value();
}

@Conditional(DemoCondition.class)
@MyAnnotation(“my flag key”)
@Bean
public MyEndpoint.Student student(){
return new MyEndpoint.Student();
}

}

上面的AnnotatedTypeMetadata包含了三个注解:其中就有MyAnnotation

其他一些知识点:

解析泛型的类型:
ResolvableType type = ResolvableType.forClass(AbstractCompositeHealthContributorConfiguration.class,
getClass());
this.indicatorType = type.resolveGeneric(1);
this.beanType = type.resolveGeneric(2);

发布者

傅, 健

程序员,Java、C++、开源爱好者. About me