[ https://issues.apache.org/jira/browse/MNG-7855?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17748556#comment-17748556 ]
Martin Desruisseaux commented on MNG-7855: ------------------------------------------ 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)