Implement Plug in Play architecture
ivan.gavlik
Posted on December 16, 2023
Intro
In the first part we start with defining plug in play architecture exploring its main components and then discussing various options for plugin identification and configurations,. loading, lifecycle management, communication ...
In the second part we study real-world examples to gain valuable insights into the diverse strategies and methodologies applied when creating Plug in Play solutions
In this part we are implementing simple plug in play framework in java, We start with Technical requirements then discuss architecture do the implementation and in the end is a example how to use framework.
Technical Requirements
Here we define options plugin identification and configurations,. loading, lifecycle management, communication
Annotation-Based Plugin Identification
Hook Annotations for Lifecycle Management:
@Start
and@Stop
Compile time import
Plugin Management Kernel (handles the loading and running of plugins)
Each plugin can be configure to run in app process
Extension points (enabling one plugin to enhance the functionality of another one)
Event-Based Communication Between Plugins
Architecture Decisions
Based on technical requirements lets discuss basic architecture and its pros and cons.
Components
Plugin Declaration/Definition Module: This module provides annotations for marking plugins and defining lifecycle hooks. Developers use these annotations in client code to identify and manage plugins.
-
Plugin Runtime Module: This module houses the "PonderaKernel," a runtime responsible for managing plugin lifecycles. It coordinates plugin loading, initialization, and communication.
- Events Sub-Module: Responsible for facilitating event-based communication between plugins. Events enable plugins to interact, exchange data, and trigger actions.
Benefits
Simplicity: Annotation-based identification and lifecycle management streamline plugin integration.
Efficiency: Plugins run in separate processes, minimizing disruptions and resource conflicts.
Innovation: Extension points and event-based communication encourage modular development and creative enhancements.
Pros and Cons
Pros:
Streamlined integration process, reducing complexity and configuration overhead.
Enables modular development and creative enhancements through extension points.
Promotes loose coupling and event-based communication among plugins.
Simplifies plugin lifecycle management through annotations and the "PonderaKernel."
Supports modern Java versions and practices, ensuring compatibility and longevity.
Cons:
Event-based communication might require careful design to ensure efficiency and responsiveness.
Annotation-based approach might be limiting in certain complex scenarios.
Plugin interactions and dependency management could become challenging in larger systems
Implementation
Create Annotations
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DependencyInstance {
}
Create central class that orchestrate plugin loading, initialization, and coordination
java
/**
* The central class that serves as the microkernel for the PonderaAssembly plugin framework.
* It handles the loading, initialization, and coordination of plugins.
*/
public final class PonderaMicrokernel {
public static PluginEventManager EVENT = new PluginEventManager(PluginRegistry.INSTANCE.getInstances());
private PluginRegistry pluginRegistry = PluginRegistry.INSTANCE;
/**
* Initializes the PonderaMicrokernel by loading dependencies and plugins,
*
* <p> Initialization steps </p>
* <ol>
* <li> init all dependencies </li>
* <li> call plugins constructor</li>
* <li> init extension points </li>
* <li> run start method</li>
* </ol>
*
* @param dependencyClasses An array of dependency class names.
* @param pluginClasses An array of plugin class names.
* */
// TODO cover the case were start is called before extended
public PonderaMicrokernel(final String[] dependencyClasses, final String[] pluginClasses) {
// load dependencies
StartUpHelper.loadDependencies(dependencyClasses)
.stream()
.forEach(pair -> {
pluginRegistry.addInstance(pair.getName(), pair.getInstance());
});
// run plugin constructor
StartUpHelper.loadPlugins(pluginClasses)
.stream()
.filter(pluginClass -> pluginRegistry.exist(pluginClass.getName()))
.map(pluginClass -> runConstructor(pluginClass))
.forEach(instance -> pluginRegistry.addInstance(instance.getClass().getName(), instance));
// TODO order is not handled
pluginRegistry.getInstances().values().stream()
.forEach(instances -> initExtensionPoint(instances));
// TODO order is not handled - should I use tree set
pluginRegistry.getInstances().values().stream()
.filter(el -> el.getClass().getDeclaredAnnotation(Plugin.class) != null)
.forEach(in -> runPluginStart(in));
List<Object> actionHandlers = pluginRegistry.getInstances().values().stream()
.filter(el -> el.getClass().getDeclaredAnnotation(Plugin.class) != null)
.filter(el -> Arrays.stream(el.getClass().getDeclaredMethods())
.anyMatch(method -> method.isAnnotationPresent(HandleEvent.class)))
.collect(Collectors.toList());
EVENT.pluginActionList.addAll(actionHandlers);
}
private Object runConstructor(Class pluginClass) {
for (int i = 0; i < pluginClass.getDeclaredConstructors().length; i++) {
Constructor con = pluginClass.getDeclaredConstructors()[i];
Object[] params = Arrays.stream(con.getParameters())
.filter(el -> el.isAnnotationPresent(Dependency.class))
.map(el -> pluginRegistry.get(el.getAnnotation(Dependency.class).name()))
.toArray(Object[]::new);
try {
return con.newInstance(params);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
try {
// if no declared use default constructor
return pluginClass.getConstructors()[0].newInstance();
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private void runPluginStart(Object instance) {
Arrays.stream(instance.getClass().getDeclaredMethods())
.map(method -> new Object() {
Method classMethod = method;
Annotation annotation = method.getAnnotation(Start.class);
}
)
.filter(el -> el.annotation != null)
.map(el -> el.classMethod)
.findFirst()
.ifPresent(el -> {
try {
el.invoke(instance); // TODO method params
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
private Object initExtensionPoint(Object instance) {
Arrays.stream(instance.getClass().getDeclaredFields())
.filter(el -> el.getAnnotation(ExtensionPoint.class) != null)
.map(el -> {
// get classes that implements this extension points class
List<Class> allImpl = this.pluginRegistry.getInstances().values()
.stream()
.map(in -> in.getClass())
.map(in -> new Object() {
Class classInfo = in;
List interfaces = Arrays.stream(in.getInterfaces()).collect(Collectors.toList());
}
)
// contains because typeName includes List<Type>
.filter(in -> in.interfaces.stream().anyMatch(i -> el.getGenericType().getTypeName().contains(((Class) i).getName())))
.map(in -> in.classInfo)
.collect(Collectors.toList());
// for each class create instance and add it to instances
List<Object> values = allImpl
.stream()
.map(item -> {
Object value = this.pluginRegistry.get(item.getName());
// find class that implements specific extension point create it and assign
// it to this instance
if (value != null) {
return value;
}
Object instanceNew = runConstructor(item);
pluginRegistry.addInstance(instanceNew.getClass().getName(), instanceNew);
return instanceNew;
})
.collect(Collectors.toList());
// set up instance extension value
try {
el.setAccessible(true);
el.set(instance, values);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return el;
})
.collect(Collectors.toList());
return instance;
}
}
Supporting classes
StartUpHelper
/**
* Utility methods to load classes and create instances (dependencies and plugins included)
*/
final class StartUpHelper {
public static List<Pair> loadDependencies(String[] dependencyClasses) {
return Arrays.stream(dependencyClasses)
.map(el -> toDependencyFactory(el))
.filter(dependencyClass -> dependencyClass.isPresent())
.flatMap(pluginClass -> toDependencyInstance(pluginClass.get()).stream())
.collect(Collectors.toList());
}
private static Optional<Class<DependencyFactory>> toDependencyFactory(String name) {
try {
Class candidateClass = Class.forName(name);
Annotation annotation = candidateClass.getDeclaredAnnotation(DependencyFactory.class);
if (annotation != null) {
return Optional.of(candidateClass);
} else {
return Optional.empty();
}
} catch (Exception ex) {
return Optional.empty();
}
}
private static List<StartUpHelper.Pair> toDependencyInstance(Class dependencyClass) {
final Optional<Object> instance = getInstance(dependencyClass);
if (!instance.isPresent()) {
return new ArrayList<>();
}
return Arrays.stream(dependencyClass.getDeclaredMethods())
.filter(el -> el.isAnnotationPresent(DependencyInstance.class))
.map(el -> {
try {
StartUpHelper.Pair pair = new Pair(el.getName(), el.invoke(instance.get()));
return Optional.of(pair);
} catch (Exception ex) {
return Optional.empty();
}
})
.filter(el -> el.isPresent())
.map(el -> (StartUpHelper.Pair) el.get())
.collect(Collectors.toList());
}
private static Optional<Object> getInstance(Class dependencyClass) {
try {
return Optional.of(dependencyClass.getConstructors()[0].newInstance());
} catch (Exception e) {
e.printStackTrace();
return Optional.empty();
}
}
public static List<Class<Plugin>> loadPlugins(String[] pluginClasses) {
return Arrays.stream(pluginClasses)
.map(el -> toPlugin(el))
.filter(pluginClass -> pluginClass.isPresent())
.map(pluginClass -> pluginClass.get())
.collect(Collectors.toList());
}
private static Optional<Class<Plugin>> toPlugin(String name) {
try {
Class candidateClass = Class.forName(name);
Annotation annotation = candidateClass.getDeclaredAnnotation(Plugin.class);
if (annotation != null) {
return Optional.of(candidateClass);
} else {
return Optional.empty();
}
} catch (Exception ex) {
return Optional.empty();
}
}
public static class Pair {
private String name;
private Object instance;
public Pair(String name, Object instance) {
this.name = name;
this.instance = instance;
}
public String getName() {
return name;
}
public Object getInstance() {
return instance;
}
}
}
-
PluginRegistry
java
/**
- Holds dependency {@link io.github.ivangavlik.PonderaAssembly.plugin.dependency.DependencyInstance}
-
and plugin {@link io.github.ivangavlik.PonderaAssembly.plugin.Plugin} instances.
*/
final class PluginRegistry {
static PluginRegistry INSTANCE = new PluginRegistry();
// TODO order is not handled - ordershoud be as used is declared plugin also take account depencies and extension points
private Map<String, Object> instances = new HashMap<>();
private PluginRegistry() {}
public void addInstance(String key, Object value) {
instances.put(key, value);
}
public Object get(String key) {
return instances.get(key);
}
public boolean exist(String key) {
return instances.get(key) == null;
}
public Map<String, Object> getInstances() {
return this.instances;
}
}
Usage
Implement plugin
java
@Plugin(id = "org.example.PluginA")
public class PluginA {
@Start
public void init() {
System.out.println("I ve started :)");
}
}
Main method
java
public static void main(String[] args) {
System.out.println("Starting");
new PonderaMicrokernel(new String[] {}, new String[] {"org.example.PluginA"});
}
Output
Starting
I ve started :)
Summary
By fulfilling these technical requirements, the implemented new plugin framework aims to offer a user-friendly, lightweight, and contemporary solution for integrating plugins into Java applications.
Here is the source code
Posted on December 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 8, 2023