Grimm refuses per-request classpath scanning: all the work happens once, at CDI container startup, and the result is cached. This page describes the exact pipeline sequence, the role of the Vauban BCE, and the threading invariants.
grimm-core / grimm-cdi-vauban separation
grimm-core is plain Java. It knows nothing about CDI or JAX-RS runtime — only the spec types (@Path is read as an annotation, not as a handler). It is usable outside any container, for example by a Maven tool that produces an OpenAPI document at build time.
grimm-cdi-vauban is the integration layer: it plugs grimm-core into the Vauban Build Compatible Extension, exposes the /openapi resource, and supplies the Supplier<OpenAPI> consumed by downstream tools.
Build pipeline
Six steps, executed once when the container activates (ModelBuilder in grimm-core), with the result stored in GrimmModelCache:
-
StaticFileReader— searches forMETA-INF/openapi.yaml,.yml, then.json. Read through the current classloader. ReturnsOptional<OpenAPI>. -
ModelReaderInvoker— ifmp.openapi.model.readeris set, instantiates the class (CDI lookup preferred, no-arg constructor as fallback) and callsbuildModel(). -
Annotation source — compile-time
$$GrimmModelfragments contributed bygrimm-processorare folded in first (ContributionRegistry+FragmentMerger);AnnotationScannerthen walks the remaining application classes recorded by the BCE —@Pathresources as well as@SchemaPOJOs and theApplicationsubclass — and builds a partialOpenAPI. The output is tagged byAnnotationSource(sealed sub-type ofModelSource). -
ModelMerger— merges the threeModelSourceinstances (sealed:StaticFileSource,ReaderSource,AnnotationSource) into a singleOpenAPI, with the spec priority annotations > reader > static file. -
ConfigApplier— applies the configuration overrides on the merged document:mp.openapi.servers,mp.openapi.servers.path.<path>,mp.openapi.servers.operation.<operationId>, and themp.openapi.schema.<FQCN>schema overrides (registered before the scan so that references resolve, applied here). -
FilterInvoker— ifmp.openapi.filteris set, loads theOASFilter(CDI lookup or no-arg constructor) and walks it across the tree. The filter is dispatched recursively via a typed visitor that `switch`es on the sealed model types.
The final OpenAPI is then stored in GrimmModelCache: a one-time initialisation guarded by a ReentrantLock, published through a volatile field.
Vauban BCE: GrimmExtension
The class discovery lives in io.vidocq.grimm.cdi.GrimmExtension, a Build Compatible Extension that uses exactly two CDI 4.1 Lite hooks — no @Synthesis, no synthetic bean:
@Discovery
public void resetForNewDeployment(ScannedClasses classes) {
// One container boot = one deployment: drop everything collected
// by a previous boot in the same JVM before this deployment's
// @Enhancement phase runs.
}
@Enhancement(types = Object.class, withSubtypes = true)
public void registerApplicationClass(ClassConfig classConfig) {
// Records every application class found by the CDI scanner —
// spec §4.4: the annotation scan covers all application classes,
// not only @Path resources. Grimm's own classes (io.vidocq.grimm.*)
// are skipped so the /openapi endpoint never documents itself.
}
The recorded class names are handed to the pipeline through plain CDI producers: GrimmConfigProducer @Produces the GrimmConfig (read from MicroProfile Config) and the ScannedTypes holder built from GrimmExtension.discoveredTypes(). GrimmModelCache is a regular @ApplicationScoped bean — no synthesis involved — whose @PostConstruct runs the pipeline once; getDocument() simply returns the cached reference.
Threading model
One-time locking, volatile reads
GrimmModelCache stores the document in a volatile field, initialised once in @PostConstruct behind a ReentrantLock (double-checked: initialize() is idempotent and subsequent calls are no-ops). After that build, getDocument() is a single volatile read on the fast path — the lock is only re-taken to lazily create an empty skeleton if the cache was never initialised. No synchronized anywhere, so a virtual thread never pins its carrier. The BCE guards its discovered-names set the same way, with a ReentrantLock held only during the container build phase.
Virtual threads for potentially blocking operations
Invoking an application-supplied OASModelReader or OASFilter may, in theory, trigger blocking calls (file or database reads). When the host runtime (Cassini + Chappe) runs on the default virtual executor (Executors.newVirtualThreadPerTaskExecutor()), these invocations inherit the virtual context — Grimm creates no platform-thread pool of its own.
YAML and JSON serialization
grimm-core ships two hand-written serializers (no SnakeYAML, no Jackson):
-
JsonSerializer— emits raw JSON 2020-12, dependency-free. -
YamlSerializer— emits OpenAPI 3.1-compatible YAML 1.2 with two-space indentation and block scalars for multi-line strings.
Symmetrically, JsonDeserializer and YamlDeserializer consume the static files.
OpenApiModelMapper and OpenApiValueMapper translate between the serialised tree and the internal OpenAPI model.
/openapi endpoint
OpenApiResource is an @ApplicationScoped JAX-RS bean in grimm-cdi-vauban. Its constructor injects GrimmModelCache. The getOpenApi(format, accept) method:
-
Resolves the format via
?format=(priority, spec §2.3) thenAccept(spec §2.2, YAML default). -
Reads the document from the cache.
-
Serialises it to YAML or JSON depending on the resolved format.
-
Returns a
Responsewith the matchingContent-Type.
No lock is taken; serialisation itself can parallelise safely.