DEV Community

ivan.gavlik
ivan.gavlik

Posted on

Implement Plug in Play architecture

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.

Basic Architecture of Plug in Play

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

Create Annotations Implementation Plug in Play architecture

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DependencyInstance {
}

Enter fullscreen mode Exit fullscreen mode

Create central class that orchestrate plugin loading, initialization, and coordination



/**
 * 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;
    }
}


Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode
  • PluginRegistry

/**
 * 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;
    }
}


Enter fullscreen mode Exit fullscreen mode

Usage

Implement plugin

@Plugin(id = "org.example.PluginA")
public class PluginA {

    @Start
    public void init() {
        System.out.println("I ve started :)");
    }
}

Enter fullscreen mode Exit fullscreen mode

Main method

    public static void main(String[] args) {
        System.out.println("Starting");
        new PonderaMicrokernel(new String[] {}, new String[] {"org.example.PluginA"});
    }

Enter fullscreen mode Exit fullscreen mode

Output

Starting
I ve started :)
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
gracemansam profile image
SAMUEL GRACEMAN • Edited

Thank you for this post. what of plugin I can install at runtime. Pls I will be grateful if you can help. Thank you.

Collapse
 
ivangavlik profile image
ivan.gavlik

in this blog post I covered case when plugins are installed/imported at Compile time for more details on the runtime installation you can write me on email: ivangavlik963@gmail.com