When we build a project, we usually need external libraries and archives of third parties, or those developed by us in other projects. These are called project dependencies. One Maven project will have other Maven projects as dependencies, and it will refer to them through their groupId
, artifactId
, and version
Maven coordinates. When we declare a dependency in a project, this is first searched in the local repository, then in the Maven central repository and other remote repositories, if specified in the POM. When the dependency is found, it is downloaded and stored in the local repository for future reuse. As we are about to see, project dependencies can be available to the build process in different ways, depending on various attributes that we can specify when we declare them.
When we declare a dependency, we can specify a dependency scope. The scope indicates the classpaths in which the dependency will be included. There are five dependency scopes, and they are summarized in the following table:
Let's see an example of a simple web application. Its POM file is as follows:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.packt.samples</groupId> <artifactId>dependency-sample-war</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> <!— Default scope (compile) --> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> <scope>runtime</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency> </dependencies> </project>
The directory structure of the project is shown in the following screenshot:
The SampleClass.java
code is as follows:
package com.packt.sample; import org.apache.log4j.Logger; public class SampleClass { private static Logger log = Logger.getLogger(SampleClass.class); public void logMessage(String msg) { log.info(msg); } }
If we try to build the project, we'll get the following error:
[...] [INFO] Compiling 2 source files to ~dependency-sample-war argetclasses [INFO] ------------------------------------------------------------- [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] ~dependency-sample-warsrcmainjavacompacktsampleSampleClass.java:[3,23] package org.apache.log4j does not exist [...]
This is because the log4j
dependency is not available at compile time. We will get a similar error if we use the JUnit API in a source under src/main/java
rather than under src/test/java
because the scope of the junit
dependency is test
.
If we change our SampleClass.java
file as follows, then the build is successful:
package com.packt.sample; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class SampleClass { private static Log log = LogFactory.getLog(SampleClass.class); public void logMessage(String msg) { log.info(msg); } }
Now, the build process will succeed; both the SampleClass.java
and SampleServlet.java
classes (the latter needs the servlet API contained in the javaee-web-api
dependency) are compiled, and we'll see the output in the target
folder, as shown in the following screenshot:
We can see the compiled classes under target/classes
, the exploded WEB application, and the WAR archive. The WEB-INF/lib
directory contains both the compile
and runtime
dependencies; it does not contain the provided
and test
dependencies.
Instead of specifying a certain version number for a dependency, we can also specify a range of versions. The syntax to be used is the following:
Some examples are summarized in the following table:
Range |
Meaning |
---|---|
(1.0, 1.7) |
Any version between 1.0 and 1.7, both excluded |
[1.0, 1.7] |
Any version between 1.0 and 1.7, both included |
[1.0, 2.0) |
Any version; 1.0 included and 2.0 excluded |
(1.0, 1.9] |
Any version; 1.0 excluded and 1.9 included |
[1.0] |
Strictly 1.0, no other version will be accepted |
(, 2.0) |
Versions up to 2.0 excluded |
[, 2.0) |
Versions up to 2.0 excluded |
(1.0, ) |
Versions greater than 1.0 (excluded) |
(1.0, ] |
Versions greater than 1.0 (excluded) |
(1.0, 1.9], [2.1, 3.0) |
Any version in the specified ranges |
We might wonder which version will be chosen by Maven when a range of versions is specified. We have to keep in mind that when we declare a dependency version (and not a range of versions), we simply give a suggestion about what version Maven should prefer. On the other hand, when we declare a version range, we tell Maven that we can't accept version numbers that are out of the specified range. Maven will use this kind of information to resolve conflicts with other declarations of the same dependency within the same build process. This can happen because of the transitive dependency mechanism or the dependency inheritance, which we'll see in the following sections. When two or more conflicting ranges are specified for the same dependency, the build process exits with an error.
When we have a project A that declares project B among its dependencies, and project B in turn depends on project C, then project A will also depend on project C. This is assured by the Maven dependency mechanism. In other words, we don't need to declare the dependency on project C in project A because project C is a transitive dependency of project A. This leads to great advantages in project dependency management because it permits you to use a certain dependency out of the box without caring whether it requires other artifacts, which are included automatically among the overall project dependencies.
Transitive dependency management depends on the scopes of the direct dependency (the project B of our sample) and the transitive dependency (the project C), as follows:
compile
, then its scope in our project A will be the same as of the direct dependency (project B).test
, then it will not be a dependency of our project.provided
, then it will be a provided
dependency of our project only if the scope of the direct dependency is also provided
. In all other cases, it will not affect our project.runtime
, it will be a runtime dependency of our project if the direct dependency is compile
; otherwise, its scope will be the same as that of the direct dependency.This behavior is summarized in the following table. The intersection of the direct and transitive scopes will give the scope that will be assigned to the transitive dependency in our project.
TRANSITIVE SCOPE (C) | ||||
---|---|---|---|---|
DIRECT SCOPE (B) |
compile |
provided |
runtime |
test |
compile |
compile |
- |
runtime |
- |
provided |
provided |
provided |
provided |
- |
runtime |
runtime |
- |
runtime |
- |
test |
test |
- |
test |
- |
This default behavior can be overridden in two different ways:
<optional>
attribute set to true
, and it will not be considered as a transitive dependency of projects that depend on our project.To take control of the dependencies of our project, know what the effective dependencies are, and from which other dependencies they come from, we can invoke the dependency:tree
goal of the Maven Dependency Plugin. Let's take the sample dependency-sample-war
and add the JAXB dependencies to the project, as follows:
[...] <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.1</version> </dependency> [...]
Now, if we invoke the Maven Dependency Plugin, we'll obtain the following result:
$ mvn dependency:tree [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ dependency-sample-war --- [INFO] com.packt.samples:dependency-sample-war:war:0.0.1-SNAPSHOT [INFO] +- javax:javaee-web-api:jar:6.0:provided [INFO] +- commons-logging:commons-logging:jar:1.1.1:compile [INFO] +- log4j:log4j:jar:1.2.16:runtime [INFO] +- junit:junit:jar:4.8.1:test [INFO] +- javax.xml.bind:jaxb-api:jar:2.1:compile [INFO] | +- javax.xml.stream:stax-api:jar:1.0-2:compile [INFO] | - javax.activation:activation:jar:1.1:compile [INFO] - com.sun.xml.bind:jaxb-impl:jar:2.1:compile [INFO] -------------------------------------------------------------- [INFO] BUILD SUCCESS
We can see that our project acquired two other dependencies, which are the stax-api
version 1.0-2
and activation
version 1.1
. Both these artifacts come from the jaxb-api
dependency. Just to give an example, if we don't need the activation library in our project, we can exclude it as follows:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <exclusions> <exclusion> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> </exclusion> </exclusions> <version>2.1</version> </dependency>
As we can see, in the exclusion element, only groupId
and artifactId
(and not version
) of the transitive dependency have to be specified. This way, the dependency tree becomes the same as is shown:
$ mvn dependency:tree [...] [INFO] com.packt.samples:dependency-sample-war:war:0.0.1-SNAPSHOT [INFO] +- javax:javaee-web-api:jar:6.0:provided [INFO] +- commons-logging:commons-logging:jar:1.1.1:compile [INFO] +- log4j:log4j:jar:1.2.16:runtime [INFO] +- junit:junit:jar:4.8.1:test [INFO] +- javax.xml.bind:jaxb-api:jar:2.1:compile [INFO] | - javax.xml.stream:stax-api:jar:1.0-2:compile [INFO] - com.sun.xml.bind:jaxb-impl:jar:2.1:compile
Our exploded WAR archive will have the structure shown in the following screenshot:
We have to remember that all the Maven projects inherit everything from their parent POMs. Dependencies are not exceptions to this rule; if the parent of our POM declares some dependencies, our project will inherit these dependencies at the same scope they have in the parent project. For example, in our transportation-project
POM, we declare the junit
dependency with the test
scope, so we don't need to declare it again in all the modules of our projects because they inherit this dependency by their parent. Of course, the dependency:tree
plugin goal will display both the inherited as well as the transitive dependencies.
Even when a Maven POM does not refer to a parent project, it inherits implicitly from a parent POM that is embedded in the Maven core libraries. This parent POM is called the super POM. In Version 3.2.1 of Maven, the super POM is located in the maven-model-builder-3.2.1.jar
archive under the /lib
folder of the Maven installation directory. This JAR and the other core JARs in the same directory are not downloaded from remote repositories.
Browsing the model-builder-3.2.1.jar
archive, we can find a pom-4.0.0.xml
file under the org.apache.maven.model
package, which is the super POM. This POM basically contains the definitions of the sources, resources, test sources, test resources, and output directories, and the declaration of the Maven central repository (but no project-default dependencies). Thanks to the super POM, Maven expects to find Java sources under /src/main/java
, builds the project output in the /target
directory, and searches for dependencies in the Maven central repository at http://repo.maven.apache.org/maven2. Remember the concept of convention over configuration!
We can be interested in the result of merging our project POM with its ancestors up to the super POM. This is provided by the help:effective-pom
plugin goal. If we invoke this goal for our sample project, dependency-sample-war
, we'll obtain the following result:
$ mvn help:effective-pom [...] <project> <modelVersion>4.0.0</modelVersion> <groupId>com.packt.samples</groupId> <artifactId>dependency-sample-war</artifactId> [...] <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> <scope>compile</scope> </dependency> [...] </dependencies> <repositories> <repository> <snapshots> <enabled>false</enabled> </snapshots> <id>central</id> <name>Central Repository</name> <url>http://repo.maven.apache.org/maven2</url> </repository> </repositories> [...] <build> <sourceDirectory>~dependency-sample-warsrcmainjava</sourceDirectory> <scriptSourceDirectory>~dependency-sample-warsrcmainscripts</scriptSourceDirectory> <testSourceDirectory>~dependency-sample-warsrc estjava</testSourceDirectory> <outputDirectory>~dependency-sample-war argetclasses</outputDirectory> [...] <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>2.5</version> <executions> <execution> <id>default-clean</id> <phase>clean</phase> <goals> <goal>clean</goal> </goals> </execution> </executions> </plugin> [...] <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <executions> <execution> <id>default-testCompile</id> <phase>test-compile</phase> <goals> <goal>testCompile</goal> </goals> </execution> <execution> <id>default-compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> [...] </plugins> </build> <reporting> <outputDirectory>~dependency-sample-war argetsite</outputDirectory> </reporting> </project>
As we can see, our project POM is merged with the super POM and with the built-in lifecycle default bindings. For example, we can see the bindings of the compiler plugin with the compile
and test-compile
phases, even if these bindings aren't declared in any of the module's POMs or the super POM. Notice that transitive dependencies are not merged—to see them, we have to invoke the dependency:tree
goal.