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, with the policy of Vauban’s Java SE launcher: every module moves except the Vidocq bricks, the platform and Jakarta modules, automatic modules, modules with a javax., sun. or com.sun. package, and what those kept modules read. The caller’s module always moves. CDI-agnostic libraries move too, so Vauban places the client proxies that must live in their packages, with zero opens. Override with -Dvidocq.app.modules=<name,name> or an explicit -Dvidocq.app.path=<archives>.

NEW When the selection finds no application module to move, the application stays in the boot layer, and there Vidocq cannot give itself access to the VidocqApp class it has to instantiate. The boot now fails with a message that names the class, its package and the two fixes, instead of a bare IllegalAccessException: add exports <package> to io.vidocq.runtime.core; to the application’s module-info.java, or make the class a CDI bean (@ApplicationScoped or @Dependent), which the container creates itself (Vidocq/vidocq#201). In the application layer, the usual case, Vidocq adds that export by itself.

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 vauban:modularize beforehand: it generates their descriptors into target/vauban-modularized/, where dev, jlink and package pick them up automatically. See the vauban: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

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:generate then writes an enriched copy of each such jar in target/vidocq-enriched/, carrying its own generated classes, with a module descriptor that declares its providers — the packages already exist in those jars, so the module path never sees a split package;

  • vidocq:dev, vidocq:run, vidocq:package and vidocq:jlink use that copy in place of the original; only a jar that cannot be given a descriptor keeps its original, and vidocq:dev attaches its classes with --patch-module.

No configuration is needed: see which jars vidocq:generate scans.

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.

Dev console NEW

While you develop, run mvn vidocq:dev: it adds the console itself when the application has vidocq-runtime-chappe-webserver-extension, and never packages it. The application logs Vidocq dev console: http://127.0.0.1:8888/, a read-only page with the startup report of the running boot and the live values of the extensions — the connections of each Mansart pool, the heap, the threads, the garbage collectors. It is on in a dev launch only, on the loopback address. See Dev console; an extension adds its own panel as Writing a dev console panel describes.

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