8

I'm using this Github project to get exposed to the new modular features in Java 9. I would like to add dependencies to the project and be able to build a native image. However, when I try to add a new dependency to the pom.xml, and add the requires statement to the module-info.java, I get a the following error from the maven-jlink-plugin:

Error: module-info.class not found for joda.time module

I'm trying to use this as a proof of concept that I can deploy images using the new linking phase, but naturally I need to be able to have external dependencies and I need to use maven (work constraint).

Changes to mod-jar/pom.xml

...
 <dependencies>
    <dependency>
      <groupId>joda-time</groupId>
      <artifactId>joda-time</artifactId>
      <version>2.9.9</version>
    </dependency>
  </dependencies>
...

mod-jar/module-info.java

module com.soebes.nine.jar {
  requires java.base;
  requires joda.time;
  exports com.soebes.example.nine.jar;
}

Logs:

[INFO] --- maven-jlink-plugin:3.0.0-alpha-1:jlink (default-jlink) @ mod-jlink ---
[INFO] Toolchain in maven-jlink-plugin: jlink [ /Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home/bin/jlink ]
[INFO] The following dependencies will be linked into the runtime image:
[INFO]  -> module: com.soebes.nine.one ( /Users/sebastianrestrepo/Projects/jdk9-jlink-jmod-example/maven-example/mod-1/target/jmods/com.soebes.nine.one.jmod )
[INFO]  -> module: com.soebes.nine.two ( /Users/sebastianrestrepo/Projects/jdk9-jlink-jmod-example/maven-example/mod-2/target/jmods/com.soebes.nine.two.jmod )
[INFO]  -> module: com.soebes.nine.jar ( /Users/sebastianrestrepo/Projects/jdk9-jlink-jmod-example/maven-example/mod-jar/target/com.soebes.nine.jar-1.0-SNAPSHOT.jar )
[INFO]  -> module: joda.time ( /Users/sebastianrestrepo/.m2/repository/joda-time/joda-time/2.9.9/joda-time-2.9.9.jar )
[ERROR] 
Error: module-info.class not found for joda.time module
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] parent ............................................. SUCCESS [  1.460 s]
[INFO] com.soebes.nine.one ................................ SUCCESS [  2.022 s]
[INFO] com.soebes.nine.two ................................ SUCCESS [  1.392 s]
[INFO] com.soebes.nine.jar ................................ SUCCESS [  1.388 s]
[INFO] mod-jlink .......................................... FAILURE [  1.061 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.911 s
[INFO] Finished at: 2017-11-03T15:27:35-04:00
[INFO] Final Memory: 26M/981M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jlink-plugin:3.0.0-alpha-1:jlink (default-jlink) on project mod-jlink: 

I would really appreciate any help. Thanks.

Naman
  • 27,789
  • 26
  • 218
  • 353
srestrepo
  • 99
  • 1
  • 5

4 Answers4

12

This has not much to do with the plugin I believe. Module joda.time in your case seems to be an automatic module.

The jlink tool does not support linking of automatic modules because they can rely on the arbitrary content of the classpath, which goes against the idea of a self-contained Java runtime.

So there are two ways to fix this probably :-

  • (you don't own the jar) Temporarily go ahead create a module-info.java[you could use jdeps tool for it] and update the jar[using jar tool] with the corresponding compiled class as in projects under Java 9.

  • (you own the dependency) Permanently migrate the jar to Java 9 itself, where it would consist of the module-info.class by itself after being compiled and packaged.

Naman
  • 27,789
  • 26
  • 218
  • 353
  • 2
    Though I would await the owner of the repo you cloned to come up with something he has already tried to overcome the same. [@khmarbaise](https://stackoverflow.com/users/296328/khmarbaise) – Naman Nov 03 '17 at 19:54
  • Thanks for the quick response. I'm having some issues adding a `module-info.java` to the existing jar. I can add the java file to the jar, but I think I need it to be a .class file. However, I can't seem to compile the module-info.java file because the compiler is saying the `org.joda.time` does not exist. Any suggestions? – srestrepo Nov 03 '17 at 20:15
  • 6
    In order to compile the module-info.java then you need to extract the contents of the JAR to a directory. Then compile the module-info.java with the output directory (-d) set to the directory where you extracted the contents. Doing this for a JAR file that you don't maintain is of course a bit uncool, better to get the maintainer of the library to migrate it to an explicit module. – Alan Bateman Nov 03 '17 at 20:59
  • 6
    Joda-Time will have a module-info.class added at some point, you'll just have to lookout for releases. The module name will be `org.joda.time`. – JodaStephen Nov 03 '17 at 22:00
  • 1
    Unfortunately you have to wait for the release of Joda-Time which contains the `module-info.java` file.... – khmarbaise Nov 05 '17 at 13:18
  • @AlanBateman but adding module-info.java in you source code requires that you compiles with java 9 (or higher) and apparently you can't set the target vm to lower than 9 then. So I can understand a lot of library maintainers are not doing it. – Anthony Aug 09 '19 at 19:40
8

You could use the ModiTect Maven plug-in to add a module descriptor to the JAR and create a modular runtime image with that module.

Disclaimer: I'm the author of ModiTect.

Gunnar
  • 18,095
  • 1
  • 53
  • 73
  • 1
    +1 for Moditect: allows libraries that can not move to JDK 9+ to still produce valid module info really nicely, esp. from Maven. – StaxMan May 25 '19 at 04:22
  • Can ModiTect be used to generate module-info and patch all automatic modules in a single step? Or do you have to configure each dependency separately? – Lii May 31 '23 at 17:10
2

I had a similar problem in one of my projects. First, I tried the moditect-maven-plugin, which worked great! However, with this plugin you have to configure each dependency that is missing a module descriptor separately. If you have many dependencies missing a module descriptor, this can get cumbersome.

Therefore, I decided to develop a new maven plugin, the jigsaw-maven-plugin (see https://github.com/ghackenberg/jigsaw-maven-plugin). The plugin provides three goals for patching all unnamed modules (see Step 3) as well as linking and packaging these modules (see Steps 4 and 5).

Before patching, linking, and packaging, you need to build the project JAR (see Step 1) in and copy the project dependencies (see Step 2) to a common location (e.g. ${project.build.directory}/modules). Maybe you can use the following build plugin configurations to get started (see GitHub page for more details):

Step 1: Build archive

<plugin>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.3.0</version>
  <configuration>
    <outputDirectory>${project.build.directory}/modules</outputDirectory>
  </configuration>
</plugin>

Step 2: Copy dependencies

<plugin>
  <artifactId>maven-dependency-plugin</artifactId>
  <version>3.4.0</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <outputDirectory>${project.build.directory}/modules</outputDirectory> 
      </configuration>
    </execution>
  </executions>
</plugin>

Step 3: Patch all unnamed modules

Search for JARs in modulePath missing a module descriptor, generate missing module descriptors using jdeps and javac, and add them to respective JARs using java.util.zip.

<plugin>
  <groupId>io.github.ghackenberg</groupId>
  <artifactId>jigsaw-maven-plugin</artifactId>
  <version>1.1.3</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>patch</goal>
      </goals>
      <configuration>
        <modulePath>${project.build.directory}/modules</modulePath>
      </configuration>
    </execution>
  </executions>
</plugin>

Step 4: Link modules

Link modules using jlink.

<plugin>
  <groupId>io.github.ghackenberg</groupId>
  <artifactId>jigsaw-maven-plugin</artifactId>
  <version>1.1.3</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>link</goal>
      </goals>
      <configuration>
        <modulePath>${project.build.directory}/modules</modulePath>
        <module>your.module.name</module>
        <output>${project.build.directory}/image</output>
      </configuration>
    </execution>
  </executions>
</plugin>

Step 5: Package modules

Package modules using jpackage.

<plugin>
  <groupId>io.github.ghackenberg</groupId>
  <artifactId>jigsaw-maven-plugin</artifactId>
  <version>1.1.3</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>package</goal>
      </goals>
      <configuration>
        <modulePath>${project.build.directory}/modules</modulePath>
        <runtimeImage>${project.build.directory}/image</runtimeImage>
        <mainClass>your.module.name/your.package.Main</mainClass>
      </configuration>
    </execution>
  </executions>
</plugin>
1

Let me try to explain the technical background on this a little bit:

joda-time in the version 2.9.9 as given in the question is a non-modular jar it does not contain a module-info.class and it does not declare itself as an automatic module by using a Automatic-Module-Name: in its META-INF/MANIFEST.MF

Classes in a real module can not call classes on the classpath thus you can not use the "requires jode.time" in your module-info with that version.

Automatic Modules can use classes on the ClassPath. In order to use Classes from an non-modular jar in a real module you can use a wrapper which itself is an automatic module.

By the time now there exists a version 2.10.1 of joda-time which is an automatic module and declares "Automatic-Module-Name: org.joda.time" thus you can use an "requires org.joda.time" in your real module with that new version.

Now the new jlink tool can not operate directly with non-modular jars or automatic jars and the maven-jlink plugin currently is basically just a maven wrapper around the jlink tool translating its configuration parameters into arguments for the jlink tool.

In order to create a custom Java Runtime for projects using mixed maven dependencies with real modules, automatic modules and non-modular jars you have to know all system module dependencies of your real-modules, automatic-modules and non-modular jars and than use a --add-modules parameter to jlink with these.

To collect the system module dependencies you can use the new jdeps tool with either the --print-module-deps or --list-deps parameter depending on the actual JDK Version being used.

If you are using maven for your project you can automate that task by creating or using a maven plugin that does this for you.

bei
  • 21
  • 3