GuicedEE JPMS Shade
Turn an automatic-module dependency into a proper, named JPMS module published as a
com.guicedee.modules.services artifact, then make the consuming GuicedEE module require the
shaded module instead of the automatic one. This yields a clean jlink image (no automatic
modules on the module path).
When This Applies
- A GuicedEE module
requires <some.automatic.module>that resolves to a plain JAR. jlink/jpmsrejects or warns about automatic modules.- Goal: a fully-named module graph for a reliable runtime image.
Inputs To Gather First
- The upstream coordinates and resolved version (e.g.
com.graphql-java:graphql-java:26.0). Resolve the EXACT version the parent already pulls (check the dependency's own POM, the BOM that imports it, ormvn dependency:tree). Pin to that version — do not guess. - The module name the JAR advertises. Read it from the JAR manifest
Automatic-Module-Name; if absent, derive a stable name from the root package. Seereferences/inspect-jar.mdfor ready-to-run commands to read the manifest and enumerate packages (needed for accurateexports). - The dependency's own external module dependencies (its compile deps that are NOT already
bundled/relocated inside the JAR). Each becomes a
requiresin the module-info. Annotation-only deps (jspecify, jsr305, checker, error_prone) becomerequires static.
Many libraries already relocate their transitives internally (e.g. graphql-java bundles ANTLR and Guava under
graphql.*). Inspect the package list — only NON-relocated, externally-visible dependencies need arequires.
Workflow
1. Create the shade module
Create GuicedEE/services/Libraries/<artifactId>/ (mirror the json / kafka-client modules):
pom.xml— parentcom.guicedee:parent, groupIdcom.guicedee.modules.services, the shadeartifactSetpinned to the single upstream artifact, and the standard plugin chain (shade → antrun → moditect → flatten → copy-rename → javadoc). Exclude transitives that you re-add as their own proper modules.src/moditect/module-info.java— the named module descriptor.
Use the exact templates in references/templates.md.
2. Write the moditect module-info
module <module-name> { ... }using the manifest's Automatic-Module-Name.exportsevery public package found in the JAR EXCEPT internally-relocated/shaded packages (e.g.*.com.google.*,*.org.antlr.*).requires transitivefor deps whose types appear in the exported API (e.g.org.reactivestreams).requires staticfor annotation-only deps.- Add
provides ... with ...for anyMETA-INF/servicesthe JAR ships (the SPI needs explicitprovidesin the descriptor, even though the shade keeps the service files).
3. Register versions and BOMs
GuicedEE/bom/Versioner/pom.xml— add<...version>properties for the upstream artifact(s).GuicedEE/bom/StandaloneBOM/pom.xml—dependencyManagementfor the UPSTREAM artifacts (so the shade module'spom.xmland its exclusions resolve versions).GuicedEE/bom/pom.xml(guicedee-bom) —dependencyManagementfor the newcom.guicedee.modules.services:<artifactId>at${guicedee.version}(so consumers omit versions).
4. Register in the dev-suite build (the "workflows")
- Root
DevSuite/pom.xml→servicesprofile<modules>list. This is the module list the dev-suite GitHub workflows build/deploy; the reusable workflows inGuicedEE/workflowsare generic templates and do NOT enumerate modules. Each shade also ships as its own GitHub repo that calls the reusableProjects Builderworkflow — mention this to the user, but it is created outside DevSuite. - Optionally document the mapping table in
GuicedEE/services/services.md.
5. Rewire the consuming module
In the GuicedEE module that used the automatic module:
- Exclude the upstream automatic artifact(s) from whatever transitively brought them in
(e.g. exclude
com.graphql-java:graphql-javafromio.vertx:vertx-web-graphql). - Add a dependency on the new
com.guicedee.modules.services:<artifactId>. - The consumer's
module-info.javausually needs no change: the shaded module keeps the same module name, sorequires <module-name>already resolves — now to a real named module.
6. Validate
Install the updated BOMs first so the shade module resolves the current local management, then build:
mvn -B -N install # in Versioner, then StandaloneBOM, then GuicedEE/bom
mvn -B install -DskipTests -Dmaven.javadoc.skip=true # in each new shade module
jar --describe-module --file target/<artifactId>-<ver>.jar # confirm requires/exports
mvn -B -o compile # in the consumer module — must compile on the module path
Confirm jar --describe-module shows the expected named module with correct requires/exports,
and that the consumer compiles with [debug release N module-path].
Pitfalls
- Build env (Windows): discover available tools with
Get-Command mvn.cmd, java.exeand verify the repository's JDK requirement. Use separate invocations, quote Maven-Darguments, and preserve existing build output. Do not require a version-specific installation path or Mavencleanfor validation. - Relocated deps need explicit
requiresfor JDK modules. When a library internally relocates its dependencies (e.g. graphql-java bundles Guava undergraphql.com.google.*and ANTLR undergraphql.org.antlr.*), those classes still use JDK APIs such asjava.util.logging. As an automatic module the jar implicitly read every module, so it worked; as a named module it only reads what itrequires. Missing ones surface at RUNTIME, not compile time, asIllegalAccessError: class X (in module M) cannot access class Y (in module java.logging) because module M does not read module java.logging. Add the JDK module (e.g.requires java.logging;). Compile +jar --describe-modulewill NOT catch this — only running the code does. Always run the consumer's integration tests after a shade. - Missing version errors in the shade module mean the updated StandaloneBOM was not installed yet.
- Split packages: inspect all contained packages, including unexported ones. A private bundled copy can still conflict with another named module; exclude it or relocate it into a private namespace.
- Don't export relocated internals — they are implementation detail and break encapsulation.
Hazelcast wrapper and effective artifact proof
The verified Hazelcast wrapper lives at GuicedEE/services/JCache/hazelcast: artifact com.guicedee.modules.services:hazelcast-all, module com.hazelcast.core. Relocate its private com.codahale.metrics copy to com.hazelcast.shaded.metrics so it can coexist with the application's metrics module. Export the public com.hazelcast.cache API required by JCache integration; keep relocated internals private.
Install the wrapper before rebuilding Hazelcast and its application consumer. Verify dependency resolution, the assembled module path, and a real multi-JVM packaged application run. jar --describe-module and compilation alone do not prove runtime exports, service discovery, or absence of split packages.
When preserving target, several versioned JARs may coexist. Select the artifact name actually present on the tested module path, then compare its SHA-256 with the framework build output. Choosing the first glob match can report or test an obsolete JAR. Keep generated-source, focused-test, packaged-runtime, and live deployment evidence distinct.
References
references/templates.md— copy-pastepom.xmlandmodule-info.javatemplates + BOM snippets.references/inspect-jar.md— PowerShell snippets to read the manifest module name and package list.references/worked-example.md— the graphql-java + java-dataloader case, end to end.