Search code examples
javakotlinjarjvmservice-provider

Service provider running outside of jar file but not inside of a jar file


import java.util.ServiceLoader

interface A

data object B : A {
    @JvmStatic
    fun provider(): A = this
}

fun main() {
    println(ServiceLoader.load(A::class.java).toList())
}

this program run just fine when ran standalone : java.exe -p <program path>;<kotlin stdlib path> -m testJARSPI/my.program.MainKt

but produce an error when ran in a jar file: java -jar <path to jar>

message :

Exception in thread "main" java.util.ServiceConfigurationError: my.program.A: my.program.B Unable to get public no-arg constructor
        at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:586)
        at java.base/java.util.ServiceLoader.getConstructor(ServiceLoader.java:679)
        at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1240)
        at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1273)
        at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1309)
        at java.base/java.util.ServiceLoader$3.hasNext(ServiceLoader.java:1393)
        at kotlin.collections.CollectionsKt___CollectionsKt.toCollection(_Collections.kt:1295)
        at kotlin.collections.CollectionsKt___CollectionsKt.toMutableList(_Collections.kt:1328)
        at kotlin.collections.CollectionsKt___CollectionsKt.toList(_Collections.kt:1319)
        at my.program.MainKt.main(Main.kt:15)
        at my.program.MainKt.main(Main.kt)
Caused by: java.lang.NoSuchMethodException: my.program.B.<init>()
        at java.base/java.lang.Class.getConstructor0(Class.java:3641)
        at java.base/java.lang.Class.getConstructor(Class.java:2324)
        at java.base/java.util.ServiceLoader$1.run(ServiceLoader.java:666)
        at java.base/java.util.ServiceLoader$1.run(ServiceLoader.java:663)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:569)
        at java.base/java.util.ServiceLoader.getConstructor(ServiceLoader.java:674)
        ... 9 more

I cannot find what change that break when inside the jar file

NB: the MANIFEST.MF file, the services folder and the module-info.java is in properly configured.

Was build using IntelliJ : Create JAR from Module -> extract to the target JAR

tested with jdk:

  • open-jdk 22
  • open-jdk 19 (shown stack trace)
  • corretto 22

tried changing to a non final member but the error is still the same

data object B : A {
    @JvmStatic
    @Suppress("NON_FINAL_MEMBER_IN_OBJECT")
    open // remove final modifier
    fun provider(): A = this
}

Solution

  • Whenever the documentation of ServiceLoader talks about provider methods1 it's always in the context of modules. Perhaps it could be clearer, but the implication is that a provider method will only be used when the service provider is found in a non-automatic named module. If the service provider is found on the class-path (i.e., the unnamed module) then the provider method, if there is one, will be ignored; only the provider constructor2 will be considered.

    When you launch your application with:

    java.exe -p <program path>;<kotlin stdlib path> -m testJARSPI/my.program.MainK
    

    Your application is loaded from the module-path and resolved as a named module. Thus, the provider() method works as you expect.

    However, when you launch your application with:

    java -jar <path to jar>
    

    Your application is loaded from the class-path into the unnamed module3 and the provider() method is ignored. So, it looks for the provider constructor, which must be public. But since your service provider is a Kotlin object, its no-argument constructor is private. Hence the NoSuchMethodException.

    If you want your service provider to be a Kotlin object, then I believe you're forced to place it on the module-path so that it is loaded into a named module. That way you can make use of the provider method to return the singleton instance.

    If you want to be able to place your code on the class-path, then don't make the service provider a Kotlin object. Though if you still want a singleton then you can introduce some indirection by making the service a "factory" that returns the singleton. Or just make sure to only load one instance of the service and pass it around as needed.


    1. A "provider method" is a public, static, no-argument method named provider.

    2. A "provider constructor" is a public, no-argument constructor.

    3. All code loaded from the class-path is put into the "unnamed module". Any module-info.class file is ignored in this case (and none of its directives apply).


    I'm confused, since the module-info.java makes it a named module should the provider method not work?

    - comment

    The module-info descriptor is ignored when the module is loaded from the class-path. This behavior is intentional; it helps non-modular code continue to work on Java 9+. The descriptor only has meaning when the module is loaded from the module-path. The -jar option places the specified JAR file (and any dependencies listed in the Class-Path manifest attribute) on the class-path, not the module-path.

    Use the -p / --module-path arguments to specify the module-path. Note you also have to make sure the module is resolved. That means the module needs to either be a root module, be directly or indirectly required by a root module, or provide a service used by a resolved module. Root modules are specified by the --add-modules and -m / --module arguments.