Plug-in Architectures for Java with Layrry & Java Module System
=>From Gunnar Morling & Andres Almiray (Java Champions) talk
What is JPMS?
JPMS
Java Platfrom Module System (AKA Jigsaw)
Introduced in Java 9
Designed to break down the JVM monolith
Applicable to user space as well
The promise is to give developers better code encapsulation and design foundations
But JPMS encountered some detractors.
Some user statements
"JPMS only allows for one version of a module"
"JPMS doesn't allow to add modules at runtime"
"I have to modularize ALL my dependencies in order to benefit from JPMS"
"It's so hard to map dependency coordinates to module dependencies"
"Help, non-exported packages of different modules collide!"
What no one ever said: "Let's use JPMS layers to tackle (some of) these issues"
Class ModuleLayer
It is like a function which allows you to define which class is used and loaded with your module.
It is like a class-loading toolkit
Using the Layers API
ModuleLayer boot = ModuleLayer.boot(); ClassLoader scl = ClassLoader.getSystemClassLoader(); Path foo = Paths.get("path/to/foo-1.0.0.jar"); Path greeter10 = Paths.get("path/to/greeter-1.0.0.jar"); NoduleFinder fooFinder = ModuleFinder.of(foo, greeter10); Configuration fooConfig = boot.configuration() .resolve( fooFinder, ModuleFinder.of(), Set.of("com.example.foo", "com.example.greerter") ); ModuleLayer fooLayer = boot.defineModulesWithOneLoader( fooConfig, scl);
What can ease the use of module layers: Layrry is the answer
Hello, Layrry!
=>An API and Launcher for Modularized Java Applications

Layrry configuration by Java class
ayers layers = Layers.layer("log") .withModule("org.apache.logging.log4j:log4j-api:jar:2.13.1") .withModule("org.apache.logging.log4j:log4j-core:2.13.1") .withModule("org.example:logconfig:1.0.0") .layer("foo") .withParent("log") .withModule("com.example:greeter:1.0.0") .withModule("com.example:foo:1.0.0") .layer("bar") .withParent("log") .withModule("com.example:greeter:2.0.0") .withModule("com.example:bar:1.0.0") .layer("app") .withParent("foo") .withParent("bar") .withModule("com.example:app:1.0.0") .build(); layers.run("com.example.app/com.example.app.App","Alice");
Layrry has a ready-Made launcher
layrry-launcher-<version>-all.jar --layers-config layers.yml
which does the work of the previous Java class thanks to a yaml or toml file
layers.yml:
log:
module:
- "org.apache.logging.log4j:log4j-api:jar:2.13.1"
- "org.apache.logging.log4j:log4j-core:2.13.1"
foo:
parents:
- "log"
modules:
- "com.example:greeter:1.0.0"
- "com.example:foo:1.0.0"
bar:
parents:
- "log"
modules:
- "com.example:greeter:2.0.0"
- "com.example:bar:1.0.0"
app:
parents:
- "foo"
- "bar"
modules:
- "com.example:app:1.0.0"
main:
module: com.example.app
class: com.example.app.AppLayrry has a built-in template support
layrry-launcher-<version>-all.jar --layers-config-config javafx.toml --properties version.properties
javafx.toml:
[layers.javafx]
modules = [
"org.openjfx:javafx-base:jar:{{os.detected.jfxname}}:{{javafx_version}}",
"org.openjfx:javafx-controls:jar:{{os.detected.jfxname}}:{{javafx_version}}",
"org.openjfx:javafx-graphics:jar:{{os.detected.jfxname}}:{{javafx_version}}",
"org.openjfx:javafx-web:jar:{{os.detected.jfxname}}:{{javafx_version}}",
"org.openjfx:javafx-media:jar:{{os.detected.jfxname}}:{{javafx_version}}"]
[layers.core]
modules = [
"org.kordamp.titles:modular-tiles-model:{{project_version}}",
"org.kordamp.titles:modular-tiles-core:{{project_version}}",
"org.kordamp.titles:modular-tiles-app:{{project_version}}",
"org.moditect.layrrys:layrryt-platform:{{layrry_version}}",
"eu.hansolo.tilesfx:{{tilesfx_version}}"]
parents= ["javafx"]
[layers.plugins]
parents = ["core"]
directory = "plugins"
[main]
module = "org.kordamp.tiles.app"
class = "org.kordamp.tiles.app.Main"versions.properties
project_version = 1.0.0 javafx_version = 11.0.2 tilesfx_version = 11.44 layrry_version = 1.0-SNAPSHOT
You can launch layrry with JBang
jbang layrry@moditect --layers-config-config javafx.toml --properties version.properties
How to handle the dependencies? => the repositories
Local repositories with Layrry
=>Artifacts can be organized in a local repository that does not require remote resolution
=>Two layouts currently supported: flat and default
layers.yml:
resolve:
localRepositories:
repoName:
layout: "flat"
path: "/path/to/repository/directory"Flat Repository:
repositories/ |__flat |__ it-app-1.0.0.jar |__ it-bar-1.0.0.jar |__ it-foo-1.0.0.jar |__ it-greeter-1.0.0.jar |__ it-greeter-2.0.0.jar |__ it-logconfig-1.0.0.jar |__ log4j-api-2.13.1.jar |__ log4j-core-2.13.1.jar
Default repository:
repositories/
|__default
|__ com
|__ example
|__ foo
|__ 1.0.0.0
|__ it-foo-1.0.0.jar
...That's not all: you can access remote repositories with Layrry
Layrry can deal with remote Layers
Layer configuration (and properties) files hosted at remote locations.
Mixing remote and local repositories is allowed.
Supports dynamic plugins, plugin directories can only be local.
Now we have Layrry to facilitate our task, let's see how it plugins and plugouts the classes
Plug-in architectures
Layrry can dynamically add (and remove) layers at runtime
/** * Implementations get notified about the addition or removal of plug-in layers. * Retrieved via the service loader API. */ public interface PluginLifecycleListener { void pluginAdded(PluginDescriptor plugin); void pluginRemoved(PluginDescriptor plugin); }
pluginAdded:
@Override public void pluginAdded(PluginDescriptor plugin) { ServiceLoader<MyService> services = ServiceLoader.load( plugin.getModuleLayer(), MyService.class); services.forEach(service -> { // only process services declared by the added layer itself, // but not from ancestor layers if (service.getClass().getModule().getLayer() == layer) { // process service... } }); }
Conclusion
The Java Module System doesn’t define any means of mapping between modules (e.g. com.acme.crm) and JARs providing such module (e.g. acme-crm-1.0.0.Final.jar) or retrieving modules from remote repositories using unique identifiers (e.g. com.acme:acme-crm:1.0.0.Final). Instead, it’s the responsibility of the user to obtain all required JARs of a modularized application and provide them via --module-path.
Furthermore, the module system doesn’t define any means of module versioning; i.e. it’s the responsibility of the user to obtain all modules in the right version. Using the --module-path option, it’s not possible, though, to assemble an application that uses multiple versions of one and the same module. This may be desirable for transitive dependencies of an application, which might be required in different versions by two separate direct dependencies.
This is where Layrry comes in: utilizing the notion of module layers, it provides a declarative approach as well as an API for assembling modularized applications, organized in module layers. The JARs to be included are described using Maven GAV (group id, artifact id, version) coordinates, solving the issue of retrieving all required JARs in the right version.
Module layers allow to use different versions of one and the same module in different layers of an application (as long as they are not exposed in a conflicting way on module API boundaries).
Module layers and thus Layrry also allow application extensions to be added and removed dynamically at runtime.