If you look through the various POMs created in Chapter 7, Multi-module Enterprise Project, note
several patterns of replication. The first pattern we can see is that some
dependencies such as spring and
hibernate-annotations are declared in several modules.
The hibernate dependency also has the exclusion on
javax.transaction replicated in each definition.
The second pattern of duplication to note is that sometimes
several dependencies are related and share the same version. This is often
the case when a project’s release consists of several closely coupled
components. For example, look at the dependencies on
hibernate-annotations and
hibernate-commons-annotations. Both are listed as
version 3.3.0.ga, and we can expect the versions of
both these dependencies to change together going forward. Both the
hibernate-annotations and
hibernate-commons-annotations are components of the
same project released by JBoss, and so when there is a new project
release, both of these dependencies will change. The third and last
pattern of duplication is the duplication of sibling module dependencies and sibling
module versions. Maven provides simple mechanisms that let you factor all
of this 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. One technique we could use is to move these up to the
dependencyManagement section, just like all the
others, 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 thegroupIdandartifactId. Child projects can omit the version and exclusions if the dependency is listed independencyManagement. - 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 samegroupId, and they almost always share the same release version. Using${project.version}will help you avoid the sibling version mismatch problem discussed previously.
