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.pluginand 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.
ModuleFinderdiscovers them in a plugin folder.Configurationresolves dependencies.ServiceLoaderloads 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
ModuleFinderfor plugin discovery. - ✅ Keep configurations minimal and explicit.
- ✅ Always resolve against a parent configuration.
- ✅ Use
ServiceLoaderto connect dynamic modules. - ✅ Avoid
open moduleunless absolutely necessary. - ✅ Test module resolution with
jdepsto 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:
ModuleFinderis the guest list manager, deciding who is available.Configurationis 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
ModuleFinderhelps locate modules on disk.Configurationdefines 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.