Maven 101 - Multi-Module

Maven 101 - Multi-Module

·

12 min read

Introduction

This is a follow-up post to Maven 101 - Basics. If you have not read that, I strongly suggest reading that before continuing on this.

In this post, I will be focusing on using Maven with multi-module setup using the following structure.

First, let's take a look at the project structure that will be used throughout this post.

root/
├── pom.xml
└── project/
    ├── pom.xml
    ├── core/
    │   └── pom.xml
    ├── project-parent/
    │   └── pom.xml
    └── web/
        └── pom.xml

root/pom.xml and root/project/pom.xml will be configured as aggregate POM while root/project/project-parent/pom.xml will be configured as parent POM.

The diagram above shows the relationship between each module

Maven Reactor

It is important to understand what Maven Reactor is, and what it does before diving into multi-module setup.

Maven Reactor is the key mechanism that handles the multi-module project and does the following:

  • Collects all the available modules to build

  • Sorts the projects into the correct build order

  • Builds the selected projects in order

Taken directly from Maven site

Maven Reactor figures out the order of the modules by using directed acyclic graph (DAG) to determine the build order of the project.

Given the project structure above, if we compile the project, this will be the output.

> ./mvnw clean compile
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] maven-101-multi-module-project-core                                [jar]
[INFO] maven-101-multi-module-project-web                                 [jar]
[INFO] maven-101-multi-module-project-aggregator                          [pom]
[INFO] maven-101-multi-module                                             [pom]
[INFO]
[INFO] ---------< com.bwgjoseph:maven-101-multi-module-project-core >----------
[INFO] Building maven-101-multi-module-project-core 0.0.1-SNAPSHOT        [1/4]
[INFO]   from project\core\pom.xml
[INFO] --------------------------------[ jar ]---------------------------------

As shown in the logs above, it shows the sequential order of the module to be built.

Remember that web depends-on core so it ensures core builds first

Command Line Options

The following command line switches are available:

  • --resume-from | -rf - resumes a reactor from the specified project (e.g. when it fails in the middle)

  • --also-make | -am - build the specified projects and any of their dependencies in the reactor

  • --also-make-dependents | -amd - build the specified projects and any that depend on them

  • --fail-fast | -ff - the default behavior - whenever a module build fails, stop the overall build immediately

  • --fail-at-end - if a particular module build fails, continue the rest of the reactor and report all failed modules at the end instead

  • --non-recursive - do not use a reactor build, even if the current project declares modules and just build the project in the current directory

Shamelessly plug off Maven site

These are very useful commands to know while working with multi-module project setups.

I have an issue using -rf when used along with -f/-pl/-am flags. Imagine while compiling, it fails on web, and if I fixed the issue and wanted to resume, I could have resumed from web rather than running all over again using this command. But for some reason which I can't quite figure out what's the correct command to run to make sure it works correctly

./mvnw clean compile -f project -rf :web

Type of POM

Aggregate POM

Also known as Project Aggregation

This POM file specifies which subprojects (or modules) to build and builds them in the specified order, managed by The Reactor.

Below is the current project setup aggregator pom file.

<!-- root -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bwgjoseph</groupId>
    <artifactId>maven-101-multi-module</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>maven-101-multi-module</name>
    <description>Maven 101 - Multi Module</description>

    <packaging>pom</packaging>

    <modules>
        <module>project</module>
    </modules>

</project>
<!-- root/project -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bwgjoseph</groupId>
    <artifactId>maven-101-multi-module-project-aggregator</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>maven-101-multi-module-project-aggregator</name>
    <description>Maven 101 - Multi Module</description>

    <packaging>pom</packaging>

    <modules>
        <module>core</module>
        <module>web</module>
    </modules>

</project>

The key difference between this and the Simple POM is that it has

  • packaging

    • define as either jar/pom (default is jar)
  • modules

    • listing of all the modules of the project

This is useful to group similar/related modules under this Aggregated POM so that one can run the command against this POM, and all the child modules will be triggered.

Parent POM

Also known as Project Inheritance

Similar to how Simple POM inherits from Super POM. We can define our own Parent POM with a set of configurations that can be shared across multiple modules.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.bwgjoseph</groupId>
    <artifactId>maven-101-multi-module-project-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>maven-101-multi-module-project-parent</name>
    <description>Maven 101 - Multi Module</description>

    <packaging>pom</packaging>

    <properties>
        <java.version>17</java.version>
        <error-handling-spring-boot-starter.version>4.2.0</error-handling-spring-boot-starter.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.wimdeblauwe</groupId>
            <artifactId>error-handling-spring-boot-starter</artifactId>
            <version>${error-handling-spring-boot-starter.version}</version>
        </dependency>
        <!-- omitted -->
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.10</version>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

In root/project/project-parent, there's only 1 file, and that's pom.xml which is used to inherit the individual modules. For example, in the core module,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- inherits from project-parent -->
    <parent>
        <groupId>com.bwgjoseph</groupId>
        <artifactId>maven-101-multi-module-project-parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../project-parent</relativePath>
    </parent>

    <artifactId>maven-101-multi-module-project-core</artifactId>
    <name>maven-101-multi-module-project-core</name>
    <description>Maven 101 - Multi Module</description>

