Java SE 11 Developer study materials
Java SE 11 Developer study resources

Split Package Blocks the Module-Path Migration

Java SE 11 Developer · Put your knowledge to work in context.

All case studies

Split Package Blocks the Module-Path Migration

A team is moving a Java 8 billing application to Java 11 and wants to launch it as a named module. The build produces four jars: com.acme.app (the launcher, with module-info), com.acme.core (with module-info, exports com.acme.core.api), com.acme.report (with module-info), and acme-legacy-3.2.jar, which has no module-info and contains both com.acme.legacy and com.acme.util. com.acme.core also publishes a package named com.acme.util. On the classpath everything runs. When all four jars are placed on --module-path, startup fails with FindException: module acme.legacy reads package com.acme.util from both com.acme.core and acme.legacy. Running jdeps --jdk-internals also reports that acme-legacy references sun.security.x509 classes in six places. One developer proposes adding requires acme.legacy to com.acme.app plus --add-exports java.base/sun.security.x509=acme.legacy; another proposes putting only acme-legacy-3.2.jar on the classpath and launching com.acme.app from the module path.

1. Explain why the classpath launch works but the module-path launch fails during resolution, then evaluate both proposals.

Read the worked answer

Why the classpath works. No module descriptor is consulted: every classpath entry is folded into the single unnamed module, and duplicate packages are resolved silently by class-loader search order. The application may have been running against the wrong copy of com.acme.util for years without anyone noticing.

Why the module path fails. The resolver rejects any configuration in which two modules in the graph contain the same package. acme-legacy-3.2.jar has no descriptor, so it becomes the automatic module acme.legacy (name derived from the file name), which contains com.acme.legacy and com.acme.util; com.acme.core also contains com.acme.util. Resolution happens before main, so no module-info directive can fix it - requires affects readability, not the split-package check.

Proposal 1 fails for exactly that reason: adding requires acme.legacy changes nothing about the split package.

Proposal 2 also fails as stated. A named module cannot requires the unnamed module. com.acme.app would get no read edge to the legacy code and would fail with package com.acme.legacy is not visible. Launching the whole application from the classpath would work, but that abandons modularity.

Viable options: relocate the duplicated package inside the legacy jar (shading to e.g. com.acme.legacy.shaded.util); or delete com.acme.util from the legacy jar after verifying the classes are binary-compatible with the com.acme.core copy and recompiling against it. --patch-module com.acme.core=acme-legacy-3.2.jar merges both packages into one module and will start, but it conceals a real ownership conflict and should be a last resort.

2. `jdeps` flags six references to `sun.security.x509` in the legacy jar. What exactly must be passed, at which points, and what is the risk?

Read the worked answer

sun.security.x509 lives in java.base but is not exported. To use it a module needs an export edge granted at compile time and run time separately:

javac --add-exports java.base/sun.security.x509=acme.legacy ...
java  --add-exports java.base/sun.security.x509=acme.legacy ...

Points that trip people up in practice:

  • The flag is not recorded in the jar and is not inherited by forked processes. Maven Surefire, the container entry point, IDE run configurations and any tool that re-launches the JVM each need it, or you get IllegalAccessError at run time (or a compile error).
  • The right-hand side must name the module that actually reads the package. If the legacy jar is left on the classpath instead, the target is ALL-UNNAMED, not acme.legacy.
  • --add-exports only permits static (compile-time/name-based) access. Reflective access to non-public members requires --add-opens, which implies export.
  • jdeps --jdk-internals reports these precisely because the package is not supported API under JEP 260 and may change or disappear in any release; jdeps --generate-module-info output should be reviewed the same way.

The durable fix is to replace the usage with standard API such as CertificateFactory/X509Certificate, or an explicit third-party dependency, and treat the flags as a temporary migration crutch. Contrast sun.misc.Unsafe: it lives in jdk.unsupported, which exports it, so requires jdk.unsupported suffices and no flag is needed.

3. `com.acme.report`'s exported interface has a method returning `com.acme.core.api.Price`, but `com.acme.app`, which only has `requires com.acme.report`, fails to compile. Why, and what are the two fixes?

Read the worked answer

Readability is not transitive by default. com.acme.app reads com.acme.report, so it can see com.acme.report types, but it has no read edge to com.acme.core. When the compiler resolves the returned type Price it must load com.acme.core.api, and it reports something like cannot access com.acme.core.api.Price; class file for com.acme.core.api.Price not found.

Two fixes:

  1. Declare the dependency as implied: module com.acme.report { requires transitive com.acme.core; exports com.acme.report.api; }. Every module that reads com.acme.report then implicitly reads com.acme.core. This is the correct choice when the core types are genuinely part of the report module's API contract.
  2. Add requires com.acme.core; to com.acme.app as well. This works but pushes knowledge of the dependency onto every client.

The judgement call is which one is honest about the API. If Price appears in exported signatures, use requires transitive; if com.acme.core is used only inside the report implementation, keep a plain requires and do not leak core types through the public API. Over-using requires transitive widens the module graph for every consumer, so apply it where the type genuinely crosses the API boundary - not on every dependency.