Maven can manage both internal and external dependencies. An external dependency for a Java project might be a library such as Plexus, the Spring Framework, or Log4J. An internal dependency is illustrated by a web application project depending on another project that contains service classes, model objects, or persistence logic. Example 3.3, “Project Dependencies” shows some examples of project dependencies.
Example 3.3. Project Dependencies
<project>
...
<dependencies>
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-java5</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
</project>
The first dependency is a compile dependency on the XFire
SOAP library from Codehaus. You would use this type of
dependency if your project depended on this library for compilation,
testing, and during execution. The second dependency is a
test-scoped dependency on JUnit. You would use a
test-scoped dependency when you need to reference this
library only during testing. The last dependency in Example 3.3, “Project Dependencies” is a dependency on the Servlet 2.4
API. The last dependency is scoped as a provided
dependency. You would use a provided scope when the application you are
developing needs a library for compilation and testing, but this library
is supplied by a container at runtime.
Example 3.3, “Project Dependencies” briefly introduced three of the
five dependency scopes: compile,
test, and provided. Scope controls
which dependencies are available in which classpath, and which
dependencies are included with an application. Let’s explore each scope
in detail:
- compile
-
compileis the default scope; all dependencies arecompile-scoped if a scope is not supplied.compiledependencies are available in all classpaths, and they are packaged. - provided
-
provideddependencies are used when you expect the JDK or a container to provide them. For example, if you were developing a web application, you would need the Servlet API available on the compile classpath to compile a servlet, but you wouldn’t want to include the Servlet API in the packaged WAR; the Servlet API JAR is supplied by your application server or servlet container.provideddependencies are available on the compilation classpath (not runtime). They are not transitive, nor are they packaged. - runtime
-
runtimedependencies are required to execute and test the system, but they are not required for compilation. For example, you may need a JDBC API JAR at compile time and the JDBC driver implementation only at runtime. - test
-
test-scoped dependencies are not required during the normal operation of an application, and they are available only during test compilation and execution phases. - system
-
The
systemscope is similar toprovidedexcept that you have to provide an explicit path to the JAR on the local file system. This is intended to allow compilation against native objects that may be part of the system libraries. The artifact is assumed to always be available and is not looked up in a repository. If you declare the scope to besystem, you must also provide thesystemPathelement. Note that this scope is not recommended (you should always try to reference dependencies in a public or custom Maven repository).
Assume that you are working on a library that provides caching behavior. Instead of writing a caching system from scratch, you want to use some of the existing libraries that provide caching on the file system and distributed caches. Also assume that you want to give the end user an option to cache on the file system or to use an in-memory distributed cache. To cache on the file system, you’ll want to use a freely available library called EHCache (http://ehcache.sourceforge.net/), and to cache in a distributed in-memory cache, you want to use another freely available caching library named SwarmCache (http://swarmcache.sourceforge.net/). You’ll code an interface and create a library that can be configured to use either EHCache or SwarmCache, but you want to avoid adding a dependency on both caching libraries to any project that depends on your library.
In other words, you need both libraries to compile this library project, but you don't want both libraries to show up as transitive runtime dependencies for the project that uses your library. You can accomplish this by using optional dependencies as shown in Example 3.4, “Declaring Optional Dependencies”.
Example 3.4. Declaring Optional Dependencies
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>1.4.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>swarmcache</groupId>
<artifactId>swarmcache</artifactId>
<version>1.0RC2</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
</dependencies>
</project>
Once you've declared these dependencies as optional, you are
required to include them explicitly in the project that depends on
my-project. For example, if you were writing an
application which depended on my-project and wanted
to use the EHCache implementation, you would need to add the following
dependency element to your project.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>my-application</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>swarmcache</artifactId>
<version>1.4.1</version>
</dependency>
</dependencies>
</project>
In an ideal world, you wouldn’t have to use optional dependencies.
Instead of having one large project with a series of optional
dependencies, you would separate the EHCache-specific code to a
my-project-ehcache submodule and the
SwarmCache-specific code to a my-project-swarmcache
submodule. This way, instead of requiring projects that reference
my-project to specifically add a dependency, projects
can just reference a particular implementation project and benefit from
the transitive dependency.
You don’t just have to depend on a specific version of a dependency; you can specify a range of versions that would satisfy a given dependency. For example, you can specify that your project depends on version 3.8 or greater of JUnit, or anything between versions 1.2.10 and 1.2.14 of JUnit. You do this by surrounding one or more version numbers with the following characters:
- (, )
-
Exclusive quantifiers
- [, ]
-
Inclusive quantifiers
For example, if you wished to access any JUnit
version greater than or equal to 3.8 but less than 4.0, your dependency
would be as shown in Example 3.5, “Specifying a Dependency Range: JUnit 3.8 - JUnit 4.0”.
Example 3.5. Specifying a Dependency Range: JUnit 3.8 - JUnit 4.0
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>[3.8,4.0)</version> <scope>test</scope> </dependency>
If you want to depend on any version of JUnit no higher than 3.8.1, you would specify only an upper inclusive boundary, as shown in Example 3.6, “Specifying a Dependency Range: JUnit <= 3.8.1”.
Example 3.6. Specifying a Dependency Range: JUnit <= 3.8.1
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>[,3.8.1]</version>ex-de <scope>test</scope> </dependency>
A version before or after the comma is not required, and means +/- infinity. For example, "[4.0,)" means any version greater than or equal to 4.0. "(,2.0)" is any version less than 2.0. "[1.2]" means only version 1.2, and nothing else.
Note
When declaring a "normal" version such as 3.8.2 for Junit,
internally this is represented as "allow anything, but prefer 3.8.2."
This means that when a conflict is detected, Maven is allowed to use
the conflict algorithms to choose the best version. If you specify
[3.8.2], it means that only 3.8.2 will be used and nothing else. If
somewhere else there is a dependency that specifies [3.8.1], you would
get a build failure telling you of the conflict. We point this out to
make you aware of the option, but use it sparingly and only when
really needed. The preferred way to resolve this is via
dependencyManagement.
A transitive dependency is a dependency of a dependency.
If project-a depends on
project-b, which in turn depends on
project-c, then project-c is
considered a transitive dependency of project-a. If
project-c depended on project-d,
then project-d would also be considered a transitive
dependency of project-a. Part of Maven’s appeal is
that it can manage transitive dependencies and shield the developer from
having to keep track of all of the dependencies required to compile and
run an application. You can just depend on something like the Spring
Framework and not have to worry about tracking down every last
dependency of the Spring Framework.
Maven accomplishes this by building a graph of dependencies and
dealing with any conflicts and overlaps that might occur. For example,
if Maven sees that two projects depend on the same
groupId and artifactId, it will
sort out which dependency to use automatically, always favoring the more
recent version of a dependency. Although this sounds convenient, there
are some edge cases where transitive dependencies can cause some
configuration issues. For these scenarios, you can use a dependency
exclusion.
Each of the scopes outlined earlier in the section Section 3.4.1, “Dependency Scope” affects not just the scope of the dependency in the declaring project, but also how it acts as a transitive dependency. The easiest way to convey this information is through a table, as in Table 3.1, “How Scope Affects Transitive Dependencies”. Scopes in the top row represent the scope of a transitive dependency. Scopes in the leftmost column represent the scope of a direct dependency. The intersection of the row and column is the scope that is assigned to a transitive dependency. A blank cell in this table means that the transitive dependency will be omitted.
Table 3.1. How Scope Affects Transitive Dependencies
| Direct Scope | Transitive Scope | |||
|---|---|---|---|---|
| compile | provided | runtime | test | |
| compile | compile | - | runtime | - |
| provided | provided | provided | provided | - |
| runtime | runtime | - | runtime | - |
| test | test | - | test | - |
To illustrate the relationship of transitive dependency scope to
direct dependency scope, consider the following example. If
project-a contains a test scoped dependency on
project-b which contains a compile scoped
dependency on project-c.
project-c would be a test-scoped transitive
dependency of project-a.
You can think of this as a transitive boundary which acts as a filter on dependency scope. Transitive dependencies which are provided and test scope usually do not affect a project. The exception to this rule is that a provided scoped transitive dependency to a provided scope direct dependency is still a provided dependency of a project. Transitive dependencies which are compile and runtime scoped usually affect a project regardless of the scope of a direct dependency. Transitive dependencies which are compile scoped will have the same scope regardless of the scope of the direct dependency. Transitive dependencies which are runtime scoped will generally have the same scope of the direct dependency except when the direct dependency has a scope of compile. When a transitive dependency is runtime scoped and a direct is compile scoped the direct dependency the transitive dependency will have an effective scope of runtime.
There will be times when you need to exclude a
transitive dependency, such as when you are depending on a project
that depends on another project, but you would like to either exclude
the dependency altogether or replace the transitive dependency with
another dependency that provides the same functionality. Example 3.7, “Excluding a Transitive Dependency” shows an example of a dependency element that
adds a dependency on project-a, but excludes the
transitive dependency project-b.
Example 3.7. Excluding a Transitive Dependency
<dependency>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>project-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>project-b</artifactId>
</exclusion>
</exclusions>
</dependency>
Often, you will want to replace a transitive dependency with another implementation. For example, if you are depending on a library that depends on the Sun JTA API, you may want to replace the declared transitive dependency. Hibernate is one example. Hibernate depends on the Sun JTA API JAR, which is not available in the central Maven repository because it cannot be freely redistributed. Fortunately, the Apache Geronimo project has created an independent implementation of this library that can be freely redistributed. To replace a transitive dependency with another dependency, you would exclude the transitive dependency and declare a dependency on the project you wanted instead. Example 3.8, “Excluding and Replacing a Transitive Dependency” shows an example of a such replacement.
Example 3.8. Excluding and Replacing a Transitive Dependency
<dependencies>
<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>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jta_1.1_spec</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
In Example 3.8, “Excluding and Replacing a Transitive Dependency”, there is nothing marking
the dependency on geronimo-jta_1.1_spec as a
replacement, it just happens to be a library which provides the same
API as the original JTA
dependency. Here are some other reasons you might want to exclude or
replace transitive dependencies:
-
The
groupIdorartifactIdof the artifact has changed, where the current project requires an alternately named version from a dependency's version - resulting in 2 copies of the same project in the classpath. Normally Maven would capture this conflict and use a single version of the project, but whengroupIdorartifactIdare different, Maven will consider this to be two different libraries. -
An artifact is not used in your project and the transitive dependency has not been marked as an optional dependency. In this case, you might want to exclude a dependency because it isn't something your system needs and you are trying to cut down on the number of libraries distributed with an application.
-
An artifact which is provided by your runtime container thus should not be included with your build. An example of this is if a dependency depends on something like the Servlet API and you want to make sure that the dependency is not included in a web application's
WEB-INF/libdirectory. -
To exclude a dependency which might be an API with multiple implementations. This is the situation illustrated by Example 3.8, “Excluding and Replacing a Transitive Dependency”; there is a Sun API which requires click-wrap licensing and a time-consuming manual install into a custom repository (Sun's JTA JAR) versus a freely distributed version of the same API available in the central Maven repository (Geronimo's JTA implementation).
Once you've adopted Maven at your super complex enterprise
and you have two hundred and twenty inter-related Maven projects, you
are going to start wondering if there is a better way to get a handle on
dependency versions. If every single project that uses a dependency like
the MySQL Java connector needs to independently list the version number
of the dependency, you are going to run into problems when you need to
upgrade to a new version. Because the version numbers are distributed
throughout your project tree, you are going to have to manually edit
each of the pom.xml files that reference a
dependency to make sure that you are changing the version number
everywhere. Even with find, xargs,
and awk, you are still running the risk of missing a
single POM.
Luckily, Maven provides a way for you to consolidate dependency
version numbers in the dependencyManagement element.
You'll usually see the dependencyManagement element
in a top-level parent POM for an organization or
project. Using the dependencyManagement element in a
pom.xml allows you to reference a dependency in a
child project without having to explicitly list the version. Maven will
walk up the parent-child hierarchy until it finds a project with a
dependencyManagement element, it will then use the
version specified in this dependencyManagement
element.
For example, if you have a large set of projects which make use of
the MySQL Java connector version 5.1.2, you could define the following
dependencyManagement element in your multi-module
project's top-level POM.
Example 3.9. Defining Dependency Versions in a Top-level POM
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>a-parent</artifactId>
<version>1.0.0</version>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.2</version>
</dependency>
...
<dependencies>
</dependencyManagement>
Then, in a child project, you can add a dependency to the MySQL Java Connector using the following dependency XML:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.sonatype.mavenbook</groupId>
<artifactId>a-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>project-a</artifactId>
...
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
You should notice that the child project did not have to
explicitly list the version of the
mysql-connector-java dependency. Because this
dependency was defined in the top-level POM's
dependencyManagement element, the version number
is going to propagate to the child project's dependency on
mysql-connector-java. Note that if this child
project did define a version, it would override the version listed
in the top-level POM's
dependencyManagement section. That is, the
dependencyManagement version is only used when
the child does not declare a version directly.
Dependency management in a top-level POM is
different from just defining a dependency on a widely shared parent
POM. For starters, all dependencies are
inherited. If mysql-connector-java were listed as
a dependency of the top-level parent project, every single project
in the hierarchy would have a reference to this dependency. Instead
of adding in unnecessary dependencies, using
dependencyManagement allows you to consolidate
and centralize the management of dependency versions without adding
dependencies which are inherited by all children. In other words,
the dependencyManagement element is equivalent to
an environment variable which allows you to declare a dependency
anywhere below a project without specifying a version number.
