<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where's the exit?! |
When (not) to use modules? |
Slides at slides.nipafx.dev/modules-irl
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where’s the exit?! |
When (not) to use modules? |
Does this compile
(when built with Maven)?
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
import org.checkerframework.checker.units.qual.Prefix;
public class Transitive {
private final Prefix prefix = Prefix.centi;
}
Should it?
Now it doesn’t
(regardless of build tool):
module com.example.main {
requires com.google.common;
}
⇝ Modules make dependencies more explicit.
Explicit dependencies in action:
# create the image
$ jlink
--module-path mods
--add-modules com.example.main
--output app-image
# list contained modules
$ app-image/bin/java --list-modules
> com.example.app
# other app modules
> java.base
# other java/jdk modules
⇝ Modules make it easy to create application images.
Which packages can I use?
Exactly these:
module org.junitpioneer {
exports org.junitpioneer.jupiter;
exports org.junitpioneer.jupiter.cartesian;
exports org.junitpioneer.jupiter.params;
exports org.junitpioneer.vintage;
}
⇝ Modules clarify APIs.
⇝ Modules add module-internal accessibility.
(enforced by compiler and runtime).
How to stop projects integrating your code
from using all your APIs?
module com.example.lib {
exports com.example.lib.internal
to com.example.main;
}
⇝ Modules add project-internal accessibility.
(enforced by compiler and runtime).
in Spring LDAP:
org.springframework.ldap...AbstractContextSource
⇝ com.sun.jndi.ldap.LdapCtxFactory
in Error Prone:
com.google.errorprone.BaseErrorProneJavaCompiler
⇝ com.sun.tools.javac.api.BasicJavacTask
in veraPDF-library:
org.verapdf.gf.model.impl.external.GFPKCSDataObject
⇝ sun.security.pkcs.PKCS7
Dependencies on internals of…
JDK
frameworks
libraries
... are a risk.
⇝ Modules make these explicit
(with errors or --add-exports
)
and incentivize fixing them.
Which services does a JAR use?
module java.sql {
uses java.sql.Driver;
}
⇝ Modules make consuming services explicit.
No more files in META-INF/services/
— instead:
module com.example.sql {
provides java.sql.Driver
with com.example.sql.ExampleDriver;
}
⇝ Modules make providing services simpler.
What describes a project
(and how to look it up):
name ⇝ build tool
API ⇝ 🤷🏾♂️
dependencies ⇝ build tool
services ⇝ META-INF/services/
high-level documentation ⇝ 🤷🏼♀️
⇝ Modules express this in one file.
Module declaration is a great place to document:
central abstraction, contract, design
unexpected dependencies
unusual API
allowance of reflective access
service interactions
Module declarations:
define and document a project
are verified by compiler and runtime
can be evaluated by other tools
are obvious to review
⇝ Modules are a living representation
of a project’s architecture.
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where’s the exit?! |
When (not) to use modules? |
Why consider it?
partial modularization gives you
partial benefits
can avoid some roadblocks
makes the process more relaxed
Why is it even an option?
most module systems are "in or out"
but modularized JDK and legacy JARs
have to cooperate!
there is a boundary between
legacy and modules
Incremental modularization means
moving that boundary.
Incremental modularization is enabled by two features:
unnamed module(s)
automatic modules
And the fact that module and class path coexist:
modular JARs can be put on either
"regular" JARs can be put on either
Contains all JARs on the class path
(including modular JARs).
has no name (surprise!)
can read all modules
exports all packages
Inside the unnamed module,
"the chaos of the class path" lives on.
⇝ Why the class path "just works".
what if your code was modularized
and your dependencies were not?
proper modules can not depend on
"the chaos on the class path"
this is not possible:
module com.example.app {
requires unnamed;
}
One is created for each "regular" JAR
on the module path.
name defined by manifest entry
AUTOMATIC-MODULE-NAME
or
derived from JAR name
can read all modules
(including the unnamed module)
exports all packages
Class Path | Module Path | |
---|---|---|
Regular JAR | Unnamed Module | Automatic Module |
Modular JAR | Unnamed Module | Explicit Module |
Unnamed or named module?
The user decides, not the maintainer!
Three strategies emerge:
bottom up
top down
inside out
In order of precedence:
no unmodularized dependencies ⇝ bottom up
bottom up or top down
roadblocks ⇝ continue elsewhere (inside out)
Remember, partial modularization brings partial benefits!
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where’s the exit?! |
When (not) to use modules? |
Before we look at specific situations:
most problems originate in dependencies
often stem from automatic modules
can often be fixed by demoting them to class path
Many problems come from JARs on the module path
that aren’t ready to be modules.
Minimize number of automatic modules!
Only put on module path:
your modular JARs
the JARs required by modular
JARs on the module path
That deals with most transitive dependencies.
If your code directly depends on
a troublesome automatic module:
put problematic JARs on class path
subprojects that depend on them:
do not modularize
define automatic module name
put on module path
⇝ Modularize elsewhere.
Some common or tricky roadblocks:
split packages
broken module descriptors
reflective access
unsupported media type
More details on GitHub:
nipafx/module-system-woes
Projects that aren’t prepared for modules:
can have various run-time issues
sometimes react poorly by
hiding the underlying cause
⇝ Search the log for module-related errors.
Search terms for module system errors:
"module", "lang.module", "module path"
"layer", "boot layer"
"visible", "exported", "public", "illegal", "access"
Sometimes, projects just swallow errors. 😔
⇝ Take the module system out of the equation.
Everything* that works on the module path also works on the class path.
(* except services in
module-info.java
)
When debugging a weird error:
create a minimal reproducible example
launch on the class path
if the error vanishes, debug harder
Two categories of problems in dependencies:
they do something they shouldn’t
they don’t tell you that
you need to do something
Such cases need to be fixed on their end!
⇝ Makes the Java ecosystem more reliable for everybody.
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where’s the exit?! |
When (not) to use modules? |
If roadblocks are insurmountable,
getting out is easy:
find . -name module-info.java -type f -delete
But first:
move documentation elsewhere
declare services the old way
update command line options
(e.g. --add-exports
and paths)
move from jlink to jpackage
What about all the work?!
Some benefits remain:
good while it lasted
cleaner architecture
documentation
Not a great step,
but at least it’s possible!
⇝ Makes it feasible to experiment.
Why use modules? |
Incremental modularization |
What are common or tricky roadblocks? |
Where’s the exit?! |
When (not) to use modules? |
Observations:
modules require & enforce a decent architecture
(re)factoring modules << (re)factoring code
most problems originate in dependencies
Assumption:
barring problematic dependencies,
modules are a net-positive
Why are dependencies so often the problem?
almost all still baseline against JDK ≤ 8
makes creating/maintaining modules tougher
number of modularized apps is small
and thus demand is small (but growing)
But more and more ship as modules:
sormuras/modules tracks 4413 unique modules
(as of September 30th, 2022).
What impact does project size have?
Simplified:
Size | Effort | Benefit |
---|---|---|
small | 📉 | 📉 |
large | 📈 | 📈 |
⇝ Project size doesn’t matter (much).
General benefits for all kinds of projects:
managing dependencies
preventing (accidental) use
of internal (JDK) APIs
new abstraction
etc.
benefit from strong encapsulation
shouldn’t limit users
strong encapsulation (!)
self-contained images (possibly)
⇝ Project type has little practical impact,
but reused code should consider users.
module declarations and architecture
evolve side by side
new dependencies can be vetted
modules must be retrofitted
architecture may require updating
(which can be the goal!)
existing dependencies must work
⇝ New projects are a better fit.
Impact of domain (knowledge) on dependencies:
maintenance of domain-specific dependencies?
choice of domain-specific dependencies?
understanding of domain to evaluate 👆🏾
and to analyze unexpected problems
⇝ Easier if domain isn’t niche
and you understand it well.
Best place to start:
new projects
in well-understood,
well-maintained domain
particularly if reused
Next best place: your current project!
(Unless it has lots of outdated dependencies.)
Accept that partial modularization brings
partial benefits. Then start small.
Use module declarations to analyze, guide,
document, and review architecture.
Get stuck trying to fix dependencies:
identify root cause, open an issue,
put on class path, wait for (or contribute) fix.
Forget that modules are a seat belt, not a rocket.
Everything will be okay in the end. If it’s not okay, it’s not the end.