</project>

We declare that the core module inherits from project-parent POM.

This makes it easier to manage configuration, especially for dependencies (pinning) and plugins.

Aggregate vs Inheritance

In short, using Aggregate means the parent knows who are the children while using Inheritance means the parent doesn't know who are the children.

Running a command against Aggregate POM will invoke its children, but not for Inheritance POM.

You don't have to choose, as both can be used together in a single project.

It is very common for developers to mix both types of POM into a single pom.xml file which is fine. However, my personal opinion is to separate them if possible, especially for slightly more complex project structures. Because separating them also means separating the concern of building the project, where I can use the same aggregator to build a group of modules but they don't necessarily share the same configuration.

Dependency Management

It is important to understand the difference between using dependencyManagement and dependencies.

dependencyManagement and dependencies are both used to define dependency information, versioning, as well as configuration. The difference is dependencyManagement does not add an actual dependency to the project/module. Whereas, dependencies will add an actual dependency.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.wimdeblauwe</groupId>
            <artifactId>error-handling-spring-boot-starter</artifactId>
            <version>4.2.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

If we add the dependency to dependencyManagement, we will not be able to import and use the library. Until we explicitly add to under dependencies tag.

<dependencies>
    <dependency>
        <groupId>io.github.wimdeblauwe</groupId>
        <artifactId>error-handling-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

Note that there is no need to declare the version anymore since it is now managed via dependencyManagement.

Plugin Management

The concept is similar to Dependency Management as described above.

Plugin dependency configurations are usually a little bit more involved, and complex. Hence, being able to define once, and apply for all modules makes it much easier to re-use, and less error-prone.

<!-- parent pom -->
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.10</version>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Would you rather declare this once at the parent-pom and reuse it across all inherited modules? Or declare it at every child pom.xml like what is shown below?

<!-- child pom -->
<build>
    <plugins>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

There is no need to declare the version, configuration, execution, or any other properties, unless, the intention is to overwrite it.

Building Project

How to use -am to build the project without installing the dependency modules

-am: Builds the specified modules, and any of their dependencies in the reactor.

We know that web depends on core, and if we run the following command

./mvnw package -f project -pl core

We get the following error

> ./mvnw package -f project -pl web
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.bwgjoseph:maven-101-multi-module-project-web >----------
[INFO] Building maven-101-multi-module-project-web 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] The POM for com.bwgjoseph:maven-101-multi-module-project-core:jar:0.0.1-SNAPSHOT is missing, no dependency information available
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.881 s
[INFO] Finished at: 2023-09-03T23:35:36+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project maven-101-multi-module-project-web: Could not resolve dependencies for project com.bwgjoseph:maven-101-multi-module-project-web:jar:0.0.1-SNAPSHOT: The following artifacts could not be resolved: com.bwgjoseph:maven-101-multi-module-project-core:jar:0.0.1-SNAPSHOT (absent): Could not find artifact com.bwgjoseph:maven-101-multi-module-project-core:jar:0.0.1-SNAPSHOT -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

That's because it's trying to find com.bwgjoseph:maven-101-multi-module-project-core:jar:0.0.1-SNAPSHOT either in local or remote repositories which is not available since we did not install locally or deploy it.

To overcome/resolve it, we can deploy it locally first by running

./mvnw install -f project -pl core

Which would have made it available locally, or we can actually use -am (also-make) command to build the dependent project first. This way, there is no need to ensure the dependent module is first deployed locally/remotely.

./mvnw package -f project -pl web -am
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] maven-101-multi-module-project-core                                [jar]
[INFO] maven-101-multi-module-project-web                                 [jar]
[INFO]
[INFO] ---------< com.bwgjoseph:maven-101-multi-module-project-core >----------
[INFO] Building maven-101-multi-module-project-core 0.0.1-SNAPSHOT        [1/2]
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
// omitted
[INFO] --- jar:3.3.0:jar (default-jar) @ maven-101-multi-module-project-web ---
[INFO] Building jar: Z:\Development\workspace\github\bwgjoseph\tutorials\maven-101-multi-module\project\web\target\maven-101-multi-module-project-web-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for maven-101-multi-module-project-core 0.0.1-SNAPSHOT:
[INFO]
[INFO] maven-101-multi-module-project-core ................ SUCCESS [ 21.384 s]
[INFO] maven-101-multi-module-project-web ................. SUCCESS [ 13.848 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  36.158 s
[INFO] Finished at: 2023-09-03T23:38:58+08:00
[INFO] ------------------------------------------------------------------------

We can see that it now build core project before web module.

Running Test

All Test

To run all tests for all modules under /project, run the following command

./mvnw test -f project

-f: Select an alternative POM file or directory containing a POM file

This also gives us the benefit of having a single maven wrapper at the root directory but being able to run any POM

It should give you the following result

> ./mvnw test -f project
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] maven-101-multi-module-project-core                                [jar]
[INFO] maven-101-multi-module-project-web                                 [jar]
[INFO] maven-101-multi-module-project-aggregator                          [pom]
[INFO]
[INFO] ---------< com.bwgjoseph:maven-101-multi-module-project-core >----------
[INFO] Building maven-101-multi-module-project-core 0.0.1-SNAPSHOT        [1/3]
[INFO]   from core\pom.xml
[INFO] --------------------------------[ jar ]---------------------------------

