Modularizing Three Tiers: Split Package, Missing Transitive Requires, and Blocked Reflection
A team is modularizing a three-tier application: com.acme.core (domain types), com.acme.report (reporting API), com.acme.app (entry point). Attempt 1 fails at compile time: core and report both contain package com.acme.util (legacy utility classes copied between teams), and javac reports that the package exists in another module. Attempt 2 merges the duplicate classes into core, adds requires com.acme.core; to report and requires com.acme.report; to app. Compilation now fails in app with 'cannot find symbol' on CoreOrder, the return type of ReportService.generate(). Attempt 3 makes com.acme.report.internal a non-exported package and reads it reflectively via Class.forName(...).getDeclaredConstructor().setAccessible(true); this throws InaccessibleObjectException at runtime. A legacy logging jar with no module-info sits on the module path and is used by all three modules. One older deployment still puts everything on the classpath.
1. Explain the split-package failure, why the usual flags do not fix it, and what the realistic remedies are.
Read the worked answer
JPMS requires that a package be owned by exactly one module in the readability graph of any module that uses it. Two named modules on the module path declaring the same package produce a compile-time error such as 'package com.acme.util is declared in module com.acme.core, which does not export it', and the resolver rejects the configuration at startup if both are read. The rule exists so that a type has exactly one defining module and one runtime identity - the whole point of the module system.
--add-reads and --add-exports adjust the readability and export graphs; they do not change package ownership, so neither fixes a split package. --add-exports com.acme.core/com.acme.util=com.acme.report is meaningless when report also contains the package - the classes will still clash. --patch-module com.acme.core=report-classes does work by merging report's copy into core for that invocation, but it is a testing/transition device, not a shippable configuration, and it silently picks which copy wins.
Realistic remedies: (a) merge - keep com.acme.util only in core and delete report's copy, then requires com.acme.core in report; (b) rename - move report's copy to com.acme.report.util if the classes have genuinely diverged, then fix imports; (c) keep everything on the classpath, where the unnamed module tolerates split packages - which is why the older deployment still works and the module path does not.
2. Why does app fail to see `CoreOrder`, and which fix belongs in which module?
Read the worked answer
Readability is not transitive by default. app reads report, report reads core, but app does not read core. ReportService.generate() returns a CoreOrder, a type from a module app cannot read, so the compiler reports the type as unavailable even though the class is on the module path - this is the classic symptom of a missing requires transitive.
Fix in com.acme.report: requires transitive com.acme.core;. Then every module that reads report also reads core implicitly, which matches the actual contract: report's exported API exposes core types, so any client needs core on its readability graph. Also confirm core actually exports the package containing CoreOrder - presence in the module is not enough.
Alternative in com.acme.app: add requires com.acme.core; directly. It compiles, but it pushes knowledge of core onto every client and breaks the moment report's API changes to expose another module's type - it treats a symptom, not the cause.
Rules of thumb: use requires transitive when the dependency appears in your exported API (parameter/return types, supertypes, generic arguments, thrown types); use plain requires for implementation-only dependencies; use requires static for compile-time-only dependencies such as annotations, and remember requires static does not make the module readable at runtime. Overusing requires transitive bloats every client's graph, so apply it only where the API demands it.
3. Explain the `InaccessibleObjectException`, the correct descriptor fix, and how the automatic logging module behaves.
Read the worked answer
exports and opens are different permissions. exports com.acme.report.internal; grants compile-time and runtime access to the package's public types; it does not permit setAccessible(true) on non-public members. Deep reflection requires the package to be open to the caller, which is what InaccessibleObjectException reports: the class loaded fine, but the access check on the reflective operation failed.
Descriptor fixes, best first:
opens com.acme.report.internal to com.acme.app;- qualified open; the package is open only to app. Preferred, because it keeps the exposure minimal.opens com.acme.report.internal;- unqualified; open to all modules.open module com.acme.report { ... }- opens every package; convenient but blunt, and it removes the compile-time protection for the whole module.
The runtime option --add-opens com.acme.report/com.acme.report.internal=com.acme.app also works, but it is a launcher flag: it must be repeated in every deployment script and in tests, and it does not help javac (unlike --add-exports, which needs to be given to the compiler as well). Better still, replace the reflection with a public factory method and export that - reflection across a module boundary is a design smell.
For the legacy jar: an automatic module derives its name from the jar filename (version suffix stripped, non-alphanumerics replaced by dots), exports all its packages and reads all other modules, so requires logging; (check with jar --describe-module) is enough and --add-exports is unnecessary. It cannot declare its own dependencies, so anything it needs must be on the module path and readable by it.