Implementation principle of SpringBoot health check

Keywords: Spring Boot

For the routine of SpringBoot automatic assembly, directly look at the spring.factories file. When we use it, we only need to introduce the following dependencies

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Then you can find this file in the org. Springframework. Boot. Spring boot actuator autoconfigure package

automatic assembly

Looking at this file, we find that many configuration classes have been introduced. Here, let's focus on the classes of XXXHealthIndicatorAutoConfiguration series. Let's take the first RabbitHealthIndicatorAutoConfiguration as an example to analyze it. You can see from the name that this is the automatic configuration class of RabbitMQ health check

@Configuration
@ConditionalOnClass(RabbitTemplate.class)
@ConditionalOnBean(RabbitTemplate.class)
@ConditionalOnEnabledHealthIndicator("rabbit")
@AutoConfigureBefore(HealthIndicatorAutoConfiguration.class)
@AutoConfigureAfter(RabbitAutoConfiguration.class)
public class RabbitHealthIndicatorAutoConfiguration extends
        CompositeHealthIndicatorConfiguration<RabbitHealthIndicator, RabbitTemplate> {

    private final Map<String, RabbitTemplate> rabbitTemplates;

    public RabbitHealthIndicatorAutoConfiguration(
            Map<String, RabbitTemplate> rabbitTemplates) {
        this.rabbitTemplates = rabbitTemplates;
    }

    @Bean
    @ConditionalOnMissingBean(name = "rabbitHealthIndicator")
    public HealthIndicator rabbitHealthIndicator() {
        return createHealthIndicator(this.rabbitTemplates);
    }
}

According to the previous practice, first parse the annotation

  1. @ConditionalOnXXX series appears again. The first two are that if there is a RabbitTemplate bean, that is, RabbitMQ can be used in our project
  2. @Conditionalonenabledhealth indicator is obviously a custom annotation of springboot actor. Take a look
@Conditional(OnEnabledHealthIndicatorCondition.class)
public @interface ConditionalOnEnabledHealthIndicator {
    String value();
}
class OnEnabledHealthIndicatorCondition extends OnEndpointElementCondition {

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

}
public abstract class OnEndpointElementCondition extends SpringBootCondition {

    private final String prefix;

    private final Class<? extends Annotation> annotationType;

    protected OnEndpointElementCondition(String prefix,
            Class<? extends Annotation> annotationType) {
        this.prefix = prefix;
        this.annotationType = annotationType;
    }

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        AnnotationAttributes annotationAttributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(this.annotationType.getName()));
        String endpointName = annotationAttributes.getString("value");
        ConditionOutcome outcome = getEndpointOutcome(context, endpointName);
        if (outcome != null) {
            return outcome;
        }
        return getDefaultEndpointsOutcome(context);
    }

    protected ConditionOutcome getEndpointOutcome(ConditionContext context,
            String endpointName) {
        Environment environment = context.getEnvironment();
        String enabledProperty = this.prefix + endpointName + ".enabled";
        if (environment.containsProperty(enabledProperty)) {
            boolean match = environment.getProperty(enabledProperty, Boolean.class, true);
            return new ConditionOutcome(match,
                    ConditionMessage.forCondition(this.annotationType).because(
                            this.prefix + endpointName + ".enabled is " + match));
        }
        return null;
    }

    protected ConditionOutcome getDefaultEndpointsOutcome(ConditionContext context) {
        boolean match = Boolean.valueOf(context.getEnvironment()
                .getProperty(this.prefix + "defaults.enabled", "true"));
        return new ConditionOutcome(match,
                ConditionMessage.forCondition(this.annotationType).because(
                        this.prefix + "defaults.enabled is considered " + match));
    }

}
public abstract class SpringBootCondition implements Condition {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            logOutcome(classOrMethodName, outcome);
            recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();
        }
        catch (NoClassDefFoundError ex) {
            throw new IllegalStateException(
                    "Could not evaluate condition on " + classOrMethodName + " due to "
                            + ex.getMessage() + " not "
                            + "found. Make sure your own configuration does not rely on "
                            + "that class. This can also happen if you are "
                            + "@ComponentScanning a springframework package (e.g. if you "
                            + "put a @ComponentScan in the default package by mistake)",
                    ex);
        }
        catch (RuntimeException ex) {
            throw new IllegalStateException(
                    "Error processing condition on " + getName(metadata), ex);
        }
    }
    private void recordEvaluation(ConditionContext context, String classOrMethodName,
            ConditionOutcome outcome) {
        if (context.getBeanFactory() != null) {
            ConditionEvaluationReport.get(context.getBeanFactory())
                    .recordConditionEvaluation(classOrMethodName, this, outcome);
        }
    }
}

