5-minute dependency hell debugging with Maven and IntelliJ
This morning I had the all too familiar experience of breaking a working Maven/Java module by simply copy-pasting it into another project. The goal was to port a working project into a multi-module Maven project. Immediately, tests stopped compiling — and the errors messages were cryptic, relating to static compilers and so forth. Unfortunately this happens frequently enough that I’m actually getting the hang of debugging, so thought I’d share.
Target folder analysis
The source code was compiling (mvn clean compile = successful) but the test code was not (mvn clean test-compile = fail), so I knew the issues were confined to my test code, resources and/or dependencies. Next, I opened both projects in IntelliJ and examined the target folders side-by-side after compiling.
It is immediately obvious that some of the resources (ie. src/main/resources) were not compiling in the new project. Turns out there was a bug in the new parent pom. More specifically, it overrode Maven’s default behavior that anything under /src/main/resources is automatically copied to /target.
Dependency Analysis
Fixing the errant parent (lol) did solve the issue of tests not compiling, but the tests were not passing. Instead, they failed with ClassNotFound errors such as:
java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory at MYCLASS:java:39 Caused by: ClassNotFoundException:org.apache.commons.logging.LogFactory
This typically means a required class from apache.commons was missing from the project — in other words, the dependency graphs are different.
Similar to the previous analysis, the first thing to do is side-by-side comparison of the Maven tool windows (ctrl+alt+M).
This is a bit more tedious, but we see that at least two libraries have differing minor versions. While probably not the culprit, I changed the versions just to be sure that it didn’t fix the errors — no dice!
Since our Google search indicated the issue is probably related to the Apache commons log libraries, I ran mvn dependency:tree on both projects and focused on terms “apache”, “commons” and “log”. Indeed, the commons-logging dependency was missing altogether from the new project.
Why this is the case — I still have no idea. Both are using the same top-level dependencies… I guess that’s why they call it dependency hell.
In any case, adding the dependency explicitly back into the project was simpler than troubleshooting and further.
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
Incidentally, this is what Googling the project recommended in the first place :)
BONUS contents
Maven’s dependency:tree is a super useful command. Another use for it is in finding vulnerable dependencies — for example in our Team City builds we first create a dependency tree
dependency:tree -DoutputFile=tree.out
and then run REGEXs to ensure we’re not using certain versions of libraries. For example, spring-data-commons older than 1.13.10 / 2.0.5 are vulnerable to CVE-2018–1273. We check all of our projects for vulnerable spring-data-commons dependencies as shown below.
#!/usr/bin/env bash# Read dependency tree output from last step into var
deptree=$(cat ./tree.out)# Tinker with regex here: https://regexr.com/45oj5
VULNERABILITY_REGEX='spring-data-commons:jar:((1\.13.(\b([0-9]|1[0])\b))|(2\.0\.\b[0-5]\b))'
COMMONS_REGEX='spring-data-commons'result=$(echo "$deptree" | grep -P $VULNERABILITY_REGEX)# If no matches
if [[ -z "$result" ]]; then
echo "No jar vulnerabilities"
else
actualversion=$(echo "$deptree" | grep -P 'spring-data-commons')
echo "Spring data commons version must be > 1.13.10 / 2.0.5 ... see line --> $actualversion"
exit 1