Add Microprofile healthchecks programmatically from an extension with CDI 2!
CDI 2 brings bean configurator features which enables to register very easily custom beans without implementing all the annotated/bean API. However, there are some cases it is not as trivial as it looks. Let investigate the health check case using microprofile health specification as base.
The use case is simple: we add though an extension several clients or bean - could be datasources - we want to add health check on programmatically.
Technically it is as easy as writing this kind of extension:
public class MyExtension implements Extension {
void registerBeans(@Observes final AfterBeanDiscovery afterBeanDiscovery) {
if (getCustomBeans().isEmpty()) {
return;
}
final AnnotationLiteral<Readiness> literal = new AnnotationLiteral<Readiness>() {};
getCustomBeans()
.forEach(beanMetadata -> {
// 1
afterBeanDiscovery
.addBean()
.id(getClass().getName() + "#mybean#" + beanMetadata)
.scope(ApplicationScoped.class)
.beanClass(MyBeanType.class)
.qualifiers(new MyQualifier(beanMetadata.id()))
.types(WebTarget.class, Object.class)
.createWith(context -> createBeanInstance(beanMetadata));
// 2
final String checkName = "mybean_" + beanMetadata.id();
afterBeanDiscovery
.addBean()
.id(getClass().getName() + "#check#" + beanMetadata)
.scope(ApplicationScoped.class)
.beanClass(HealthCheck.class)
.qualifiers(literal) // 3
.types(HealthCheck.class, Object.class) // 4
.createWith(context -> (HealthCheck) () -> HealthCheckResponse.named(checkName).up().build()); // 5
});
}
}
- we assume we have a getCustomBeans() method which returns the required bean metadata to build the bean, this is generally done processing annotated types or injection points. Then we just register for each entry a bean instance (here the bean detail is not important).
- For each bean registered, we register a healthcheck as well.
- Since microprofile-health has two check buckets (liveness and readyness) we dispatch it in the right bucket using the related qualifier (@Readiness here).
- To ensure the microprofile-health implementation can capture and call our bean we type it as expected: HealthCheck.
- Finally we implement the check - using lambdas here - just returning up. A real implementation would test the bean. For a http client it would ensure a GET works or for a remote service that the remote health check is accessible for example.
This looks simple and good right? However this does not work as soon as you have > 1 bean/check since both will be ambiguous in CDI context.
In general the health check is typed, it is class implementing HealthCheck so the implementation type is enough to make it no more ambiguous but not here since bean class is always the same.
Indeed we can solve that by generating a fake class at runtime but it is not very elegant nor efficient.
To solve it more elegantly, we will disambiguate the health check beans just adding a qualifier on them.
To ensure the qualifier changes we will use a binding identifier which changes for each check. Here is a potential implementation:
@Qualifier
@Target(TYPE)
@Retention(RUNTIME)
public @interface InternalCheck {
int value();
class Literal extends AnnotationLiteral<InternalCheck> implements InternalCheck {
protected static int ID_GENERATOR = 0;
protected final int id = ++ID_GENERATOR;
@Override
public int value() {
return id;
}
}
}
This is a very trivial qualifier with one binding method and its literal companion class which auto-values the binding parameter (value()) using an increment. Note that the increment as an int works since we will only use it in the extension which is a thread safe context.
Then we go back in our check bean we have in our extension and add this qualifier:
.qualifiers(literal, new InternalCheck.Literal())
Now each bean is different - has two qualifier and the internal check qualifier is different for each of them.
In other words our automatic check registration through an extension is now functional.
This way to implement a SPI with CDI is very powerful and avoids to materialize all the implementation which makes most of the feature transversal and just a win for the end user so don't hesitate to use it in libraries.
Side note: a better implementation would make the internal check qualifier generation deterministic, using the bean metadata if there is a natural identifier there, to ensure the checks can be looked up individually if needed. Here it was not important but it can be neat for some more advanced use cases.
From the same author:
In the same category: