Principle of SpringBoot Health Examination

I believe that all the students who read the previous article know the routine of SpringBoot automatic assembly. Look directly 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 the file under the package org. spring framework. boot. spring - boot - actuator - autoconfigure

automatic assembly

Looking at this file, we find that a lot of configuration classes have been introduced. Let's first look at the classes of the XXX Health Indicator AutoConfiguration series. Let's take the first Rabbit Health Indicator AutoConfiguration as an example. As you can see from the name, this is RabbitMQ's automatic configuration class for health checks.

@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);
    }
}

In accordance with past practice, the annotations should be analyzed first.

  1. @ The Conditional OnXXX series has appeared again. The first two are that if there is a Rabbit Template bean, that is to say, Rabbit MQ is used in our project to continue.
  2. @ Conditional On Enabled Health Indicator This annotation is obviously a Spring Boot actuator custom annotation, 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 top entry method is the matches method of the SpringBootCondition class, and the getMatchOutcome method is the subclass OnEndpointElementCondition. This method first looks for the existence of the management.health.rabbit.enabled attribute in the environment variable, and if not, for the management.health.defaults.enabled attribute. If this attribute does not already exist, then set it up. Set the default value to true

When true is returned here, the automatic configuration of the entire RabbitHealthIndicator AutoConfiguration class can continue.

  1. @ In that case, look at the HealthIndicator AutoConfiguration class before you come 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 HealthIndicator Properties, which is a configuration class related to system state.

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

    private List<String> order = null;

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

Then you register two beans, Application Health Indicator and Ordered Health Aggregator.
The role of these two bean s will be discussed later, and now we return to the RabbitHealth Indicator AutoConfiguration class.

  1. @ AutoConfigureAfter has no effect on the overall logic, let alone mention it.
  2. A bean HealthIndicator is registered in the class. The creation logic of the 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, we inject an object called HealthAggregator, which is the Ordered HealthAggregator just registered.
  2. The first createHealthIndicator method executes the logic as follows: if the size of the incoming beans is 1, call createHealthIndicator to create HealthIndicator
    Otherwise, create Composite Health Indicator, traverse the incoming beans, create HealthIndicator in turn, and add it to Composite Health Indicator
  3. The execution logic of the second createHealth Indicator is to obtain the generic parameters in Composite Health Indicator Configuration. According to the class corresponding to the generic parameter H and the corresponding class corresponding to S, the constructor declaring that the parameter is S type is found in the corresponding class of H for instantiation.
  4. Finally, the bean created here is Rabbit Health Indicator
  5. Recalling that when we used to learn health checks before, if we needed to customize health checks, the general operation was to implement the HealthIndicator interface, so we can guess that Rabbit Health Indicator should do the same. Looking at the inheritance relationship of this class, you can see that this class inherits a class AbstractHealthIndicator that implements this interface, while RabbitMQ's monitoring and checking process is shown in the following code
    //This method is AbstractHealth Indicator's
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 RabbitHealth Indicator class
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 a series of operations above, we actually came up with a Harvest Indicator implementation class of RabbitMQ, which is responsible for checking the health of RabbitMQ. So we can imagine that if there are MySQL, Redis, ES and so on in the current environment, it should be the same operation.

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

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

The operation described above is in the class HealthEndpoint AutoConfiguration, 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 point here is to introduce the HelthEndpoint Configuration class

@Configuration
class HealthEndpointConfiguration {

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

}

This class simply builds a HealthEndpoint class, which we can understand as a Controller for Spring MVC, which handles the following requests

http://ip:port/actuator/health

So let's first look at what object its constructor is passing in.

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 through the Spring container to get all the implementation classes of the HealthIndicator interface. I have only a few default and RabbitMQ implementations here.

They are then put into one of the aggregated implementation classes, Composite Health Indicator.

Now that HealthEndpoint is built, there's 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();
    }

}

As we just know, this class is built through Composite Health Indicator, so the implementation of the 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 sets
        return this.healthAggregator.aggregate(healths);
    }

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

Tags: Java Spring RabbitMQ Attribute SpringBoot

Posted on Wed, 09 Oct 2019 19:30:08 -0700 by fantomel