Managing POM dependencies

In a large-scale development project with hundreds of Maven modules, managing dependencies could be a hazardous task. There are two effective ways to manage dependencies: POM inheritance and dependency grouping. With POM inheritance, the parent POM file has to define all the common dependencies used by its child modules under the dependencyManagement section. This way we can avoid any duplicate dependencies. Also, if we have to update the version of a given dependency, then we only have to make a change in one place. Let's take the same example we discussed before using the WSO2 Carbon Turing project. Let's have a look at the dependencyManagement section of parent/pom.xml (only a part of the POM file is shown here):

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-transport-mail</artifactId>
      <version>${axis2-transports.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.ws.commons.axiom.wso2</groupId>
      <artifactId>axiom</artifactId>
      <version>${axiom.wso2.version}</version>
    </dependency>
  </dependencies>
</dependencyManagement>

Note

To know more about dependency management, refer to Introduction to the Dependency Mechanism at http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html.

Let's have a look at the dependency section of identity/org.wso2.carbon.identity.core/4.2.3/pom.xml, which extends from components/pom.xml. Here, you will see only groupId and artifactId of a given dependency but not version. The version of each dependency is managed through the dependencyManagement section of the parent POM file. If any child Maven module wants to override the version of an inherited dependency, it can simply add the version element:

<dependencies>
  <dependency>
    <groupId>org.apache.axis2.wso2</groupId>
    <artifactId>axis2</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.ws.commons.axiom.wso2</groupId>
   <artifactId>axiom</artifactId>
  </dependency>
</dependencies>

Another best practice to highlight here is the way dependency versions are specified in the parent POM file, which is as follows:

<version>${axiom.wso2.version}</version>

Instead of specifying the version number inside the dependency element itself, here we have taken it out and represented the version as a property. The value of the property is defined under the properties section of the parent POM file, as shown in the following line of code. This makes POM maintenance extremely easy:

<properties>
  <axis2.wso2.version>1.6.1.wso2v10</axis2.wso2.version>
</properties>

The second approach to manage dependencies is through dependency grouping. All the common dependencies can be grouped into a single POM file. This approach is much better than POM inheritance. Here, you do not need to add references to individual dependencies. Let's go through a simple example. First, we need to logically group all dependencies into a single POM file.

Apache Axis2 is an open source SOAP engine. To build an Axis2 client, you need to have all the following dependencies added into your project. If you have multiple Axis2 client modules, in each module, you need to duplicate all these dependencies:

<dependency>
  <groupId>org.apache.axis2</groupId>
  <artifactId>axis2-kernel</artifactId>
  <version>1.6.2</version>
</dependency>
<dependency>
  <groupId>org.apache.axis2</groupId>
  <artifactId>axis2-adb</artifactId>
  <version>1.6.2</version>
</dependency>
<dependency>
  <groupId>org.apache.axis2</groupId>
  <artifactId>axis2-transport-http</artifactId>
  <version>1.6.2</version>
</dependency>
<dependency>
  <groupId>org.apache.axis2</groupId>
  <artifactId>axis2-transport-local</artifactId>
  <version>1.6.2</version>
</dependency>
<dependency>
  <groupId>org.apache.axis2</groupId>
  <artifactId>axis2-xmlbeans</artifactId>
  <version>1.6.2</version>
</dependency>

To avoid the dependency duplication, we can create a Maven module with all the previously mentioned five dependencies as shown in the following code. Make sure to set the value of the packaging element to pom:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>axis2-client</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <dependencies>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-kernel</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-adb</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-transport-http</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-transport-local</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.axis2</groupId>
      <artifactId>axis2-xmlbeans</artifactId>
      <version>1.6.2</version>
    </dependency>
  </dependencies>

</project>

Now, in all of your Axis2 client projects, you only need to add a dependency to the com.packt.axis2-client module, as follows:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>my-axis2-client</artifactId>
  <version>1.0.0</version>

  <dependencies>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>axis2-client</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>

</project>

Transitive dependencies

The transitive dependency feature was introduced in Maven 2.0, which automatically identifies the dependencies of your project dependencies and get them all into the build path of your project. Let's take the following POM as an example; it has only a single dependency:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>jose</artifactId>
  <version>1.0.0</version>
  
  <dependencies>
    <dependency>
      <groupId>com.nimbusds</groupId>
      <artifactId>nimbus-jose-jwt</artifactId>
      <version>2.26</version>
    </dependency>
  </dependencies>

</project>

If you try to create an Eclipse project from the previous POM file using the mvn eclipse:eclipse command, it will result in the following .classpath file. Here you can see, in addition to the nimbus-jose-jwt-2.26.jar file, three more JAR files have been added. These are the transitive dependencies of the nimbus-jose-jwt dependency:

<classpath>
  <classpathentry kind="src" path="src/main/java" including="**/*.java"/>
  <classpathentry kind="output" path="target/classes"/>
  <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
  <classpathentry kind="var" path="M2_REPO/com/nimbusds/nimbus-jose-jwt/2.26/nimbus-jose-jwt-2.26.jar"/>
  <classpathentry kind="var" path="M2_REPO/net/jcip/jcip-annotations/1.0/jcip-annotations-1.0.jar"/>
  <classpathentry kind="var" path="M2_REPO/net/minidev/json-smart/1.1.1/json-smart-1.1.1.jar"/>
  <classpathentry kind="var" path="M2_REPO/org/bouncycastle/bcprov-jdk15on/1.50/bcprov-jdk15on-1.50.jar"/>
</classpath>

If you look at the POM file of the nimbus-jose-jwt project, you will see that the previously mentioned transitive dependencies are defined here as dependencies. Maven does not define a limit for transitive dependencies. One transitive dependency can have a reference to another transitive dependency, and it can go on like this endlessly, given that there are no cyclic dependencies found.

Transitive dependencies can cause some pain too if not used with care. If we take the same Maven module we discussed before as an example and have the following Java code inside src/main/java directory, it will compile without any errors/complaints. This has only a single dependency, which is nimbus-jose-jwt-2.26.jar. However, the net.minidev.json.JSONArray class comes from a transitive dependency, which is json-smart-1.1.1.jar. The build works fine, because Maven gets all the transitive dependencies into the project build path. Everything will work fine till one fine day, you update the version of nimbus-jose-jwt, and the new version could have a reference to a new version of json-smart jar, which is not compatible with your code. This could easily break your build or might cause test cases to fail. This would create hazards and it would be a nightmare to find out the root cause. The following Java code uses the JSONArray class from json-smart-1.1.1.jar:

import net.minidev.json.JSONArray;
import com.nimbusds.jwt.JWTClaimsSet;

public class JOSEUtil {
    public static void main(String[] args) {

        JWTClaimsSet jwtClaims = new JWTClaimsSet();

        JSONArray jsonArray = new JSONArray();

        jsonArray.add("maven-book");

        jwtClaims.setIssuer("https://packt.com");

        jwtClaims.setSubject("john");

        jwtClaims.setCustomClaim("book", jsonArray);

    }
}

To avoid such a nightmare, you need to follow a simple rule of thumb. If you have any import statements in a Java class, you need to make sure that the dependency JAR file corresponding to this is being added to the project POM file.

The Maven dependency plugin helps you to find out such inconsistencies in your Maven module. Run the following command and observe its output;

$ mvn dependency:analyze
[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ jose ---
[WARNING] Used undeclared dependencies found:
[WARNING] net.minidev:json-smart:jar:1.1.1:compile

Note the two warnings in the previous output. It clearly says we have an undeclared dependency for json-smart jar.

Note

The Maven dependency plugin has several goals to find out inconsistencies and possible loopholes in your dependency management. For more details on this, refer to http://maven.apache.org/plugins/maven-dependency-plugin/.

Dependency scopes

Maven defines the following six scope types; if there is no scope element defined for a given dependency, the default scope, compile, will get applied:

  • compile: This is the default scope. Any dependency defined under the compile scope will be available in all the class paths and also packaged into the final artifact produced by the Maven project. If you are building a WAR type artifact, then the referred JAR file with the compile scope will be embedded into the WAR file itself.
  • provided: This scope expects that the corresponding dependency will be provided either by JDK or a container, which runs the application. The best example is the servlet API. Any dependency with the provided scope will be available in the build time class path, but it won't be packaged into the final artifact. If it's a WAR file, the servlet API will be available in the class path during the build time, but won't get packaged into the WAR file.
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
      </dependency>
  • runtime: Dependencies defined under the runtime scope will be available only during the runtime, not in the build time class path. These dependencies will be packaged into the final artifact. You can have a web app that in runtime talks to a MySQL database. Your code does not have any hard dependency to the MySQL database driver. Code is written against the Java JDBC API, and it does not need the MySQL database driver at the build time. However, during the runtime, it needs the driver to talk to the MySQL database. For this, the driver should be packaged into the final artifact.
  • test: Dependencies are only needed for test compilation (for example, JUnit and TestNG), and execution must be defined under the test scope. These dependencies won't get packaged into the final artifact.
  • system: This is very similar to the scope provided. The only difference is with the system scope, you need to tell Maven how to find it. System dependencies are useful when you do not have the referred dependency in a Maven repository. With this, you need to make sure that all system dependencies are available to download with the source code itself. It is always recommended to avoid using system dependencies. The following code snippets shows how to define a system dependency:
      <dependency>
        <groupId>com.packt</groupId>
        <artifactId>jose</artifactId>
        <version>1.0.0</version>
        <scope>system</scope>
        <systemPath>${basedir}/lib/jose.jar</systemPath>
      </dependency>

    Note

    $basedir is a built-in property defined in Maven to represent the directory, which has the corresponding POM file.

  • import: This is only applicable for dependencies defined under the dependencyManagement section with the pom packaging type. Let's take the following POM file; it has the packaging type defined as pom:
    <project>
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.packt</groupId>
      <artifactId>axis2-client</artifactId>
      <version>1.0.0</version>
      <packaging>pom</packaging>
    
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.apache.axis2</groupId>
            <artifactId>axis2-kernel</artifactId>
            <version>1.6.2</version>
          </dependency>
          <dependency>
            <groupId>org.apache.axis2</groupId>
            <artifactId>axis2-adb</artifactId>
            <version>1.6.2</version>
          </dependency>
        </dependencies>
      </dependencyManagement>
    </project>

    Now, from a different Maven module, we add a dependency under the dependencyManagement section to the previous module, with the scope value set to import and the value of type set to pom:

    <project>
    
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.packt</groupId>
      <artifactId>my-axis2-client</artifactId>
      <version>1.0.0</version>
    
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>com.packt</groupId>
            <artifactId>axis2-client</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
    <project>

    Now, if we run mvn help:effective-pom against the last POM file, we will see the dependencies from before are being imported, as shown here:

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.apache.axis2</groupId>
          <artifactId>axis2-kernel</artifactId>
          <version>1.6.2</version>
        </dependency>
        <dependency>
          <groupId>org.apache.axis2</groupId>
          <artifactId>axis2-adb</artifactId>
          <version>1.6.2</version>
        </dependency>
      </dependencies>
    </dependencyManagement>

Optional dependencies

Let's say that we have a Java project that has to work with two different OSGi runtimes. We have written almost all the code to the OSGi API, but there are certain parts in the code that consume OSGi runtime-specific APIs. In runtime, only the code path related to the underneath OSGi runtime will get executed, not both. This raises the need to have both OSGI runtime JAR files at the build time. However, at runtime, we do not need both the code execution paths, only the one related to the OSGi runtime is needed. We can meet these requirements by optional dependencies, which is as follows:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>osgi.client</artifactId>
  <version>1.0.0</version>
  
  <dependencies>
    <dependency>
      <groupId>org.eclipse.equinox</groupId>
      <artifactId>osgi</artifactId>
      <version>3.1.1</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.apache.phoenix</groupId>
      <artifactId>phoenix-core</artifactId>
      <version>3.0.0-incubating</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
  </dependencies>

</project>

For any client project that needs com.packt.osgi.client to work in an Equinox OSGi runtime, it must explicitly add a dependency to the Equinox JAR file.

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>my.osgi.client</artifactId>
  <version>1.0.0</version>
  
  <dependencies>
    <dependency>
      <groupId>org.eclipse.equinox</groupId>
      <artifactId>osgi</artifactId>
      <version>3.1.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>osgi.client</artifactId>
      <version>1.0.0</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

</project>

Dependency exclusion

Dependency exclusion helps avoid getting a selected set of transitive dependencies. Say for example, we have the following POM file with two dependencies, one for the nimbus-jose-jwt and the other for the json-smart artifact:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>jose</artifactId>
  <version>1.0.0</version>

  <dependencies>
    <dependency>
      <groupId>com.nimbusds</groupId>
      <artifactId>nimbus-jose-jwt</artifactId>
      <version>2.26</version>
    </dependency>
    <dependency>
      <groupId>net.minidev</groupId>
      <artifactId>json-smart</artifactId>
      <version>1.0.9</version>
    </dependency>
  </dependencies>

</project>

If we try to run mvn eclipse:eclipse against the previous POM file, you will see the following .classpath file that has a dependency to json-smart version 1.0.9 as rightly expected:

<classpathentry kind="var" path="M2_REPO/net/minidev/json-smart/1.0.9/json-smart-1.0.9.jar"/>

Let's say we have another project that refers to the same nimbus-jose-jwt artifact and a newer version of the json-smart JAR file:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>jose.ext</artifactId>
  <version>1.0.0</version>

  <dependencies>
    <dependency>
      <groupId>com.nimbusds</groupId>
      <artifactId>nimbus-jose-jwt</artifactId>
      <version>2.26</version>
    </dependency>
    <dependency>
      <groupId>net.minidev</groupId>
      <artifactId>json-smart</artifactId>
      <version>1.1.1</version>
    </dependency>
  </dependencies>

</project>

If we try to run mvn eclipse:eclipse against the previous POM file, you will see the following .classpath file that has a dependency to the json-smart artifact version 1.1.1:

<classpathentry kind="var" path="M2_REPO/net/minidev/json-smart/1.1.1/json-smart-1.1.1.jar"/>

We still did not see a problem. Now, say we build a WAR file that has dependencies to both the previous Maven modules:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>jose.war</artifactId>
  <version>1.0.0</version>
  <version>war</version>

  <dependencies>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>jose</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>jose.ext</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>

</project>

Once the WAR file is created, inside WEB-INF/lib, we can see only the 1.1.1 version of json-smart JAR file. This comes as a transitive dependency of the com.packt.jose.ext project. There can be a case where the WAR file does not need the 1.1.1 version in its runtime, but needs the 1.0.9 version. To achieve this, we need to exclude the 1.1.1 version of the json-smart JAR file from the com.packt.jose.ext project, as shown in the following code:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.packt</groupId>
  <artifactId>jose.war</artifactId>
  <version>1.0.0</version>
  <version>war</version>

  <dependencies>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>jose</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>com.packt</groupId>
      <artifactId>jose.ext</artifactId>
      <version>1.0.0</version>
      <exclusions>
        <exclusion>
          <groupId>net.minidev</groupId>
          <artifactId>json-smart</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

</project>

Now, if you look inside WEB-INF/lib, you can see only the 1.0.9 version of the json-smart JAR file.

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

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