Understanding Java's Module System: A Deep Dive into module-info.java
This article explains why Java modularization was introduced, outlines the problems with the pre‑Java 9 classpath, introduces the Java Platform Module System (JPMS) and its core file module-info.java, and provides detailed guidance on concepts, syntax, best practices, real‑world examples, IDE setup, and common pitfalls.
Why Java modularity is needed
Before Java 9 the class‑path mechanism caused four major problems:
Monolithic JDK – the whole JDK had to be packaged even when only a small part (e.g., java.io) was used, leading to large artifacts unsuitable for embedded or mobile environments.
Class and package conflicts – different third‑party JARs could contain the same classes (e.g., different versions of commons‑lang3), causing runtime NoClassDefFoundError and difficult debugging.
Poor encapsulation – any code could reflectively access any class, exposing internal implementation details and increasing coupling.
Unstructured project layout – large code bases lacked clear boundaries, making maintenance and onboarding hard.
Java 9 introduced the Java Platform Module System (JPMS) to address these issues by making dependencies explicit and enforcing strong encapsulation.
Core concepts of JPMS
Module – a logical unit identified by a unique name and containing a module-info.java descriptor.
ModulePath – replaces the traditional ClassPath; the JVM loads modules from this path and resolves their dependencies.
module-info.java – the module descriptor placed in src/main/java. It declares the module name, required modules, exported packages, opened packages, and service contracts.
Strong encapsulation – only packages listed in exports are visible to other modules; all other packages are inaccessible, even via reflection unless opened.
Minimal module declaration
module com.example.modulea {
// No explicit requires, exports, or opens are mandatory.
}Even an empty module must contain the module keyword followed by the module name and braces.
Module naming conventions
Use reverse‑domain notation (e.g., com.example.modulea) to avoid name clashes.
All lower‑case letters, separated by dots to reflect hierarchy.
The name must be unique within a project and, preferably, across projects.
Avoid Java keywords and built‑in module names such as java.base or java.sql.
Correct examples: com.example.modulea, com.example.service.user, shop.order. Incorrect examples: ModuleA (capital letters), com.example.module-a (hyphen), java.base (reserved).
Key keywords in module-info.java
1. module – define a module
// Syntax: module <module-name> { ... }
module com.example.modulea {
// Additional directives go here
}Only one module declaration per file; nesting is prohibited.
2. requires – declare dependencies
module com.example.moduleb {
requires com.example.modulea; // Custom module
requires java.sql; // JDK module
requires spring.context; // Third‑party modular JAR
requires static lombok; // Compile‑time only
requires transitive com.example.core; // Re‑exported dependency
} requires transitivepropagates the dependency to modules that depend on this module. requires static makes the dependency optional at runtime.
3. exports – export packages
module com.example.modulea {
exports com.example.modulea.service; // Public API
exports com.example.modulea.entity; // Public entities
// No export for internal DAO package
}Exported packages become the module’s public API. Sub‑packages are not exported automatically; they must be declared explicitly (e.g., exports com.example.modulea.service.impl;).
4. opens – open packages for reflection
module com.example.modulea {
opens com.example.modulea.entity; // Open to all modules
opens com.example.modulea.entity to spring.core, org.mybatis; // Targeted opening
opens com.example.modulea.dao to org.mybatis; // DAO for MyBatis only
} opens … torestricts reflective access to the listed modules.
5. Service Provider Interface (SPI) – provides … with and uses
// Service interface module
module com.example.pay.api {
exports com.example.pay.api;
}
// Provider module
module com.example.pay.alipay {
requires com.example.pay.api;
provides com.example.pay.api.PayService with com.example.pay.alipay.AlipayService;
}
// Consumer module
module com.example.order {
requires com.example.pay.api;
uses com.example.pay.api.PayService;
}At runtime ServiceLoader discovers implementations of PayService without a compile‑time dependency on the concrete provider.
Standard Maven project layout for modules
my-modular-project/
├── module-a/
│ └── src/main/java/
│ ├── module-info.java
│ └── com/example/modulea/…
│ └── pom.xml
├── module-b/
│ └── src/main/java/
│ ├── module-info.java
│ └── com/example/moduleb/…
│ └── pom.xml
└── pom.xml // Parent POM (no source code)Each module must place its module-info.java directly under src/main/java. The parent project aggregates the modules.
Practical guide: creating a modular project in IntelliJ IDEA
Create a non‑modular Maven parent project (GroupId com.example, ArtifactId modular-demo, Version 1.0‑SNAPSHOT). Delete the generated src folder because the parent only aggregates modules.
Add Module A:
Create a Maven module with ArtifactId module-a.
Add module-info.java containing:
module com.example.modulea {
exports com.example.modulea.service;
}Create package com.example.modulea.service and class UserService:
package com.example.modulea.service;
public class UserService {
public String getUserName(Long userId) {
// Simulated business logic
return "张三";
}
}Add Module B (depends on Module A):
Create a Maven module with ArtifactId module-b.
Add module-info.java:
module com.example.moduleb {
requires com.example.modulea;
}Create package com.example.moduleb.controller and class UserController:
package com.example.moduleb.controller;
import com.example.modulea.service.UserService;
public class UserController {
public static void main(String[] args) {
UserService userService = new UserService();
String userName = userService.getUserName(1001L);
System.out.println("用户名:" + userName); // Expected output: 用户名:张三
}
}Run UserController.main(). The console prints 用户名:张三, confirming that Module B successfully depends on and uses Module A.
Verify strong encapsulation: add a non‑exported package com.example.modulea.dao with class UserDao. Attempting to import UserDao from Module B results in a compilation error, proving that unexported packages are inaccessible.
Common issues and solutions
Module not found
Causes:
Incorrect module name (case‑sensitive typo).
Dependency not added to the ModulePath (e.g., IDEA module dependency missing).
Third‑party JAR without a module descriptor.
Solutions:
Check spelling and case of the module name.
In IDEA, add the required module via Open Module Settings → Dependencies → + → Module Dependency .
For non‑modular JARs, use requires automatic <jar‑module‑name>; (e.g., requires automatic org.apache.commons.lang3;) to let the JVM generate a descriptor.
Package not visible
Cause: the required package is not exported by the provider module.
Fix: add an exports <package-name>; statement to the provider’s module-info.java.
Frameworks (Spring, MyBatis) cannot work
Cause: the framework needs reflective access to certain packages that are not opened.
Fix: open the necessary packages, e.g.:
opens com.example.modulea.entity to spring.core, org.mybatis;or open the whole module with open module com.example.modulea { … } if appropriate.
Circular dependencies
Cause: two modules depend on each other, creating a dependency cycle.
Fixes:
Extract the common code into a new core module and let both original modules depend on it.
Use the SPI pattern ( uses / provides) to break the compile‑time cycle.
Exported parent package but sub‑package not accessible
Cause: exports com.example.modulea.service; does not automatically export com.example.modulea.service.impl.
Fix: add an explicit export for the sub‑package:
exports com.example.modulea.service.impl;JDK built‑in modules (selected)
java.base– fundamental classes (String, List, IO, reflection). Automatically available; no explicit requires needed. java.sql – JDBC API. Explicit requires java.sql; required. java.xml – XML parsing (DOM, SAX). Explicit requires java.xml; required. java.desktop – AWT/Swing UI. Explicit requires java.desktop; required. java.net.http – HTTP client (Java 11+). Explicit requires java.net.http; required. java.compiler – Dynamic compilation APIs. Explicit requires java.compiler; required.
List all modules with java --list-modules.
Full‑length summary
The Java Platform Module System introduced in Java 9 solves long‑standing class‑path problems by enforcing explicit module boundaries, reducing the JDK footprint, preventing class conflicts, and improving project maintainability. The heart of JPMS is the module-info.java descriptor, which defines the module’s identity, required modules, exported packages, opened packages for reflection, and service contracts via provides / uses. Proper module naming (reverse‑domain, all lower‑case) ensures uniqueness. Developers can create minimal modules, declare rich dependency graphs, and leverage transitive or static requires for compile‑time versus runtime needs. Exported packages become the public API, while non‑exported packages remain inaccessible, guaranteeing strong encapsulation. The opens directive enables reflective frameworks while preserving encapsulation elsewhere. Service Provider Interface (SPI) support allows decoupled service consumption without direct dependencies. A conventional Maven layout places each module under its own directory with a module-info.java at the root of src/main/java. The article walks through creating a parent Maven project, adding two modules (A and B), implementing a simple UserService, and wiring them together in IntelliJ IDEA. It also covers common pitfalls such as missing modules, invisible packages, framework integration issues, circular dependencies, and sub‑package export nuances, offering concrete fixes. Finally, it lists essential JDK modules and how to query them. Mastering JPMS equips Java developers with the tools to build modular, maintainable, and secure applications, especially for large‑scale or framework‑heavy projects.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
