This page covers Vidocq Runtime usage beyond the hello world: the application entry point, packaging via the vidocq-runtime-maven-plugin (jlink, jpackage, Docker), configuration via https://microprofile.io/specifications/microprofile-config/, observability (https://microprofile.io/specifications/microprofile-health/, https://microprofile.io/specifications/microprofile-metrics/, https://microprofile.io/specifications/microprofile-open-api/), profiles, and AOT preparation.

The application entry point — @VidocqMain

The official application main is a trampoline: it contains nothing but Vidocq.run(…​).

@VidocqMain
public final class App implements VidocqApp {

    public static void main(String[] args) {
        Vidocq.run(App.class, args);      // no business logic before this line
    }

    @Override
    public int run(String... args) throws Exception {
        // runs AFTER boot, inside the Vauban class loader, with transformed classes
        Vidocq.waitForExit();             // block for a server application
        return 0;
    }
}

Why a trampoline? Because Vidocq does not let the application main "own" the runtime: Vidocq.run() re-resolves the application into a child module layer defined by the Vauban class loader, where every class flows through the load-time transformations (client-proxy weaving…) at definition. This is what makes a plain IDE launch — right-click → Run, everything on the module path, no special configuration — behave exactly like a production launch: same layer, same transformed classes, no instrumentation agent, no JVM warning.

