Customizing Module Resolution with ModuleFinder and Configuration in Java Modules

Illustration for Customizing Module Resolution with ModuleFinder and Configuration in Java Modules
By Last updated:

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

  1. Classpath mindset → You can’t just drop JARs and expect visibility.
  2. Split packages → Still not allowed across resolved configurations.
  3. Overuse of layers → Too many layers create complexity.
  4. 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.