ProGuard with Maven and PluginManagement



I recently made use of ProGuard to reduce the size of a jar through tree shaking. Tree shaking is basically the art of holding the software upside down in the roots and shaking it so that you only keep the classes and methods you need.

There is a Maven plugin called proguard-maven-plugin that allows you to run ProGuard from your Maven build with ease.

How to use this plugin in a way that cooperates well with the standard Maven Shade plugin was not entirely straightforward and I'd like to share some of my findings.

Plugin Management / One Config to Rule Them All

It turns out that the proguard-maven-plugin works really well together with the Maven "plugin management feature. If you have not heard of it please read more here: https://maven.apache.org/pom.html#Plugin_Management

An imagined company super 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>

	<!-- ... -->

    <properties>
    	<proGuardVersion>5.3.3</proGuardVersion>
        <proGuardMavenPluginVersion>2.0.14</proGuardMavenPluginVersion>
        
    </properties>

    <!-- ... -->

    <build>
        <pluginManagement>
            <plugins>
                <!-- ProGuard -->
                <plugin>
                    <groupId>com.github.wvengen</groupId>
                    <artifactId>proguard-maven-plugin</artifactId>
                    <version>${proGuardMavenPluginVersion}</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>proguard</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <proguardVersion>${proGuardVersion}</proguardVersion>
                        <proguardInclude>${basedir}/proguard.conf</proguardInclude>
                        <dependencies>
                            <dependency>
                                <groupId>net.sf.proguard</groupId>
                                <artifactId>proguard-base</artifactId>
                                <version>${proGuardVersion}</version>
                                <scope>runtime</scope>
                            </dependency>
                        </dependencies>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <!-- ... -->
    
</project>

With such a super pom we can simply add ProGuard processing using the following short addition the actual Maven project:

<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>

    <!-- ... -->

    <build>
        <plugins>
            <plugin>
                <groupId>com.github.wvengen</groupId>
                <artifactId>proguard-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <!-- ... -->
    
</project>

Now you can put all ProGuard specific configuration in the file proguard.conf in your base dir. An example of this layout and how I used it can be seen in these projects:

Before or after Shade?

The proguard-maven-plugin applies ProGuard to the project itself. That means you should shade in any external libraries using maven-shade-plugin before you use the proguard-maven-plugin. In practice this means proguard-maven-plugin should be furthest down in your pom.xml.

ProGuard itself operates the best when you supply it with loads of so called libraryjars. The proguard-maven-plugin helps out by automatically adding known Maven dependencies as library jars. This is a great feature that we want to make use of. It usually works fine, but if you use the "relocate" feature of Maven shade the library jars may no longer link properly. Because of this I have actually made use of a multi step build in my case. MassiveCoreXlibGuava is just a tree shaked Guava. In MassiveCoreXlib I relocate that treeshaked Guava into another Java package. I'm not sure this is 100% necessary but it allows me to keep things isolated and easier to debug.

Picking the Right Java JDK libraryjars

For some odd reason it seems you have do supply certain JDK libraryjars yourself. Take a look at this proguard.conf example:

# These two should be commented out while experimenting with ProGuard configuration.
# They silence all the notes and warnings we have chosen to ignore.
# Without these two there are hundreds of lines of output that clutters the readability of the Maven build.
-dontnote
-dontwarn

# Our goal with using ProGuard is only to cherry pick the classes we want to reduce the final jar size.
# We want neither obfuscation nor optimization.
-dontobfuscate
-dontoptimize

# The Java libraries relevant to us.
# To come up with this list I added all jars in /lib then removed those where the warning count did not change.
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jsse.jar
-libraryjars <java.home>/lib/jce.jar

# This is suggested for libraries on the ProGuard website. We basically want to keep all attributes.
# https://www.guardsquare.com/en/proguard/manual/examples#library
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod

# Finally we cherry pick what we want from Guava.
-keep,includedescriptorclasses class com.google.common.collect.ImmutableList { *; }
-keep,includedescriptorclasses class com.google.common.collect.ImmutableList$* { *; }
-keep,includedescriptorclasses class com.google.common.collect.BiMap { *; }
-keep,includedescriptorclasses class com.google.common.collect.BiMap$* { *; }
-keep,includedescriptorclasses class com.google.common.collect.HashBiMap { *; }
-keep,includedescriptorclasses class com.google.common.collect.HashBiMap$* { *; }
-keep,includedescriptorclasses class com.google.common.collect.ImmutableBiMap { *; }
-keep,includedescriptorclasses class com.google.common.collect.ImmutableBiMap$* { *; }
-keep,includedescriptorclasses class com.google.common.collect.ImmutableSet { *; }
-keep,includedescriptorclasses class com.google.common.collect.ImmutableSet$* { *; }
-keep,includedescriptorclasses class com.google.common.base.Objects { *; }
-keep,includedescriptorclasses class com.google.common.base.Objects$* { *; }
-keep,includedescriptorclasses class com.google.common.reflect.ClassPath { *; }
-keep,includedescriptorclasses class com.google.common.reflect.ClassPath$* { *; }

The section I'm going to discuss is this one:

# The Java libraries relevant to us.
# To come up with this list I added all jars in /lib then removed those where the warning count did not change.
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jsse.jar
-libraryjars <java.home>/lib/jce.jar

So I basically did the following:

$ cd "${JAVA_HOME}/jre/lib"
$ ls -a *.jar | sort
charsets.jar
deploy.jar
javaws.jar
jce.jar
jfr.jar
jfxswt.jar
jsse.jar
management-agent.jar
plugin.jar
resources.jar
rt.jar

And came up with these initial libraryjars:

-libraryjars <java.home>/lib/charsets.jar
-libraryjars <java.home>/lib/deploy.jar
-libraryjars <java.home>/lib/javaws.jar
-libraryjars <java.home>/lib/jce.jar
-libraryjars <java.home>/lib/jfr.jar
-libraryjars <java.home>/lib/jfxswt.jar
-libraryjars <java.home>/lib/jsse.jar
-libraryjars <java.home>/lib/management-agent.jar
-libraryjars <java.home>/lib/plugin.jar
-libraryjars <java.home>/lib/resources.jar
-libraryjars <java.home>/lib/rt.jar

Then I commented out the -dontwarn line at the beginning of proguard.conf and tried removing one at a time. If the warning count increased I added it back. Other wise I kept it in place.