The above entry method is the matches method of the SpringBootCondition class, and getMatchOutcome is the method of the subclass OnEndpointElementCondition. This method will first look for the existence of the management.health.rabbit.enabled property in the environment variable. If not, look for the management.health.defaults.enabled property, If this property is not available, set the default value to true

When true is returned here, the automatic configuration of the entire RabbitHealthIndicatorAutoConfiguration class can continue

  1. @Autoconfigurebeefore in this case, let's see what the class HealthIndicatorAutoConfiguration has done before coming back
@Configuration
@EnableConfigurationProperties({ HealthIndicatorProperties.class })
public class HealthIndicatorAutoConfiguration {

    private final HealthIndicatorProperties properties;

    public HealthIndicatorAutoConfiguration(HealthIndicatorProperties properties) {
        this.properties = properties;
    }

    @Bean
    @ConditionalOnMissingBean({ HealthIndicator.class, ReactiveHealthIndicator.class })
    public ApplicationHealthIndicator applicationHealthIndicator() {
        return new ApplicationHealthIndicator();
    }

    @Bean
    @ConditionalOnMissingBean(HealthAggregator.class)
    public OrderedHealthAggregator healthAggregator() {
        OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator();
        if (this.properties.getOrder() != null) {
            healthAggregator.setStatusOrder(this.properties.getOrder());
        }
        return healthAggregator;
    }

}

First, this class introduces the configuration file HealthIndicatorProperties. This configuration class is related to the system state

@ConfigurationProperties(prefix = "management.health.status")
public class HealthIndicatorProperties {

    private List<String> order = null;

    private final Map<String, Integer> httpMapping = new HashMap<>();
}

Next, we registered two beans, beanApplicationHealthIndicator and OrderedHealthAggregator. We will talk about the role later. Now we return to the rabbithealthindicator autoconfiguration class

  1. @Autoconfigurareafter has no impact on the overall logic, so I won't mention it for the time being
  2. Class registers a beanHealthIndicator. The creation logic of this bean is in the parent class
public abstract class CompositeHealthIndicatorConfiguration<H extends HealthIndicator, S> {

    @Autowired
    private HealthAggregator healthAggregator;

    protected HealthIndicator createHealthIndicator(Map<String, S> beans) {
        if (beans.size() == 1) {
            return createHealthIndicator(beans.values().iterator().next());
        }
        CompositeHealthIndicator composite = new CompositeHealthIndicator(
                this.healthAggregator);
        for (Map.Entry<String, S> entry : beans.entrySet()) {
            composite.addHealthIndicator(entry.getKey(),
                    createHealthIndicator(entry.getValue()));
        }
        return composite;
    }

    @SuppressWarnings("unchecked")
    protected H createHealthIndicator(S source) {
        Class<?>[] generics = ResolvableType
                .forClass(CompositeHealthIndicatorConfiguration.class, getClass())
                .resolveGenerics();
        Class<H> indicatorClass = (Class<H>) generics[0];
        Class<S> sourceClass = (Class<S>) generics[1];
        try {
            return indicatorClass.getConstructor(sourceClass).newInstance(source);
        }
        catch (Exception ex) {
            throw new IllegalStateException("Unable to create indicator " + indicatorClass
                    + " for source " + sourceClass, ex);
        }
    }

}
  1. First, an object HealthAggregator is injected here, which is the OrderedHealthAggregator just registered
  2. The execution logic of the first createHealthIndicator method is: if the size of the incoming beans is 1, call createHealthIndicator to create a HealthIndicator; otherwise, create a CompositeHealthIndicator, traverse the incoming beans, create a HealthIndicator in turn, and add it to the CompositeHealthIndicator
  3. The execution logic of the second createHealthIndicator is: obtain the generic parameters in the compositehealthindicator configuration. According to the class corresponding to the generic parameter H and the class corresponding to S, find the constructor declaring the parameter of type S in the class corresponding to H for instantiation
  4. Finally, the bean created here is RabbitHealthIndicator
  5. Recall that when learning the use of health check before, if we need to customize the health check items, the general operation is to implement the HealthIndicator interface. Therefore, we can guess that RabbitHealthIndicator should do the same. Observing the inheritance relationship of this class, we can find that this class inherits a class AbstractHealthIndicator that implements this interface, and the monitoring and inspection process of RabbitMQ is shown in the following code
    //This method is based on AbstractHealthIndicator
