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:
- 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
- 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.
- 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 ↩
- http://www.maestrodev.com/better-builds-with-maven/creating-applications-with-maven/resolving-dependency-conflicts-and-using-version-ranges/ ↩
- http://www.maestrodev.com/better-builds-with-maven/creating-applications-with-maven/resolving-dependency-conflicts-and-using-version-ranges/ ↩
Thanks for this post, I’ve been battling dependency issues at my workplace at well.
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
It is tough to fix such issues unless you have helpers like this. Thanks for putting the post.
Neo
http://javawithneo.blogspot.com
Found some more sources here :
http://maven.apache.org/plugins/maven-dependency-plugin/examples/resolving-conflicts-using-the-dependency-tree.html
http://lotusmediacentre.com/maven-dependency-version-conflict-problem-and-resolution/
Anyways quite informative post , learnt something new :)
Thanks