This section discusses the functionality of optional dependencies and dependency exclusions. This will help users to understand what are they, how to use them, how they work and when is the best way to use them. It also explains why exclusions are made as per dependency basis not in a POM level.
Optional dependencies are used when it's not really possible (for whatever reason) to split a project up into sub-modules. The idea is that some of the dependencies are only used for certain features in the project, and will not be needed if that feature isn't used. Ideally, such a feature would be split into a sub-module that depended on the core functionality project...this new subproject would have only non-optional dependencies, since you'd need them all if you decided to use the subproject's functionality.
However, since the project cannot be split up (again, for whatever reason), these dependencies are declared optional. If a user wants to use functionality related to an optional dependency, they will have to redeclare that optional dependency in their own project. This is not the most clear way to handle this situation, but then again both optional dependencies and dependency exclusions are stop-gap solutions.
It's not only important to declare optional dependencies in order to save space/memory/etc. It's vital to control the list of actual dependencies a person needs in order to use a project, since these jars may eventually make it into a WAR, EAR, EJB, etc. Inclusion of the wrong jars may violate a license agreement, cause classpath issues, etc.
A dependency is declared as optional by simply setting the <optional> tag to true in your dependency declaration. See the sample below:
<project> ... <dependencies> <!-- declare the dependency to be set as optional --> <dependency> <groupId>sample.ProjectA</groupId> <artifactId>Project-A</artifactId> <version>1.0</version> <scope>compile</scope> <optional>true</optional> <!-- value will be true or false only --> </dependency> </dependencies> </project>
Project-A -> Project-B
The diagram above says that Project-A depends on Project-B. When A declares B as an optional dependency in its POM, this relationship remains unchanged. Its just like a normal build where Project-B will be added in its classpath.
Project-X -> Project-A
But when another project(Project-X) declares Project-A as a dependency in its POM, the optional dependency takes effect. You'll notice that Project-B is not included in the classpath of Project-X; you will need to declare it directly in your POM in order for B to be included in X's classpath.
Let us say that there is a project named X2 that has similar functions with Hibernate which supports many database drivers/dependencies such as mysql, postgre, oracle etc. All of these dependencies are needed for X2 to build but not for your project, so it is very practical for X2 to declare these dependencies as optional, so that whenever your project declares X2 as a direct dependency in your POM, all the drivers supported by the X2 will not be automatically included to your project's classpath instead you'll have to declare it directly on what driver/dependency of the database you are going to use.
Since maven 2.x resolves dependencies transitively, it is possible for unwanted dependencies to be included in your project's classpath. Projects that you depend on may not have declared their set of dependencies correctly, for example. In order to address this special situtation, maven 2.x has incorporated the notion of explicit dependency exclusion. Exclusions are set on a specific dependency in your POM, and are targeted at a specific groupId and artifactId. When you build your project, that artifact will not be added to your project's classpath by way of the dependency in which the exclusion was declared.
We add the <exclusions> tag under the <dependency> section of the pom.
<project> ... <dependencies> <dependency> <groupId>sample.ProjectA</groupId> <artifactId>Project-A</artifactId> <version>1.0</version> <scope>compile</scope> <exclusions> <exclusion> <!-- declare the exclusion here --> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </project>
Project-A -> Project-B -> Project-D <! -- This dependency should be excluded --> -> Project-E -> Project-F -> Project C
The diagram shows that Project-A depends on both Project-B and C. Project-B depends on Project-D. Project-D depends on both Project-E and F. By default, Project A's classpath will include:
B, C, D, E, F
What if we dont want project D and its dependencies to be added to Project A's classpath because we know some of Project-D's dependencies (maybe Project-E for example) was missing from the repository, and you don't need/want the functionality in Project-B that depends on Project-D anyway. In this case, Project-B's developers could provide a dependency on Project-D that is <optional>true</optional>, like this:
<dependency> <groupId>sample.ProjectD</groupId> <artifactId>ProjectD</artifactId> <version>1.0-SNAPSHOT</version> <optional>true</optional> </dependency>
HOWEVER, they didn't. As a last resort, you still have the option to exclude it on your side, in Project-A, like this:
<project> <modelVersion>4.0.0</modelVersion> <groupId>sample.ProjectA</groupId> <artifactId>Project-A</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> ... <dependencies> <dependency> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>sample.ProjectD</groupId> <!-- Exclude Project-D from Project-B --> <artifactId>Project-D</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </project>
If we deploy the Project-A to a repository, and Project-X declares a normal dependency on Project-A, will Project-D be excluded from the classpath still?
Project-X -> Project-A
The answer is Yes. Project-A has declared that it doesn't need Project-D to run, so it won't be brought in as a transitive dependency of Project-A.
Now, consider that Project-X depends on Project-Y, as in the diagram below:
Project-X -> Project-Y -> Project-B -> Project-D ...
Project-Y also has a dependency on Project-B, and it does need the features supported by Project-D. Therefore, it will NOT place an exclusion on Project-D in its dependency list. It may also supply an additional repository, from which we can resolve Project-E. In this case, it's important that Project-D is not excluded globally, since it is a legitimate dependency of Project-Y.
As another scenario, what if the dependency we don't want is Project-E instead of Project-D. How will we exclude it? See the diagram below:
Project-A -> Project-B -> Project-D -> Project-E <!-- Exclude this dependency --> -> Project-F -> Project C
Exclusions work on the entire dependency graph below the point where they are declared. If you wanted to exclude Project-E instead of Project-D, you'd simply change the exclusion to point at Project-E, but you wouldn't move the exclusion down to Project-D...you cannot change Project-D's POM. If you could, you would use optional dependencies instead of exclusions, or split Project-D up into multiple subprojects, each with nothing but normal dependencies.
<project> <modelVersion>4.0.0</modelVersion> <groupId>sample.ProjectA</groupId> <artifactId>Project-A</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> ... <dependencies> <dependency> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>sample.ProjectE</groupId> <!-- Exclude Project-E from Project-B --> <artifactId>Project-E</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </project>
This is mainly done to be sure the dependency graph is predictable, and to keep inheritance effects from excluding a dependency that should not be excluded. If you get to the method of last resort and have to put in an exclusion, you should be absolutely certain which of your dependencies is bringing in that unwanted transitive dependency.