One of the biggest misconceptions about the Java Platform Module System (JPMS) is that module resolution is fixed at compile time. Many developers assume the module path works like the old classpath: just drop in JARs, and everything magically works. But with JPMS, resolution is explicit, strict, and configurable.
In complex systems—like enterprise applications, plugin-based architectures, or microservices—you often need to dynamically discover and resolve modules at runtime. This is where ModuleFinder
and Configuration
come into play. They allow developers to take control of the module resolution process and define how modules are wired together.
What is ModuleFinder?
ModuleFinder
is a JPMS utility class that locates modules in specified paths.
Example: Finding Modules
ModuleFinder finder = ModuleFinder.of(Path.of("plugins"));
finder.findAll().forEach(moduleRef ->
System.out.println("Found module: " + moduleRef.descriptor().name()));
This will list all modules available in the plugins/
directory.
What is Configuration?
Configuration
defines how modules are resolved and connected in a layer. It determines which modules are loaded, their dependencies, and how they interact.
Example: Creating a Configuration
ModuleFinder finder = ModuleFinder.of(Path.of("plugins"));
Configuration parentConfig = ModuleLayer.boot().configuration();
Configuration pluginConfig = parentConfig.resolve(
finder, ModuleFinder.of(), Set.of("com.example.plugin")
);
Here:
- The parent configuration is the boot layer.
- The new configuration includes
com.example.plugin
and resolves its dependencies.
Putting It Together: Loading Modules Dynamically
ModuleFinder finder = ModuleFinder.of(Path.of("plugins"));
Configuration parentConfig = ModuleLayer.boot().configuration();
Configuration pluginConfig = parentConfig.resolve(
finder, ModuleFinder.of(), Set.of("com.example.plugin")
);
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
ModuleLayer pluginLayer = ModuleLayer.defineModulesWithOneLoader(
pluginConfig, List.of(ModuleLayer.boot()), systemClassLoader
);
pluginLayer.modules().forEach(m ->
System.out.println("Loaded module: " + m.getName()));
This creates a new layer containing dynamically loaded modules.
Real-World Use Case: Plugin Systems
Imagine a payment system where new payment providers (PayPal, Stripe, Razorpay) can be added without restarting the application.
- Each provider is packaged as a JPMS module.
ModuleFinder
discovers them in a plugin folder.Configuration
resolves dependencies.ServiceLoader
loads implementations dynamically.
This provides a clean, extensible, and maintainable design.
Pitfalls & Misuse Cases
- Classpath mindset → You can’t just drop JARs and expect visibility.
- Split packages → Still not allowed across resolved configurations.
- Overuse of layers → Too many layers create complexity.
- Security risks → Dynamically opening modules can reintroduce vulnerabilities.
Best Practices
- ✅ Use
ModuleFinder
for plugin discovery. - ✅ Keep configurations minimal and explicit.
- ✅ Always resolve against a parent configuration.
- ✅ Use
ServiceLoader
to connect dynamic modules. - ✅ Avoid
open module
unless absolutely necessary. - ✅ Test module resolution with
jdeps
to validate boundaries.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → Introduced JPMS, ModuleFinder, and Configuration APIs
- Java 11 → Improved integration with IDEs and build tools
- Java 17 → Security and performance refinements
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of ModuleFinder
and Configuration
like a conference organizer:
ModuleFinder
is the guest list manager, deciding who is available.Configuration
is the seating chart, deciding who sits where and who can talk to whom.- Without them, the event (application) would descend into chaos, just like classpath spaghetti.
Summary + Key Takeaways
ModuleFinder
helps locate modules on disk.Configuration
defines how modules are resolved into layers.- Together, they enable dynamic, flexible modular applications.
- Use them in plugin architectures, microservices, and dynamic runtime systems.
- Avoid pitfalls like split packages and overuse of open modules.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit exports.
Q2. Why do I get “module not found” errors?
Because the module wasn’t in the ModuleFinder
search path.
Q3. Can I resolve multiple modules at once?
Yes, pass multiple module names to Configuration.resolve()
.
Q4. Can configurations be nested?
Yes, each configuration can resolve modules against a parent configuration.
Q5. What is the difference between exports
and opens
?
exports
→ compile + runtime visibility.opens
→ runtime reflection only.
Q6. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Good for migration, not production.
Q7. How does JPMS improve security compared to classpath?
By hiding internals, blocking split packages, and restricting reflection.
Q8. When should I use jlink
vs jmod
?
jlink
→ build custom runtime images.jmod
→ distribute modules.
Q9. Can I migrate a legacy project incrementally?
Yes, start with automatic modules, then gradually add module-info.java
.
Q10. Do frameworks like Spring and Hibernate support JPMS?
Yes, but they require selective opens
for reflection.