A Maven Story
This blog shares my journey of transitioning a large Java project from the Ant build tool to Maven.
Join the DZone community and get the full member experience.
Join For FreeBack with a fresh blog on Maven – no saga, just a simple update on an old project of mine. It's like catching up with an old buddy and sharing what's new. In this blog, we'll dive into the world of Maven, exploring the tweaks and improvements I've made to an old favorite. Whether you're a Maven pro or just getting started, let's take a stroll through My Maven adventure together!
TL;DR
- The Challenge: Figuring out how to upgrade an application server in a project with lots of parts using Ant.
- The Solution: Use Maven's Multi-Module plan and a Bill Of Materials (BOM) to control versions in one place, making upgrades and managing parts easier.
- The Journey: Facing issues like circular dependencies and copying projects, Maven's tools, and multi-module system helped a lot.
- Key Takeaways: Sharing practical lessons learned during the move, showing how Maven made the project better and ready for more improvements.
Once Upon a Time in Engineering
In a tech adventure, I have been tasked to upgrade the Application Server for a product used on servers like Glassfish or WebLogic. The goal? Boost security and prepare for future JDK upgrades. The product included 20+ EJB projects and 20+ war projects packed into an EAR.
These EJB(*-ejb.jar) and WEB (*.war projects) were Ant based Java projects.
The Ant Challenge
In this world of Ant projects, a big problem arose—figuring out and upgrading application server-related dependencies. The current setup used two projects holding all kinds of dependencies, making it hard to know which project used what. Updating during server upgrades turned into a manual, error-prone task.
The Maven Journey
To tackle these issues, I aimed to simplify future upgrades and centralize dependency control. Although Ant IVY seemed helpful, I worried about managing dependencies and the messy project structure. Maven and Gradle were options, but Maven gaining the upper hand due to its robust tools and plugins for application servers.
Taking cues from open-source projects and the structure of Spring projects, the Maven Multi-module with Bill Of Materials (BOM) emerged as a compelling choice.
Choosing the Path
To make it easier to understand, think of the picture below as the old way we organized projects.
Module_A projects in Ant are compiled into four binaries. However, this contradicts the Maven policy of having One binary per POM by maintaining a pom.xml for each Ant project.
Here comes the pivotal question: Could I seamlessly integrate Maven into the existing Ant project structure?
Laying New Road
The answer lay in creating a parent project with a POM serving as a BOM. This approach offered a refined project structure, aligning with Maven's directory layout standards.
The final setup turned into a Maven Multi-Module project, with /Application/pom.xml controlling versions for all modules. The application_ear project wrapped EJB and WAR files into a deployable EAR, while BuildModules made an executable application server zip.
In this context, /Module_A itself functions as a Maven module with /Application as its parent. Moreover, for each binary of Module_A created in the old project structure, I established a Maven module with /Module_A as the parent. This design mirrors an ideal Maven multi-module project structure for our project.
- The *_ejb project compiles EJB classes into module_a_ejb.jar.
- The *_web project assembles WAR classes into module_a_web.war.
- Additionally, *_interface projects bundle Interfaces and DTOs for inter-module dependencies into module_a_interface.jar.
I'll delve into the interesting reason behind this *_interface packaging in a later section of this blog.
Deep Dive
As previously mentioned, I've established a primary Maven project called /Application, listing all the subsidiary domain projects beneath it.
Concerning version control, the /Application/pom.xml serves as a Bill-Of-Materials, encompassing dependency and plugin management for centralized control. The XML snippet below showcases version declarations for log4j-1.2-api and Gson.
<properties>
<version.log4j.1.2.api>2.19.0</version.log4j.1.2.api>
<version.gson>2.8.6</version.gson>
</properties>
<dependencyManagement>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-1.2-api</artifactId>
<version>${version.log4j.1.2.api}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${version.gson}</version>
</dependency>
</dependencyManagement>
Consequently, all sub-modules inheriting or using these dependencies are relieved from specifying version information, as demonstrated in the subsequent XML snippet:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-1.2-api</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<scope>compile</scope>
</dependency>
Moreover, the concept of inheritance plays a role where Maven POMs inherit values from a parent POM. This includes Maven plugins defined at the parent level, providing default implementations for sub-module plugins, as exemplified below:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${version.maven.jar.plugin}</version>
<configuration>
<archive>
<index>false</index>
<manifestEntries>
<Built-By>${user.name}</Built-By>
<Build-Date>${maven.build.timestamp}</Build-Date>
<Build-Jdk>${java.vendor}</Build-Jdk>
</manifestEntries>
<manifest>
<addClasspath>false</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>
To handle internal or in-house dependencies, we've established an internal Maven repository akin to JFrog Artifactory within our intranet network. A dedicated repository has been crafted for our product dependencies, and this repository has been integrated for effective dependency resolution.
<repositories>
<repository>
<name>Internal Dev</name>
<id>maven-dev</id>
<url>${maven.dev.repo.url}</url>
<layout>default</layout>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
To ensure uniformity in Java distribution and version during compilation, we've encountered instances of unintentional compilation with different JDK versions in the Ant build model. Addressing this concern, I resolved the issue using the maven enforcer plugin, This plugin allows us to enforce rules on JDK version and distribution, as illustrated in the example below:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${version.maven.enforcer.plugin}</version>
<executions>
<execution>
<id>enforce-java</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVendor>
<excludes>
<exclude>${oracle.jdk}</exclude>
</excludes>
<includes>
<include>${eclipse.temurin.jdk}</include>
</includes>
</requireJavaVendor>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Facing Challenges
Throughout our smooth migration process, we encountered a few hurdles:
1. 160+ Project Creation: With over 40 main modules totaling 160+ projects and submodules, replicating the project structure across all with default configurations and files proved time-consuming. To streamline this, I utilized the Maven project templating kit Archetype. This tool facilitates the creation of templated project structures, including resource files and default classes for submodules.
2. Circular Dependencies: Given the Ant-based nature of the old projects and the absence of a strict component dependency model, circular dependencies emerged as a challenge during project compilation in multiple areas. Maven does not permit circular dependencies. To address this, I introduced *-ejb, *-web, and *-interfaces projects under submodules, segregating Java classes by concern. The INTERFACE projects act as saviors, resolving circular dependencies by holding only DTOs and EJB Interfaces without inter-project dependencies. Other module EJB/WEB projects now reference either their self-module or other modules' *-interfaces as dependencies.
3. Distribution Packaging: Creating a product distribution that bundles *.ear with the application server into a zip file was essential. Since the complete product build script is based on Ant, I decided to employ the maven assembly plugin for packaging the distribution zip file. Initially, I considered placing the assembly scripts in the parent project /Application, assuming that building the parent project would cover all submodules. However, this approach led to inherited assembly behavior across submodules, causing confusion about distribution dependencies and configuration. Despite attempts to restrict inheritance to submodules, the solution proved ineffective.
Referring to the assembly plugin guide, I then crafted a distribution project named BuildModules. This project generates the zip file alongside the EAR and application server, with dependencies over the EAR modules, which, in turn, have transitive dependencies over the submodule binaries (*-ejb.jar and *.war).
The Grand Finale
Despite surpassing the estimated timeline, the project's ultimate results were commendable. Subsequent minor upgrades to the Application Server were effortlessly accomplished with a mere Maven property change.
Presently, the OWASP dependency-check-maven conducts more effective scans of dependencies for any known published vulnerabilities. Upgrading the project's dependencies to align with compatible JDK versions or distributions proved to be a straightforward and uncomplicated process.
In the end, it's a well-rounded success story. Moving forward, we're delving into Maven improvements and developing custom plugins for the product in the next phase.
Thank you for joining in on this journey. I look forward to sharing more insights in the upcoming blog.
Opinions expressed by DZone contributors are their own.
Comments