Here, we'll learn how to use two versions of the same library in a single JVM project.
Problem statement:
In modern Java development we have the luxury of using build automation tools like maven or gradle to handle our dependencies once we tell them what we need. They are amazing at managing things when dealing with multiple libraries and transient dependencies.
But sometimes we can run into an issue where they may not be able to pick the correct dependency version.
Consider the following example.
A library has 2 versions:
- Version 1 (v1) is the older one and has 3 methods
- methodA, methodB and methodC
- Version 2 (v2) is the newer one and has 3 methods
- methodA, methodB (with slightly different logic but same signature) and methodD
You have your application setup with maven and your application needs version1 and specifically methodC.
Your application also has a dependency on a 3rd party library which needs version 2 and methodD or maybe even newer methodB.
What to do in that case? As maven will ensure only one version is used. Basically "nearest win" strategy. Either v1 or v2. And you can't have 2 classes with same package_name + file_name in your application. Diamond dependency?
Shading to the rescue
- In a nutshell, it packs the complete version1 (and its dependency if needed) of the library in your jar.
- It also renames the v1 package/path. So com.organization.project.library becomes something like com.organization.project.shaded.library.
- While packaging maven will replace all the references (like import statements and fully qualified name) of the non-shaded (v1) package name with the shaded path.
- The 3rd party library will obviously keep on using the non-shaded (v2) path as it's not been modified by Maven.
Its like my wife telling me not to buy any new bike and me try to convince her it's not exactly a bike but a lawn mower (Partially true story).
FYI: Shading plugin basically rewrites the bytecode.
A real-world example
Guava is a high-quality utility library used in many projects. But it suffers from one major flaw: it often breaks backward compatibility. Let's say your application uses v32, but you include a 3rd party library that relies on v23. This can lead to java.lang.NoSuchMethodError.
To handle this you bundle a copy of Guava v23 classes inside your final JAR and relocate its packages from com.google.common.* to a private path like com.myapp.shaded.guava.com.google.common.*
Sample pom for above mentioned example (check the build/plugin part):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myapp</groupId>
<artifactId>shading-example</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<guava.version>32.1.3-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>some-data-library</artifactId>
<version>1.2.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<include>com.example:some-data-library</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>com.myapp.shaded.guava</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Jackson (com.fasterxml.jackson) and Kryo (com.esotericsoftware.kryo) are also 2 more examples where developers can face similar issues.
Shading is Simple. Right?
A more complex problem statement
What if your application itself needs both versions of the library simultaneously? I know it’s a very unusual scenario. And I hope you don’t have to face something similar. But we faced this and lived to tell the tale (you are reading it, right?).
Here, the standard shading approach fails. As the shading plugin modifies all occurrences of the original package, renaming them to the new shaded path. And we don’t want that. We want some of the references to use v1 and other v2.
I sincerely hope you are not in a scenario to support 3 different versions. If yes, do write an article about your own miserable coding life.
In our case, the environment itself where we had to deploy the application was providing us with a runtime dependency. The newer version of the library was essential for our application to function in the evolving runtime environment. At the same time, we needed the older version of that library to read existing, persisted data. So the older version was mandatory for us. And of course, no backward compatibility (you thought this would be easy?).
Solution
- To solve this, we moved all the code that relied on the older library version into its own, separate library project.
- In the pom.xml of our library project, we shaded the old dependency much like the Guava example (with a small difference we'll explain later).
- And then we basically generated two distinct artifacts from this project: the original, standard JAR and the new, shaded JAR.
- This new, shaded JAR (containing the old library) and the original version of the library were both added as dependencies to our main application.
- And the code in our main application that needed the v1 was updated to import the new, relocated packages from our custom-shaded JAR. And any code which needed referencing to v2 was kept as is.
Library pom.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myapp.wrappers</groupId>
<artifactId>guava-v23-wrapper</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedClassifierName>shaded-guava-v23</shadedClassifierName>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>com.myapp.shaded.guava.v23</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Major change is introduction of tag shadedClassifierName. It tells the maven-shade-plugin not to replace the main artifact. Instead, it creates a new JAR and appends -shaded-guava-v23 to its name.
In application pom.xml:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.latest.version}</version>
</dependency>
<dependency>
<groupId>com.myapp.wrappers</groupId>
<artifactId>guava-v23-wrapper</artifactId>
<version>1.0.0</version>
<classifier>shaded-guava-v23</classifier>
</dependency>
In your application code, you can basically now do this:
import com.google.common.base.Strings;
public class GuavaVersionHandler {
public String useNewGuava(String text) {
return Strings.padEnd(text, 15, '.');
}
public boolean useOldGuava(String text) {
return com.myapp.shaded.guava.v23.base.Strings.isNullOrEmpty(text);
}
public static void main(String[] args) {
GuavaVersionHandler handler = new GuavaVersionHandler();
String result1 = handler.useNewGuava("modern");
System.out.println("New Guava result: " + result1);
boolean result2 = handler.useOldGuava("");
System.out.println("Old Guava result: " + result2);
}
}
Final thoughts
So there you have it. Dependency shading is a powerful, if slightly deceptive, tool in the fight against dependency hell. Sometimes, you just need to put a cat costume on a library to keep a third-party dependency happy. Other times, you have to convince your own application that your new bicycle is actually a lawnmower to maintain backward compatibility.
While it shouldn't be your first resort—as it adds complexity and size to your project—knowing how to effectively shade a dependency is the perfect escape hatch for otherwise impossible version conflicts. Use this power wisely, and happy (and less miserable) coding! And remember if you dabble into shading, test the hell out of your application/code.
Would love to hear if you have used any other tools or strategies besides shading to resolve version conflicts in your projects.
Top comments (0)