main_java

JPMS Reference Guide - Complete Documentation

Last Updated: 2026-03-01
Purpose: Comprehensive guide for JPMS usage in this project


Table of Contents

  1. Package-Hiding Strategy
  2. Opens Directives - Best Practices
  3. IntelliJ Run Configuration
  4. Encapsulation Improvements
  5. Run Configuration Details
  6. Troubleshooting

1. Package-Hiding Strategy

🎯 Core Principle

JPMS enables:

Example:

module 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;
}

Pattern 1: API + Internal Pattern

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!
}

Pattern 2: Facade Pattern (Used in this project!)

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
}

βœ… Best Practices in This Project

backend.common.mapping.jpa.dto ⭐ Best Practice!

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:


2. Opens Directives - Best Practices

βœ… Current Implementation is Optimal!

The opens directives in project modules follow best practices:

opens de.ruu.app.jeeeraaah.common.api.domain to lombok, com.fasterxml.jackson.databind;

Why is this optimal?

1. Targeted (βœ… CORRECT)

// βœ… Only specific modules have reflection access
opens package.name to module1, module2;

// ❌ AVOID: All modules would have access
opens package.name;

2. Minimal (βœ… CORRECT)

Only the really needed frameworks:

3. Necessary (βœ… CORRECT)

Both frameworks need reflection for:

JPMS Opens Variants Comparison

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

When do you need opens?

Reflection-based Frameworks

  1. Dependency Injection (CDI, Spring, Guice)
    opens de.ruu.app.services to weld.core.impl, org.jboss.weld.se.core;
    
  2. JSON Mapping (Jackson, Jsonb)
    opens de.ruu.app.dto to com.fasterxml.jackson.databind;
    
  3. ORM (Hibernate, EclipseLink)
    opens de.ruu.app.entities to org.hibernate.orm.core;
    
  4. Annotation Processing (Lombok, MapStruct at runtime)
    opens de.ruu.app.domain to lombok;
    
  5. Testing (JUnit, Mockito)
    opens de.ruu.app.internal to org.junit.platform.commons;
    

Pattern Template for module-info.java

/**
 * [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];
}

3. IntelliJ Run Configuration

Problem Solved βœ…

IntelliJ Run Configurations now use JPMS Module Path instead of classpath.

What was changed?

1. .mvn/jvm.config extended

root/app/jeeeraaah/frontend/ui/fx/.mvn/jvm.config

This file contains all JVM options and is automatically read by Maven and IntelliJ.

2. ConfigHealthCheck corrected

Missing property names were added:

How to use it in IntelliJ?

  1. Right-click on DashAppRunner.java
  2. Run β€˜DashAppRunner.main()’
  3. βœ… Done! IntelliJ automatically uses JPMS configuration

Option B: Manual

  1. Run β†’ Edit Configurations…
  2. + β†’ Application
  3. Name: DashAppRunner
  4. Main class: de.ruu.app.jeeeraaah.frontend.ui.fx.dash.DashAppRunner
  5. Use classpath of module: de.ruu.app.jeeeraaah.frontend.ui.fx
  6. Build and run: <Default> (Module Path) ← Important!
  7. VM options: Leave empty (read from .mvn/jvm.config)
  8. OK

What’s the difference?

❌ Before (wrong)

-cp <long list of JARs>
--add-modules jakarta.annotation,jakarta.inject

βœ… Now (correct)

--module-path <module path>
--module de.ruu.app.jeeeraaah.frontend.ui.fx/de.ruu.app.jeeeraaah.frontend.ui.fx.dash.DashAppRunner

Advantages

  1. Single Point of Truth: All JVM options in one file
  2. Maintainable: Changes only in one place
  3. Team-consistent: Works the same for all developers
  4. JPMS-compliant: Uses Java Module System correctly

4. Encapsulation Improvements

Project-wide Statistics (as of 28 Feb 2026)

Before improvements:

Total modules analyzed: 50
Total public types: 479
Hidden public types: 20
Encapsulation ratio: 4.0%

After improvements:

Total modules analyzed: 50
Total public types: 481 (+2 new interfaces)
Hidden public types: 43 (+23)
Encapsulation ratio: 8.9% (doubled!)

Key Improvements

1. lib.jpa.core - Removed criteria.restriction export βœ…

Problem:

Solution:

Result:

2. backend.persistence.jpa - Hidden Implementation Classes βœ…

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:

Modules with Hidden Implementation Classes

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)

5. Run Configuration Details

Important Principles for JPMS

  1. 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.

  2. No –add-modules when possible: Better to explicitly declare modules as dependencies.

  3. Module Path over Classpath: JPMS applications should consistently use the Module Path.

  4. IntelliJ Module Setting: Make sure β€œUse classpath of module” is disabled for JPMS configurations.

POM Corrections Made

The provided scope was removed from jakarta.annotation-api in the following modules:

Now it’s a normal compile dependency:

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
</dependency>

JPMS-compliant Run Configurations

Two new IntelliJ run configurations were created in the .run folder:

DashAppRunner (JPMS).run.xml

DBClean (JPMS).run.xml


6. Troubleshooting

β€œModule X not found”

➜ 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.

β€œUnable to make field accessible”

➜ 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;

β€œRestricted method called”

➜ Check --enable-native-access in .mvn/jvm.config

Compile error: β€œpackage X is not visible”

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.

IntelliJ doesn’t recognize modules

Solution:

  1. File β†’ Invalidate Caches… β†’ Invalidate and Restart
  2. Maven β†’ Reload All Maven Projects
  3. Ensure IntelliJ is using correct JDK (File β†’ Project Structure β†’ Project SDK)

Summary & Best Practices

βœ… Key Takeaways

  1. exports vs opens
    • exports: Compile-time API visibility
    • opens: Runtime reflection access
    • Frameworks need opens, not exports!
  2. Package organization
    • Public API β†’ exported packages
    • Implementation β†’ internal packages (not exported)
    • Framework SPI β†’ qualified exports
  3. Avoid test-driven exports
    • Don’t export types just because tests need them
    • Adjust tests instead!
  4. Continuous improvement
    • Encapsulation is not a one-time goal
    • Regular analysis finds improvement opportunities
    • This project: 4.0% β†’ 8.9% encapsulation ratio
  5. Pragmatism before dogma
    • backend.api.ws.rs remains classpath-based (Jakarta EE requirement)
    • Use JPMS where it provides value

πŸ“Š Project Statistics

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:


Last updated: 2026-03-01