Managing dependencies

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.

Dependency scopes

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:

Scope

Description

compile

This is the default scope. Dependencies at compile scope will be available in all the classpaths with which Maven deals; they are used to compile and test our project, and they are packaged in WAR and EAR archives.

provided

Dependencies at this scope are available only during the compile, test-compile, and test phases. They are not packaged in WAR and EAR archives.

runtime

Runtime dependencies are used during the test phase and packaged in WAR and EAR archives. They are included in the runtime classpath of WEB and EE applications, but are not used to compile our project and its unit tests. We should use this scope if we need these dependencies just to run our project and its unit tests.

test

These dependencies are available only in the test-compile and test phases to compile and run the unit tests.

system

This scope is not recommended. It is similar to the provided scope, but we have to specify the full path of the artifact using the <systemPath> child element of the <dependency> element. Dependencies at this scope will not be searched in Maven repositories.

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:

Dependency scopes

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:

Dependency scopes

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.

Dependency version ranges

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:

  • The (<from version>,<to version>) syntax specifies an excluding range
  • The [<from version>,<to version>] syntax specifies an including range
  • We can use the mixed forms (,] and [,)
  • The version numbers before and after the comma are optional
  • We can specify multiple ranges, which are separated by commas

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.

Transitive dependencies and the dependency tree

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:

  • If the scope of the transitive dependency (project C) is compile, then its scope in our project A will be the same as of the direct dependency (project B).
  • If the scope of the transitive dependency is test, then it will not be a dependency of our project.
  • If the scope of the transitive dependency is 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.
  • Finally, if the scope of the transitive dependency is 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:

  • We can specify the exclusion of a transitive dependency in the direct dependency declaration.
  • We can declare a dependency with the <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:

Transitive dependencies and the dependency tree

Dependency inheritance

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.

The super and the effective POMs

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset