Java
Article
By Robert Scholte

Why Maven Cannot Generate Your Module Declaration

By Robert Scholte

To declare modules for Java 9’s module system we need to create a file module-info.java – a so called module declaration. Amongst other things, it declares dependencies on other modules. If the project uses a build tool, though, Maven for example, it already manages dependencies. Keeping two files in sync surely seems redundant and error-prone, so the question arises: “Can’t Maven generate the module-info file for me?” Unfortunately, the answer is “No”, and here is why.

(To get the most out of this article, you should be familiar with the module system’s basics, particularly how it manages dependencies. If you want to read up on that, check out this hands-on guide.)

The Motivation

So everyone wonders, can Maven generate the module declaration? The reason is obvious: If I add a dependency to a POM, it is very likely a requirement, too. Or the other way around: If I add a requirement to the declaration, probably a matching dependency must be added to the POM as well. Sounds like something which could be automated.

Let’s start with the attempt to go from requirement back to a dependency: This is not possible because of the lack of information. A module name cannot be transformed to a Maven coordinate, information like the groupId and artifactId is missing. And also: Which version to choose? The module declaration is not interested in which version of an artifact is on the path, only that it is available.

On the other hand, to go from dependency-file to module name is possible. However, that’s not enough to generate all the elements of the module declaration.

Separation of Concerns

When talking about this topic there are three entities in play: Maven’s POM, the module system’s module-info file, and Maven plugins that are involved in building class and module paths. The three together make it possible to work with modules, but they all have their own responsibilities.

POM Dependencies

The task of a dependency is (1) to have a unique reference to a file based on the coordinate and (2) to specify when to use it. The when to use it part is controlled by the scope and is basically any combination of compile, test, and runtime.

The coordinate is a combination of at least the groupId, artifactId, version and file-extension, which is derived from the type. Optionally a classifier could be added as well. Based in this information it is possible to refer to a file in the local repository. It’ll look like the following, where every dot in the groupId is replaced with a slash:

${localRepo}/${groupId}/${artifactId}/${version}/
    ${artifactId}-${version}[-${classifier}].${ext}

As you can see, you can refer to any file: a text-file, an image, an executable. Within the context of the dependency it doesn’t matter. To add to this, the dependency has no idea about the content of the file. For instance in case of a JAR: was it built for Java 8 or Java 1.4, which could make a big difference in case your project has to be compatible with a rather old Java version.

Module Declaration

The module declaration file is built up with five declarations:

