2
votes

I am building a client/server application. The client runs a small loader that downloads the client in the form of a module jar, but only if the client.jar has changed. The loader then attempts to run the client through ServiceLoader.

Here is the code that is to run the service provider in the client jar.

static PokerGameInstance getPokerGame() {
    URL[] urls = null;

    try {
        urls = new URL[] { Paths.get("client.jar").toUri().toURL() };
        System.out.println(urls[0]);
    }
    catch (Exception e) {
        System.out.println("Could not create URL[] to use to create " +
                "ClassLoader for client.jar.jar.");
        return null;
    }

    URLClassLoader classLoader;
    try {
        classLoader = new URLClassLoader(urls);
    }
    catch (Exception e) {
        System.out.println("Could not create classloader for " +
                "client.jar.");
        return null;
    }

    try { // Test code
        classLoader.loadClass("com.brandli.jbpoker.client.PokerGame");
    }
    catch (ClassNotFoundException e) {
        System.out.println("Could not find PokerGame class");
    }

    ServiceLoader<PokerGameInstance> loader = ServiceLoader
            .load(PokerGameInstance.class, classLoader);
    Optional<PokerGameInstance> optional = loader.findFirst();
    if (optional.isEmpty()) {
        System.out.println("Could not load client service provider.");
        return null;
    }

    return optional.get();
}

The first time it runs, there is no client.jar. Other code downloads client.jar, and then the code above is run. Reviewing the output of this method, the URLClassLoader is able to load the service provider class (which happens to be called PokerTable). However, the ServiceLoader finds nothing, and the method prints "Could not load client service provider."

However, the second time it runs, client.jar is already there, and a fresh one is not downloaded. In that case, ServiceLoader returns the proper class and everything works.

I am running with a module path that includes the entire directory of jars. Client.jar is loaded there as well. So, in the second run, the system ClassLoader is picking up client.jar. In other words, the second pass works not because ServiceLoader is getting client.jar from URLClassLoader. I verified this by doing the second run with the ClassLoader parameter to ServiceLoader.load() set to null.

I also changed the module path to include only the discrete jars so that the system ClassLoader will not pick up client.jar if it is there. In that case, the code above always fails.

The upshot is that ServiceLoader is not recognizing the service in client.jar even though URLClassLoader will load the object. This has nothing to do with client.jar being downloaded, because the problem exists even if client.jar is there from the beginning (unless picked up by the system ClassLoader).

Remember that client.jar is a module jar. The code above is in a module that has this module-info.java:

module com.brandli.jbpoker.loader {
    exports com.brandli.jbpoker.loader;

    requires transitive javafx.controls;
    requires transitive com.brandli.jbpoker.core;
    uses com.brandli.jbpoker.loader.PokerGameInstance;
}

Client.jar has this module-info.java:

    module com.brandli.jbpoker.client {

    requires transitive javafx.controls;
    requires transitive com.brandli.jbpoker.core;
    requires transitive com.brandli.jbpoker.loader;
    requires transitive com.brandli.jbpoker.common;

    provides com.brandli.jbpoker.loader.PokerGameInstance with
    com.brandli.jbpoker.client.PokerGame;
}

I suspect that this has something to do with modules. Anyone has any ideas?

1
Unlike a classpath, a module path’s entries must be directories only, not .jar files. It is those directories which should contain modular .jars.VGR
I don't think that's true. In my question, I referred to the time in which I had the module path refer to discrete modules. The only directory it included was for javafx. Partially: "loader.jar:core.jar:common.jar" and a few more. When it ran, the client.jar that was there was not picked up, whereas it was when the module path referred to the entire directory. Unlike classpaths, module paths CAN refer to directories, but I don't think they have to.Steve Brandli
The documentation says it’s a list of directories. java --help also says it’s a list of directories. If you specified something else and it happened to work, I still would not consider that behavior I could rely on.VGR
@VGR That's something I've always been confused about. The documentation you point out only mention "directory of modules", but ModuleFinder#of(Path...) accepts "directory of modules", "exploded modules", and "packaged modules". I would be surprised if the implementation of java --module-path didn't use that implementation of ModuleFinder, but there's no documentation saying one way or the other.Slaw
@Slaw I have wondered that myself, since the first time I saw that documentation. But in the absence of any further clarification, I feel like the tool documentation takes precedence. It is of course possible that ModuleFinder provides more capabilities than a command line tool’s --module-path option does.VGR

1 Answers

1
votes

A comment to my question caused me to look into ModuleLayer/ModuleFinder. I noticed that there is a ServiceLoader.load(ModuleLayer, Class). The following code works:

static PokerGameInstance getPokerGame() {
    ModuleFinder finder = ModuleFinder.of(Paths.get("client.jar"),
            Paths.get("common.jar"));
    ModuleLayer parent = ModuleLayer.boot();
    Configuration cf = null;
    try {
        cf = parent.configuration()
                .resolveAndBind(finder, ModuleFinder.of(),
                 Set.of("com.brandli.jbpoker.client"));
    }
    catch (Throwable e) {
        return null;
    }

    ClassLoader cl = ClassLoader.getSystemClassLoader();

    ModuleLayer layer = null;
    try {
        layer = parent.defineModulesWithOneLoader(cf, cl);
    }
    catch (Throwable e) {
        return null;
    }
    ServiceLoader<PokerGameInstance> loader = ServiceLoader
            .load(layer, PokerGameInstance.class);

    Optional<PokerGameInstance> optional = loader.findFirst();
    if (optional.isEmpty()) {
        return null;
    }

    return optional.get();
}

I don't know why the code in my question does not work.

EDIT: This explanation from @Slaw:

To keep backwards compatibility JPMS has the concept of the unnamed module (there's one per ClassLoader). This is where code on the class-path is placed. It's also where your client.jar ends up when loaded by your URLClassLoader, despite it having a module-info file. Classes in the unnamed module function as they did in the pre-module world; in order for a ServiceLoader to find a provider you need a provider-configuration file under META-INF/services. The uses and provides directives only take effect in named modules, which is what you get when creating a ModuleLayer.