How it works
The Introduction said the launcher reconstructs, in process, what java -p modulepath -cp classpath -m module/ would have done. This chapter shows how: the shape of the jar it reads, the sequence it runs at
start-up, the one class loader it builds, and how it serves classes and resources without holding their
bytes.
The executable-jar layout
A launcher jar is an ordinary jar whose Main-Class is the launcher, plus a fixed set of entries the
launcher knows how to read:
app.jar
├── META-INF/MANIFEST.MF # Main-Class: build.jenesis.launcher.Launcher
├── build/jenesis/launcher/… # the launcher's own classes
├── application.properties # the descriptor: mainClass, mainModule, classpath, modulepath
└── jars/
├── demo.app-0-SNAPSHOT.jar/… # the application's own module, exploded
├── org.slf4j-2.0.16.jar/… # a modular or automatic dependency, exploded
└── <group>%2F<artifact>%2F<version>.jar/… # a dependency that names no module, exploded
Each dependency is exploded into its own subfolder, so nothing is merged: every one keeps its own
module-info, META-INF/ files and resources. Each class is then a direct entry of the outer jar,
readable with a plain java., so there is no nested-jar addressing.
The subfolder name is the file name the dependency had when the build resolved it. The Jenesis build tool
names a resolved jar after the module it carries, at the version the closure resolved
(org.), names an aliased jar <alias>-<version>. - or <alias>. when a module path
could not derive that version from the file name - and falls back to the URL-encoded coordinate
(<group>%2F<artifact>%2F<version>.) for a jar that declares no module at all. It names the application's
own jar by the same rule, after its module and the project's version - 0-SNAPSHOT when a project published
to Maven declares none - or after its coordinate without a module. The name follows what the jar declares, not where it lands: a jar with an
Automatic-Module-Name is named for that module even when it goes on the class path.
Every dependency sits in that one jars/ store, and the descriptor decides how each is read: the
modulepath key names the entries resolved as modules - the jars that describe one, when the application is
modular - and classpath names the rest. A non-modular application therefore lists every dependency under
classpath and builds no module layer at all.
The descriptor
application. is the small text file that tells the launcher what to run. The build tool writes
four keys:
| Key | Meaning |
|---|---|
mainClass |
the fully qualified class whose main is invoked |
mainModule |
the module owning mainClass, when the application is modular |
classpath |
the jars/ entries to read as a class path, in search order |
modulepath |
the jars/ entries to resolve as modules; empty when the application is not modular |
A project that keeps a dependency private in a module layer also gets that layer's modulepath. key,
and a classpath. key when the layer holds jars with no module identity. The launcher understands a
few more - bundled agents, module-access grants, and signer reconstruction - which are for a jar you
assemble yourself. The Reference chapter lists them all.
How a launch proceeds
Running java -jar app. starts the launcher's main, which then:
- finds itself - it locates the running jar from its own
CodeSourceand opens it. A packaged jar and an exploded directory of the same layout both work. - reads the descriptor and indexes the entries - it loads
application.and records the entry names under eachproperties jars/subfolder. It also reads each dependency's manifest, and for a jar the<entry>/ modulepathkey names, itsmodule-info.andclass META-INF/files, since those describe the module. Class bytes are not read here.services - builds one class loader over the entries
classpathnames. This loader's unnamed module is the analogue of everything a-cpclass path would carry. It holds no class bytes, only the index. - reconstructs the module layer, if
modulepathnames any entries. An in-memory module finder resolves them and defines a childModuleLayeragainst the boot layer, mapping every module to that same loader. When amainModuleis declared, the layer grants the launcher access to the main class's package, somainruns even if the package is not exported - exactly asjava -m module/allows.Class - invokes
main- it sets the thread context class loader, runs any bundled agents, and calls the main method.
Which modules are resolved
The module layer is resolved the way java -m <mainModule> resolves it: the main module is the root, and its
requires closure is pulled in, services included. That works only for a self-contained graph - a
modular main module over a module path of explicit named modules, with nothing on the class path.
An automatic module breaks that, because it declares no requires, so a named module it uses only
internally would never be resolved. A class path breaks it too. In either case the launcher roots every
bundled module instead, the in-jar equivalent of --add-modules ALL-MODULE-PATH. You never configure this;
the launcher decides from what the jar contains.
One loader, two kinds of module
The reconstruction rebuilds a real module graph over a single class loader - named modules in the child
layer and the unnamed module over the class path - which is what one application loader has under
java -p modulepath -cp classpath, and what keeps the JDK's own rules:
- an automatic module can read the class path, while a strict named module cannot;
- a package owned by a module shadows the same package on the class path.
The in-memory module finder builds a descriptor for each jar the modulepath key names: from its module-info., or
derived for an automatic module from its Automatic-Module-Name or its file name, with the providers in
META-INF/ scanned in. Its version is derived as a module path derives it: the rest of the file name
after the first dash followed by a digit, kept only when it parses as a module version. A bundled automatic
module therefore reports the identity it would report under java -p, in Module::getDescriptor and in
stack traces. The
boot layer is immutable, so a fresh child layer is the only way to add modules at run time - and the right
one, because they stay real named modules. What these rules mean in
practice is the subject of Running & troubleshooting.
A jar that names no module
A dependency that declares neither a module-info nor an Automatic-Module-Name has no name to derive. The
build tool gives such a jar a name through a module alias, and it renames the
resolved file to <alias>-<version>. before packaging, or to <alias>. when a module path could not
derive that version from the file name. Inside the launcher jar the subfolder carries the same name, and the
automatic-module rule derives the declared name - and the version, where there is one - from it. Nothing
else is needed.
The launcher also understands a manifest header for a jar that kept its coordinate-encoded name. The module that declared the alias carries, in its own manifest:
Jenesis-Aliases: org.kohsuke.args4j=args4j/args4j
Each entry maps a module name onto the <groupId>/ it applies to, matched against the
coordinate in a bundled jar's file name. The launcher offers that jar as an automatic module under the
declared name, so a requires and a qualified opens naming it both resolve inside the layer. Only a jar
with no identity of its own is considered - a jar that names itself keeps its own name - and two names
claimed for one jar is an error rather than a choice the launcher makes for you.
Reading the jar on demand
Because every class and resource is a direct entry of the outer jar, the launcher never merges anything into
memory or spills it to disk. It opens the outer jar (a ZipFile) or the exploded directory at start-up,
indexes the entry names, and reads an entry's bytes only when first needed, discarding them afterwards.
Heap use is therefore roughly the size of the entry-name index rather than the dependencies' bytes.
Two details make this transparent to the application:
- Resources come back as ordinary URLs. The loader hands out standard
jar:andfile:URLs, soClassLoader.- and thereforegetResources ServiceLoader- works through the JDK's own handlers, with no custom URL scheme to configure. - Multi-release jars are honoured. For a dependency whose manifest says
Multi-Release: true, the launcher serves the highestMETA-INF/entry the running JVM supports, just as the JDK does for a real jar.versions/ <n>/
ZipFile stays open while the application runs, as it must, and each module layer the
application defines from the jar opens it once more. That, and the rest of the
launcher's boundaries, are covered in Running & troubleshooting.