Multi-module project builds with Maven and Gradle

Just came back from wonderful JCrete® 2018. This year didn’t disappoint, we had close to 110 people openly sharing ideas and knowledge on both technical and soft skills matters. Here’s a picture of the final schedule, captured by fellow Java Champion Badr Elhouari:

Every single one of those notes represents a session where JCrete participants poured their passion to share and exchange ideas. On Tuesday morning Robert Scholte (@rfscholte) held a Maven Q&A session and I felt this was the right opportunity to ask some questions regarding an specific setup for a multi-project build. We had briefly discussed the matter after lunch on the day prior so we had a head start getting into the session. What I didn’t expect was that the session turned into a big pair programming experience as everyone in attendance had an opinion on how to solve the problem, yay for mob programming!

What was the problem I faced? Here’s the main idea: suppose you have a 2 module multi-project build with module 1 named project1 and module 2 named project2; project1 is set as a dependency on project2, and project2 defines an executable entry point. The source code for this setup is available at aalmiray/maven-gradle-playground, and comes with Gradle and Maven variants. The file structure looks like this

I’ll explain the Gradle build first as it shorter and it happens to be the one where I have more experience. The root build.gradle file defines common configuration to all modules, gradle.properties defines project properties that have static values (these properties could also be defined inside build.gradle), and settings.gradle defines the modules that belong to this multi-project build.

These 3 files provide the equivalent behavior of a parent POM in a Maven multi-project build, which right now is limited to setting publishing coordinates (group and version) as well as marking every module as a Java project. You may notice that the build.gradle file for project1 is missing, that’s because it’s not needed at all given that its parent has defined all required behavior. It this missing file is bugging you then you can create an empty build.gradle. Now, the build file for project2 is where the final piece of the puzzle is found:

This build file establishes the relationship between modules and defines the executable coordinates for project2. Alright, with this setup it’s possible to compile and execute project2 in one go. We can do this at the root level (the directory where the root build.gradle file is located) or inside the directory that contains project2 (if you’re having trouble figuring out how to invoke Gradle or its wrapper inside a multi project build do yourself a favor and install gdub).

Notice that project1 is compiled before project2, project2 is executed right after that. This is possible because the application plugin is aware of project dependencies and it knows it requires all binaries and dependencies before proceeding, in this case it requires the compilation output of project1. This is an important aspect that we’ll look again with the equivalent Maven configuration. If the same command line is executed again without making any changes to the sources we get

Very well, this sums up my expectations for this multi-project build when using Gradle. Now the case for Maven. We’ll need a parent POM that defines common settings among modules as well as the modules that belong to this build. We’ll also need POM files for both modules, making sure that project1 is defined as a dependency of project2 and that project2 can be executed. This is how these files looked after a few refinements were made during the mob programming session:

My first problem was caused by a confusion between the usage of the -am vs -amd flags. The mvn -h command line yields the following descriptions for each flag

-am,--also-make If project list is specified, also
build projects required by the
list
-amd,--also-make-dependents If project list is specified, also
build projects that depend on
projects on the list

It turns out I was invoking -amd where actually I needed -am. If you haven’t used the -am flag in a multi-module build before let me tell you, it’s the bee’s knees! This is part of the behavior that I was missing in Maven after spending so much time with Gradle. This flag lets you build dependent projects and use their compilation outputs as part of the dependency graph, no need to push intermediate artifacts to the local repository by mindlessly invoking (and forgetting to invoke from time to time) mvn install as we often do. With this new information in mind I was able to compile project2 and all of its required dependencies using the following command line

Oops, definitely not what I was expecting to see. As it turns out, every goal defined on the command line will be applied to every single project in the reactor, this means exec:java will be invoked on all three projects, hmmm not good. The mob tried a couple of different combinations but we couldn’t get it to work, but not all was lost as Robert pointed out this is a feature that may be available in a future version of Maven, tracked by MNG-6118. He also mentioned there’s a workaround to this problem which is applying the Exec plugin at the root and marking it as skipped, making sure the skip flag is disabled in project2. This changes the configuration to the following

Notice that the root POM file defines a bogus value for the mainClass property of the Exec plugin, as this property is required even if the plugin is configured as skipped. Alright, time to try out the previous command line again

Success! Notice that the exec:java goal is configured on all projects but skipped when invoked on the parent and project1 projects. This is so cool despite the verbose configuration when compared to Gradle. By the way did you catch that the command line goals are compile exec:java? It turns out that the Exec plugin does not have an explicit dependency on the compilation output, you must make it explicit! This is the key difference with the Gradle equivalent setup. If only the exec:java goal is configured we get

Aaaand no luck. Which is strange given that the compile step for project1 clearly states that there were changes and recompiled the sources. I must be missing something here and will follow up with the Maven team. Adding a clean goal does produce the expected result but it may not be a good idea in a big multi-project build as it clears the previous results resulting in slower build times.

UPDATE: Robert has pointed out there may be a bug in the compiler when incremental compilation is in use, see MCOMPILER-349.

The learning experience during the mob programming session was great, as we were able to share different configuration options from the point of view of two different build systems: Gradle and Maven. Robert also shared some of the possible features coming in Maven 4.0 by discussing the roadmap (some can be seen at this link). Inspired by what happened in this session Robert decided to run a Maven Bug Fixing session on Friday, where different teams rolled their sleeves and got busy solving a couple of Maven bugs. This was a great opportunity for newcomers that haven’t had the chance to experience the thrill of contributing to an Open Source project. Later Robert decided to continue the idea with this

Maven is not a perfect tool (nor is Gradle for that matter), we all have heard the Maven/Gradle bashing from colleagues (or ourselves) at some point in our developer career. Instead of spending time raging about the state of the tools, what if we used that energy to create something positive? If you use Maven at regular intervals and have the passion to help others, please consider joining Robert and the rest of the Maven team to pick up some of these issues, we all win!