Knock rests on a simple principle: implement MicroProfile Health 4.0 with the JDK and Jakarta specs alone. No third-party library, no reflection-heavy machinery, no platform-thread pool. This page documents the implementation choices that uphold that discipline.
Module architecture
| Module | Responsibility |
|---|---|
|
Vidocq fork of the MicroProfile Health API, repackaged with a |
|
Knock SPI: |
|
Standalone runtime: registry, aggregator, JSON-P serialiser, response builder, |
|
CDI-driven auto-registration of qualified checks on Vauban. |
|
Jakarta REST resource exposing |
|
Official MicroProfile Health 4.0 TCK runner — joins the reactor only under the |
Runtime flow
-
KnockAggregator.aggregate(HealthCheckRegistry, ProbeType)pulls the checks for the requested probe viaHealthCheckRegistry.getChecks(ProbeType). -
Each
HealthCheck.call()runs on its own virtual thread; the responses are reduced to aHealthSnapshot(spec §3.2 rules). -
KnockJsonSerializer.serialize(HealthSnapshot)renders the snapshot to a JSONStringthrough Jakarta JSON-P;httpStatus(HealthSnapshot)maps UP/DOWN to 200/503. -
NEW After the run,
KnockAggregatorrecords each answer in the registry as aCheckResult, under the probe the check is registered for (a/healthrequest records each check under its own probe), timestamped by an injectableClock. Recording reuses the answer: no check is called twice. See Last results. -
KnockHealthService.report(ProbeType)wraps both in aHealthReport(int httpStatus, String json)record.
Core classes
| Class | Role |
|---|---|
|
|
|
Applies the §3.2 aggregation rule and folds exceptions into |
|
Builds the response with Jakarta JSON-P ( |
|
|
|
|
|
Immutable aggregation result: probe type, overall status, checks list. |
|
Exported factory — |
|
Public facade tying registry + aggregator + serialiser together. |
|
HTTP response wrapper: status code + serialised JSON. |
CDI discovery
knock-cdi-vauban ships two internal pieces:
-
HealthCheckRegistrar— an@ApplicationScopedbean whose observer methodonApplicationStart(@Observes @Initialized(ApplicationScoped.class) Object)registers each qualifiedHealthCheckinto the registry by probe type, under the check’s class name (spec §4.1). It injectsInstance<HealthCheck>qualified with@Liveness,@Readinessand@Startup; aHealthCheckbean with none of those qualifiers is never selected and is silently ignored (spec §4.2). -
KnockCdiHealthCheckRegistry— the@ApplicationScopedregistry bean, injectable asHealthCheckRegistry, delegating to an instance obtained fromHealthCheckRegistries.newRegistry().
There is no BuildCompatibleExtension: both beans are instantiated, field-injected and notified through the _VaubanComponents provider generated at compile time by the Vauban annotation processor and declared as provides io.vidocq.vauban.api.VaubanComponentProvider in module-info.java — no runtime classpath scanning, no setAccessible(true), no opens to the container.
JAX-RS resource
KnockHealthResource is annotated @Path("/health") and exposes the four endpoints. Each method maps its path to a ProbeType, calls KnockHealthService.report(…) on the injected HealthCheckRegistry, and returns a Response with the report’s HTTP status. The resource uses only the standard jakarta.ws.rs and CDI APIs — never a Cassini-internal package — and knock-jaxrs declares its own generated _VaubanComponents provider to wire the resource bean.
Threading model
All check execution goes through Executors.newVirtualThreadPerTaskExecutor() (JEP 444). Consequences:
-
a slow check never blocks the rest of a probe group;
-
no platform-thread pool, no bounded queue, no
ThreadLocal; -
shared state (the registry) uses
ConcurrentHashMapexclusively.
Java Modules workaround (ADR-001)
The upstream MicroProfile Health API jar ships without a module-info.class, which breaks strict Java Modules and jlink. Knock forks it as knock-mp-health-api and rebuilds it with a proper module descriptor. Every Knock module keeps its module-info.java in src/main/java and its tests run on the module path; the earlier build workaround (a separate src/main/module-info/ source root compiled at prepare-package) is gone, with the same module descriptors. The SPI is registered twice — through Java Modules provides and META-INF/services — so the implementation resolves on both the module path and the classpath. Full rationale: docs/adr/ADR-001-java-modules-workaround-microprofile-health.md.
AOT compatibility
No dynamic proxy generation, no setAccessible(true), no hot reflection in the request path. Knock is native-image friendly and works inside a minimal jlink image.
Sources
-
knock-core — registry, aggregator, serialiser, facade.
-
knock-cdi-vauban — CDI discovery.
-
knock-jaxrs — REST endpoints.
-
ADR-001 — Java Modules workaround.