1

Have been trying this since few days and can't get it working!

I am trying to build a pluggable java application where I can run it from command line and provide plugins (jars) in a separated folder. It seems the ServiceLoader would fit my requirement but I think I need a special case where the jars are not part of the classpath whereas they are stored in a different location, and for this reason I would need to use a ClassLoder pointing its url to this file system path.

One of the plugin i want to provide to the main application is a log jar with some custom features.

Here below the code I am using , but can't get to go into the for/loop .. it means that the ServiceLoader is not able to identify/match any class implementation :

final URL u = new File("C:\\data\\myLogJar-1.0-SNAPSHOT.jar").toURI().toURL();
ClassLoader ucl = new URLClassLoader(new URL[] {u});

ServiceLoader<Log> loader = ServiceLoader.load(Log.class, ucl);
for (Iterator<Log> iterator = loader.iterator(); iterator.hasNext(); ) {
    System.out.println(iterator.next());
}
loader = ServiceLoader.load(Log.class,ucl);
for (final Log log : loader) {
    log.info("Test log");                    
}

I wish you could help! Thanks a lot

==== adding project files :

Main pluggable application :

enter image description here

    package com.company.dep.automation;

import com.company.dep.automation.pluggable.Log;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

import java.util.*;

public class Main {

    private static ServiceLoader<Log> serviceLoader;

    public static void main(String[] args) {

        final URL u;
        ClassLoader ucl = null;

        try {
            u = new File("C:\\data\\myLogJar-1.0-SNAPSHOT.jar").toURI().toURL();
             ucl = new URLClassLoader(new URL[]{u});
        } catch (MalformedURLException e1) {
            e1.printStackTrace();
        }

        ServiceLoader<Log> loader = ServiceLoader.load(Log.class, ucl);
        for (Iterator<Log> iterator = loader.iterator(); iterator.hasNext(); ) {
            System.out.println(iterator.next());
        }

        loader = ServiceLoader.load(Log.class, ucl);
        for (final Log log : loader) {
            log.info("Test log");
        }

    }

}

the "Log" plugin

The interface Log

package com.company.automation.service;

public interface Log {

    void trace(String message);
    void debug(String message);
    void info(String message);
    void warn(String message);
    void severe(String message);
    void error(String message);
    void fatal(String message);

}

Its implementation

package com.company.automation.service.impl;

import com.company.automation.service.Log;

public class LogImpl implements Log {

    @Override
    public void trace(String message) {
        log("TRACE --> " + message);
    }

    @Override
    public void debug(String message) {
        log("DEBUG --> " + message);
    }

    @Override
    public void info(String message) {
        log("INFO --> " + message);
    }

    @Override
    public void warn(String message) {
        log("WARN --> " + message);
    }

    @Override
    public void severe(String message) {
        log("SEVERE --> " + message);
    }

    @Override
    public void error(String message) {
        log("ERROR --> " + message);
    }

    @Override
    public void fatal(String message) {
        log("FATAL --> " + message);
    }

    private void log(String message) {
        System.out.println(message);
    }

}
  • Structure

enter image description here

=================

Adjusted the project structure as following but still doesnt work :

Main App : enter image description here

Extension app : enter image description here

user1611183
  • 311
  • 1
  • 5
  • 12

2 Answers2

2

It doesn't work because it is not the same class Log, your main method try to find implementations of com.company.dep.automation.pluggable.Log while your jar defines an implementation of com.company.automation.service.Log such that ServiceLoader.load simply cannot find anything.

