Dependencies
Every non-trivial build pulls in libraries. This chapter is about where you declare them, how Jenesis turns each declaration into a downloaded jar, and which version it settles on when two paths disagree. It ends with the two tags that let you correct a closure you do not control: dropping a transitive you do not want, and naming a library that arrives without a module name of its own.
Declaring a dependency
You never add a dependency in a build script. You declare it the same way the ecosystem already does, and the place depends on your layout (see Core concepts):
-
A
pom.project lists a dependency the normal Maven way, inxml <dependencies>:<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.12.0</version> </dependency> -
A modular project (
module-info.) declares ajava requires, and nothing else - the module name is the dependency:module demo.app { requires org.apache.commons.text; }
That is the whole surface. Jenesis reads these existing files, resolves the transitive closure, and puts the result on the compile and runtime paths.
A requires names no version, so Jenesis takes the newest release of the module, leaving out pre-releases.
To hold a module at a version of your choosing, name it in a @jenesis. tag in the comment above the
declaration:
/**
* @jenesis.pin org.apache.commons.text 1.12.0
*/
module demo.app {
requires org.apache.commons.text;
}
The tag takes the module name and the version. In the modular layout it applies wherever that module
turns up in the closure, directly or through another module; in the modular_ layout it fixes the
module a requires names, and a module that arrives through that module's POM is pinned by its Maven
coordinate instead. Jenesis can also write these tags for you, fixing each dependency at the version it
resolved, as Recording the pins in the next chapter describes.
The two repositories
Jenesis resolves through two named repositories, one per kind of coordinate. Each comes with a default source, and either can be pointed elsewhere:
maven- Maven coordinates (groupId:artifactId:version). By default fetched over HTTPS from Maven Central (https:/) into your local Maven repository (/ repo1. maven. org/ maven2/ ~/), exactly where. m2/ repository mvnkeeps them, and hard-linked from there into the build.module- Java module names. By default resolved through the Jenesis Module Index atrepo., which maps a name likejenesis. build com.to its artifact and redirects to the file on Maven Central.fasterxml. jackson. databind
Which one a dependency uses follows from the layout. A pom. declares Maven coordinates, so it resolves
through maven. A requires names a module, so it resolves through module, and this is the step that turns
a module name into something downloadable. The Jenesis Module Index section documents that
lookup in full.
modular_to_maven layout, a requires is resolved to the declaring
module's Maven coordinate (its POM is fetched through the module index), and transitive resolution
then proceeds through Maven. A module project therefore reaches automatic-module and plain class-path
libraries too. The strict modular layout resolves purely by module name. Core concepts
covers the difference; the dependencies selector below shows it concretely.
Pointing at a different repository
To resolve through a corporate mirror or a private repository instead of the public defaults, set a system property or an environment variable before the build. No project change is required, and a property beats the variable of the same name:
| Property (environment variable) | What it overrides |
|---|---|
jenesis. (MAVEN_) |
The Maven upstream. Accepts a comma-separated list, queried left to right; an entry may append |-separated group ids to serve only those groups, and a bare @ splices the default chain back in (https:/). |
jenesis. (MAVEN_) |
Sent verbatim as the Authorization header on every Maven fetch (e.g. Bearer … or Basic …; a Jenesis Repository key can be given as is). |
jenesis. (MAVEN_) |
The local Maven repository directory (default ~/). |
jenesis. (JENESIS_) |
The module index base URL (default https:/), with the same list/filter/@ grammar. A maven:<uri> entry reads a Maven repository by the publishing convention instead, and a mapped:<uri or @>:<list>[;<list>. entry reads the modules a company's . lists map to Maven coordinates, as com., from that Maven repository or, for @, from the build's own. |
jenesis. (JENESIS_) |
The Authorization header for module fetches, when jenesis. points at a server that needs one. |
jenesis. (JENESIS_) |
The local module repository directory (default ~/). |
jenesis. |
Who resolves a module name: service (the default) asks the index at jenesis., git reads the index's published data itself and fetches what it resolves to from jenesis.. |
jenesis. (JENESIS_) |
Where that published data is read from when git resolves, a folder of per-module files (default: the data/ folder of the jenesis/ repository). A fork or a mirror of it stands in here, as jenesis. stands in for the index itself. |
http - only https and file are
allowed - and over https only from a server whose certificate verifies. A build that must pull
from an internal http mirror, or from a repository deployed with a self-signed certificate, has
to opt in explicitly with -Djenesis.repository.insecure=true, which then accepts both; the
check is switched off for Jenesis's own connections, never for the JVM. Neither authenticates the server, so
a token sent there is no better protected than the network. A credential token is dropped before any
redirect to a different host, so it never leaks to a redirect target.
What the build tells the module index
The module index does not serve jars, it redirects to them, and three of its choices are the build's to make. Jenesis states each one only when you have configured it, so an unconfigured build leaves every choice with the index and gets the same answer any other client gets.
| What you set | What the index is told |
|---|---|
jenesis. (MAVEN_) |
Redirect to the same repository Jenesis resolves Maven artifacts from, so module jars and Maven artifacts come from one host rather than two that disagree on what exists yet. |
jenesis. |
Whether a module asked for without a version may resolve to a pre-release. |
jenesis. |
Whether a version the index has not recorded may be resolved from the module's newest coordinate, rather than answering that it has never seen it. |
The last two are choices rather than questions, so they hold either way. With jenesis.
the build asks no service at all. It reads the index's data from the
jenesis/ repository on
GitHub, one file per module, which can be cloned or forked like any other repository; jenesis.
points a build at such a copy. No build therefore has to rely on an index that Jenesis hosts. The two choices
are applied there by the same rule the index applies - a version counts as a release when the version a
module is keyed by and the Maven version it resolves to both carry no pre-release qualifier.
Not every repository can be named to a third party, and Jenesis says nothing rather than guess: an entry
restricted to some groups cannot stand for the redirect of a module outside them, an @ reference is not
expanded here, and a file: repository or one carrying credentials has no URL the index could use. Only
a repository reached over http or https is told anything at all.
Seeing what resolved
The dependencies selector prints each module's resolved tree, the way mvn dependency:tree does:
java build/jenesis/Make.java dependencies
Each module gets one tree, starting from the module itself and written like any other node: the coordinate it
is published under, its version, the scopes it resolves and its module name, tagged local with the folder it
is built from (maven/). A
module built in the project carries the same local tag and folder wherever it appears in another module's
tree. Each node below shows the version every parent requested, the negotiated version inline when it
differs ([1,2] -> 2), the scopes it is resolved in, the dependency's licence ({Apache-2.), and the module
name. A dependency that only one scope reaches names that scope alone ([runtime]), and one whose negotiated
version differs between the scopes is listed once per version. A per-module Resolved dependencies list,
sorted by name, follows the tree, and a licence and module summary closes the output. It is the fastest way to
answer "why is this version on my class path?" before you pin anything.
The list and the summary count only what the build downloads: the modules the project builds itself stay in
the trees but are left out of both unless -Djenesis. is set. -Djenesis.
prints one tree per module and scope instead of one per module, each starting from the module in that scope,
with every node below it carrying the scope it was declared with.
When the whole closure is more than you want to read, -Djenesis. narrows what the trees show:
| Value | What it prints |
|---|---|
full |
(the default) Every module's graph in full, external closure and all. |
compact |
Only the local modules, with everything external folded into a count per branch, so a large multi-module project shows its own shape at a glance. |
-Djenesis. is a second, independent switch that applies under either format. It leaves out
the test modules (see Building and running), which are not part of what the
project releases, so neither the trees nor the licence summary count what only a test run pulls in.
Version negotiation
When two paths through the graph ask for different versions of the same library, Jenesis picks one. By default, the rule matches the repository:
- Maven coordinates use Maven's own nearest-wins conflict resolution, and understand version ranges
and the
LATEST/RELEASEselectors - the same behaviourmvngives you. - Module names use first-parent-wins: the first requirer reached in the resolution walk fixes the version, and a later, deeper requirer asking for a different version is ignored.
To override the negotiated result, declare the version you want directly: a <version> (or a
<dependencyManagement> entry) in Maven, or a @jenesis. tag in a modular
project. A declared version always beats what negotiation would have chosen.
Choosing a different strategy
Each repository's rule is the sensible default, and each is selectable when you want another. On the Maven
side, -Djenesis. takes:
| Value | Rule |
|---|---|
maven |
(the default) Maven's own: declared versions, ranges and RELEASE/LATEST resolved from repository metadata, nearest-wins on a conflict, with ranges intersected when one competes. |
closest |
The same minus the range arbitration - the nearest declaration simply stands, and no metadata is fetched to settle a conflict. |
latest / release |
Ignore every declared version and take the <latest> or <release> entry of each coordinate's metadata. |
stable |
Like release, but skipping every version whose qualifier marks it a pre-release - a milestone, a release candidate, an early-access build. |
fail |
Refuse to arbitrate: a coordinate two dependencies require at different versions stops the build, naming both versions. |
managed |
fail, and additionally refuse any version the project did not name itself - see below. |
On the module side, -Djenesis. decides what happens when two compiled module-info files
record different versions of the same requirement. first (the default) keeps the one nearest the roots,
fail reports the disagreement instead of discarding one, ignore keeps no compiled version at all, and
managed is fail plus the rule below.
Letting nothing in that you did not name
fail turns a silent decision into a stopped build. managed goes one step further: every version that
reaches the closure has to be one the project named.
-Djenesis.resolver.maven=managed
-Djenesis.resolver.module=managed
On the Maven side that means a coordinate the project neither declares itself nor names in dependency management - one that arrived only because a dependency's own POM mentioned it - stops the build:
No managed version for com.fasterxml.jackson.core:jackson-core which resolved to 2.22.1
as another dependency's POM declares it (add it to dependencyManagement, or run the pin selector)
On the module side the rule is the same: a module reached only through another module's requires stops
the build unless the project names its version itself, in a @jenesis. tag. A module the project declares itself - a sibling of a
multi-project build among them - is a declaration of the project and passes.
latest and release are upgrade probes, not build modes. They
override pinned versions too, so the checksum recorded beside a pin no longer describes the artifact that
resolves and stops applying - under strict pinning the build then fails. Use them to find out what an
upgrade would pull in, then record the result with pin.
How fresh the metadata is
Anything that resolves from repository metadata - a range, RELEASE, LATEST, STABLE - is only as current
as the maven-metadata. behind it, so Jenesis reads that file from the repository every time it resolves
one. There is no expiry to tune and no -U to remember: a version that appeared five minutes ago is found on
the next resolution.
The copy kept in . is the fallback, not the source. It is rewritten on every successful
read and used only when the repository cannot be reached, so a build that resolved once keeps resolving
offline, at the versions it last saw. Nothing is written into ~/, whose maven-metadata.
belongs to Maven.
To make sure a build touches no network at all, say so with -Djenesis.. Everything then
comes from ., the local Maven repository or a local module folder, and a file that is in none
of them fails the build with its URL named instead of being downloaded. A pinned project needs nothing else once
it has built once, which the pinning demo shows.
A resolution is still only repeated when the dependency set changes, because the step that performs it is
cached like every other. RELEASE therefore means the newest version as of the last resolution, which is
what pin exists to make explicit.
Excluding a transitive
A dependency can drag in a transitive you do not want. Pruning it is a Maven mechanism, so it works wherever
a POM is read: the maven layout, and the default modular_, whose requires resolve through
Maven. In a pom. it is the usual <exclusions> block:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.12.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
A module-info. states the same thing as a tag, since it has no <dependencies> block to hang it on:
/**
* @jenesis.exclude org.apache.commons.text org.apache.commons/commons-lang3
*/
module demo.sample {
requires org.apache.commons.text;
}
One line names the module to prune and any number of <groupId>/ targets; repeated lines add up.
A target is an artifact rather than one of its variants, so it carries no version, type or classifier, and
excluding from a module the declaration does not requires is an error rather than a silent no-op.
Either way the artifact takes its whole subtree with it and never enters the resolved closure: off the compile and test paths, absent from the generated POM, the bill of materials and the compliance reports. The build never fetched it.
modular layout is the one place this does not apply. Resolution there matches module
descriptors and never reads a POM, so there is no transitive POM dependency to prune and the tag is rejected
rather than ignored. Nothing is lost: a module only ever sees what it requires.
Naming a library that has no module name
Some libraries still ship as a plain jar: no module-info, and not even an Automatic-Module-Name. On the
module path such a jar becomes an automatic module named after its file, which changes with the file and so
cannot be requiresd reliably. An alias gives one a name your project chooses. That works for a jar you
require yourself and equally for a transitive dependency your project never mentions, which would otherwise
be loaded on the class path. A closure of plain jars is brought onto the module path this way, one
deliberate name at a time:
/**
* @jenesis.alias org.kohsuke.args4j args4j/args4j
*/
module demo.cli {
requires org.kohsuke.args4j;
opens demo.cli to org.kohsuke.args4j;
}
The tag maps a module name onto a <groupId>/ the resolved closure already contains, and the name
is then a module name like any other; the opens above is what lets args4j set the annotated fields by
reflection. Nothing is synthesised and no jar is rewritten.
Aliases are a modular_ feature: they reach an artifact by its Maven coordinate, which the strict
modular layout does not use.
Replacing a module another artifact already carries
Few projects need this. A handful of libraries use the Java Module System incorrectly: rather than requiring
an API's module, they copy its packages into their own jar. Tomcat Embed is one of them, as
org. exports the jakarta. packages itself. A library that correctly
requires jakarta. then cannot share a module path with Tomcat, because two modules would export one
package.
An override names the module to replace and the module that already carries its packages:
/**
* @jenesis.override jakarta.servlet org.apache.tomcat.embed.core
* @jenesis.override jakarta.el org.apache.tomcat.embed.el
*/
module demo.override {
requires jakarta.servlet;
requires jakarta.servlet.jsp;
requires org.apache.tomcat.embed.core;
requires org.apache.tomcat.embed.el;
}
Jenesis drops the replaced artifact from the closure and puts a module of that name in its place, which reads
the carrier's copy of the packages. requires jakarta. keeps meaning what it says, in your code and in
the libraries you depend on. Like aliases, overrides need the modular_ layout.
Keeping a dependency private
Every section so far assumed the module path can hold what the build resolves. It cannot always. A module
path admits one module per name, so a library that needs a different version of some dependency than its
consumer has nowhere to put it. The usual answer elsewhere is shading: rewrite the dependency's bytecode
under new package names and copy it in. That takes reflection, Class., resource lookup,
META-INF/, jar signatures and stack traces with it, and it leaves the seam implicit.
The JVM already has the mechanism for this, and it serves the class path just as well as the module path. A
second copy of a library goes behind a class loader of its own and keeps every package name it had; for
modules, that loader backs a ModuleLayer of its own. Jenesis lets the module that needs the isolation
declare it:
/**
* @jenesis.layer render api my.library.spi
* @jenesis.layer render provider com.example.renderer
*/
module my.library {
requires build.jenesis.launcher;
requires my.library.spi;
}
Two lines, and each says which side it declares. api names the one module the library shares with the
layer; provider names a root the layer holds, and its whole closure comes with it. Repeat the provider
line for more roots. A root is a module name, as here, or any other coordinate, such as
maven/, and the layer resolves in a dependency group of its own,
layer:render. A pin for something in the layer carries that group in front, as in
@jenesis.. A third line,
@jenesis., passes the library's native access on to a module of the layer -
see Granting native access.
The library reaches its layer by name, and gets back the implementation:
Report report = Launcher.instance(MethodHandles.lookup(), "render", Report.class);
Consumers declare nothing. They require the library and know nothing of what it hides; a consumer may
even resolve a different version of the same dependency for itself. The declaration travels to them in the
Jenesis-Layer manifest attribute of the produced jar, exactly as an alias or an override does, and any
build that resolves that jar reconstructs the layer from it. Discovery runs to a fixpoint, so a module
inside a layer may isolate a dependency of its own, without limit.
Libraries that name themselves nowhere
A layer splits a module path and a class path exactly as an application does, because the libraries worth
isolating are usually the ones that were never modularized. What carries a module identity - a
module-info, an Automatic-Module-Name, or a name you give it with @jenesis. - is resolved into
the layer. The rest is the layer's own class path, read by the layer's automatic modules as they would read
a plain -cp.
So a legacy tree costs one line, for the jar your code actually calls:
/**
* @jenesis.alias commons.beanutils commons-beanutils/commons-beanutils
*/
module my.library.impl {
requires commons.beanutils;
requires my.library.spi;
provides my.library.spi.Beans with my.library.impl.ConvertingBeans;
}
Commons Logging and Commons Collections arrive as Commons BeanUtils' own dependencies, are named nowhere,
and become the layer's class path. One rule of the Java Module System decides how this can be used: only an
automatic module reads the unnamed module, which is why the alias matters - a jar with no identity becomes
an automatic module when you name it, and an automatic module can read a class path. A module with a
descriptor of its own cannot, and javac will not let it try.
What the build refuses
Each of these is reported when it is declared, naming what to write instead:
- a layer declared without
requires build., the module a layer is reached through;jenesis. launcher - a layer that names an API module but nothing to isolate, or something to isolate but no API module;
- an API module the declaring module does not itself require;
- a
requireson a module the same module isolates - it is off that module's path, andjavacwould say so anyway; - a layer that isolates nothing, because the API module already shares all of it;
- a layer that provides a contract it also holds, which would look the service up against a different class of the same name and find no provider;
- a layer that holds no module at all, since a layer is reached through the modules it holds;
- two layers of one name, because a name is global - it is the dependency group the layer resolves in.
Execute, a bundle, a launcher jar, and a jpackage image, which
ships the layers' jars in a layers/ folder beside the application's own. A runtime image links the
platform modules the layers require but not the layers, because two versions of one module cannot share an
image. A native image resolves its module graph ahead of time, so it refuses a project that declares a layer
rather than flattening it.
jlayer.* system properties
when a module first asks for it. The JVM lets any code overwrite a system property at any time and offers no
way to protect one, so code that runs earlier - in the application or in an outer layer - can change which
jars that layer holds, and so place its own code in another module's layer, outside the encapsulation that
layer was declared for. A layer bundled in a launcher jar is read from the jar and is not affected.