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