Specific Test Class

If wish to run a specific test class, then you can filter it using -Dtest parameter.

./mvnw test -f project -Dtest=SampleTests

Note that if we are on a multi-module, it is better to indicate a specific module, otherwise, it might fail if the test class does not exist in any other submodule.

./mvnw test -f project -pl core -Dtest=SampleTests

-pl: select a specific set of projects to apply your goal

> ./mvnw test -f project -pl core -Dtest=SampleTests
[INFO] Scanning for projects...
[INFO]
[INFO] ---------< com.bwgjoseph:maven-101-multi-module-project-core >----------
[INFO] Building maven-101-multi-module-project-core 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
// omitted
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.bwgjoseph.multimodule.SampleTests
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.432 s - in com.bwgjoseph.multimodule.SampleTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.10:report (report) @ maven-101-multi-module-project-core ---
[INFO] Loading execution data file Z:\Development\workspace\github\bwgjoseph\tutorials\maven-101-multi-module\project\core\target\jacoco.exec
[INFO] Analyzed bundle 'maven-101-multi-module-project-core' with 5 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.193 s
[INFO] Finished at: 2023-09-03T23:32:41+08:00
[INFO] ------------------------------------------------------------------------

To run multiple classes, separate them with comma

./mvnw test -f project -pl core -Dtest=SampleTests,AnotherSampleTests

Specific Method

If wish to run a specific method within a class, use # like such

./mvnw test -f project -pl core -Dtest=SampleTests#test1

Similarly, you can chain up other test from other class using

./mvnw test -f project -pl core -Dtest="SampleTests#test1,AnotherSampleTests#test2"

Remember to wrap it with quotes

It is also possible to provide wildcards such as

./mvnw test -f project -pl core -Dtest=SampleTests#test*

This will run all test method name that starts with test

Specific Groups

If wish to run through a group of tests via @Tags or @Tag annotation

@Tag("sample")
class SampleTests {}
./mvnw test -f project -pl core -Dgroups=sample

It is also possible to exclude using -DexcludedGroups

JaCoCo Plugin

The JaCoCo Maven plug-in provides the JaCoCo runtime agent to your tests and allows basic report creation.

Quoted directly from jacoco-maven

In this section, I will be showing the difference between generating jacoco report in a single module and consolidating across various modules.

Single Module

To generate code coverage, I added jacoco-maven-plugin to one of the module

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.10</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

prepare-agent goal is set up to record the execution data such as lines executed. The execution data file is written to /target/jacoco.exec.

report goal is to read the file - jacoco.exec, and write the report and output to /target/site/jacoco/index.html

Once added, run the following

./mvnw test

Which works, but this is a bit more tricky for multi-module because jacoco would have generated a report for each of the individual modules but not as an aggregated report.

Navigate to ./project/core/target/site/jacoco/index.html to view the report

Multi Module

In a multi-module setup, having an aggregated jacoco report across all submodules is much better for reporting purposes. To do so, I created an aggregated-report submodule to house the aggregated report

In aggregated-report pom.xml

<!-- add dependencies on all the modules that we want to include -->
<dependencies>
    <dependency>
        <groupId>com.bwgjoseph</groupId>
        <artifactId>maven-101-multi-module-project-core</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.bwgjoseph</groupId>
        <artifactId>maven-101-multi-module-project-web</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

<!-- define report-aggregate for jacoco -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.10</version>
    <executions>
        <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report-aggregate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Note that, we do not change the jacoco pom configuration that we defined previously in /project-parent/pom.xml. This configuration combines the reports so that running ./mvnw package -f project -pl aggregated-report -am will create the aggregated report view

But.... this will trigger all the dependent submodules to run the package lifecycle again to collect the reports.

Is there a better way? Yes! By running the report-aggregate goal directory which will collect the jacoco result from the various dependency submodules instead of triggering them to run again (provided that those individual module has already run the report).

./mvnw jacoco:report-aggregate -f project -pl aggregated-report -am

The number of reports that will be aggregated within aggregated-report submodule depends on which dependency you have included.

The aggregated-report submodule can be standalone, meaning it does not have to point to /project-parent as its parent pom (which is very often shown in examples in the internet).

Conclusion

While most of the examples are shown with multi-module setup, most of them should be the same for normal single-module setup, just remove/ignore -pl or -f or -am argument (in most cases).

Maven has pretty good support for multi-module setup (with an even better one coming from Maven v4), but I think that it isn't very well documented, at least in terms of samples that one could refer to and rely on.

By using it myself, I have gained much more understanding about Maven multi-module setup and able to slowly take advantage of it bit by bit.

Source Code

As usual, the full source code is available on GitHub

References