Last Updated: 2026-03-01
Purpose: Comprehensive guide for JPMS usage in this project
JPMS enables:
public type in a non-exported package is completely hiddenmodule my.module {
// β
Public API
exports com.example.api;
// β NOT exported = completely hidden
// com.example.internal remains private, even if classes are public!
// β
Only for specific modules
exports com.example.impl to framework.module;
}
my.module/
βββ api/ β Exported (public API)
β βββ MyService.java
β βββ MyDTO.java
βββ internal/ β NOT exported (implementation)
β βββ MyServiceImpl.java
β βββ MyHelper.java
βββ spi/ β Qualified export (for frameworks)
βββ MyExtension.java
module-info.java:
module my.module {
exports my.module.api; // Public API
exports my.module.spi to framework; // Only for framework
// my.module.internal remains hidden!
}
mapping.module/
βββ Mappings.java β Exported (facade)
βββ jpa.dto/ β Qualified export (MapStruct)
β βββ MapperImpl.java
βββ dto.jpa/ β Qualified export (MapStruct)
βββ MapperImpl.java
module-info.java:
module mapping.module {
exports mapping.module; // Facade
exports mapping.module.jpa.dto to org.mapstruct; // Only MapStruct
exports mapping.module.dto.jpa to org.mapstruct; // Only MapStruct
}
module de.ruu.app.jeeeraaah.backend.common.mapping.jpa.dto {
// β
Only facade exported
exports de.ruu.app.jeeeraaah.backend.common.mapping;
// β
Mappers only for MapStruct
exports de.ruu.app.jeeeraaah.backend.common.mapping.jpa.dto
to org.mapstruct;
// β
Minimal reflection
opens de.ruu.app.jeeeraaah.backend.common.mapping
to weld.core.impl, weld.spi;
}
Advantages:
Mappings (the facade)The opens directives in project modules follow best practices:
opens de.ruu.app.jeeeraaah.common.api.domain to lombok, com.fasterxml.jackson.databind;
// β
Only specific modules have reflection access
opens package.name to module1, module2;
// β AVOID: All modules would have access
opens package.name;
Only the really needed frameworks:
lombok - for @AllArgsConstructor, @Getter, etc.com.fasterxml.jackson.databind - for @JsonPropertyBoth frameworks need reflection for:
| Variant | Syntax | Access | Recommended? |
|---|---|---|---|
| Fully open | opens package; |
All modules | β Only when necessary |
| Targeted | opens package to module1, module2; |
Only these modules | β BEST PRACTICE |
| Not at all | (no opens) | No reflection | β If possible |
opens?opens de.ruu.app.services to weld.core.impl, org.jboss.weld.se.core;
opens de.ruu.app.dto to com.fasterxml.jackson.databind;
opens de.ruu.app.entities to org.hibernate.orm.core;
opens de.ruu.app.domain to lombok;
opens de.ruu.app.internal to org.junit.platform.commons;
/**
* [Module description]
*
* @since [version]
*/
module de.ruu.[module.name]
{
// Public API
exports de.ruu.[module.name];
exports de.ruu.[module.name].api;
// Dependencies
requires transitive [api.module];
requires [impl.module];
requires static [optional.module];
// Reflection access (minimal, targeted)
// - Framework X: Reason Y
// - Framework Z: Reason W
opens de.ruu.[module.name].internal to [framework.x], [framework.z];
}
IntelliJ Run Configurations now use JPMS Module Path instead of classpath.
.mvn/jvm.config extendedroot/app/jeeeraaah/frontend/ui/fx/.mvn/jvm.config
This file contains all JVM options and is automatically read by Maven and IntelliJ.
Missing property names were added:
keycloak.test.userkeycloak.test.passwordDashAppRunner.javade.ruu.app.jeeeraaah.frontend.ui.fx.dash.DashAppRunnerde.ruu.app.jeeeraaah.frontend.ui.fx<Default> (Module Path) β Important!.mvn/jvm.config)-cp <long list of JARs>
--add-modules jakarta.annotation,jakarta.inject
--module-path <module path>
--module de.ruu.app.jeeeraaah.frontend.ui.fx/de.ruu.app.jeeeraaah.frontend.ui.fx.dash.DashAppRunner
.mvn/jvm.configTotal modules analyzed: 50
Total public types: 479
Hidden public types: 20
Encapsulation ratio: 4.0%
Total modules analyzed: 50
Total public types: 481 (+2 new interfaces)
Hidden public types: 43 (+23)
Encapsulation ratio: 8.9% (doubled!)
Problem:
de.ruu.lib.jpa.core.criteria.restriction was exportedSolution:
Result:
New package structure:
backend.persistence.jpa/
βββ de.ruu.app.jeeeraaah.backend.persistence.jpa/ [EXPORTED]
β βββ TaskJPA.java (Entity)
β βββ TaskGroupJPA.java (Entity)
β βββ TaskCreationService.java (Interface - NEW)
β βββ TaskLazyMapper.java (Interface - NEW)
β
βββ de.ruu.app.jeeeraaah.backend.persistence.jpa.ee/ [NOT EXPORTED]
β βββ TaskServiceJPAEE.java (CDI Bean)
β βββ TaskGroupServiceJPAEE.java (CDI Bean)
β βββ TaskRepositoryJPAEE.java (CDI Bean)
β βββ TaskGroupRepositoryJPAEE.java (CDI Bean)
β
βββ de.ruu.app.jeeeraaah.backend.persistence.jpa.internal/ [NOT EXPORTED - NEW]
βββ TaskServiceJPA.java (Abstract Service)
βββ TaskGroupServiceJPA.java (Abstract Service)
βββ TaskRepositoryJPA.java (Abstract Repository)
βββ TaskGroupRepositoryJPA.java (Abstract Repository)
module-info.java:
module de.ruu.app.jeeeraaah.backend.persistence.jpa {
// Only entities and interfaces exported
exports de.ruu.app.jeeeraaah.backend.persistence.jpa;
// CDI access via 'opens' (NO export!)
opens de.ruu.app.jeeeraaah.backend.persistence.jpa.ee to weld.se.shaded;
opens de.ruu.app.jeeeraaah.backend.persistence.jpa.internal to weld.se.shaded;
}
Hidden classes (before 4, now 8):
Advantages:
internal.* result in compiler errorsopens| Module | Hidden Classes | Packages |
|---|---|---|
| lib.jpa.core | 19 | criteria.restriction |
| backend.persistence.jpa | 8 | ee, internal |
| lib.jpa.core.mapstruct.demo.bidirectional | 6 | tree |
| backend.common.mapping | 3 | lazy.jpa |
| sandbox.office.microsoft.word.docx4j | 3 | (root) |
| frontend.api.client.ws.rs | 1 | example |
| lib.fx.demo | 1 | bean.tableview |
| lib.jsonb | 1 | recursion |
| lib.jasperreports.example | 1 | (root) |
No provided scope for module dependencies: If a module is declared with requires in module-info.java, the corresponding Maven dependency must have compile scope.
No βadd-modules when possible: Better to explicitly declare modules as dependencies.
Module Path over Classpath: JPMS applications should consistently use the Module Path.
IntelliJ Module Setting: Make sure βUse classpath of moduleβ is disabled for JPMS configurations.
The provided scope was removed from jakarta.annotation-api in the following modules:
r-uu.app.jeeeraaah.frontend.api.client.ws.rsr-uu.app.jeeeraaah.common.api.domainr-uu.app.jeeeraaah.common.api.beanr-uu.app.jeeeraaah.frontend.ui.fx.modelr-uu.lib.utilr-uu.lib.mapstructr-uu.lib.junitNow itβs a normal compile dependency:
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
Two new IntelliJ run configurations were created in the .run folder:
-Dglass.gtk.uiScale=1.5β Add module in .mvn/jvm.config under --add-modules
Example:
error: java.lang.module.FindException: Module jakarta.annotation not found
Solution:
Check POM - dependency should be compile scope, not provided.
β Check --add-opens in .mvn/jvm.config
Example:
java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.String accessible
Solution: Add targeted opens in module-info.java:
opens de.ruu.app.package to framework.module;
β Check --enable-native-access in .mvn/jvm.config
Example:
error: package de.ruu.app.jeeeraaah.backend.persistence.jpa.internal is not visible
(package de.ruu.app.jeeeraaah.backend.persistence.jpa.internal is declared in module
de.ruu.app.jeeeraaah.backend.persistence.jpa, which does not export it)
This is GOOD! This is JPMS working as intended - preventing access to non-exported packages.
Solution: Use the public API/interfaces instead of internal implementation classes.
Solution:
exports: Compile-time API visibilityopens: Runtime reflection accessopens, not exports!| Metric | Value | Benefit |
|---|---|---|
| Modules with JPMS | 50 | Clear structuring |
| Exported packages total | 125 | Minimal API surface |
| Encapsulation ratio | 8.9% | Implementation details protected |
| Hidden public types | 43 | Β |
Qualified opens |
27 directives | Minimal reflection attack surface |
| Split-package conflicts | 0 | Clean package structure |
For publication documentation, see:
Analysis tools:
count-jpms-encapsulation.py - Analyzes JPMS encapsulation metricsLast updated: 2026-03-01