Skip to main content

GraalVM Native Image & Docker Layering

This page covers two packaging concerns for Juneau applications: Docker image layering (a concrete recommendation you can apply today) and GraalVM native-image support (a feasibility assessment with a forward plan).

Docker image layering​

Juneau itself ships libraries, not runnable applications, so there is no Juneau-side fat-jar to layer. Layering happens in your deployable application image. Two practical levers:

  • Spring Boot apps — spring-boot-maven-plugin's repackage goal produces a layered jar by default (since Spring Boot 2.4), separating slow-changing dependency layers from your fast-changing application classes. Combined with a multi-stage Dockerfile using java -Djarmode=tools -jar app.jar extract --layers, Docker caches the dependency layer across rebuilds. The Juneau Spring Boot petstore sample is built this way.
  • Embedded-Jetty / plain-jar apps — order your Dockerfile COPY steps coarsest-changing first: copy the dependency jars (which include the stable Juneau artifacts) in an earlier layer than your application jar, so a code-only change re-uploads only the small application layer. The starter projects on the apache/juneau microservice-*-starter branches ship minimal Dockerfiles; a production image can apply the layered COPY shape described above.

Depending on Juneau through the version-aligned BOM and curated bundles keeps the dependency layer stable and reproducible across rebuilds, which is what makes the cache hit reliable.

GraalVM native-image — feasibility verdict​

Juneau's marshalling/bean core is reflection-heavy, so native-image (closed-world AOT) support is non-trivial. A feasibility assessment was performed against the reflective source; the verdict is CONDITIONAL GO.

Why "conditional go", not "no-go"​

All of Juneau's reflection flows through a single wrapper layer — org.apache.juneau.commons.reflect.* (ClassInfo / MethodInfo / FieldInfo / ConstructorInfo) with one setAccessible() choke point in AccessibleInfo — and there is no sun.misc.Unsafe and no bytecode generation anywhere in main source. The blockers are the standard native-image reflection blockers, not anything pathological. Critically, the same MarshallingContext / BeanMeta / ClassMeta model that drives marshalling already computes exactly the constructor + field + accessor set that reachability metadata needs — so a Juneau-authored metadata emitter is realistic. That is what tips the verdict from no-go to conditional-go.

The hard blockers (all standard, all addressable)​

  1. Open-world bean reflection — every marshalled bean's full member set must be registered; application bean types are unknowable at Juneau-build time. There is no "zero-config native Juneau" — consumers always need a generation step.
  2. User-supplied dynamic-proxy interface sets (@Remote clients, interface beans) — proxy metadata can't be derived from Juneau's bytecode; it comes from the GraalVM tracing agent or explicit declaration.
  3. _type dictionary class resolution via Class.forName — classes reachable only through a serialized _type string are invisible to static analysis and must be registered by name.
  4. Reflective swap/converter/DI instantiation through BeanInstantiator.
  5. Classpath resource + NLS ResourceBundle loading — broad but low-risk (regex resource includes cover it).

SPIs / ServiceLoader usage, despite intuition, is the easiest category — finite and statically known.

The maintainer-approved direction is option (b): a native-image-ready juneau-marshall core (JSON/XML/HTML/UON + swaps + dictionaries), driven by a Juneau-authored reachability-metadata emitter built on BeanContext introspection, with the GraalVM tracing agent covering the statically-invisible parts (@Remote/interface-bean proxies, _type dictionary classes). Target the modern unified reachability-metadata.json format, delivered as a Maven plugin goal / module the consumer points at their bean classes (not checked-in static metadata, which silently drifts).

The REST server (embedded Jetty/Tomcat, ServletContainerInitializer, transport providers) is deferred — most of its residual native-image risk lives in third-party dependencies, not Juneau's own code.

Rough effort: a minimal single-bean JSON round-trip to native is S (~1–3 days); a generally-usable native marshalling core is L (~3–6 weeks); the full framework including @Remote proxies + REST server is XL and out of scope.

This assessment is a feasibility verdict, not a shipped feature. The native-image emitter is tracked as a dedicated committed follow-on; native-image builds of Juneau are not yet supported.