Help configuring build for Java 9+ modules: extraneaous command-line arguments passed to application

Hi everyone, I am having issues with configuring build.gradle for using Java 9+ modules.

Short description of my problem

I have been following the tutorial in the official documentation.
However, I ran into an issue: some extraneous arguments get passed to the application’s command line. This is due to the fact that gradle appends JVM args after the --module <module_name> arg, and in Java 9+, all args after the --module <module_name> arg get passed to the application itself, instead of to the underlying JVM. This issue has to do with roughly step 3 onward in the linked tutorial.

Steps to reproduce

1 - Clone the repo corresponding to the above tutorial above.
2 - cd into src/0-original, src/1-single-module/, or src/2-all-modules, and execute ./gradlew :fairy:run. Observe how in every case the JVM arguments look something like this for the executed application (a newline was inserted between each argument to improve readability):

/usr/lib/jvm/java-11-openjdk-amd64/bin/java
-Dfile.encoding=UTF-8
-Duser.country=US
-Duser.language=en
-Duser.variant
-cp
/home/user/building-java-9-modules-master/src/2-all-modules/fairy/build/classes/java/main:/home/user/building-java-9-modules-master/src/2-all-modules/fairy/build/resources/main:/home/user/building-java-9-modules-master/src/2-all-modules/pigs/build/libs/pigs.jar:/home/user/building-java-9-modules-master/src/2-all-modules/bears/build/libs/bears.jar:/home/user/building-java-9-modules-master/src/2-all-modules/formula/build/libs/formula.jar:/home/user/building-java-9-modules-master/src/2-all-modules/tale/build/libs/tale.jar:/home/user/building-java-9-modules-master/src/2-all-modules/actors/build/libs/actors.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/22.0/3564ef3803de51fb0530a8377ec6100b33b0d073/guava-22.0.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/1.3.9/40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf/jsr305-1.3.9.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.0.18/5f65affce1684999e2f4024983835efc3504012e/error_prone_annotations-2.0.18.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/ed28ded51a8b1c6b112568def5f4b455e6809019/j2objc-annotations-1.1.jar:/home/user/.gradle/caches/modules-2/files-2.1/org.codehaus.mojo/animal-sniffer-annotations/1.14/775b7e22fb10026eed3f86e8dc556dfafe35f2d5/animal-sniffer-annotations-1.14.jar
org.gradle.fairy.app.StoryTeller

Notice how:

  1. Gradle passes a bunch of -D parameters to the JVM
  2. Then comes the classpath
  3. Finally, the main class to execute

3 - cd into src/3-application, and execute ./gradlew :fairy:run. In this case, gradle appends the --module-path and --module args before anything else, resulting in a command-line for the executed application similar to the following:

--module-path
/home/user/building-java-9-modules-master/src/3-application/fairy/build/classes/java/main:/home/user/building-java-9-modules-master/src/3-application/fairy/build/resources/main:/home/user/building-java-9-modules-master/src/3-application/pigs/build/libs/pigs.jar:/home/user/building-java-9-modules-master/src/3-application/bears/build/libs/bears.jar:/home/user/building-java-9-modules-master/src/3-application/formula/build/libs/formula.jar:/home/user/building-java-9-modules-master/src/3-application/tale/build/libs/tale.jar:/home/user/building-java-9-modules-master/src/3-application/actors/build/libs/actors.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/22.0/3564ef3803de51fb0530a8377ec6100b33b0d073/guava-22.0.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/1.3.9/40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf/jsr305-1.3.9.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.0.18/5f65affce1684999e2f4024983835efc3504012e/error_prone_annotations-2.0.18.jar:/home/user/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/ed28ded51a8b1c6b112568def5f4b455e6809019/j2objc-annotations-1.1.jar:/home/user/.gradle/caches/modules-2/files-2.1/org.codehaus.mojo/animal-sniffer-annotations/1.14/775b7e22fb10026eed3f86e8dc556dfafe35f2d5/animal-sniffer-annotations-1.14.jar
--module
org.gradle.fairy.app/org.gradle.fairy.app.StoryTeller
-Dfile.encoding=UTF-8
-Duser.country=US
-Duser.language=en
-Duser.variant
org.gradle.fairy.app/org.gradle.fairy.app.StoryTeller

Notice how:

1 - All of the -D parameters plus the repeated $MODULENAME/$MAINCLASSNAME are appended after the --module-path and --module args.
2 - Thus, they will get passed to the application’s command-line and not the JVM.

4 - In the main application code, i.e. src/3-application/fairy/src/main/java/org/gradle/fairy/app/StoryTeller.java, do a simple System.out.println("ARGS: " + Arrays.toString(args)); to confirm the presence of extraneous arguments that were intended for the JVM and not for the application.

Note: to see the args passed to the JVM, I used doLast{println allJvmArgs} inside the run block of the build.gradle file and also via cat /proc/$PID/cmdline; both yielded the same results.

Impact of this issue

Running a modular application via the gradle run task with this configuration will pass arbitrary arguments to the running application depending on the system properties (I think that is what the -D parameters are, please correct me if I am wrong).

This leads to unexpected or broken behavior in the general case, since it might interfere with command-line processing for the application at hand. For example, what if the application depends on flags starting with -D for its business logic?

This is obviously undesirable, and does not happen when using the classpath versions.

What should happen instead

Gradle should append the --module-path and --module args at the end, but I don’t know how to do that.

Is there a workaround or a different configuration that I can use to avoid this? i.e Is there really no way at this time to have gradle append the --module-path , --module , etc args only at the very end of the JVM command-line generated for the run task?

If not, feel free to move this post to the Bugs section.

Note - affected plugins

By the way, this problem is also present when using plugins such as:

  • org.javamodularity.moduleplugin
  • org.openjfx.javafxplugin

The plugin authors would also benefit from a workaround for this problem, if such a workaround is possible in current versions of gradle.

Update:
I ran into an even worse problem. ./gradlew run --debug-jvm is broken because the corresponding JVM options only get passed to the application and not the JVM (i.e. -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 )

That guide is very outdated and has bad advice. I’m going to see about retiring it/removing it from guides.gradle.org so it doesn’t cause more confusion.

Your best bet is to look for a community plugin that adds support for Java modules. We don’t have anything built-in yet and adoption of Java modules has been very slow in general.

@sterling

Thanks for the reply.

Ok, so there were problems with the tutorial after all.

So far, org.javamodularity.moduleplugin seems to be the most promising plugin for providing Java modules support. However, as I mentioned above, it still suffers from the same problem as of today. Fortunately, a quick look the plugin’s issue tracker shows that the developers are aware of the problems and trying to fix them.

Even so, community plugins are just a stop-gap solution. I hope Gradle adds built-in support for Java modules soon, as more and more projects are turning to modularity. It’s about time, it’s been 2 years since Java modules have hit the scene.