1. Motivation: The Road to Modular Isolation
When building extensible applications in Java, developers often start with a simple question: "How can I let users add functionality without recompiling the core application?" The journey usually begins with the standard java.util.ServiceLoader, which provides a simple mechanism for discovering implementations of an interface.
However, as the application grows, a critical problem emerges: "Classpath Hell."
Imagine you have a host application that uses library-v1. You create a plugin system, and someone writes a "Twitter Plugin" that requires library-v2. If you run everything on the same flat classpath, you get a conflict. Either the host crashes because it gets the wrong version of the library, or the plugin fails. You cannot have two versions of the same library on the classpath without facing the risk of runtime exceptions such as ClassDefNotFoundError or NoSuchMethodError.
This was the driving motivation behind JExten. I needed a way to strictly encapsulate plugins so that each one could define its own dependencies without affecting the host or other plugins.
Enter JPMS (Java Platform Module System)
Java 9 introduced the Module System (JPMS), which provides strong encapsulation and explicit dependency graphs. It allows us to create isolated "layers" of modules.
- Boot Layer: The JVM and platform modules.
- Host Layer: The core application and its dependencies.
- Plugin Layers: Dynamically created layers on top of the host layer.
By leveraging JPMS ModuleLayers, JExten allows Plugin A to rely on Jackson 2.14 while Plugin B relies on Jackson 2.10, and both can coexist peacefully within the same running application.
2. Architecture and Design
JExten is designed to be lightweight and annotation-driven, abstracting away the complexity of raw ModuleLayers while providing powerful features like Dependency Injection (DI) and lifecycle management.
The architecture consists of three main pillars:
The Extension Model
At the core, JExten uses a clean separation between the "contract" (API) and the "implementation".
-
Extension Point (
@ExtensionPoint): An interface defined in the host application (or a shared API module) that defines what functionality can be extended.
@ExtensionPoint(version = "1.0") public interface PaymentGateway { void process(double amount); } -
Extension (
@Extension): The concrete implementation provided by a plugin.
@Extension(priority = Priority.HIGH) public class StripeGateway implements PaymentGateway { // ... }
Notice that you can make use of the ExtensionManager without the PluginManager. This is useful for testing or when you want to use JExten in a non-plugin environment and all the extensions are already available in the modulepath.
The Manager Split
To separate concerns, the library splits responsibilities into two distinct managers:
-
PluginManager ("The Physical Layer"):
- This component handles the raw artifacts (JARs/ZIPs).
- It verifies integrity using SHA-256 checksums ensuring that plugins haven't been tampered with.
- It builds the JPMS
ModuleLayergraph. It reads theplugin.yamlmanifest, resolves dependencies (from a local cache or Maven repo), and constructs the classloading environment.
-
ExtensionManager ("The Logical Layer"):
- Once layers are built, this component takes over.
- It scans the layers for classes annotated with
@Extension. - It manages the lifecycle of these extensions (Singleton, Session, or Prototype scopes).
- It handles Dependency Injection.
Dependency Injection (DI)
Since plugins run in isolated layers, standard DI frameworks (like Spring or Guice) can sometimes be "too heavy" or tricky to configure across dynamic module boundaries. JExten includes a built-in, lightweight DI system.
You can simply use @Inject to wire extensions together:
@Extension
public class MyPluginService {
@Inject
private PaymentGateway gateway; // Automatically injects the highest priority implementation
}
This works seamlessly across module boundaries. A plugin can inject a service provided by the host, or even a service provided by another plugin (if the module graph allows it).
3. Usage Example
Here is a quick look at how to define an extension point, implement it in a plugin, and use it in your application.
I. Define an Extension Point
Create an interface and annotate it with @ExtensionPoint. This is the contract that plugins will implement.
@ExtensionPoint(version = "1.0")
public interface Greeter {
void greet(String name);
}
II. Implement an Extension
In your plugin module, implement the interface and annotate it with @Extension.
@Extension
public class FriendlyGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
III. Discover and Use
In your host application, use the ExtensionManager to discover and invoke extensions.
public class Main {
public static void main(String[] args) {
// Initialize the manager
ExtensionManager manager = ExtensionManager.create(pluginManager);
// Get all extensions for the Greeter point
manager.getExtensions(Greeter.class)
.forEach(greeter -> greeter.greet("World"));
}
}
IV. Package your Extension(s) as a Plugin
Finally, use the jexten-maven-plugin Maven plugin to check your module-info.java at compile time and package your extension into a ZIP bundle that includes all dependencies and the generated plugin.yaml manifest.
<plugin>
<groupId>org.myjtools.jexten</groupId>
<artifactId>jexten-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>generate-manifest</goal>
<goal>assemble-bundle</goal>
</goals>
</execution>
</executions>
<configuration>
<hostModule>com.example.app</hostModule>
</configuration>
</plugin>
You can then install the generated ZIP bundle to your host application:
public class Application {
public static void main(String[] args) throws IOException {
Path pluginDir = Path.of("plugins");
// Create plugin manager
PluginManager pluginManager = new PluginManager(
"org.myjtools.jexten.example.app", // Application ID
Application.class.getClassLoader(),
pluginDir
);
// Install plugin from bundle
pluginManager.installPluginFromBundle(
pluginDir.resolve("my-plugin-1.0.0.zip")
);
// Create extension manager with plugin support
ExtensionManager extensionManager = ExtensionManager.create(pluginManager);
// Get extensions from the plugin
extensionManager.getExtensions(Greeter.class)
.forEach(greeter -> greeter.greet("World"));
}
}
4. Comparison with Other Solutions
Choosing the right plugin framework depends on your specific needs. Here is how JExten compares to established alternatives:
PF4J (Plugin Framework for Java)
PF4J is a mature, lightweight plugin framework that relies on ClassLoader isolation.
- Isolation: PF4J uses custom ClassLoaders to isolate plugins. JExten uses JPMS ModuleLayers. The latter is the "native" Java way to handle isolation since Java 9, strictly enforcing encapsulation at the JVM level.
- Modernity: While PF4J is excellent, JExten is designed specifically for the modern modular Java ecosystem (Java 21+), taking advantage of module descriptors (
module-info.java) for defining dependencies rather than custom manifests.
OSGi
OSGi is the gold standard for modularity, powering IDEs like Eclipse.
- Complexity: OSGi is powerful but comes with a steep learning curve and significant boilerplate (Manifest headers, Activators, complex service dynamics). JExten offers a fraction of the complexity ("OSGi Lite") by focusing on the 80% use case: strictly isolated extensions with simple dependency injection, without requiring a full OSGi container.
- Runtime: OSGi brings a heavy runtime. JExten is a lightweight library that sits on top of the standard JVM features.
Layrry
Layrry is a launcher and API for executing modular Java applications.
- Scope: Layrry focuses heavily on the configuration and assembly of module layers (often via YAML/TOML) and acts as a runner. JExten focuses on the programming model within those layers.
- Features: Layrry is great for constructing the layers, but it doesn't provide an opinionated application framework. JExten provides the "glue" code—Extension Points, Dependency Injection, and Lifecycle Management—that you would otherwise have to write yourself when using raw Module Layers or Layrry.
| Feature | JExten | PF4J | OSGi | Layrry |
|---|---|---|---|---|
| Isolation | JPMS ModuleLayers | File/ClassLoader | Bundle ClassLoaders | JPMS ModuleLayers |
| Configuration | Java Annotations | Properties/Manifest | Manifest Headers | YAML/TOML |
| Dependency Injection | Built-in (@Inject) |
External (Spring/Guice) | Declarative Services | None (ServiceLoader) |
| Learning Curve | Low | Low | High | Medium |
5. Conclusion
JExten is a lightweight, annotation-driven plugin framework that leverages JPMS ModuleLayers to provide isolation and dependency management. It is designed to be easy to use and understand, with a focus on simplicity and ease of use.
Finally, keep in mind that JExten is still in its early stages, and there is much room for improvement. Feel free to contribute to the project on GitHub and/or engage in a discussion in the issues section. Link to the repository is here .
Top comments (0)