How to customize where ServiceLoader looks for Plugin Jars

1k views Asked by At

I have a multimodule project with two projects: Core and A. The idea is to launch/ run A whenever Core is launched.

How can I customize the ServiceLoader to look up and call the modules in the Plugins folder from core?

plugin-Project
    + Core
        + src\main\java
            Core.java

    + A
        + src\main\java
            A.java

    + Plugins

Core

public class Core extends Application {

    private ServiceLoader<View> views;
    private BorderPane mainBorderPane;

    @Override
    public void init() {
        loadViews();
    }

    private void loadViews() {
        views = ServiceLoader.load(View.class);
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Ui Application");

        mainBorderPane = new BorderPane();
        mainBorderPane.setTop(createMenuBar());

        Scene scene = new Scene(new Group(), 800, 605);
        scene.setRoot(mainBorderPane);

        stage.setScene(scene);
        stage.show();
    }

    private MenuBar createMenuBar() {
        MenuBar menuBar = new MenuBar();
        Menu viewMenu = new Menu("Views");
        menuBar.getMenus().add(viewMenu);

        ToggleGroup toggleGroup = new ToggleGroup();

        views.forEach(v -> {
            RadioMenuItem item = new RadioMenuItem(v.getName());
            item.setToggleGroup(toggleGroup);
            item.setOnAction(event -> {
                Label label = new Label(v.getName());
                mainBorderPane.setLeft(label);
                mainBorderPane.setCenter(v.getView());
            });
            viewMenu.getItems().add(item);
        });
        return menuBar;
    }

    public static void main(String[] args) {
        launch(args);
    }

}

View.java

public interface View {

    String getName();

    Node getView();
}

Scenario

The application I'm working on is an multi-module stand-alone desktop application. For example, Core would hold a pane on the left (left-pane). left-pane will accept nodes from any module that implements an interface called LeftPane. A implements the LeftPane interface. Whenever Core is launched, it should scan through a folder, plugins in this case and automatically start all the bundles there, including A, which would go on to populate the left pane.

1

There are 1 answers

1
hotzst On BEST ANSWER

The easiest way of course would be to have the plugins already on the classpath. Then you can simply access the interfaces through the ServiceLoader.

Or you provide a mechanism, that will detect your plugin jar files at a specific location and adds them to the class path. This is the tricky part. One way to do this is by using a custom ClassLoader for your application that allows adding jar files to the classpath.

I have chosen a different approach, that access the non public API of the ClassLoader that is in use for my application:

private void addFile(File f) throws IOException // URL to your plugin jar file
{
    addURL(f.toURI().toURL());
}

private void addURL(URL u) throws IOException
{
    URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
    Class sysclass = URLClassLoader.class;

    try {
        Method method = sysclass.getDeclaredMethod("addURL", parameters);
        method.setAccessible(true);
        method.invoke(sysloader, new Object[] {u});
    } catch (Throwable t) {
        t.printStackTrace();
    }

}

When the plugin jar file is on the classpath you can access an exposed interface through the ServiceLoader. Let me illustrate this with an example. The interface that is exposed can look like this:

/**
 * Interface to allow plugins to contribute resource reference properties.
 */
public interface IResourcePropertyLoader {
    /**
     * Retrieve the base name for the additional text resource.
     * @return
     */
    String getResourcePropertyBaseName();
}

The interface (which can also be a base class) is part of your core application. The plugin has a class that implements this interface.

Next you do the lookup for all implementations of that interface:

ServiceLoader<ITextPropertyLoader> loader = ServiceLoader.load(ITextPropertyLoader.class, ClassLoader.getSystemClassLoader());

The ServiceLoader implements Iterable, and therefore you can loop over all the implementations:

for (ITextPropertyLoader textProperty : loader) {
    final String textPropertyBaseName = textProperty.getTextPropertyBaseName();
    System.out.println("Found text property with name: " + textPropertyBaseName);
}

Also take a look at Oracles documentation on this as well as this question