Mittwoch, 14. Mai 2014

Solving Conflicts with Transitive Maven Dependencies ...

Or how to escape the dependency hell by detecting dependency convergence with the maven enforcer plugin.

Dependency con...what?

Consider the following scenario: Maven module M has a dependency to artifact A and B. A also has a dependency to artifact B. M needs B in version 1.0 and A needs B in Version 2.0:

M -+-> A -> B(2.0)
   |
   +-> B(1.0)

This is an example for dependency convergence.

Dependency Mediation

The JVM is not capable of handling different versions of library in the class path (assuming these versions share some packages / classes) in a deterministic way, that's why maven will choose one single artifact version for you in case of dependency convergence.
This is achieved by a mechanism called dependency mediation (see Transitive Dependecies in Maven). If you think of the dependencies as a tree, the version of the artifact with the smallest distance to the root node will win. In our example this would be 1.0.
In most cases this strategy will do what we want, but what if A uses features of B that were not present in version 1.0? Well, then we'll get some ugly RuntimeException as soon as this functionality is used. And we don't want that, do we?

Detect Convergence

Maven itself doesn't tell you, when there's convergence. But luckily, the Maven Enforcer Plugin can be configured to do so. Just add the following to the plugin section of your pom (see Fight Dependency Hell in Maven):

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>1.0.1</version>
  <configuration>
    <rules>
      <DependencyConvergence/>
    </rules>
  </configuration>
  <executions>
    <execution>
      <id>enforce</id>
      <goals>
        <goal>enforce</goal>
      </goals>
      <phase>validate</phase>
    </execution>
  </executions>
</plugin>

This will cause the build fail in case of dependency convergence. The error message will look like this:

Dependency convergence error for org.codehaus.jackson:jackson-mapper-asl:1.9.2 paths to dependency are:
+-com.foo:foo-services:1.10.0.1-SNAPSHOT
  +-com.foo.messaging.abstractJmsClient:DefaultJmsClient:1.4.0
    +-com.foo.messaging.abstractJmsClient:JmsClients:1.4.0
      +-org.codehaus.jackson:jackson-mapper-asl:1.9.2
and
+-com.foo:foo-services:1.10.0.1-SNAPSHOT
  +-com.foo.cs.common:RonlAbstractMsg:2.40
    +-org.codehaus.jackson:jackson-mapper-asl:1.9.4

Take Control

When you encounter dependency convergence, you have to choose the artifact version that fits best for you. That depends on which features are used and in which version they are present. Usually the newest version will work quite well. Once you have decided which version to take, there are different ways to tell maven.

Dependency Management

IMHO the best way is to use Maven Dependency Management. For the example error output from above you could just add

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.4</version>
</dependency>

to the dependency management section of your pom, if you should have decided for version 1.9.4.

Explicit Exclusion

Another possibility - if you should for some reason not want to employ dependency management - is to explicitly exclude the the undesired dependency an explicitly include the desired one. If you prefer that, you could in the above case add

<exclusions>
<exclusion>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</exclusion>
</exclusions>

to the dependency transitively referencing the undesired version. You might (or might not) want to exclude it from all other dependencies referencing it, because then there won't be a convergence if any those transitively referenced versions changes.
Don't forget to then add

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.4</version>
</dependency>

to your pom, to explicitly reference the desired version. Otherwise you might end up missing a jar.

A Little Help

Especially when you introduce the enforcer plugin for the first time you will have to introduce a lot on new dependencies to your dependency management section / add a lot of exclusions and new dependencies depending on your approach. I've written a little python script to ease that up a little:

format_dependency.py on Google Drive

If called like this

./format_dependency.py org.codehaus.jackson:jackson-mapper-asl:1.9.4

it will output

formatting org.codehaus.jackson:jackson-mapper-asl:1.9.4

<exclusions>
<exclusion>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</exclusion>
</exclusions>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.4</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</dependency>

This might save you some time compared to typing it :-)

Specific version

I've left out the (rather not at all used) possibility of specific versions in maven [those in square brackets]. If you are interested in this topic have a look here:

Feedback ...

is always appreciated! Please let me know if this was helpful or total crap. If you disagree or have any suggestions, you are very welcome to contact me as well!