The contract (deliberately identical to Quarkus' @QuarkusMain):

  • no business logic before Vidocq.run(…​) — the trampoline class is loaded by the JVM’s boot layer while the application runs in the Vauban layer; code executed before run() lives in the wrong class loader and sees untransformed classes;

  • application logic that must run after boot goes into a VidocqApp passed to Vidocq.run(Class, args) — the class is re-loaded through the layer and instantiated there (as a CDI bean when it is one); its return value is the exit code;

  • a plain server without post-boot logic just calls Vidocq.run(args).

Vidocq.run() picks the application modules of the boot layer automatically (the caller’s module, every non-runtime module carrying META-INF/vauban-beans.list, and the modules owning the beans that vidocq:generate scanned from dependencies); override with -Dvidocq.app.modules=<name,name> or an explicit -Dvidocq.app.path=<archives>. A legacy main that boots the runtime directly still works — unwoven beans are then covered by the load-time weaving agent (with the JDK’s dynamic-agent warning) and the log suggests migrating to the trampoline.

One more benefit: applications never declare generated providers in module-info.java. The annotation processors emit standard META-INF/services files (_VaubanComponents, Cassini CassiniAdapter`/`CassiniRoutes…), and the Vauban layer promotes them to synthetic provides when it resolves the module — your module-info.java stays free of any generated class name.

custom JUL log handlers declared in the application’s logging.properties must live on the module path — LogManager instantiates handlers through the system class loader, which cannot see the layer.

Packaging with vidocq-runtime-maven-plugin

The Maven plugin exposes three packaging targets. See the dedicated page for the full inventory and the internal JLINK.md details.

Goal Output Typical startup

vidocq:jlink

target/dist/ (binary + embedded Java runtime, ~40 MB)

~4 s

vidocq:jpackage

target/installer/<name>.app or .exe/.msi/.deb/.rpm

~1 s (CDS)

vidocq:docker

target/Dockerfile distroless based on distroless/base-debian12:nonroot

~50 MB image

Quick start (from vidocq-runtime-cassini-rest-example):

cd vidocq-runtime-examples/vidocq-runtime-cassini-rest-example
./mvnw -ntp package -DskipTests
./target/dist/bin/todo-app

jlink and jpackage require every dependency to be a named module. When some third-party jars ship without module-info.class, run vidocq:modularize beforehand: it generates their descriptors into target/vidocq-modularized/, where dev, jlink and package pick them up automatically. See the vidocq:modularize goal.

Multi-module applications (scanned dependencies)

Hexagonal and multi-module applications typically keep their business and adapter modules spec-only — Jakarta EE / MicroProfile APIs only, no Vidocq dependency, no Vidocq APT — and concentrate everything runtime-specific in a small main module. That main module asks vidocq:generate to scan the app modules and produce the CDI/REST glue for them:

<execution>
    <id>generate</id>
    <goals><goal>generate</goal></goals>
    <configuration>
        <scanDependencies>
            <scanDependency>com.acme:my-rest-adapter</scanDependency>
            <scanDependency>com.acme:my-domain</scanDependency>
        </scanDependencies>
    </configuration>
</execution>

Two rules for each scanned module’s module-info.java — and remember that the Java module system only lets a module opens (or exports) packages it owns: you cannot open another module’s package from the main module, which is why the single-module opens shown in Getting started must move into the module that contains the classes.

module com.acme.rest.adapter {
    exports com.acme.rest;          // resource classes
    exports com.acme.rest.dto;      // JSON-B reads records via their public accessors
    requires jakarta.cdi;
    requires jakarta.ws.rs;

    // The consuming runtime generates/reflects the CDI plumbing for these beans.
    // Unqualified `opens` names no runtime module: the module stays spec-only,
    // and classpath-based runtimes ignore it entirely.
    opens com.acme.rest;
}
New in 0.3.0-SNAPSHOT

The classes that vidocq:generate produces for scanned dependencies (client proxies, interceptors) live in packages owned by those dependencies. Since 0.3.0 the plugin keeps them out of the main module automatically:

  • vidocq:generate parks them in target/vidocq-patches/<artifactId>/ (log line: JPMS: parked N generated class(es) of '…');

  • vidocq:package and vidocq:jlink copy/stage enriched dependency jars that carry their own generated classes — the packages already exist in those jars, so their module descriptor stays exact and the module path never sees a split package;

  • vidocq:dev adds the matching --patch-module options to the child JVM automatically.

No extra configuration is needed beyond <scanDependencies>.

On 0.2.0 (released)

Vidocq 0.2.0 leaves the generated classes in the main module’s target/classes, which creates an illegal split package on the module path: the boot layer fails with ResolutionException: Module <main> contains package <pkg>, module <dep> exports package <pkg> — or, if the dependency is not requires-ed, its beans are silently absent (HTTP 404). The workaround on 0.2.0 is to do by hand what 0.3.0 does natively:

  1. move the foreign-package classes out of target/classes before the jar is built (e.g. maven-antrun-plugin at prepare-package — moving them after the fact is not enough, because maven-jar-plugin records the packages in the ModulePackages attribute of module-info.class);

  2. launch with --patch-module <dep.module>=<dir> for each dependency, e.g. via the <jvmArgs> of the package goal for the generated launch scripts.

MicroProfile configuration

Vidocq Runtime implements https://microprofile.io/specifications/microprofile-config/. Source hierarchy, highest to lowest priority:

Ordinal Source Description

400

SystemPropertiesConfigSource

-Dkey=value

300

EnvConfigSource

Environment variables (KEY_NAME)

250

ExternalFileConfigSource

${java.home}/conf/vidocq.properties (jlink image)

100

PropertiesFileConfigSource

classpath vidocq.properties

ExternalFileConfigSource lets you override a deployed jlink/jpackage binary without rebuilding, simply by editing the conf/vidocq.properties file next to the launcher. A boot log line traces the effective load.

Health

@Liveness
@ApplicationScoped
public class DatabaseHealth implements HealthCheck {
    @Inject DataSource ds;

    @Override
    public HealthCheckResponse call() {
        try (Connection c = ds.getConnection()) {
            return HealthCheckResponse.up("database");
        } catch (SQLException e) {
            return HealthCheckResponse.down("database");
        }
    }
}

Endpoints exposed by vidocq-runtime-knock-health-extension: /health, /health/live, /health/ready, /health/started (see TCK).

Metrics

https://microprofile.io/specifications/microprofile-metrics/ is shipped by the vidocq-runtime-dirac-metrics-extension extension (127 MicroProfile Metrics TCK tests green). Endpoints: /metrics, /metrics/{scope} and /metrics/{scope}/{name}.

OpenAPI

The vidocq-runtime-grimm-openapi-extension extension serves the OpenAPI document, generated from Jakarta REST + MicroProfile OpenAPI annotations, at /openapi; the companion vidocq-runtime-grimm-openapi-ui-extension serves Swagger UI at /openapi/ui.

Profiles

MicroProfile convention: prefix a key with %<profile>. to scope it.

vidocq.http.port=8080
%dev.vidocq.http.port=8081
%prod.vidocq.http.port=80

Activate via the -Dvidocq.profile=dev system property (there is no environment-variable equivalent).

Docker deployment

Minimal distroless image produced by vidocq:docker:

./mvnw -ntp package -Dvidocq.docker.build=true
docker run --rm -p 8080:8080 \
    -e VIDOCQ_CONFIG_DIR=/etc/myapp \
    -v $(pwd)/conf:/etc/myapp:ro \
    example/my-app:1.0.0

The distroless/base-debian12:nonroot base image has no shell, no package manager — minimal attack surface. The nonroot user (UID 65532) is enforced.

AOT — GraalVM and Leyden

  • Leyden CDS — supported out of the box. Static Class-File API codegen produces bytecode that archives perfectly.

  • GraalVM native-image — Vidocq Runtime’s zero-reflection + zero-proxy strategy minimises reachability-metadata. See vidocq/JLINK.md for runtime prerequisites (logging.properties, named Java modules, serialised records).

Next steps