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?
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.