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>
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>
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
.
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/.
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>
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>
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 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.
3.12.136.119