Dependency Management and Versioning With a Maven Multi-Module Project
Learn more about basic dependency management and versioning with Maven.
Join the DZone community and get the full member experience.
Join For FreeIn this article, we are going to look at how to implement a multi-module project in Maven with versioning and dependency management, as well as the best practices for building big, large-scale projects from both a developer perspective and a DevOps/management perspective.
However, if you are not familiar with Maven, I highly recommend reading this article first and getting some experience using Maven. This article will not cover Maven basics.
So, with that in mind, let's get started.
Introduction
When dealing with large-scale software, we have to communicate, work, and cooperate with a lot of people — whether in an organization or a community. Besides that, we have to deal with the environment we are working with, which may consist of many projects, large or small, external projects, libraries, shared modules, utilities, and many others. Likewise, let's not forget that we are not only working with developers but DevOps and management teams, who have to coordinate the entire process so it can run smoothly.
Below, we are going to see how Maven can make our lives a lot easier.
Master-Root POM Project
This is the desired structure we want to accomplish:
I used NetBeans 8.2, but it can be accomplished with simple mvn commands.
Now, we begin by creating a POM project company-root similar in the picture. The following are the important parts of the pom.xml:
The packaging
tag defines the type of project, which is a type of POM:
<packaging>pom</packaging>
POM is basically a container of sub-modules.
The properties
tag:
<properties>
<appx.version>1.0.0</appx.version>
</properties>
Properties
are value placeholders. Their values are accessible anywhere within POM using the notation ${X}
where X is the property.
We are going to define versioning in the properties but will explain later.
The dependencyManagement
tag:
<dependencyManagement>
<dependencies>
...
</dependencies>
</dependencyManagement>
A very important use of the dependency management section is to control, consolidate, and centralize the versions of artifacts used in dependencies and inherited by all children.
So, what we are going to do is define the company-root pom.xml as a base/parent project, define the dependencies that are going to be used, and set the versioning and the child projects.
A Simple Real-Life Scenario
We are going to define the usage of the JUnit framework, version, and scope for the projects below company-root. This is really useful when having dozens or more projects because you know exactly what version of dependencies they have. The projects don't define the versions themselves, rather the people who control the company-root project and 'enforce' the child projects on what to use.
So, the QA department wants everyone working in any project to use the JUnit 4 version with the test scope, which defines what is needed only in test phases and not the normal use of the app). This is done in the company-root
pom.xml
First, set the desired version for the JUnit in the properties
section.
<properties>
<junit.version>4.12</junit.version>
</properties>
And then, the dependency:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
With the above declaration, everyone will be using JUnit 4.12 with a test scope.
The declaration of JUnit on child projects is as simple as that.
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
Now, let's assume the DevOps engineer wants to test everything with the new JUnit 5 Framework. The only action required for him would be to change the property version on company-root pom from 4.12 to 5.0 and run the tests.
More info on the Maven dependency mechanism can be found here.
Versioning
Let's not forget the versioning numbers and the role they have to play:
Also, let's take a moment for a quick recap:
- Bug fix: just bug fixes and related stuff.
- Minor: Improvements, new features, deprecation notices, don't break user code (backward-compatibility). Same API.
- Major: new features, to complete API changes.
More info for software versioning can be found here
A Common Project
In the next step, we want to create a base framework with core functionality or a library with common utilities for all of our projects to use, extend, or explore whatever the usage is.
To succeed that, we created a new Maven Java application with a name called common. If we open pom.xml of the newly created project, we observe that the parent section is missing.
Let's adjust it by concluding a parent section so commons will turn into a child (or a Leaf POM, a child with packaging other than POM) of the company-root project.
After we finish editing pom.xml, the outcome is as follows:
<?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>com.protectsoft.company</groupId>
<artifactId>commons</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>com.protectsoft</groupId>
<artifactId>company</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>
If you are not familiar with Maven, I highly recommend reading this article first and establishing some experience around Maven.
Two important things we need to mention:
1) The parent section. Now, this project has an inheritance.
2) The dependency section. We defined a JUnit dependency without version or scope because they are inherited. That means the usage of the JUnit framework is predefined from someone else.
Now, let's update company-root POM and add a version for commons project for others to use and keep things organized. To do this, we add the following in the company-root POM:
<properties>
<!-- Our projects versioning -->
<company.commons>1.0-SNAPSHOT</company.commons>
<!-- External dependencies versioning -->
<junit.version>4.12</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.protectsoft.company</groupId>
<artifactId>commons</artifactId>
<version>${company.commons}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
Creating a 3-Tier Web Application
Here, we are going to create our first web application using 3-tier architecture. But we want this application to be developed into three separate sub-projects and glued all together as one.
We are considering this stack for the 3-tier.
1) Presentation Layer
REST
2) Bussines/LogicLayer
EJB Enterprise Java Beans
3) Data Access Layer
JPA Java Persistence Object
First, in the presentation layer, we have the endpoints to be consumed by clients through HTTP. We might say it is an API for clients. In the business layer, this is where you can find all of the logic and functionality that really matters, and finally, you can also find the data access layer, which is the store and access of data through persistent storage.
The desired result we want to achieve is this:
App1 is a POM project and a child of company-root. App1-web
is a web project that exposes the endpoints. App1-web
is the presentation layer of our 3-tier. App1-web
project contains a dependency of app1-ejb
project, which is the business layer. The app1-ejb
contains a dependency of app1-jpa,
which is the data access layer and mostly contains entity classes and metadata information. The parent project from app1-web
, app1-ejb
, and app1-jpa
is the app1
.
After we create the app1
POM project, the important sections of pom.xml is the following:
<packaging>pom</packaging>
<parent>
<groupId>com.protectsoft</groupId>
<artifactId>company</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>com.protectsoft.company</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
This project has company-root as its parent, and in dependencies, the version is inherited.
Now, let's create the three separate sub-projects. We can do it in two different ways. One is from the NetBeans IDE, which will update parent POM with sub-modules section and add a parent to the children, and the second is by hand and we have to edit pom.xml in the parent and children.
We are going to do it from the NetBeans IDE. Eclipse or IntelliJ have the same support.
From NetBeans, we click on Modules in the app1
project and complete the 'Create new module...' option.
We do that three times for app1-jpa
, app1-ejb
, and app1-web
.
After that, we going to edit the pom.xml for the new three projects.
We open app1-jpa
POM and we see that it has app1
as a parent. This is also present in app1-ejb
and app1-web
. All three projects have app1
as a parent.
<parent>
<groupId>com.protectsoft.company</groupId>
<artifactId>app1</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
In app1-ejb
, we add the app1-jpa
as a dependency:
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>app1-jpa</artifactId>
</dependency>
</dependencies>
And in app1-web
, theapp1-ejb
is a dependency.
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>app1-ejb</artifactId>
</dependency>
</dependencies>
We have completed the relationship in the picture above.
Next, we going to define versioning and dependency management in the app1 pom.xml for the children.
<!--app1.pom will define what version of child projects/modules can be used -->
<properties>
<app1.jpa.version>1.0-SNAPSHOT</app1.jpa.version>
<app1.ejb.version>1.0-SNAPSHOT</app1.ejb.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>app1-ejb</artifactId>
<version>${app1.ejb.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>app1-jpa</artifactId>
<version>${app1.jpa.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- app1 has this dependencies to be used be the sub-modules -->
<dependencies>
<dependency>
<groupId>com.protectsoft.company</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
Reactor
We also notice this new section in the app1
pom.xml
<modules>
<module>app1-ejb</module>
<module>app1-jpa</module>
<module>app1-web</module>
</modules>
Meaning that the app1
POM project also has the role of the aggregator. That means the app1
project will build all sub-modules/projects defined in the modules
section with a specific order that is analyzed by the Reactor.
Indeed, if we choose to build from app1
or run the command mvn package
from the app1
folder, we notice this:
That means that the reactor analyzed all the projects in modules and their dependencies and a specific build order is generated. Also, cyclic dependencies are not allowed. In the above picture, first, we have app1-jpa
as a result of having no dependencies; next, the app1-ejb
can be built afterapp1-jpa
because it is using it, and finally, the app1-web
takes place after app1-ejb
is completed.
Reactor summary prints the success output.
Reactor and project aggregation are very useful in packaging and testing environments.
Using a Different Version for Different Projects
What to do in a situation where a version X of a library must be used in the project app1
and version Y in the project app2
?
We are in a situation where QA management decided that the usage of a reporting library will be version 1 for app1
and version 2 for app2
. App2
needs some extra features that only version 2 provides.
We cannot set the version in the company-root as one property because only one version will take place.
One solution to this new requirement is to define dependency management and versioning at the app1
and app2
projects.
So, app1
POM can include this for version 1.
<properties>
<report.version>1.0.0</report.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.protectsoft.company</groupId>
<artifactId>jasper-report-ejb</artifactId>
<version>${report.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
And app2
POM includes the following:
<properties>
<report.version>2.0.0</report.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.protectsoft.company</groupId>
<artifactId>jasper-report-ejb</artifactId>
<version>${report.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
App1
and App2
are responsible and in control of the context of the sub-module projects. Furthermore, changes can take place more easily.
Last But Not Least, the Profile Section
At this point, we have to mention the profile section and its use. A lot of the staff being said above can be grouped into different profiles. For example, we can have one profile for the X version of our commons library and another profile for the Y version of commons. Another example would be two have different profiles for the environments like test environment and a demo environment.
We are going to modify the parent root pom.xml
and add two profiles. One is versioning for Java EE 7 and the other for Java EE 8.
<profiles>
<profile>
<id>java-ee-7-profile</id>
<properties>
<javax.version>7.0</javax.version>
</properties>
</profile>
<profile>
<id>java-ee-8-profile</id>
<properties>
<javax.version>8.0</javax.version>
</properties>
</profile>
</profiles>
And with Maven, we can run:
mvn clean install -P java-ee-7-profile
ORmvn clean install -P java-ee-7-profile,
for both profiles.java-ee-8-profile
Also according to Maven's introduction to profiles. The profile section can declare and modify the following elements.
<repositories>
<pluginRepositories>
<dependencies>
<plugins>
<properties>
(not actually available in the main POM, but used behind the scenes)<modules>
<reporting>
<dependencyManagement>
<distributionManagement>
- a subset of the
<build>
element, which consists of:<defaultGoal>
<resources>
<testResources>
<finalName>
Conclusion
When following these steps, or setting up a similar architecture, be sure none of this is taken for granted. Rather, one should study and research what the needs and requirements really are, and take actions accordingly. This article is meant to provide a more general look at building multi-module projects with Maven, providing ideas and solutions to more general problems.
The complete git repo is available here.
Opinions expressed by DZone contributors are their own.
Comments