requires
The module(s) that must be available to compile or run this application.
exports
The package(s) whose types are visible to a few or all modules.
opens
The package(s) whose types are [accessible via reflection](https://www.sitepoint.com/reflection-vs-encapsulation-in-the-java-module-system/) to a few or all modules.
uses
The service interface(s) that this module may discover.
provides
The implementation(s) provided for a certain service interface.

From these declarations the requires clauses are closely related to the dependencies of the Maven project. If the JDK/JRE together with the dependency-files provided by Maven doesn’t cover these requirements, the project simply won’t compile or run. Consider it as a quality rule one must obey.

Maven Plugins

Every plugin (or actually plugin-goal) can specify the resolution scope for dependencies and get access to these file. For instance the compile-goal of the maven-compiler-plugin states that it uses compile-time dependency resolution. It is up to this plugin to construct the correct arguments for the Java compiler based on the dependency-files provided by Maven and the configuration of the plugin.

--ADVERTISEMENT--

Class Paths and Module Paths

Up until Java 8 it was quite simple: All JARs need to be added to the class path. But with Jigsaw, JARs can end up either on the module path or the class path. It all depends on whether a JAR is required by another module or not. It is important to realize that we have to get this right 100% of the time because if an artifact ends up on the wrong path, the module system will reject the configuration and compilation or launch will fail.

Let’s assume we’ve written a module declaration for our project. Where do we have to specify if a JAR is a required module or not? In other words, does it belong on the class path or the module path? One of the obvious locations is the dependency in the POM. However, there are several reasons why this won’t work.

Not a Concern

First of all, it is not the concern of the dependency. A dependency is about a reference to a file and when to use it (compile time? run time?) not how.

No Place in the POM

Then, from all the elements of a dependency there’s only one which might be used to control a more fine-grained usage of the JAR: the scope. However, the POM’s modelVersion 4.0.0 has a strict set of scopes. Although the POM is filled with the build instructions for Maven, once installed or deployed it is also a meta-file with dependency information for other build tools and IDEs, so new scopes might confuse or break such products.

For the same reason the introduction of a new XML-element for dependencies would be problematic; it would conflict with the XSD and other tools are not prepared for it yet. So there’s no space for modular information in the dependency declaration of the POM and for the short term one shouldn’t expect a new POM definition just for Jigsaw.

So how about configuring it with the maven-compiler-plugin?

<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <!-- <version>x.y.z</version> -->
        <configuration>
                <!-- example -->
                <modulePath>
                        <!-- by dependency? -->
                        <dependency>com.foo.bar:library</dependency>
                        <!-- by module? -->
                        <module>library</module>
                </modulePath>
        </configuration>
</plugin>

But what about the transitive dependencies, in this case the dependencies of library? Those dependencies need to be divided over both the paths as well. Do you need to configure that?

Alternatively we might want to resolve the build information of a dependency to infer where to place its dependencies but that is not feasible as this kind of information isn’t always there. Even when such a JAR was built with Maven, trying to reverse engineer the content from the JAR to the effective plugin configurations is often not possible (think of the effect of multiple execution blocks).

Reading the Module Descriptor

So if the POM is not the way to go, where else could we get the information from? Better focus on the files inside the JAR as-is, for instance a module-info.class… (the compiled module declaration, called the module descriptor).

So the key file seems to be the module-info file as it exactly specifies the requirements. Once tools like ASM can read the module descriptor, the maven-compiler-plugin can extract that information and decide per dependency if it matches a required module. For our project there should be a module-info.java so Maven knows where to place our dependencies. For that it needs to be analyzed before compilation, which is already possible with QDox.

So with all module-info.class-files inside JARs and the module-info.java-file of our project it is possible to decide where every JAR belongs; either the class path or the module path. This makes the module declarations and descriptors an ingredient for a successful build. But the question remains, could it not also be a result?

Maven can not create module declarations

The Module Declaration Generator

When analyzing all the module descriptors used in this project we see that it should be possible to divide all JARs over the module path and the class path. But is it possible to generate the module declaration for our project?

For the requirements in src/main/java/module-info.java we can get quite far. We could say that all dependencies with scopes compile or provided are requirements, test of course not, but for runtime (which means at run time only) it depends. But there are also requirements, which are not mapped to a JAR. Instead they refer to a java or jdk module (e.g. java.sql, java.xml or jdk.packager). These must be configured for a generator or the code needs to be analyzed up front.

A requirement can also have additional modifiers. With a requires static clause you can specify that a module is mandatory at compile but optional at run time. In Maven you would mark such a dependency as optional. With the transitive modifier you specify that projects using this module don’t have to add the transitive module in their own module declaration file. So transitive has zero effect on this project, it only helps other projects using this one as a dependency. Such information can only be provided as configuration.

Even if the POM definition would be redesigned to support this kind of dependency metadata in order to be transformed to the requirements in the module declaration file, there are much more declarations, which are much harder to generate. It already starts with the module itself: What is its name, is it open or not?

And how about the exported and open packages? It might look as if they could be inferred by code analyses. Analyzing java files as sources at this detail is not yet possible, though. Analyzing class files as binaries means double compiling, first with only the class path to get all classes for analysis, next with both the module path and class path, being the actual compilation. There is no feasible solution.

In the end every line in the module declaration is a choice by the developer. Does it make sense to add configuration to the POM, which already looks a lot like the intended module declaration file?

The closest we could get is with a template like the following. Let’s use Velocity and call the template module-info.java.vm.

open module M.N
{
     #set ( $requiredModules = ... ) ## these must somehow be available in the context

     ## requires {RequiresModifier} ModuleName ;
     #set( $transitiveModules = ["org.acme.M1", "org.acme.M2"] )
     #foreach( $requiredModule in ${requiredModules} )
         #if( $transitiveModules.contains( $requiredModule.name ) ) transitive #end #if( $requiredModule.optional ) static #end ${requiredModule.name} ;
     #end

     requires java.sql;
     requires java.xml;
     requires jdk.packager;

     ## exports PackageName [to ModuleName {, ModuleName}] ;
     exports com.acme.product;
     exports com.acme.service;

     ## opens PackageName [to ModuleName {, ModuleName}] ;

     ## uses TypeName ;

     ## provides TypeName with TypeName {, TypeName} ;
}

Transforming this to the module-info.java requires probably a separate templating maven-plugin, because this goes beyond the compilation task of the maven-compiler-plugin and the resource copying with optional filtering task of the maven-resource-plugin. It’s up to the developer if such template with some scripting syntax is better to read and maintain compared to a plain old (new) module declaration file.

Some might have heard about jdeps, a tool available since JDK8 which can analyze dependencies. It also has an option to generate the module declaration file, but not in the way we want it. Jdeps only uses compiled classes, so it must be used after the compile phase. This features was introduced for developers to have a module-info.java-file to start with, but once generated it is up to the developer to adjust and maintain it.

Conclusion

If there were a module declaration generator, it still required quite a lot of configuration to get the resulting file just right. This would not be less work than just writing the file directly. All together, writing and maintaining the module declaration yourself gives the guarantee that it will always be as you would expect.

Of course some small open source projects will pop up and try to generate the module declaration anyway, but it’ll only work for a subset of projects, which means we cannot expose it for Maven.

I am more hopeful that IDEs will resolve this for you. For instance if you add a dependency to the POM, you could get the option to add it as requirement to the module declaration as well. Or that they provide a wizard showing all dependencies and packages from your project, giving you the option what to add to the module declaration file.

However that part of the story plays out, though, I’m here to tell you that for the time being Maven won’t be writing your module declaration for you.

Recommended
Sponsors
Get the latest in Java, once a week, for free.