Skip to content

Java Spring REST Controller classes as runtime plugins

currently I want to implement a plugin system into my spring application. The idea is that there is a main spring application which monitors a folder for new jar files. When I put a new jar file in the folder then the main appliation should automatically lift up the RestController classes for usage without downtime. In the plugin jar there is no main class or something like that.

Is this possible in Java Spring to start external RestController classes during runtime?

KR, BlackRose01

Answer

I’ve found a nice repository from tsarenkotxt about this topic. The abstract class is the “interface” for the plugin initialization class. The thread class is from the main application which monitors a directory.

KR, BlackRose01

Abstract class IPlugin

package eu.arrowhead.plugin.types;

import eu.arrowhead.plugin.TargetModule;
import eu.arrowhead.plugin.TargetSystem;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.ResponseEntity;

import java.util.UUID;

/**
 * Interface for Plugin Startclass
 */
public abstract class IPlugin {
    protected final static Logger LOG = LogManager.getLogger(IPlugin.class);

    private UUID PLUGIN_ID;

    protected final static String PLUGIN_NAME = "Testplugin";
    protected final static String PLUGIN_DESCRIPTION = "This is a Testplugin.";
    protected final static String PLUGIN_VERSION = "1";
    protected final static TargetSystem PLUGIN_TARGETSYSTEM = null;
    protected final static TargetModule PLUGIN_TARGETMODULE = null;

    /**
     * Description of the plugin
     * @return
     */
    public static String getPluginDescription() {
        return PLUGIN_DESCRIPTION;
    }

    /**
     * Name of the plugin
     * @return
     */
    public static String getPluginName() {
        return PLUGIN_NAME;
    }

    /**
     * Version of the plugin
     * @return
     */
    public static String getPluginVersion() {
        return PLUGIN_VERSION;
    }

    /**
     * Target System of the plugin
     * @return
     */
    public static TargetSystem getPluginTargetSystem() {
        return PLUGIN_TARGETSYSTEM;
    }

    /**
     * Target Module of the plugin
     * @return
     */
    public static TargetModule getPluginTargetModule() {
        return PLUGIN_TARGETMODULE;
    }

    public void setId(UUID id) {
        this.PLUGIN_ID = id;
    }

    public UUID getId() {
        return this.PLUGIN_ID;
    }

    public static ResponseEntity restTest() {
        return null;
    }

    public void beforeStart() {
    }

    public void start() {

    }

    public void beforeStop() {

    }

    public void stop() {

    }
}

Thread PluginLoader

@Component
public class PluginLoader extends Thread {
    protected final static Logger logger = LogManager.getLogger(PluginLoader.class);

    @Value("${sip.integrator.plugin.dir:./plugin}")
    private String pluginDirectory;

    @Autowired
    private RequestMappingHandlerMapping handlerMapping;

    private File dir;
    private boolean isFirstStart = true;

    public PluginLoader() {
    }

    /**
     * Check if given plugin directory exists, is directory and readable. If path does not
     * exists than it will create the given directory. Default: ./plugin
     * @return
     */
    private boolean createPluginDirectory() {
        if (
                Files.isDirectory(Path.of(this.pluginDirectory)) &&
                Files.exists(Path.of(this.pluginDirectory)) &&
                Files.isReadable(Path.of(this.pluginDirectory))
        )
            return true;

        try {
            Files.createDirectory(Path.of(this.pluginDirectory),
                    PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-r--")));
            return true;
        } catch (IOException e) {
            logger.error("Cannot create plugin path: " + this.pluginDirectory);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Create a dynamic rest endpoint
     * @return
    */
    private Object createRestHandler(String method) throws InstantiationException, IllegalAccessException {
        return new ByteBuddy()
                .subclass(Object.class)
                .name("Initializer")
                .annotateType(AnnotationDescription.Builder
                        .ofType(RestController.class)
                        .build()
                )
                .defineMethod(method, ResponseEntity.class, Modifier.PUBLIC)
                .intercept(MethodDelegation.to(IPlugin.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
    }

    public void run() {
        if (this.isFirstStart) {
            this.createPluginDirectory();
            this.isFirstStart = false;
        }

        if (this.isInterrupted())
            return;

        try {
            WatchService watcher = dir.toPath().getFileSystem().newWatchService();
            WatchKey watckKey;
            List<WatchEvent<?>> events;

            dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_CREATE,/*StandardWatchEventKinds
            .ENTRY_DELETE,*/
                    StandardWatchEventKinds.ENTRY_MODIFY);
            watckKey = watcher.take();

            while (!this.isInterrupted()) {
                events = watckKey.pollEvents();

                for (WatchEvent event : events) {
                    if (!event.context().toString().endsWith(".jar")) {
                        logger.error("JUMP");
                        continue;
                    }

                    File f = new File(dir.getAbsolutePath() + "\" + event.context().toString());
                    URLClassLoader child = new URLClassLoader(
                            new URL[] {f.toURI().toURL()},
                            this.getClass().getClassLoader()
                    );
                    logger.error("FOUND");
                    JarInputStream jarStream;
                    try {
                        jarStream = new JarInputStream(f.toURL().openStream());
                    } catch (Exception e) {
                        e.printStackTrace();
                        logger.error("NOOOOO");
                        continue;
                    }

                    try {
                        Class<?> classToLoad = Class.forName("de.sip.plugin.Initializer", true, child);

                        handlerMapping
                                .registerMapping(
                                        RequestMappingInfo.paths("/t/a")
                                                .methods(RequestMethod.GET)
                                                .produces(MediaType.ALL_VALUE)
                                                .build(),
                                        createRestHandler("restTest"),
                                        classToLoad.getMethod("restTest")
                                );
                    } catch (Exception e) {
                        e.printStackTrace();
                        logger.error("Cannot load plugin");
                        child.close();
                        jarStream.close();
                    }
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}