Maven by Example
8.3. Optimizing Dependencies

If you look through the various POMs duplication into a parent POM.
Just as in your project’s source code, any time you have duplication in your POMs, you open the door a bit for trouble down the road. Duplicated dependency declarations make it difficult to ensure consistent versions across a large project. When you only have two or three modules, this might not be a primary issue, but when your organization is using a large, multimodule Maven build to manage hundreds of components across multiple departments, one single mismatch between dependencies can cause chaos and confusion. A simple version mismatch in a project’s dependency on a bytecode manipulation package called ASM three levels deep in the project hierarchy could throw a wrench into a web application maintained by a completely different group of developers who depend on that particular module. Unit tests could pass because they are being run with one version of a dependency, but they could fail disastrously in production where the bundle (WAR, in this case) was packaged up with a different version. If you have tens of projects using something like Hibernate Annotations, each repeating and duplicating the dependencies and exclusions, the mean time between someone screwing up a build is going to be very short. As your Maven projects become more complex, your dependency lists are going to grow, and you are going to want to consolidate versions and dependency declarations in parent POMs.
The duplication of the sibling module versions can introduce a
particularly nasty problem that is not directly caused by Maven and is
learned only after you’ve been bitten by this bug a few times. If you
use the Maven Release plugin to perform your releases, all these
sibling dependency versions will be updated automatically for you, so
maintaining them is not the concern. If simple-web version
1.3-SNAPSHOT depends on simple-persist version 1.3-SNAPSHOT, and
if you are performing a release of the 1.3 version of both projects,
the Maven Release plugin is smart enough to change the versions
throughout your multimodule project’s POMs automatically. Running the
release with the Release plugin will automatically increment all of
the versions in your build to 1.4-SNAPSHOT, and the release plugin
will commit the code change to the repository. Releasing a huge
multimodule project couldn’t be easier, until…
Problems occur when developers merge changes to the POM and interfere
with a release that is in progress. Often a developer merges and
occasionally mishandles the conflict on the sibling dependency,
inadvertently reverting that version to a previous release. Since the
consecutive versions of the dependency are often compatible, it does
not show up when the developer builds, and won’t show up in any
continuous integration build system as a failed build. Imagine a very
complex build where the trunk is full of components at 1.4-SNAPSHOT,
and now imagine that Developer A has updated Component A deep within
the project’s hierarchy to depend on version 1.3-SNAPSHOT of
Component B. Even though most developers have 1.4-SNAPSHOT, the
build succeeds if version 1.3-SNAPSHOT and 1.4-SNAPSHOT of
Component B are compatible. Maven continues to build the project using
the 1.3-SNAPSHOT version of Component B from the developer’s local
repositories. Everything seems to be going quite smoothly—the project
builds, the continuous integration build works fine, and so
on. Someone might have a mystifying bug related to Component B, but
she chalks it up to malevolent gremlins and moves on. Meanwhile, a
pump in the reactor room is steadily building up pressure, until
something blows….
Someone, let’s call them Mr. Inadvertent, had a merge conflict in
component A, and mistakenly pegged component A’s dependency on
component B to 1.3-SNAPSHOT while the rest of the project marches
on. A bunch of developers have been trying to fix a bug in component B
all this time and they’ve been mystified as to why they can’t seem to
fix the bug in production. Eventually someone looks at component A and
realizes that the dependency is pointing to the wrong
version. Hopefully, the bug wasn’t large enough to cost money or
lives, but Mr. Inadvertent feels stupid and people tend to trust him a
little less than they did before the whole sibling dependency
screw-up. (Hopefully, Mr. Inadvertent realizes that this was user
error and not Maven’s fault, but more than likely he starts an awful
blog and complains about Maven endlessly to make himself feel better.)
Fortunately, dependency duplication and sibling dependency mismatch
are easily preventable if you make some small changes. The first thing
we’re going to do is find all the dependencies used in more than one
project and move them up to the parent POM’s dependencyManagement
section. We’ll leave out the sibling dependencies for now. The
simple-parent pom now contains the following:
<project>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.5.ga</version>
<exclusions>
<exclusion>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
...
</project>
Once these are moved up, we need to remove the versions for these
dependencies from each of the POMs; otherwise, they will override the
dependencyManagement defined in the parent project. Let’s look at only
simple-model for brevity’s sake:
<project>
...
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
</dependency>
</dependencies>
...
</project>
The next thing we should do is fix the replication of the
hibernate-annotations and hibernate-commons-annotations version
since these should match. We’ll do this by creating a property called
hibernate.annotations.version. The resulting simple-parent section
looks like this:
<project>
...
<properties>
<hibernate.annotations.version>3.3.0.ga</hibernate.annotations.version>
</properties>
<dependencyManagement>
...
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>${hibernate.annotations.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>${hibernate.annotations.version}</version>
</dependency>
...
</dependencyManagement>
...
</project>
The last issue we have to resolve is with the sibling dependencies and
define the versions of sibling projects in the top-level parent
project. This is certainly a valid approach, but we can also solve the
version problem just by using two built-in
properties—${project.groupId} and
${project.version}. Since they are sibling dependencies, there
is not much value to be gained by enumerating them in the parent, so
we’ll rely on the built-in ${project.version}
property. Because they all share the same group, we can further
future-proof these declarations by referring to the current POM’s
group using the built-in ${project.groupId} property. The
simple-command dependency section now looks like this:
<project>
...
<dependencies>
...
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>simple-weather</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>simple-persist</artifactId>
<version>${project.version}</version>
</dependency>
...
</dependencies>
...
</project>
Here’s a summary of the two optimizations we completed that reduce duplication of dependencies:
- Pull-up common dependencies to dependencyManagement
-
If more than one project depends on a specific dependency, you can
list the dependency in dependencyManagement. The parent POM can
contain a version and a set of exclusions; all the child POM needs
to do to reference this dependency is use the
groupId +and +artifactId. Child projects can omit the version and exclusions if the dependency is listed in dependencyManagement. -
Use built-in project
versionandgroupIdfor sibling projects -
Use ${project.version} and ${project.groupId} when
referring to a sibling project. Sibling projects almost always
share the same
groupId, and they almost always share the same release version. Using ${project.version} will help you avoid the sibling version mismatch problem discussed previously.
