What’s better to resolve legacy jar hell? Maven or Gradle?
Introduction
In this article, I am going to give a brief introduction to Gradle and Apache Maven, then talk about their pros and cons, and finally move on to the case study. On the case study we are going to discuss about a problem we encountered while working on an integration project and how we solved it by using Gradle instead of Maven without much effort .
Maven and Gradle are proven build and project management tools. Both of them are used very intensively, especially in the Java world. Using a build tool like Maven or Gradle has taken several advantages.
Some of these includes;
· Managing all the process, such as building, documentation, testing, releasing, and distrubiton, dependency management
· Running in different environent with same configuration.
Here are some pros and cons of Gradle and Maven:
- Both Gradle and Maven follow the convention over configuration paradigm. However, while Maven provides a very rigid model, Gradle offers a much more flexible approach.
- Both Gradle and Maven employ some form of parallel project building and parallel dependency resolution. However, Gradle has more mechanisms, including incrementality, build cache, and gradle daemon.
- Gradle provides more options by default in dependency management.
- Maven has a vast plugin ecosystem, providing a wide range of functionality for building, testing, and deploying applications.
- Gradle has a good ecosystem of plugins, it might not have plugins for every specific use case or library that Maven does.
- Gradle uses a domain-specific language based on the programming languages Groovy or Kotlin. On the other hand, Apache Maven uses XML for its project configuration.
- Both follow best practices.
Let’s take a look Maven and Gradle lifecycle:
Maven has predifend and rigid lifecycle, that has consisting of threee main phases. These are Clean, Build, and Site. The default Maven build lifecycle consist of 8 steps or phases. These phases are executed in a linear sequence. You can find default lifecylce of Maven below figure [4]
Gradle has a more flexible and customizable build lifecycle which based on the concept of tasks. Each task represents a specific unit of work. The Gradle build lifecycle based of consept build has three distinct phases. You can find a figure descibing The Gradle Build Lifecycle below [5]
Problem description
After considering all these aspects, let’s delve into our case study:
In essence, the company I work for has entered into a collaboration with another company. As part of this collaboration, our team will write code for our own projects into the software produced by our partner company. This can be considered like a plugin.
The main project coming from the other company is an Eclipse project. Unfortunately, they directly utilize Eclipse’s build tool without employing any standardized build automation tools like Maven, Gradle, or Ant. Our team has experienced with Maven and each member is proficient in its usage.
Our project depends on the other software’s libraries and rules. Finally, our code will function within their software products.
They have provided us with three files that bearing a “.war” extension. However, these are not conventional “.war” files; They consist of their folder, each containing approximately 200 JAR files.
So, we have to manage all these jars. But how? Most of these jar files don’t have the Maven coordinator.
Evaluation of Maven solutions
The first assessment is it is possible to upload all these jar files to our Nexus repository by assigning them group Id, artifact Id, and version number. However, these “.war” files provided by partner company are constantly updated. This time another issue comes to our table; how can we track and manage these dependencies and their version? While this solution is feasible, it may lead to maintenance problems as it’s not as effective.
The second assessment is to create a fat JAR (also known uber-JAR) by combining multiple JAR files into a single file. However, this approach presents several potential issues, including:
Duplicate classes,
Resources collisions,
Classpath order issues,
Dependency version conflicts,
META-INF Manifest issues.
Additionally, the partner company informed us that dependency order is important between of these three “.war” folders. Therefore, this solution also brings maintenance problems.
The third assessment is System Scope Dependency:
You can find an example.
<dependency>
<groupId>com.extenal</groupId>
<artifactId>external-dependency</artifactId>
<version>1.1.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/external-dependency-1.1.2.jar</systemPath>
</dependency>
Fourth and final assessment with Maven involves defining a local respository in the pom.xml file.
<repositories>
<!-- Local Repository -->
<repository>
<id>local-repo</id>
<url>file://${project.basedir}/lib</url>
</repository>
</repositories>
<dependencies>
<!-- Dependency 1 -->
<dependency>
<groupId>com.external</groupId>
<artifactId>lib-1</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Dependency 2 -->
<dependency>
<groupId>com.external</groupId>
<artifactId>lib-2</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
It’s evident that defining each of the roughly 500 dependencies in these two ways, managing version changes, and sharing them among team members, is challenging.
The potential and actual drawbacks of these assessments led us away from these methods.
Now, we need to find a solution to manage these dependencies while leveraging the benefit of using Maven build automation tool and supporting environments.
Solution
This led us to consider Gradle as the solution. With Gradle, we can utilize all maven environmental tools like remote, local repository as well as setting.xml etc. The crucial aspect for us is the ability to include multiple files or directories as dependencies in our project using Gradle. Our solution hinges on this capability.
Hence, we preferred to use Gradle. In this way, we can add the dependencies they give us to our project as they are.
We decided to zip these three folders and place them in the Maven repository, assigning each zip file a groupId, artifactId, and version number provided by the partner company.
This approach allows us to continue enjoying all the benefits that the Maven Repository Manager offers. The challenge of sharing and keeping nearly 500 JAR files up to date among team members has been effectively eliminated.
In essence, our solution is Gradle downloading these three zip files, unpacking them into three different folders where we want to store them, and then adding these three folders as dependencies to our project.
First, we need to define artifact repositories that we have to download zip files from.
Let’s have look at the below config how to define repository In Gradle
#build.gradle.kts
repositories {
maven {
url = uri("https://your-remote-repo.com/repository/maven/");
}
maven {
url = uri("https://your-remote-repo-2.com/repository/maven/");
}
mavenCentral()
mavenLocal()
}
Let’s have a look at the build.gradle Kotlin code. Here, if the zip file we previously uploaded to the Maven Repository is not located in the local location, it is downloaded from the remote repository. And it is passed as a parameter to the copyFile method.
#build.gradle.kts
fun downloadDependency(groupId: String, artifactId: String, version: String) {
val dependencyCoordinator = "$groupId:$artifactId:$version"
if (!isExist(artifactId, version)) {
var file = configurations.detachedConfiguration(dependencies.create(dependencyCoordinator)).resolve().first()
copyFile(artifactId, version, file)
}
}
In the following code block, if another version of the same artifact exists, it is deleted before the new zip file is copied to the target folder. Then, our new zip file is unzipped and placed in the relevant folder.
#build.gradle.kts
fun copyFile(artifactId: String, version: String, file: File) {
val pathForDelete = "$libPath/$artifactId"
val actualLibPath = "$pathForDelete/$version"
try {
project.copy {
if (!File(actualLibPath).exists()) {
File(pathForDelete).deleteRecursively();
from(zipTree(file))
into(actualLibPath)
println("Artifact ID :$artifactId:$version loaded!")
} else {
println("Artifact ID :$artifactId:$version already has. No neet to load")
}
}
} catch (e: Exception) {
println("An unexpected error occurred $e")
}
}
We now have a directory containing the required JAR files for our project. The code block below demonstrates how these folders are added to the project as dependencies.
dependencies {
implementation(fileTree(libPath+project.property("zip-files-1").toString()).matching{include("**/*.jar")})
implementation(fileTree(libPath+project.property("zip-files-2").toString()).matching{include("**/*.jar")})
}
In Gradle, the Task.named() method is a way to access and structure tasks by their names. This method allows you to perform actions or changes on a specific task identified by its name without having to reference it directly. Here, we specify with the dependsOn command that this task should run after the task we added to the system with tasks.registery has run.
Below is our code block that initiates this entire process:
tasks.named("build").configure {
dependsOn("preBuildTaskResolveDependency")
}
tasks.register("preBuildTaskResolveDependency") {
val mavenGroupName = project.property("external.jars").toString()
downloadDependency(
mavenGroupName,
project.property("zip-files-1").toString(),
project.property("zip-files-1-version").toString()
)
downloadDependency(
mavenGroupName,
project.property("zip-files-2").toString(),
project.property("zip-files-2-version").toString()
)
}
Conclusion
In this case study, we took advantage of Gradle’s flexibility. We expanded its dependency management capability based on our needs. While having Groovy DSL or Kotlin DSL in the Gradle configuration (build.gradle) file provides significant advantages to users, it also presents some challenges. Although not overly difficult to learn, it may require slightly more time compared to Maven. However, for someone familiar with Maven, this learning curve should be relatively quick.
As a result, the following points can be emphasized:
You can prefer Maven,
- if you have a small to large-sized project with a relatively simple build process.
- if you value convention over configuration. Maven enforces a standard directory structure and builds lifecycle, making it easy to learn and use.
Don’t overlook Maven’s extensive plugin and extension support.
You can prefer Gradle,
- If you have a project ranging from small to large or complex with a need for customization. Gradle offers greater flexibility in configuring the build process and project structure.
- If you require fine-grained control over dependency management, performance, etc. Gradle provides advanced features for handling dependency conflicts, defining custom dependency scopes, and more.
- if you’re working with multi-language projects or projects that use non-Java technologies. Gradle is better equipped to handle building projects with diverse components effectively.
Ultimately, the best choice depends on your specific project requirements and team preferences.
Resources ;
2. https://docs.gradle.org/current/userguide/userguide.html
3. https://gradle.org/maven-vs-gradle/
4. https://www.geeksforgeeks.org/maven-lifecycle-and-basic-maven-commands/
5. https://docs.gradle.org/current/userguide/build_lifecycle.html
About the author.
Alperen Kethudaoglu— Senior Java developer at Luxoft.
Throughout my nearly 20-year career, I have held various roles in different companies, including architect, developer, DevOps engineer, lead, and technical lead.