Agent Skills: GuicedEE JPMS Shade

Convert an automatic-module (or non-modular) JAR dependency into a GuicedEE shaded JPMS service module so the GuicedEE/jlink pipeline stays clean. Use when a GuicedEE module requires an automatic module (a JAR with only an Automatic-Module-Name or none), when jlink/jpms complains about automatic modules on the module path, or when adding a new `com.guicedee.modules.services` shade under `services/Libraries`. Covers the maven-shade + moditect module-info pattern, version + BOM registration (Versioner, StandaloneBOM, guicedee-bom), dev-suite build registration, and rewiring the consuming module.

UncategorizedID: GuicedEE/ai-rules/guicedee-jpms-shade

Install this agent skill to your local

pnpm dlx add-skill https://github.com/GuicedEE/ai-rules/tree/HEAD/skills/.system/guicedee-jpms-shade

Skill Files

Browse the full folder contents for guicedee-jpms-shade.

Download Skill

Loading file tree…

skills/.system/guicedee-jpms-shade/SKILL.md

Skill Metadata

Name
guicedee-jpms-shade
Description
"Wrap non-modular JARs as GuicedEE JPMS service modules for jlink, including shade/moditect setup, BOM registration, and consumers."

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 / jpms rejects or warns about automatic modules.
  • Goal: a fully-named module graph for a reliable runtime image.

Inputs To Gather First

  1. 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, or mvn dependency:tree). Pin to that version — do not guess.
  2. 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. See references/inspect-jar.md for ready-to-run commands to read the manifest and enumerate packages (needed for accurate exports).
  3. The dependency's own external module dependencies (its compile deps that are NOT already bundled/relocated inside the JAR). Each becomes a requires in the module-info. Annotation-only deps (jspecify, jsr305, checker, error_prone) become requires 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 a requires.

Workflow

1. Create the shade module

Create GuicedEE/services/Libraries/<artifactId>/ (mirror the json / kafka-client modules):

  • pom.xml — parent com.guicedee:parent, groupId com.guicedee.modules.services, the shade artifactSet pinned 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.
  • exports every public package found in the JAR EXCEPT internally-relocated/shaded packages (e.g. *.com.google.*, *.org.antlr.*).
  • requires transitive for deps whose types appear in the exported API (e.g. org.reactivestreams).
  • requires static for annotation-only deps.
  • Add provides ... with ... for any META-INF/services the JAR ships (the SPI needs explicit provides in 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 — dependencyManagement for the UPSTREAM artifacts (so the shade module's pom.xml and its exclusions resolve versions).
  • GuicedEE/bom/pom.xml (guicedee-bom) — dependencyManagement for the new com.guicedee.modules.services:<artifactId> at ${guicedee.version} (so consumers omit versions).

4. Register in the dev-suite build (the "workflows")

  • Root DevSuite/pom.xml → services profile <modules> list. This is the module list the dev-suite GitHub workflows build/deploy; the reusable workflows in GuicedEE/workflows are generic templates and do NOT enumerate modules. Each shade also ships as its own GitHub repo that calls the reusable Projects Builder workflow — 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-java from io.vertx:vertx-web-graphql).
  • Add a dependency on the new com.guicedee.modules.services:<artifactId>.
  • The consumer's module-info.java usually needs no change: the shaded module keeps the same module name, so requires <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.exe and verify the repository's JDK requirement. Use separate invocations, quote Maven -D arguments, and preserve existing build output. Do not require a version-specific installation path or Maven clean for validation.
  • Relocated deps need explicit requires for JDK modules. When a library internally relocates its dependencies (e.g. graphql-java bundles Guava under graphql.com.google.* and ANTLR under graphql.org.antlr.*), those classes still use JDK APIs such as java.util.logging. As an automatic module the jar implicitly read every module, so it worked; as a named module it only reads what it requires. Missing ones surface at RUNTIME, not compile time, as IllegalAccessError: 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-module will 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-paste pom.xml and module-info.java templates + 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.