Complex Java applications often consist of lots of JAR files. Particularly when it comes to open source projects, there is a deep “swamp” of transitive libraries your (possibly rather small) own code depends on. It might be that you do not care initially, but when it comes to deployment, chances are good that you will. At least your customers running that application in elastic clouds environments might not like the idea to have hundres of possibly huge JARs loaded into RAM again and again by more and more replicas of a kubernetes pod. That’s roughly the point when you start to think about squeezing excess material out of your box.
The Maven Shade Plugin
A possible solution is using the Maven Shade Plugin, in particular the plugin’s minimizeJar goal. It is a standard tool which, roughly spoken, takes all content from all JARs (yes, including transitive dependencies) and puts it all into one common target JAR. But not enough, while doing that it filters out anything not needed. This means, yes, it really understands your Java binary code, and, thanks to some fixes I contributed in the past months, the recently published version 3.2.2 even understands how to deal with loosely couples services (aka ServiceLoader). So this new release is able to even scan complex frameworks like Jersey.
To proof the usability of this new release, I set up a small hello world REST application using JAX-RS and Jersey, and added the following shading configuration:
<plugin> <artifactId>maven-shade-plugin</artifactId> <version>3.2.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <minimizeJar>true</minimizeJar> <filters> <filter> <excludeDefaults>false</excludeDefaults> <artifact>*:*</artifact> <includes> <include>org/glassfish/json/JsonProviderImpl</include> <include>com/sun/xml/bind/v2/model/nav/ReflectionNavigator</include> </includes> <excludes> <exclude>**/*.md</exclude> <exclude>**/*.markdown</exclude> <exclude>**/*.header</exclude> <exclude>**/*.xml</exclude> <exclude>**/pom.properties</exclude> <exclude>**/io.netty.versions.properties</exclude> <exclude>**/*.args</exclude> <exclude>**/*.so</exclude> </excludes> </filter> </filters> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>HelloWorldBootstrap</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> </transformers> </configuration> </execution> </executions> </plugin>
It is essentials to really use version 3.2.2 of this plugin, so we explicitly provide that particular version here. Next we declared that we want the shading happing in the package phase of the project lifecycle. Then you’ll find the minimizeJar option being enabled, which is the essence of what this blog post talks about: Tree shaking with JARs. In my particular case, in addition to the tree shaking, I wanted to remove more stuff which the shade plugin doesn’t touch normally: Resource files. Also I wanted to keep stuff which the plugin would other discard: Classes which are actually not referenced otherwise. How could that happen? This is because Jersey uses their string names instead of their class constants to load them. The plugin cannot know what a string is good for, so I added these explicit includes. For services this is no needed, because I extended the shade plugin in 3.2.2 with the ability to discover services — which is, what makes it so easy to shade Jersey with its lots of service-loaded modules. But what we do need is to prevent our includes to override the default includes, which is what the excludeDefaults option does that I also added in 3.2.2. And we need to tell the shade plugin that is must combine all service metafiles into a single one, which is what the secondfilter does. The first simply makes the final JAR executable.
So the outcome is…
7261 [INFO] --- maven-shade-plugin:3.2.2:shade (default) @ helloworld --- 7608 [INFO] Including jakarta.ws.rs:jakarta.ws.rs-api:jar:2.1.6 in the shaded jar. 7611 [INFO] Including org.glassfish.jersey.core:jersey-server:jar:2.30 in the shaded jar. 7612 [INFO] Including org.glassfish.jersey.core:jersey-common:jar:2.30 in the shaded jar. 7612 [INFO] Including org.glassfish.hk2:osgi-resource-locator:jar:1.0.3 in the shaded jar. 7613 [INFO] Including com.sun.activation:jakarta.activation:jar:1.2.1 in the shaded jar. 7613 [INFO] Including org.glassfish.jersey.core:jersey-client:jar:2.30 in the shaded jar. 7613 [INFO] Including org.glassfish.jersey.media:jersey-media-jaxb:jar:2.30 in the shaded jar. 7614 [INFO] Including jakarta.annotation:jakarta.annotation-api:jar:1.3.5 in the shaded jar. 7617 [INFO] Including org.glassfish.hk2.external:jakarta.inject:jar:2.6.1 in the shaded jar. 7617 [INFO] Including jakarta.validation:jakarta.validation-api:jar:2.0.2 in the shaded jar. 7617 [INFO] Including jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.2 in the shaded jar. 7618 [INFO] Including jakarta.activation:jakarta.activation-api:jar:1.2.1 in the shaded jar. 7618 [INFO] Including org.glassfish.jersey.inject:jersey-hk2:jar:2.30 in the shaded jar. 7622 [INFO] Including org.glassfish.hk2:hk2-locator:jar:2.6.1 in the shaded jar. 7625 [INFO] Including org.glassfish.hk2.external:aopalliance-repackaged:jar:2.6.1 in the shaded jar. 7626 [INFO] Including org.glassfish.hk2:hk2-api:jar:2.6.1 in the shaded jar. 7626 [INFO] Including org.glassfish.hk2:hk2-utils:jar:2.6.1 in the shaded jar. 7626 [INFO] Including org.javassist:javassist:jar:3.25.0-GA in the shaded jar. 7627 [INFO] Including org.glassfish.jersey.containers:jersey-container-netty-http:jar:2.30 in the shaded jar. 7627 [INFO] Including org.glassfish.jersey.connectors:jersey-netty-connector:jar:2.30 in the shaded jar. 7627 [INFO] Including io.netty:netty-all:jar:4.1.31.Final in the shaded jar. 7629 [INFO] Including org.glassfish.jersey.media:jersey-media-json-binding:jar:2.30 in the shaded jar. 7629 [INFO] Including org.glassfish:jakarta.json:jar:1.1.5 in the shaded jar. 7629 [INFO] Including org.eclipse:yasson:jar:1.0.3 in the shaded jar. 7631 [INFO] Including jakarta.json.bind:jakarta.json.bind-api:jar:1.0.2 in the shaded jar. 7633 [INFO] Including jakarta.json:jakarta.json-api:jar:1.1.5 in the shaded jar. ... 14383 [INFO] Minimized 6074 -> 4079 (67%) ...
A reduction from 26 JARs to one single JAR holding only the actually needed stuff, and a strong reduction of size by 33% is pretty amazing. And it was done without any proprietary frameworks or magic, just by one official Maven plugin! Who knows what future will bring to proprietary frameworks – we know it for Apache Maven: It’s here to stay since decades.
At this point I want to say thank you to Robert, Karl-Heinz, Hervé and Mark from Apache who helped me to add these features to the plugin. And I want to encourage everybody to not simply adopt a proprietary framework (even when it open source), but to think first always: Do I need that? Isn’t it simpler to just use JDK + Maven?
While the plugin is not really perfect in sorting out all kinds of includes and excludes, the recent version 3.2.2 is already pretty good in dealing with loosesly coupled frameworks. If your target is to reduce footprint of your delivered classes, it is definitively worth a try – particularly because it is an official part of the de-facto build standard Maven.