[ 
https://issues.apache.org/jira/browse/MNG-7855?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17748556#comment-17748556
 ] 

Martin Desruisseaux edited comment on MNG-7855 at 7/28/23 11:30 AM:
--------------------------------------------------------------------

Java  code for generating workarounds has been added in the [test case GitHub 
repository|https://github.com/Geomatys/MavenModulepathBug], in the 
{{workaround}} sub-directory. That code parses de {{module-info.class}} entries 
of all specified JAR files and generates a {{META-INF/services/}} directory 
with all service providers found. If a service provider declares a public 
static {{provider()}} method, then the program also generates a {{java}} 
sub-directory with Java code for wrappers. Those wrappers redirect all methods 
of the service interface to the same methods of the provider obtained by a call 
to the {{provider()}} static method.

Note that this workaround does not fix the real issue, which is that 
dependencies are loaded as unnamed modules when they should not. The workaround 
allows libraries and application to find some service providers despite this 
problem, sometime not in the way that the providers should be (because of 
wrappers). But any other features that depend on named modules are still broken.


was (Author: desruisseaux):
Java  code for generating workarounds has been added in the [test case GitHub 
repository|https://github.com/Geomatys/MavenModulepathBug], in the 
{{workaround}} sub-directory. That code parses de {{module-info.class}} entries 
of all specified JAR files and generates a {{META-INF/services/}} directory 
with all service providers found. If a service provider declares a public 
static {{provider()}} method, then the program also generates a {{java}} 
sub-directory with Java code for wrappers. Those wrappers redirect all methods 
of the service interface to the same methods of the provider obtained by a call 
to the {{provider()}} static method.

This workaround is of course unsatisfying, especially the wrappers which will 
not work in all cases.

> Dependencies wrongly put on class-path rather than module-path
> --------------------------------------------------------------
>
>                 Key: MNG-7855
>                 URL: https://issues.apache.org/jira/browse/MNG-7855
>             Project: Maven
>          Issue Type: Bug
>          Components: Dependencies
>    Affects Versions: 3.8.6, 4.0.0-alpha-7
>            Reporter: Martin Desruisseaux
>            Priority: Blocker
>         Attachments: MavenModulepathBug.zip
>
>
> When invoking Java tools such as {{java}} or {{javac}}, the project 
> dependencies can be declared either in a {{\--class-path}} or 
> {{\--module-path}} option. Maven choose automatically the module-path if all 
> the following conditions are true:
> # the dependency is modularized (i.e. contains a {{module-info.class}} file 
> or an {{Automatic-Module-Name}} attribute in {{MANIFEST.MF}}), and
> # the project using the dependency is itself modularized.
> Condition #1 is fine, but #2 is problematic. The fact that a dependency is 
> declared on the class-path rather than the module-path changes the way that 
> {{java.util.ServiceLoader}} discovers the provided services.
> * If the dependency is on the class-path, {{ServiceLoader}} scans the content 
> of {{META-INF/services}} directory.
> * If the dependency is on the module-path, {{ServiceLoader}} uses the 
> declarations in {{module-info.class}}.
> Even if condition #2 is false (i.e. a project is not modularized), 
> modularized dependencies still need to be declared on the module-path _for 
> allowing the dependency to discover its own services, or the services of a 
> transitive modularized dependency_. If a modularized dependency is put on the 
> class-path instead, it has consequence not only for the project using that 
> dependency, *but also for the dependency itself, which become unable to use 
> its own {{module-info.class}}*.
> h1. Demonstration
> The attached test case contains two Maven modules, named {{service}} and 
> {{client}}. The first Maven module declares a dummy services with 4 
> providers, named A, B, C and D. Providers A and D are declared in 
> {{module-info}}. Providers B and C are declared in {{META-INF/services}}. A 
> {{ShowMyServices}} class lists the services discovered by 
> {{java.util.ServiceLoader}}.
> The second Maven module has only a main method invoking {{ShowMyServices}}. 
> This second module intentionally has no {{module-info.java}} file. The use 
> case is a big module that we cannot modularize immediately (because 
> modularization brings stronger encapsulation, which requires significant 
> changes in the project to modularize), but still want to use modularized 
> dependencies. The test case can be run with {{mvn install}}. During test 
> execution, the following is printed:
> {noformat}
> Running test.client.MainTest
> 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.
> {noformat}
> The above test demonstrates that {{module-info}} has been ignored in the 
> context of JUnit test execution. The same behaviour happens also with {{mvn 
> exec:java}} executed in the {{client}} sub-directory.
> h2. Expected behaviour
> The Maven behaviour can be reproduced on the command-line as below (Linux 
> convention). This command put everything on the class-path:
> {code:bash}
> java --class-path service/target/service-1.0.jar:client/target/client-1.0.jar 
> test.client.Main
> {code}
> The expected behaviour can be reproduced with the following command-line. 
> This command put the modularized dependency on the module-path while keeping 
> the non-modularized client on the class-path:
> {code:bash}
> java --module-path service/target/service-1.0.jar --class-path 
> client/target/client-1.0.jar --add-modules ALL-MODULE-PATH test.client.Main
> {code}
> The latter command produces the following output:
> {noformat}
> 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.
> {noformat}
> h1. Discussion
> Unless Maven provides configuration options that we did not see, the way that 
> Maven decides what to put on {{\--class-path}} and what to put on 
> {{\--module-path}} is a blocker issue for gradual modularisation of large 
> projects. This is because Maven choices break usages of 
> {{java.util.ServiceLoader}} in the dependencies themselves, which developers 
> may not control. The workaround for library developers is to declare all 
> service providers in both {{module-info}} and {{META-INF/services}}, with the 
> risk of inconsistencies. This workaround forces developers to renounce to the 
> usage of {{provider()}} static methods (which was making possible to use 
> singleton provider instances), because {{provider()}} static method works 
> only for providers declared in {{module-info}}. If the library developers 
> didn't applied such workaround, then the library users are blocked if they 
> are not in capacity to modularize their own project immediately (unless those 
> users are experts capable to create workarounds themselves).
> Ideally, developers should have explicit control on whether to put a 
> dependency on the class-path or module-path. There are scenarios where a 
> developer way want to force Maven to put a dependency on the module-path even 
> for a non-modularized module, for example if the developer really wants 
> automatic module. Conversely, forcing a modularized dependency to be on the 
> class-path may be useful for testing purposes, for example for replacing the 
> service providers declared in that module by patched services declared in 
> {{META-INF/services}} elsewhere (it does not need to be in the patched 
> module).
> h2. Proposal
> Keep current behavior unchanged as the default behavior. Add somewhere an 
> option for declaring how to handle a dependency. I suggest to do that in the 
> {{<dependency>}} block. For example:
> {code:xml}
> <dependency>
>     <groupId>foo</groupId>
>     <artifactId>bar</artifactId>
>     <module>true</module>    <!-- Allowed values: true, false, auto -->
> </dependency>
> {code}
> More JPMS-related options could be added in the future. For example an 
> {{<imports>}} block meaning "when using this dependency, add 
> {{\--add-exports}} statement from this project to that dependency for the 
> packages listed inside this {{<imports>}} block. That would be useful for 
> JUnit testing among other. But this is not the purpose of the JIRA issue.
> *References:*
> * [Test case on GitHub|https://github.com/Geomatys/MavenModulepathBug]
> * [Post on user mailing 
> list|https://lists.apache.org/thread/2kn195rrpxnw2t2bvxk4drzoo0c04959]



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to