Extend your application capabilities (using third-party libraries)
Table of contents
It is not recommended to reinvent the wheel and in many cases a library already exists which does exactly what you need.
Import the third-party library (dependency)
The application also contains the Guava dependency.
dependencies { implementation 'com.google.guava:guava:28.2-jre' }
Classes that are part of this library (referred to as dependency as our project depends on this library) can now be used by our program.
Use the third-party library
One of the most popular classes with the Guava library is the
Preconditions
class. This class contains useful methods that can be used to check parameters to make sure that these adhere to the method contract. For example, a function may only accept positive numbers. ThePreconditions
class has methods to validate such parameters.package demo; import com.google.common.base.Preconditions; public class App { public String getGreeting() { return "Hello world."; } public static void main( String[] args ) { String greeting = Preconditions.checkNotNull( new App().getGreeting() ); System.out.println( greeting ); } }
The above is a naïve example use of the
Preconditions
class.Build the project
$ ./gradlew clean build
Run the project
$ java -jar build/libs/demo.jar
Note that this time the application will not run and will produce a
NoClassDefFoundError
error as shown next.Exception in thread "main" java.lang.NoClassDefFoundError: com/google/common/base/Preconditions at demo.App.main(App.java:11) Caused by: java.lang.ClassNotFoundException: com.google.common.base.Preconditions at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) ... 1 more
Our program is making use of a class that is not part of the Java standard library and it is not part of our code. Java has no way to locate this class and use it in our program. Not that our JAR file only contains two files. The Preconditions
class is not part of our JAR file.
The Classpath
Java searches for classes that are on the classpath. When using the -jar
option, all classes within that JAR file are automatically included as part of the classpath.
Instead of using the -jar
option, we can use the -cp
to set the claspath, similar to what is shown below
$ java -cp path/to/lib-a.jar:path/to/lib-b.jar path.to.mainclass
One or more JAR files can be included in a classpath separated by a colon (:
) on a Mac or semicolon (;
) on a Windows OS. We need to include two JAR files for our application to run.
Our application JAR file, found at
./build/libs/demo.jar
The above JAR is built and produced by Gradle
The Guava JAR file.
Gradle downloaded this library at
/Users/albertattard/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/28.2-jre/8ec9ed76528425762174f0011ce8f74ad845b756/guava-28.2-jre.jar
You can locate the Guava library, used by our application, using the following command
$ find -L ~/.gradle/caches -name "guava-28.2-jre.jar"
Note that if you have not yet built the application Gradle may have not yet downloaded the Guava library into the local cache.
Run the application using the
-cp
option$ java -cp ./build/libs/demo.jar:/Users/albertattard/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/28.2-jre/8ec9ed76528425762174f0011ce8f74ad845b756/guava-28.2-jre.jar demo.App
The above command needs to be updated according to the location where your Guava library is found.
The application should now be able to run and output
Hello world.
This is quite inconvenient as together with our application we need to also include the libraries this application needs. In this case, we only needed one library, but a typical program makes use many libraries. Together with the libraries our program uses, we also need to include the transitive libraries (libraries used by the libraries we are using and so on and so forth).
This can be a nightmare for large project as Java will only fail at runtime, when that particular library is required. A missing library can go unnoticed for some time, until the functionality that requires it is executed at runtime.
Running the application is not as simple as before, when we used the -jar
option. A better way is to make use of a fat JAR where all classes are packaged in one fat JAR.
Make a fat JAR
There are several ways how to create a fat JAR
Update the
jar
taskjar { manifest { attributes 'Main-Class': application.mainClassName } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } }
Build the project and unzip it
$ ./gradlew clean build $ rm -rf temp $ unzip build/libs/demo.jar -d temp
Notice that this time much more many files are included
Archive: build/libs/demo.jar creating: temp/META-INF/ inflating: temp/META-INF/MANIFEST.MF creating: temp/demo/ inflating: temp/demo/App.class creating: temp/META-INF/maven/ creating: temp/META-INF/maven/com.google.guava/ ... inflating: temp/com/google/j2objc/annotations/RetainedWith.class inflating: temp/com/google/j2objc/annotations/Weak.class inflating: temp/com/google/j2objc/annotations/WeakOuter.class
The JAR file now contains runtime dependencies (dependencies with scope
implementation
). Note that no JUnit related classes are included as this dependency has a test scope (testImplementation
).Create custom task
task fatJar(type: Jar) { group = 'Distribution' description = 'Create an executable fat JAR' archiveBaseName = 'fat-jar' manifest { attributes 'Main-Class': application.mainClassName } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } with jar }
List the available Gradle tasks (some tasks may not be visible but still be available).
$ ./gradlew tasks
Note that
fatJar
task under theDistribution tasks
section.... Distribution tasks ------------------ assembleDist - Assembles the main distributions distTar - Bundles the project as a distribution. distZip - Bundles the project as a distribution. fatJar - Create an executable fat JAR installDist - Installs the project as a distribution as-is. ...
Create the fat JAR using the custom task
$ ./gradlew fatJar
The JAR file:
build/libs/fat-jar.jar
will be created containing the application together with its runtime dependencies.Run the application.
$ java -jar build/libs/fat-jar.jar Hello world.
For more information about Gradle tasks, please refer to the tasks user guide.
Use a plugin
This is the preferred approach as we are reusing an existing Gradle plugin rather than adding a new Gradle task ourselves.
The shadowJar plugin is a very popular plugin.
Add the
shadowJar
plugin (do no remove the other plugins)plugins { id 'com.github.johnrengelman.shadow' version '5.2.0' }
List the available tasks
$ ./gradlew tasks
shadowJar
is one of the newly available tasks... Shadow tasks ------------ knows - Do you know who knows? shadowJar - Create a combined JAR of project and runtime dependencies ...
Create the fat JAR using the
shadowJar
task$ ./gradlew shadowJar
JAR file will be created
$ ls -l build/libs -rw-r--r-- demo-all.jar
The
shadowJar
task is also triggered with thebuild
task.$ ./gradlew clean build
In this case two JAR files will be produced
$ ls -l build/libs -rw-r--r-- demo-all.jar -rw-r--r-- demo.jar
The
demo.jar
file does not include dependencies, while thedemo-all.jar
file does.Run the application.
$ java -jar build/libs/demo-all.jar Hello world.
Irrespective from which approach we use, running the application as a fat JAR is simpler than running the application using the -cp
option. It also makes it simpler to distribute as all we need to share is one JAR file.