Uploaded image for project: 'Maven'
  1. Maven
  2. MNG-7855

Dependencies wrongly put on class-path rather than module-path

    XMLWordPrintableJSON

Details

    • Bug
    • Status: Open
    • Major
    • Resolution: Unresolved
    • 3.8.6, 4.0.0-alpha-7
    • None
    • Dependencies
    • None

    Description

      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:

      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.

      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.

      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:

      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.
      

      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.

      Expected behaviour

      The Maven behaviour can be reproduced on the command-line as below (Linux convention). This command put everything on the class-path:

      java --class-path service/target/service-1.0.jar:client/target/client-1.0.jar test.client.Main
      

      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:

      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
      

      The latter command produces the following output:

      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.
      

      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).

      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:

      <dependency>
          <groupId>foo</groupId>
          <artifactId>bar</artifactId>
          <module>true</module>    <!-- Allowed values: true, false, auto -->
      </dependency>
      

      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:

      Attachments

        1. MavenModulepathBug.zip
          8 kB
          Martin Desruisseaux

        Issue Links

          Activity

            People

              Unassigned Unassigned
              desruisseaux Martin Desruisseaux
              Votes:
              0 Vote for this issue
              Watchers:
              3 Start watching this issue

              Dates

                Created:
                Updated: