JPMS ServiceLoader does not work for me as expected
Asked Answered
T

1

3

JPMS ServiceLoader does not work for me as expected.

I am trying to provide a desktop program as an executable jar with a default service, that can be overloaded by the individual user. A user provides their own service class and give their name as an argument on the commandline.

The service:

package eu.ngong.myService;

public interface MyService {
    public String name();
    public void doSomething();
}

The program together with the default service (the first lines in the if and the for are inserted for logging):

package eu.ngong.myService;

import java.util.ServiceLoader;

public class ServiceUser implements MyService {
    private static MyService myService = new ServiceUser();

    public static void main(String[] args) {
        if (args.length > 0) {
            System.out.println("trying to load " + args[0] + " envirionment.");
            ServiceLoader<MyService> myServices = ServiceLoader.load(MyService.class);
            for (MyService ms : myServices) {
                System.out.println(ms.name());
                if (ms.name().equalsIgnoreCase(args[0])) {
                    myService = ms;
                }
            }
        }
        myService.doSomething();
    }

    @Override
    public void doSomething() {
        System.out.println("The default service is acting.");
    }

    @Override
    public String name() {
        return "Default";
    }
}

Both collected in myService.jar with the main class of ServiceUser hosting the module-info.java

module MyService {
    exports eu.ngong.myService;
    provides eu.ngong.myService.MyService with eu.ngong.myService.ServiceUser;
}

An individual jar at a user may be

package eu.ngong.user1;
import eu.ngong.myService.MyService;
public class User1 implements MyService {

    @Override
    public String name() {
        return "User1";
    }

    @Override
    public void doSomething() {
        System.out.println("User1 is acting.");
    }

}

with the module-info.java

module User1 {
    requires MyService;
    provides eu.ngong.myService.MyService with eu.ngong.user1.User1;
}

However, running the program with

java -p ..\user1\user1.jar;myService.jar -jar myService.jar User1

leads only to the unexpected output

trying to load User1 environment.
The default service is acting.

While I expected with logging

trying to load User1 environment.
Default
User1
User1 is acting.

What did I miss?

Toothbrush answered 7/8, 2021 at 10:57 Comment(3)
And if you remove the "-jar myService.jar" and replace it by the main class directly: eu.ngong.myService.ServiceUser ? And, did you add a META-INF/eu.ngong.myService.MyService referencing the implementation?Minster
@Minster The -jar versus -m should be mostly similar. The META-INF/services is a good suggestion, but as long as modules are recognized as explicit, that might not be required either in my opinion.Reformer
@NoDataFound: Switching to -m was one part of the solution. citing Oracle: When you use -jar, the specified JAR file is the source of all user classesToothbrush
R
2
  1. You need to declare

    uses eu.ngong.myService.MyService;
    

in the module where you want to execute the service loading code.

  1. Try ensuring your packaging added META-INF/services for the correct resolution of modules via jar files if processed as automatic.

Since this wouldn't fit in the comments, hence sharing the findings of a similar example here. I could notice upon execution via IntelliJ that the code just worked with the uses declaration and the classpath of user-service module.

Further I tried to use a similar command line:

java --show-module-resolution -p base-service/target/classes:user-service/target/classes --add-modules base.service,user.service -m base.service/base.service.ServiceUser user1
root base.service file://.../base-service/target/classes/
root user.service file://.../user-service/target/classes/
user.service requires base.service file://.../base-service/target/classes/
base.service binds user.service file://.../user-service/target/classes/
...
trying to load user1 environment.
Services found : 2
User1
Default
User1 is acting.

and added the --show-module-resolution to seek why providing the jar's on the module path didn't work. The output was as follows and had a difference where the service module was not able to bind the user module.

java --show-module-resolution -p base-service/target/base-service-1.0-SNAPSHOT.jar:user-service/target/user-service-1.0-SNAPSHOT.jar --add-modules base.service,user.service -m base.service/base.service.ServiceUser user1
root base.service file://...base-service/target/base-service-1.0-SNAPSHOT.jar automatic
root user.service file://...user-service/target/user-service-1.0-SNAPSHOT.jar
user.service requires base.service file://...base-service/target/base-service-1.0-SNAPSHOT.jar automatic
.....
trying to load user1 environment.
Services found : 1
User1
User1 is acting.

This might be the cause for unexpected output in the way you are executing as well. The slight difference being the Default implementation is not resolved in the latter for me.

Reformer answered 7/8, 2021 at 14:24 Comment(6)
The module-info.java in the module where I want to execute the service loading code has to get a uses interface statement. -- There is nothing to be put below META-INF or in MANIFEST.MF with respect to ServiceLoader. module-info.class hosts all the information required to specify a jar file as a Java module, even a module using a service or providing one. Automatic modules may be different.Toothbrush
@Toothbrush true, the behavior with automatic module seems to be different. That's why I have asked this question to follow up. Related to the uses the ServiceLoader API doc reads it clearly I believe.Reformer
@Toothbrush I’m confused by your comment. You are right that you don’t need to mess around with META-INF nor MANIFEST.MF, but how does that relate to the beginning of your comment? The uses directive is precisely for modular software, going into the module-info.Omphale
@Omphale I'm also confused by your comment. module-info.class goes into the jar file. It is not related to META-INF or MANIFEST.MF. What I am happy about is, that I can stay on Java level, maintaining module-info.java, produce a simple jar file and do not have to worry about getting information from the Java source to any text file (MANIFEST.MF) in the jar file. For me this is an improvement of ServerLoader technology that comes with JPMS.Toothbrush
@Toothbrush and I never said anything else. The integration of the service loader into the language was a great improvement. But this doesn’t change the fact that your module declaration (the version posted in your question) lacks a uses declaration.Omphale
@Holger: correct, we are on the same track - just confusions.Toothbrush

© 2022 - 2024 — McMap. All rights reserved.