Hello Garret

Le 01/08/2023 à 18:32, Garret Wilson a écrit :

On 7/26/2023 1:42 PM, Martin Desruisseaux wrote:
… If a dependency is on the classpath, then the dependency is loaded as an unnamed module, its "module-info" file is ignored and the services that it contains are not discovered.
Can you elaborate on the last point a little more? I haven't modularized my core libraries yet, and want to know how they would work with non-modularized applications.

There is a very simple test case here, for both Maven and Gradle, with a README that tries to explain the problem. I tried to make the most trivial "hello world" reproducing the problem. The test case can also be run by invoking java directly on the command-line, which makes easy to experiment different Java options for understanding the problem:

   https://github.com/Geomatys/MavenModulepathBug

The problem can be reproduced with the following command-line (omitting paths and version number for simplicity). This command-line reproduces what Maven and Gradle do:

   java --class-path service.jar:client.jar test.client.Main

In this example:

 *   "client" is the main application and is not modularized.
 * "service" is a modularized dependency. In this JAR, I put four services:
     o 2 services in "module-info.class"
     o 2 services in "META-INF/services/" but with different texts, so
       we can see which services is loaded.

If you run above command-line, you will see the following text:

   |Start searching for services... Provider B declared in META-INF.
   Provider C declared in META-INF. Done. The dependency has been
   loaded as an unnamed module. Consequently its `module-info` file has
   been ignored, and the `META-INF/services` directory is used instead.|

The call to "java.util.ServiceLoader" was done from the "service" JAR file, which was supposed to be modularized. Despite that fact, we see that "module-info.class" has been ignored and "META-INF/services/" used instead. In other words, the modularized "service.jar" is unable to access to its own "module-info.class". We are not even talking about the behavior of the non-modularized "client.jar" file here.

The correct behavior can be reproduced with the following command-line (again omitting paths and version numbers). The main difference is that the modularized "service.jar" file is put on the module-path instead than the class-path. I will skip discussion about the "-add-modules" option for this email.

   java --module-path service.jar --class-path client.jar --add-modules 
ALL-MODULE-PATH test.client.Main

The output is then:

   |Start searching for services... Provider A declared in module-info.
   Provider D declared in module-info. Done. The dependency has been
   loaded as named module. Great! This is what we need for the
   `module-info` to be used.|

This time "module-info.class" has been used and "META-INF/services/" ignored (as it should be). I claim that it should be the Maven and Gradle behavior. If this claim is disputed (e.g. for compatibility reason), then at least it should be configurable. The fact that there is no way I could find for overriding the (in my opinion broken) Maven behavior is what I call the bug. Note that this is not a JPMS issue, this is a Maven/Gradle issue. Maven and Gradle uses the following heuristic rules for deciding if a dependency should be put on the module-path:

1. the dependency is modularized (i.e. contains a module-info.class
   file or an Automatic-Module-Name attribute in MANIFEST.MF), and
2. the project using the dependency is itself modularized.

I claim that condition 2 should be removed, i.e. a modularized dependency should be put on the module-path regardless if the project using it is modularized or not. Or if condition 2 is not removed, then at least the developer should have some way to control that.


You say that if the modularized library is put on the classpath, its services are not discovered. But wouldn't normal pre-module-era classpath-based service discovery still work via the `META-INF/services` configurations in the library JAR files? Please clarify what won't work if I modularize my core libraries, which contain services to be discovered, and I try to use that with a non-modularized application.

"META-INF/services/" is not necessary for compatibility with non-modularized applications. It is necessary only for environments that decide to put the modules on the class-path rather than the module-path (reminder: putting dependencies on the module-path works as well with non-modules client application). Maintaining both "module-info.class" and "META-INF/services/" with identical content is duplication, with risk of inconsistent behavior if their content accidentally diverge or declare services in different order. If a library chose to not do this duplication, its services will not be discovered if the JAR file is not used in the proper way.

Even if a library decide to do the duplication, it may still not work. Starting with Java 9, a service provider can be instantiated by invoking a public static method named "provider" in the provider declared in "module-info.class". This is an alternative to invoking the default public constructor. This alternative is very convenient when we want the provider to be a singleton for example. However the public static "provider" method is invoked only for services declared in "module-info.class", not for services declared in the old way. So if a service provider relies on that, it will not work on the class-path.

Finally the consequence on putting a dependency on the class-path rather than the module-path is much larger than only "java.util.ServiceLoader". The behavior of any methods annotated @CallerSensitive in OpenJDK source code may be impacted. It includes for example "ClassLoader.getResource(String)". If a library depends on those methods behaving the way the behave when the file is loaded at a module, that library may be broken if put on the class-path.

Whether a library has been put on the class-path or module-path can be observed by invoking the following Java code, where "Foo" is any class of the library:

   boolean b = Foo.class.getModule().isNamed()

If true, "Foo" is in a modularized JAR file which was on the module-path. If "b" is false despite the fact that the JAR file containing "Foo" was modularized, than it is because that JAR file was on the class-path.

So to summarize: putting a JAR file on the module-path or class-path has deep consequences. A developer way want either ways on intend (including putting a modularized JAR file on the class-path or a non-modularized JAR file on the module-path, yes the opposite of the "sane" way, there is use cases for that). Maven should not keep developers prisoners of black magic. We need control on that.

    Martin

Reply via email to