ServiceLoader using ClassLoader pointing to different path
Asked Answered
J

2

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

Joggle answered 27/10, 2016 at 12:18 Comment(4)
please show the structure of your jar fileStilliform
Sorry! adding more info ...Joggle
your first loop would loop for ever if you have at least one result as you don't call next()Stilliform
yeah .. i did so many attempts that might have left error - The second block doesnt loop anyway .. thanks for pointing it outJoggle
S
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.

Stilliform answered 27/10, 2016 at 12:53 Comment(9)
I have just adjusted the path on both projects and still does not work. see my "edit" above. Thanks Nicolas!Joggle
remove com.company.automation.pluggable.Log from your extension app, please show the content of your file META-INF/services/com.company.automation.pluggable.LogStilliform
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.LogImplJoggle
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.Stilliform
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 ServiceLoaderJoggle
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.LogStilliform
com.company.automation.pluggable.impl.LogImplJoggle
Let us continue this discussion in chat.Joggle
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!Joggle
C
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);
        }
    }
}
Crowded answered 27/10, 2016 at 12:59 Comment(2)
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 ..Joggle
Basically, this line File[] bundleFilesList = bundleFolder.listFiles((dir, name) -> name.endsWith(".jar")); is reading all the jar files of the folder.Crowded

© 2022 - 2024 — McMap. All rights reserved.