Enterprise Java

When Maven Dependency Plugin Lies

Problem:

We had an integration test which creates a spring ClassPathXmlApplicationContext and while doing so the test blew up with a NoSuchMethodError. It turned out that we had conflicting versions of dependencies on Spring artifacts. This in itself is not an unusual problem – such problems are addressed with the maven dependency plugin using the verbose option. However, what do you do when the maven plugin is wrong?
 
 
 

Investigation:

We started to dig in and we found that the AbstractAutowireCapableBeanFactory in its getTypeForFactoryMethod method tries to access the GenericTypeResolver resolveReturnTypeForGeneric method and fails on a java.lang.NoSuchMethodError: org.springframework.core.GenericTypeResolver.resolveReturnTypeForGenericMethod(Ljava/lang/reflect/Method;.

Initial investigation and googling we found that the method was added in 3.2.0 while we’re supposed to be running with 3.1.1. Further investigation determined that spring-data-mongodb depends on spring framework in range [3.0.7-4) 1 and because maven takes the latest available version given that range 2 it tried to take 3.2.2.
Note that the above changes a bit given a conflict between an explicit version dependency and a range dependency but, IINM, when determining the dependencies of spring mongo there is no conflict.

The problem was further masked by two symptoms:

  1. We have other projects that use this pattern and have no problem- This was explained by the fact that the conflict-resolving mechanism of maven chooses the nearest version it finds by default 3 and since all other projects which need spring-data-mongodb depend on this project they were lucky enough to grab the 3.1.1 version and not 3.2.2
  2. dependency:tree shows it brings 3.1.1 while bringing 3.2.2- Since the stack trace showed other results I wrote a test which checks from which jar each of the above classes comes from and verified that indeed the AbstractAutowireCapableBeanFactory class arrives from spring-beans 3.2.2 and not 3.1.1 as “mvn dependency:tree” showed (a big thanks to http://bit.ly/10zD1iV for the code snippet of finding the jar of a class in runtime).

Maven dependency:tree output showing spring-beans:3.1.1 is used in the artifact

>:mvn dependency:tree -Dverbose -Dincludes=org.springframework
...
(omitted for clarity)
...
[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ wix-feature-toggle-administration ---
[INFO] artifact org.springframework:spring-beans: checking for updates from central
[INFO] artifact org.springframework:spring-beans: checking for updates from snapshots
[INFO] artifact org.springframework:spring-expression: checking for updates from central
[INFO] artifact org.springframework:spring-expression: checking for updates from snapshots
[INFO] artifact org.springframework:spring-tx: checking for updates from central
[INFO] artifact org.springframework:spring-tx: checking for updates from snapshots
[INFO] com.wixpress.common:wix-feature-toggle-administration:jar:2.180.0-SNAPSHOT
...
[INFO] +- org.springframework.data:spring-data-mongodb:jar:1.0.1.RELEASE:compile
[INFO] |  +- org.springframework:spring-beans:jar:3.1.1.RELEASE:compile
[INFO] |  |  \- (org.springframework:spring-core:jar:3.2.2.RELEASE:compile - omitted for conflict with 3.1.1.RELEASE)
[INFO] |  +- org.springframework:spring-expression:jar:3.1.1.RELEASE:compile
[INFO] |  |  \- (org.springframework:spring-core:jar:3.2.2.RELEASE:compile - omitted for conflict with 3.1.1.RELEASE)
[INFO] |  \- org.springframework.data:spring-data-commons-core:jar:1.2.1.RELEASE:compile
[INFO] |     +- (org.springframework:spring-beans:jar:3.1.1.RELEASE:compile - omitted for duplicate)
[INFO] |     \- (org.springframework:spring-tx:jar:3.1.1.RELEASE:compile - omitted for duplicate)
[INFO] +- com.wixpress.common:wix-framework:jar:2.180.0-SNAPSHOT:compile
[INFO] |  +- org.springframework:spring-core:jar:3.1.1.RELEASE:compile
[INFO] |  |  \- org.springframework:spring-asm:jar:3.1.1.RELEASE:compile
...
I've removed additional outputs for clarity. The additional outputs were all 3.1.1 and were further down the tree (so irrelevant due to maven conflict resolving mechanism)

Test which proves spring-beans:3.2.2 is used in the artifact (asserting what the jvm in the error was saying)

package com.wixpress.springVersionBug;


import org.junit.*;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.core.GenericTypeResolver;
import java.security.CodeSource;
import static org.hamcrest.Matchers.endsWith;

/**
 * @author ittaiz
 * @since 3/24/13
 */

public class SpringVersionTest {


    @Test
    public void verifySpringBeans311InClasspath(){
        verifyCorrectSpringVersionInClasspathFor(AbstractAutowireCapableBeanFactory.class,"spring-beans-3.1.1.RELEASE.jar");
    }
    @Test
    public void verifySpringCore311InClasspath(){
        verifyCorrectSpringVersionInClasspathFor(GenericTypeResolver.class,"spring-core-3.1.1.RELEASE.jar");
    }
    public void verifyCorrectSpringVersionInClasspathFor(Class springClass,String expectedJarFileName){
        CodeSource springClassCodeSource = springClass.getProtectionDomain().getCodeSource();
        Assert.assertNotNull("expecting "+expectedJarFileName+" to be loaded by non-system class loader",springClassCodeSource);
        Assert.assertThat(springClassCodeSource.getLocation().toString(),endsWith(expectedJarFileName));
    }
}

The reason spring-core artifact came in 3.1.1 when spring-beans came as 3.2.2 is that our framework explicitly depends on spring-core and this artifact explicitly depends on the framework. This means spring-core 3.1.1 from framework is 2 hops which is shorter than the 3.2.2 from spring-data-mongodb.

Solution:

Depend on spring-data-mongodb while excluding spring-beans like so:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-mongodb</artifactId>
  <version>1.0.1.RELEASE</version>
  <exclusions>
    <exclusion>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
    </exclusion>
  </exclusions>
</dependency>

The Open question mark:

Why dependency:tree (in verbose mode) did not show that it brings spring-beans in 3.2.2 but in 3.1.1 while explicitly specifying that spring-core 3.2.2 was removed due to conflict? I chalk this up to a bug in the dependency plugin.

  1. http://repo1.maven.org/maven2/org/springframework/data/spring-data-mongodb-parent/1.0.1.RELEASE/spring-data-mongodb-parent-1.0.1.RELEASE.pom
  2. http://www.maestrodev.com/better-builds-with-maven/creating-applications-with-maven/resolving-dependency-conflicts-and-using-version-ranges/
  3. http://www.maestrodev.com/better-builds-with-maven/creating-applications-with-maven/resolving-dependency-conflicts-and-using-version-ranges/

 

Reference: When Maven Dependency Plugin Lies from our JCG partner Yoav Abrahami at the Wix IO blog.

Yoav Abrahami

Yoav is the Chief Architect at Wix.com, working with developers and operations to build the company's future products as well as accelerating and improving development processes. Prior to joining Wix, Yoav was an Architect at Amdocs Cramer OSS division. Yoav holds a MS in Physics and a BS in Computer Science from the Tel Aviv University.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Keith Sader
Keith Sader
10 years ago

Thanks for this post, I’ve been battling dependency issues at my workplace at well.

Sean Flanigan
Sean Flanigan
10 years ago

According to a comment in the original blog the bug was fixed in maven-dependency-plugin:2.5: http://wix.io/2013/04/25/maven-classpath-hell-nosuchmethoderror-when-building-a-spring-context/#comment-55

Neo Lite
Neo Lite
10 years ago

It is tough to fix such issues unless you have helpers like this. Thanks for putting the post.

Neo

http://javawithneo.blogspot.com

Back to top button