JPMS support & capabilities¶
This page documents Brewlet's support for modularized Java applications (the
Java Platform Module System, JPMS) via entry.mode: "module" and the optional
modulepath.layer.v1+tar layer. The schema and launch behavior are available in
the launch core, CLI, and Maven plugin.
1. TL;DR¶
- Modular JARs are a natural fit for Brewlet. JPMS moves encapsulation and
dependency metadata into the artifact (
module-info.class) but still runs on the node's shared JDK installation viajava --module-path … --module …. It requires no JVM baked into the artifact, so it aligns with Brewlet's core model exactly the way a fat JAR does. - The launch mode is
entry.mode: "module". It tells the shim to emitjava -p <module-path> -m <module>[/<mainClass>]instead ofjava -jar. Single modular JARs need nothing else. - Multi-module apps use an optional module-path layer: a directory of JARs
mounted at
/app/modsthat becomes the--module-path. - Per-application
jlinkimages andjmodartifact payloads are out of scope. Administrators may instead install one shared, centrally controlled jlink runtime per node pool through aNodeProfile.
2. Background: fat JARs vs. JPMS¶
2.1 What a fat / uber JAR is¶
A "fat JAR" (Spring Boot bootJar, Maven Shade, Gradle Shadow) bundles the
application and all its dependencies into a single archive that runs on the
class path:
java -jar app.jar # entry.mode = jar (default)
java -cp app.jar com.acme.Main # entry.mode = classpath
Everything lands in the unnamed module. There is no reliable dependency graph,
no strong encapsulation, and split packages are silently tolerated. This is what
Brewlet supports both forms (internal/runtime/launch.go, entry modes jar and
classpath).
2.2 What JPMS is¶
JPMS (JSR 376, "Project Jigsaw", since Java 9) introduces named modules. Each
module carries a module-info.class compiled from a module-info.java:
module com.acme.orders {
requires com.acme.commons;
requires java.net.http;
exports com.acme.orders.api;
// opens com.acme.orders.model; // reflection (e.g. Jackson)
// uses / provides … with …; // ServiceLoader
}
Modules run on the module path rather than the class path:
java --module-path mods --module com.acme.orders/com.acme.orders.Main
# short form:
java -p mods -m com.acme.orders/com.acme.orders.Main
If the module declares a Main-Class (via jar --main-class …), the launch
class can be omitted: java -p mods -m com.acme.orders.
Key properties JPMS adds over the class path:
| Property | Effect |
|---|---|
| Reliable configuration | Missing/duplicate modules fail fast at startup, not with a NoClassDefFoundError deep in a request. |
| Strong encapsulation | Only exports/opens packages are reachable; internal packages are hidden even via reflection unless opens. |
| Explicit services | uses/provides replaces META-INF/services scanning. |
| Smaller, auditable surface | The module graph is declared, so tooling (and jlink) can reason about exactly what is needed. |
2.3 Three things people mean by "modular"¶
These are frequently conflated; Brewlet treats them very differently:
| Term | What it is | Runs on node JDK? | Brewlet fit |
|---|---|---|---|
| Modular JAR | An ordinary JAR with a module-info.class at its root. |
✅ Yes — java -p … -m …. |
In scope (this doc). |
jmod file |
A build/link-time package format (native libs, config, headers). Not runnable with java -jar/-m; consumed by jlink. |
❌ | Out of scope. |
jlink runtime image |
A self-contained Java runtime containing a selected module set. | ✅ when installed as a shared NodeProfile runtime. |
Supported as a platform-managed runtime; not as an application artifact. |
3. Alignment with the Brewlet model¶
Brewlet's thesis: ship only the developer's bytecode; the JDK installation lives on the node, shared and patched centrally (see the project landing page and concepts). Testing each JPMS flavor against that thesis:
- Modular JAR + module path — aligns perfectly. The artifact is still just
JAR bytes; the module path is resolved against the JAR(s) we already mount at
/app, and the node JDK does the launching. Nothing about JPMS requires a bundled runtime. The difference is the argv the shim assembles. jlinkimage — placement matters. Shipping one in every application artifact re-introduces the duplicated JVM Brewlet removes. Installing one shared runtime throughNodeProfile, however, keeps patching and the approved module set under central platform control.jmod— not a runtime artifact at all. It only feedsjlink. No runtime support is meaningful.
Conclusion: Brewlet supports modular JARs launched on the module path and
shared administrator-provided jlink runtimes. It does not ship jlink runtimes or
.jmod files inside application artifacts.
4. Capability matrix¶
| Capability | Without JPMS support | With JPMS support | Notes |
|---|---|---|---|
Fat JAR (java -jar) |
✅ | ✅ | entry.mode: jar (default). |
Class-path main (java -cp) |
✅ | ✅ | entry.mode: classpath. |
Single modular JAR (java -p app.jar -m mod) |
❌ | ✅ | New entry.mode: module. |
Multi-JAR module path (-p /app/mods) |
❌ | ✅ | Needs the optional module/classpath layer (§6). |
| Mixed class path + module path | ❌ | ✅ | entry.classPath + module path (§6.3). |
Automatic modules (plain JAR on -p) |
❌ | ✅ | Works once a module path exists; name from Automatic-Module-Name or filename. |
--add-modules / --add-opens / --add-reads |
✅¹ | ✅ | ¹ --add-modules, --add-opens, and --add-exports are first-class artifact fields (addModules, addOpens, addExports); --add-reads and other exotic flags go through descriptor jvm.args. |
Shared NodeProfile jlink runtime |
N/A | ✅ | Platform-managed runtime and module set; application remains a JAR. |
Per-application jlink runtime |
❌ | ❌ (by design) | Duplicates the JVM in the application artifact. |
jmod packaging |
❌ | ❌ (by design) | Build-time only. |
5. Launch config: entry.mode: "module"¶
The launch config (SPECIFICATION §4.2 / reference) supports a
third entry mode. Existing configs omit it and get jar.
{
"schemaVersion": 1,
"mainJar": "orders.jar",
"entry": {
"mode": "module", // "jar" | "classpath" | "module"
"module": "com.acme.orders", // required when mode == "module"
"mainClass": null, // optional; selects the module's main class (omit if declared)
"modulePath": null // optional; defaults to the app dir (see §6)
}
}
Resolved argv:
| Config | Emitted command |
|---|---|
mode: module, single mainJar |
java -p /app/orders.jar -m com.acme.orders |
mode: module + mainClass |
java -p /app/orders.jar -m com.acme.orders/com.acme.orders.Main |
mode: module + modulePath: mods |
java -p /app/mods -m com.acme.orders[/…] |
The change to the launch core (BuildJVMArgs in internal/runtime/launch.go)
is a single new case "module" in the existing switch cfg.Entry.Mode, mirroring
the existing jar/classpath cases:
case "module":
if cfg.Entry.Module == "" {
return nil, fmt.Errorf("entry.mode=module but entry.module is empty")
}
mp := modulePath(cfg, jarPath) // modulePath override or the mounted jar/dir
target := cfg.Entry.Module
if cfg.Entry.MainClass != "" {
target += "/" + cfg.Entry.MainClass
}
args = append(args, "-p", mp, "-m", target)
No shim, provisioner, RuntimeClass, or operator change is required for the single modular JAR case — the artifact and mount path are unchanged; only the argv differs.
6. Multi-module apps: the optional module/classpath layer¶
A real modular app is usually several JARs (the app module + library modules). Brewlet carries those dependencies in an optional module-path layer:
6.1 Artifact format¶
The artifact carries the main JAR layer
(application/vnd.brewlet.jar.layer.v1+jar) and can carry an optional tar layer
of dependency JARs (application/vnd.brewlet.modulepath.layer.v1+tar) that the
shim unpacks to /app/mods.
This tar-of-JARs layer is the module-path twin of the class-path
classpath.layer.v1+tardesigned in the layered classpath deployment note; the two can use the same mechanism (unpack a tar of JARs to a well-known dir, feeding-phere vs.-cpthere). See that note's §8 for the mapping.
6.2 Mounting & launch¶
/app/orders.jar # mainJar (the app module)
/app/mods/ # library modules (from the mods layer)
├── commons.jar
└── json.jar
entry.modulePath (when set) is resolved relative to /app; when unset it
defaults to mainJar for the single-JAR case, or /app/mods when a mods layer is
present.
6.3 Mixed class path + module path¶
Some apps run library JARs on the class path and only the app (plus modular
dependencies) on the module path — e.g. a JPMS app that also depends on
automatic-module or non-modular libraries best carried on the class path. Brewlet
supports this by keeping entry.mode: module and additionally permitting
entry.classPath: the launcher emits -cp <classPath> before
-p <modulePath> -m <module> so the terminal -m stays last:
Both a classpath.layer.v1+tar (→ /app/lib) and a modulepath.layer.v1+tar
(→ /app/mods) may ship together. See
layered deployment §8.1 for the full example
and CLI usage. This supports applications that mix modular and non-modular
dependencies.
6.4 JDK matching is descriptor-driven¶
Module resolution happens entirely inside the node JDK. The existing
spec.jvm.version / brewlet.sh/jdk matching (and NoCompatibleJDK admission
event) already covers "the module app needs JDK N"; nothing new is required. (One
caveat: module-info.class is versioned by the compiler --release, so the
descriptor should keep the requested feature ≥ the compile release.)
7. Tooling implications¶
The tooling implements module detection and layout end to end:
brewletCLI (push). When generating a launch config it detects amodule-info.classat the JAR root (equivalentlyjar --describe-modulesucceeds with a named module); when present it defaultsentry.modetomoduleand reads the module name and itsMain-Class, falling back tojar/classpathotherwise (cmd/brewlet/main.go). This mirrors the manifest-based inference in the Maven plugin'sJarInspector(entryMode()).- Maven plugin.
JarInspectordetects a module descriptor (the JDK'sjava.lang.module.ModuleDescriptor.read(...)over the JAR'smodule-info.class) and setsentry.mode=modulewith the derived module name (brewlet-maven-plugin/.../util/JarInspector.java). Thelayered=trueflag assembles themodslayer (modulepath.layer.v1+tar) from the project's resolved runtime module dependencies and setsentry.modulePath=[mainJar, "mods"]— the module-path twin of the layered classpath deployment. - Docs. The "modular apps" example lives in
building & publishing and the
entry.moderow (jar | classpath | module) in reference.
Ship a modular JAR, not a
.jmod. A Maven build targeting Brewlet'smodulemode should produce an ordinary.jarthat happens to contain amodule-info.classat its root (the default output of a normalmaven-compiler-plugin/maven-jar-pluginbuild with amodule-info.javain the module). It should not produce a.jmodfile (via thejmodtool) or a per-applicationjlinkruntime image (viamaven-jlink-plugin/jpackage):.jmodis a link-time-only package format thatjava --module-path/-mrefuses to run, and a per-applicationjlinkimage duplicates the JVM. In short, the shippable JPMS artifact is a modular JAR; a platform team may separately provision a shared jlink runtime throughNodeProfile.
8. References¶
- JEP 261: Module System
- JSR 376: Java Platform Module System
javalauncher:--module-path/--module,jar --describe-module- Layered classpath deployment — the class-path counterpart (thin JAR + dependency layers for registry dedup)
- Brewlet: SPECIFICATION §4 (artifact), building & publishing, reference