How Gradle disagreed with our Maven project
Tags: java, maven, gradle
Published: 2023-12-07 10:59:30

## Preamble
I'm a maintainer of a popular open-source project called [Logbook](https://github.com/zalando/logbook/). It is "an extensible Java library for HTTP request and response logging". The project uses Maven for build automation.

I have recently [made a change](https://github.com/zalando/logbook/commit/3004477032652125ee4b302c1d5b4e69ec4ac198#diff-97593056ecbf49ece87cde9da9d7bf29153431ca92e89a86e7f190edd7e5c621R21) in the _dependency management_ section of one of the modules, that resulted in an unwanted effect: [Lombok became a transitive dependency of our project](https://github.com/zalando/logbook/issues/1705), but only if you use Gradle in your project! If you use Maven, you won't face this issue.

But why?!

I'm going to share the result of my investigations in this post. The structure of the article is as follows:

- Toolchain used, plus an intro to dependency management
- Example: Creating a library (with the dependency management section, and using it in a Maven and a Gradle project
- Showing disagreement: Adding submodules to the library, overriding the dependency management, and seeing how Maven and a Gradle projects interpret it differently
- Conclusion and personal opinion


## Toolchain
I used:
- OpenJDK Temurin 17.0.9+9 
- Apache Maven 3.9.6
- Gradle 8.5

You can find the [source code developed below](https://github.com/msdousti/maven-vs-gradle) on GitHub.


## Dependency management
Dependency management is a way to tell Maven a few key information about the dependencies of the project. These include:

- Dependency version
- Dependency scope
- Excluded dependencies
- Optional dependencies

For a refresher, see [Introduction to the Dependency Mechanism](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html).

## Example

Consider a simple project with the following `pom.xml` file (here, I'm using `jcip-annotations` as a no-fluff dependency).

```xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.msdousti</groupId>
    <artifactId>maven-library</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>net.jcip</groupId>
                <artifactId>jcip-annotations</artifactId>
                <version>1.0</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>net.jcip</groupId>
            <artifactId>jcip-annotations</artifactId>
        </dependency>
    </dependencies>

</project>
```

You can see that the `dependencyManagement` section specifies both version and scope for the dependency, so we can simply include the dependency in the `dependencies` section without specifying those.

If you run `mvn dependency:tree` in the project root (I assume that you have installed Maven and Java, and they are properly configure), you'll see

```
[INFO] --- dependency:3.6.1:tree (default-cli) @ maven-library ---
[INFO] io.msdousti:maven-library:jar:1.0-SNAPSHOT
[INFO] \- net.jcip:jcip-annotations:jar:1.0:provided
```

This clearly shows that the JAR file of `net.jcip:jcip-annotations` is imported with proper version (`1.0`) and scope (`provided`). Cool!

You can also use your IDE to see this info graphically. For instance, in IntelliJ IDEA, you can open the Maven tool window:


![Viewing dependencies in Maven tool window of IntelliJ IDEA](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqbty9gjgm9uwfrsgtma.png)

As a side note, IntelliJ IDEA also provides this nifty feature called "Analyze Dependencies", which is very handy in seeing which dependencies conflict and which version is ultimately chosen by Maven:

![Analyze Dependencies feature of IntelliJ IDEA](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qbvgnx6pt0cldnasszjn.png)

## Including the project as a dependency in other project

To including the project as a dependency in other project, we first need to build and install it in the local Maven repository. Run this in the root of the project:

```bash
mvn install
```

And then check the local Maven repository:

```bash
ls ~/.m2/repository/io/msdousti/maven-library/1.0-SNAPSHOT
```

It should contain the project files:

```
_remote.repositories  maven-library-1.0-SNAPSHOT.jar  maven-library-1.0-SNAPSHOT.pom  maven-metadata-local.xml
```

Now let's include it as a dependency in a Maven and a Gradle project.

### Maven Project

The `pom.xml` would look like this:

```xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.msdousti</groupId>
    <artifactId>test-maven</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.msdousti</groupId>
            <artifactId>maven-library</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
```

If you check the dependency tree by running `mvn dependency:tree`, you will see:

```
[INFO] --- dependency:3.6.1:tree (default-cli) @ test-maven ---
[INFO] io.msdousti:test-maven:jar:1.0-SNAPSHOT
[INFO] \- io.msdousti:maven-library:jar:1.0-SNAPSHOT:compile
```

Note that `jcip-annotations` is not included. This is because it has the scope `provided` in the `io.msdousti:maven-library` dependency, so it won't be transitively included.

### Gradle

The `build.gradle` will be like this (Notice that I added `mavenLocal()` to the list of `repositories`, as I want to include the dependency `maven-library` from the local `.m2` repository):

```groovy
plugins {
    id 'java'
}

group = 'io.msdousti'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {
    implementation 'io.msdousti:maven-library:1.0-SNAPSHOT'
}
```

Using `gradle :dependencies`, we can view the dependencies, but as the output is bulky, I just show the graphical version from my IDE:


![Dependencies of the Gradle project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nh1kvwf1oiylh5raab0s.png)

We see that `maven-library` is included in all four Gradle "class paths". But again, `jcip-annotations` is not included, which is in agreement with the Maven project.


## Disagreement begins

Maven and Gradle start to diverge when our `maven-library` has a sub-module with overriding `dependencyManagement` section.

What does that mean?! Let's find out.

### Creating sub-modules

Let's add two sub-modules to `maven-library`. The directory structure will be like this (For simplicity, I ignored directories like `src` that are not relevant here):

```
.
|
├─ pom.xml
├─ my-bom
|   └─ pom.xml
└─ my-module
    └─ pom.xml
```

> 💡 BOM stands for **Bill of Material**. It is often a dependency that includes the `dependencyManagement` for the project.


In the parent `pom.xml`, we have to specify a packaging of `pom`, plus two `modules` elements (add them just below the project coordinates):

```
<groupId>io.msdousti</groupId>
<artifactId>maven-library</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- add below -->
<packaging>pom</packaging>
<modules>
    <module>my-bom</module>
    <module>my-module</module>
</modules>
```

The `pom.xml` file of the first child (`my-bom`) will look like this:

```xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>io.msdousti</groupId>
        <artifactId>maven-library</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>my-bom</artifactId>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>net.jcip</groupId>
                <artifactId>jcip-annotations</artifactId>
                <version>1.0</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
```

Note that it has a `dependencyManagement` section, that specifies the `compile` scope for `jcip-annotations`.

The `pom.xml` file of the other child (`my-module`) will look like this:

```xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>io.msdousti</groupId>
        <artifactId>maven-library</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>my-module</artifactId>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.msdousti</groupId>
                <artifactId>my-bom</artifactId>
                <version>1.0-SNAPSHOT</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
```

It includes `my-bom` in the `dependencyManagement` section, with scope `import` and type `pom`. If you look at the dependencies of this module, you see that it honors the scope specified by the parent project:


![Dependencies of module my-module](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c16x7exdpgmmofp21a8v.png)

Do a `mvn install` on the parent project, and let's find out what how our Maven and Gradle projects that use `maven-module` will look like.

### Maven project

Refresh the Maven project `test-maven` in your IDE (or use Maven command `mvn dependency:tree` at the root of the project).
The dependencies are still the same:


![dependencies of the Maven project `test-maven`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vm46t8eblezork6j7wuk.png)

### Gradle project

Refresh the Maven project `test-gradle` in your IDE (or use Maven command `gradle :dependencies` at the root of the project).
You'll see that `net.jcip:jcip-annotations:1.0` will appear as a transitive dependency:


![dependencies of the Gradle project `test-gradle`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cv8ivukys72am1w2uvbf.png)



## Conclusion
Looking at the [article](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) I shared at the beginning of this post, we stumble upon this section about the `import` scope (emphasis mine):

> This scope is only supported on a dependency of type pom in the `<dependencyManagement>` section. It indicates the dependency is to be **replaced** with the effective list of dependencies in the specified POM's `<dependencyManagement>` section. Since they are replaced, dependencies with a scope of import do not actually participate in limiting the transitivity of a dependency.

So, the expectation is that the dependencies are **replaced**. This is exactly what Gradle does, and Maven does not seem to honor this section.

At least this is what I understand! If you have further insights, please feel free to share them in the comments section.