You should move the interface com.company.automation.service.Log from the extension jar to the project with your Main class and import com.company.automation.service.Log instead of com.company.dep.automation.pluggable.Log in your Main class then everything should work.

Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
  • I have just adjusted the path on both projects and still does not work. see my "edit" above. Thanks Nicolas! – user1611183 Oct 27 '16 at 13:16
  • remove `com.company.automation.pluggable.Log` from your extension app, please show the content of your file `META-INF/services/com.company.automation.pluggable.Log` – Nicolas Filotto Oct 27 '16 at 14:05
  • removing the file com.company.automation.pluggable.Log from the extension project, causes a compilation problem as the LogImpl class implements Log. he is the services file content : com.company.automation.pluggable.impl.LogImpl – user1611183 Oct 27 '16 at 14:15
  • move Log in a new project and make both projects depend on the new one or simply make your extension app depends on the main app (ugliest and simplest solution). The idea is to prevent having twice the class Log in the classpath otherwise you will get weird bugs related to the conflict. Indeed keep in mind that even 2 exact same classes loaded loaded from 2 different CL are considered as different. – Nicolas Filotto Oct 27 '16 at 14:18
  • I am not fully convinced although being a good advice. I want to isolate the main application from the plugin extensions. We want people here in the company writing plugins and not having dependencies with the main project. Then we just want to put the jar in a folder in the file system and let the main application load it.Would it be possible to have a generic interface (name it "Plugin") and then all the extension implements the "Plugin" interface? I googled and did not find any example and this is a bit strange considering the benefit the ClassLoader could bring to the ServiceLoader – user1611183 Oct 27 '16 at 14:25
  • that is why I proposed the first solution, `Log` has nothing to do in your extension app, it should be in a totally different project (for example spi or api). It would be a real mess if every implementers would have to bring the `Log` interface in his extension app. Please show what you have in `META-INF/services/com.company.automation.pluggable.Log` – Nicolas Filotto Oct 27 '16 at 14:30
  • com.company.automation.pluggable.impl.LogImpl – user1611183 Oct 27 '16 at 14:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/126842/discussion-between-user1611183-and-nicolas-filotto). – user1611183 Oct 27 '16 at 14:51
  • 1
    Your proposal worked at the first attempt. You were right indeed in suggesting the creation of three single projects : The Main executor, the plugin extension and of course the Api which works as bridge between the latter two. Thanks! – user1611183 Oct 30 '16 at 15:00
0

Regarding the "pluggable" part of your application and the loading of jars in a folder, instead of re-inventing the wheel, maybe you could have a look at OSGI. By using an OSGI implementation (Apache Felix for example), you could load jars plugins into your application. This is how Eclipse plugins are working for example. (Atlassian is also using this OSGI mechanism for their Jira/Confluence/etc plugins).

OSGI will handle a lot of issue that you will encounter one day if you want a pluggable plugin system.

For example, this is how I'm doing it:

public ChannelListener init() {
    private ChannelListener listener = new ChannelListener();
    logger.info("Initializing Felix...");
    felix = new Felix(getConfig());

    try {
        felix.start();
    } catch (BundleException e) {
        logger.error("Error while initializing Felix", e);
        throw new RuntimeException(e);
    }

    try {
        // On the next line, you are registering a listener that will listen for all 
        //Log implementations service registration.
        felix.getBundleContext().addServiceListener(listener, "(objectClass=com.company.automation.service.Log)");
    } catch (InvalidSyntaxException e) {
        logger.error("Error while registering service listener", e);
    }
    listener.start(felix.getBundleContext());

    startAllBundlesInDirectory(BUNDLE_DIRECTORY, felix.getBundleContext());
    return listener;
}

private void startAllBundlesInDirectory(String directory, BundleContext context) {
    File bundleFolder = new File(directory);
    File[] bundleFilesList = bundleFolder.listFiles((dir, name) -> name.endsWith(".jar"));

    List<Bundle> installedBundles = new ArrayList<>();
    logger.info("Installing {} bundles in {}.", bundleFilesList.length, directory);
    for(File bundleFile : bundleFilesList) {
        logger.info("Installing {}", bundleFile.getName());
        try {
            installedBundles.add(context.installBundle("file:" + directory + bundleFile.getName()));
        } catch (BundleException e) {
            logger.error("Error while installing bundle {}{}", directory, bundleFile.getName(), e);
        }
    }

    for(Bundle bundle : installedBundles) {
        try {
            bundle.start();
        } catch (BundleException e) {
            logger.error("Error while starting bundle {}{}", directory, bundle.getSymbolicName(), e);
        }
    }
}
jchampemont
  • 2,673
  • 16
  • 19
  • 1
    it looks interesting and need to play a bit with it .. the only issue is that for now there is one jar only but they should be much more , so the jar file name should come from a list of jar in the folders .. need to understand your code and try to adjust mine a bit .. – user1611183 Oct 27 '16 at 13:25
  • Basically, this line `File[] bundleFilesList = bundleFolder.listFiles((dir, name) -> name.endsWith(".jar"));` is reading all the jar files of the folder. – jchampemont Oct 27 '16 at 13:27