public final Health health() {
        Health.Builder builder = new Health.Builder();
        try {
            doHealthCheck(builder);
        }
        catch (Exception ex) {
            if (this.logger.isWarnEnabled()) {
                String message = this.healthCheckFailedMessage.apply(ex);
                this.logger.warn(StringUtils.hasText(message) ? message : DEFAULT_MESSAGE,
                        ex);
            }
            builder.down(ex);
        }
        return builder.build();
    }
//The following two methods are implemented by the class RabbitHealthIndicator
protected void doHealthCheck(Health.Builder builder) throws Exception {
        builder.up().withDetail("version", getVersion());
    }

    private String getVersion() {
        return this.rabbitTemplate.execute((channel) -> channel.getConnection()
                .getServerProperties().get("version").toString());
    }
health examination

After the above series of operations, a RabbitMQ health indicator implementation class is actually created, and this class is also responsible for checking the health of RabbitMQ. From this, we can imagine that if MySQL, Redis, ES, etc. exist in the current environment, it should be the same operation

Then the next step is to call the health methods of all health indicator implementation classes of the whole system when a caller accesses the following address

http://ip:port/actuator/health
HealthEndpointAutoConfiguration

The above operation process is in the class HealthEndpointAutoConfiguration, which is also introduced in the spring.factories file

@Configuration
@EnableConfigurationProperties({HealthEndpointProperties.class, HealthIndicatorProperties.class})
@AutoConfigureAfter({HealthIndicatorAutoConfiguration.class})
@Import({HealthEndpointConfiguration.class, HealthEndpointWebExtensionConfiguration.class})
public class HealthEndpointAutoConfiguration {
    public HealthEndpointAutoConfiguration() {
    }
}

The key point here is the introduced HealthEndpointConfiguration class

@Configuration
class HealthEndpointConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnEnabledEndpoint
    public HealthEndpoint healthEndpoint(ApplicationContext applicationContext) {
        return new HealthEndpoint(HealthIndicatorBeansComposite.get(applicationContext));
    }

}

This class only constructs a class HealthEndpoint, which we can understand as a spring MVC Controller, that is, the Controller that handles the following requests

http://ip:port/actuator/health

First, let's take a look at the object passed in by its constructor

public static HealthIndicator get(ApplicationContext applicationContext) {
        HealthAggregator healthAggregator = getHealthAggregator(applicationContext);
        Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
        indicators.putAll(applicationContext.getBeansOfType(HealthIndicator.class));
        if (ClassUtils.isPresent("reactor.core.publisher.Flux", null)) {
            new ReactiveHealthIndicators().get(applicationContext)
                    .forEach(indicators::putIfAbsent);
        }
        CompositeHealthIndicatorFactory factory = new CompositeHealthIndicatorFactory();
        return factory.createHealthIndicator(healthAggregator, indicators);
    }

As we imagined, it is to obtain all the implementation classes of the HealthIndicator interface through the Spring container. I have only a few default and RabbitMQ

Then they are put into one of the aggregated implementation classes, CompositeHealthIndicator

Now that the HealthEndpoint is built, there is only one last step left to process the request

@Endpoint(id = "health")
public class HealthEndpoint {

    private final HealthIndicator healthIndicator;

    @ReadOperation
    public Health health() {
        return this.healthIndicator.health();
    }

}

We just know that this class is built through CompositeHealthIndicator, so the implementation of health method is in this class

public Health health() {
        Map<String, Health> healths = new LinkedHashMap<>();
        for (Map.Entry<String, HealthIndicator> entry : this.indicators.entrySet()) {
          //Loop call
            healths.put(entry.getKey(), entry.getValue().health());
        }
        //Sort result set
        return this.healthAggregator.aggregate(healths);
    }

So far, the implementation principle of SpringBoot's health check has been fully analyzed

Posted by pgudge on Sat, 04 Dec 2021 22:10:47 -0800