Original title: How to build a lean JAR File with Gradle
In this step by step guide, we will show that Gradle is a good alternative to Maven for packaging java code into executable jar files. In order to keep the executable jar files „lean“, we will keep the dependent jar files outside of the jar in a separate folder.
Tools Used
- Maven 3.3.9
- JDK 1.8.0_101
- log4j 1.2.17 (downloaded automatically)
- Joda-time 2.5 (downloaded automatically)
- Git-2.8.4 with GNU bash 4.3.42(5)
Why using Gradle for a Maven Project?
In this blog post, we will show how Gradle can be used to create a executable/runnable jar. The task has been accomplished on this popular Mkyong blog post by using Maven. Why would we want to do the same task using Gradle?
By working with both, Maven and Gradle, I have found that:
- Gradle allows me to move any resource file to outside of the jar without the need of any additional Linux script or alike;
- Gradle allows me to easily create an executable/runnable jar for the JUnit tests, even if those are not separated into a separate project.
Moreover, while Maven is descriptive, Gradle is procedural in nature. With Maven, you describe the goal and you rely on Maven and its plugins to perform the steps you had in mind. Whereas with Gradle, you have explicit control on each step of the build process. Gradle is easy to understand for programmers and it gives them fine-grained control over the build process.
The Goal: a lean, executable JAR File
In the following step by step guide, we will create a lean executable jar file with all dependent libraries and resources.
Step 1 Download Hello World Maven Project of Mkyong
Download this hello world Maven project you can find on this popular HowTo page from Mkyong:
curl -OJ http://www.mkyong.com/wp-content/uploads/2012/11/maven-create-a-jar.zip unzip maven-create-a-jar.zip cd dateUtils
Logs:
$ curl -OJ http://www.mkyong.com/wp-content/uploads/2012/11/maven-create-a-jar.zip % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 7439 100 7439 0 0 23722 0 --:--:-- --:--:-- --:--:-- 24963 olive@LAPTOP-P5GHOHB7 /d/veits/eclipseWorkspaceRecent/MkYong/ttt $ unzip maven-create-a-jar.zip Archive: maven-create-a-jar.zip creating: dateUtils/ inflating: dateUtils/.classpath inflating: dateUtils/.DS_Store creating: __MACOSX/ creating: __MACOSX/dateUtils/ inflating: __MACOSX/dateUtils/._.DS_Store inflating: dateUtils/.project creating: dateUtils/.settings/ inflating: dateUtils/.settings/org.eclipse.jdt.core.prefs inflating: dateUtils/log4j.properties inflating: dateUtils/pom.xml creating: dateUtils/src/ creating: dateUtils/src/main/ creating: dateUtils/src/main/java/ creating: dateUtils/src/main/java/com/ creating: dateUtils/src/main/java/com/mkyong/ creating: dateUtils/src/main/java/com/mkyong/core/ creating: dateUtils/src/main/java/com/mkyong/core/utils/ inflating: dateUtils/src/main/java/com/mkyong/core/utils/App.java creating: dateUtils/src/main/resources/ inflating: dateUtils/src/main/resources/log4j.properties creating: dateUtils/src/test/ creating: dateUtils/src/test/java/ creating: dateUtils/src/test/java/com/ creating: dateUtils/src/test/java/com/mkyong/ creating: dateUtils/src/test/java/com/mkyong/core/ creating: dateUtils/src/test/java/com/mkyong/core/utils/ inflating: dateUtils/src/test/java/com/mkyong/core/utils/AppTest.java olive@LAPTOP-P5GHOHB7 /d/veits/eclipseWorkspaceRecent/MkYong/ttt $ cd dateUtils/ olive@LAPTOP-P5GHOHB7 /d/veits/eclipseWorkspaceRecent/MkYong/ttt/dateUtils $
Step 2 (optional): Create GIT Repository
In order to see, which files have been changed by which step, we can create a local GIT repository like follows
git init # echo "Converting Maven to Gradle" > Readme.txt git add . git commit -m "first commit"
After each step, you then can perform the last two commands with a different message, so you can always go back to a previous step, if you need to do so. If you have made changes in a step that you have not committed yet, you can go back easily to the last clean commit state by issuing the command
# go back to status of last commit: git stash -u
Warning: this will delete any new files you have created since the last commit.
Step 3 (required): Initialize Gradle
gradle init
This will automatically create a file build.gradle file from the Maven POM file with following content:
apply plugin: 'java' apply plugin: 'maven' group = 'com.mkyong.core.utils' version = '1.0-SNAPSHOT' description = """dateUtils""" sourceCompatibility = 1.7 targetCompatibility = 1.7 repositories { maven { url "http://repo.maven.apache.org/maven2" } } dependencies { compile group: 'joda-time', name: 'joda-time', version:'2.5' compile group: 'log4j', name: 'log4j', version:'1.2.17' testCompile group: 'junit', name: 'junit', version:'4.11' }
Step 4 (required): Gather Data
Since we are starting from a Maven project, which is prepared to create a runnable JAR via Maven already, we can extract the needed data from the POM.xml file:
MAINCLASS=`grep '<mainClass' pom.xml | cut -f2 -d">" | cut -f1 -d"<"`
Note: In cases with non-existing maven plugin, you need to set the MAINCLASS manually, e.g.
MAINCLASS=com.mkyong.core.utils.App
We also can define, where the dependency jars will be copied to later:
DEPENDENCY_JARS=dependency-jars
Logs:
$ MAINCLASS=`grep '<mainClass' pom.xml | cut -f2 -d">" | cut -f1 -d"<"` $ echo $MAINCLASS com.mkyong.core.utils.App $ DEPENDENCY_JARS=dependency-jars echo $DEPENDENCY_JARS dependency-jars
Step 5 (required): Prepare to copy dependent Jars
Here, we will add instructions to the build.gradle file, which dependency JAR files are to be copied into a directory accessible by the executable jar.
We will need to copy the jars, we depend on, to a folder the runnable jar will access later on. See e.g. this StackOverflow question on this topic.
cat << END >> build.gradle // copy dependency jars to build/libs/$DEPENDENCY_JARS task copyJarsToLib (type: Copy) { def toDir = "build/libs/$DEPENDENCY_JARS" // create directories, if not already done: file(toDir).mkdirs() // copy jars to lib folder: from configurations.compile into toDir } END
Step 6 (required): Prepare the Creation of an executable JAR File
In this step, we define in the build.gradle file, how to create an executable jar file.
cat << END >> build.gradle jar { // exclude log properties (recommended) exclude ("log4j.properties") // make jar executable: see http://stackoverflow.com/questions/21721119/creating-runnable-jar-with-gradle manifest { attributes ( 'Main-Class': '$MAINCLASS', // add classpath to Manifest; see http://stackoverflow.com/questions/30087427/add-classpath-in-manifest-file-of-jar-in-gradle "Class-Path": '. dependency-jars/' + configurations.compile.collect { it.getName() }.join(' dependency-jars/') ) } } END
Step 7 (required): Define build Dependencies
Up to now, a task copyJarsToLib was defined, but this task will not be executed, unless we tell Gradle to do so. In this step, we will specify that each time, a Jar is created, the copyJarsToLib task is to be performed beforehand. This can be done by telling Gradle that the jar goal depends on the copyJarsToLib task like follows:
cat << END >> build.gradle // always call copyJarsToLib when building jars: jar.dependsOn copyJarsToLib END
Step 8 (required): Build Project
Meanwhile, the build.gradle file should have following content:
apply plugin: 'java' apply plugin: 'maven' group = 'com.mkyong.core.utils' version = '1.0-SNAPSHOT' description = """dateUtils""" sourceCompatibility = 1.7 targetCompatibility = 1.7 repositories { maven { url "http://repo.maven.apache.org/maven2" } } dependencies { compile group: 'joda-time', name: 'joda-time', version:'2.5' compile group: 'log4j', name: 'log4j', version:'1.2.17' testCompile group: 'junit', name: 'junit', version:'4.11' } // copy dependency jars to build/libs/dependency-jars task copyJarsToLib (type: Copy) { def toDir = "build/libs/dependency-jars" // create directories, if not already done: file(toDir).mkdirs() // copy jars to lib folder: from configurations.compile into toDir } jar { // exclude log properties (recommended) exclude ("log4j.properties") // make jar executable: see http://stackoverflow.com/questions/21721119/creating-runnable-jar-with-gradle manifest { attributes ( 'Main-Class': 'com.mkyong.core.utils.App', // add classpath to Manifest; see http://stackoverflow.com/questions/30087427/add-classpath-in-manifest-file-of-jar-in-gradle "Class-Path": '. dependency-jars/' + configurations.compile.collect { it.getName() }.join(' dependency-jars/') ) } } // always call copyJarsToLib when building jars: jar.dependsOn copyJarsToLib
Now is the time to create the runnable jar file:
gradle build
Note: Be patient at this step: it can appear to be hanging for several minutes, if it is run the first time, while it is working in the background.
This will create the runnable jar on build/libs/dateUtils-1.0-SNAPSHOT.jar and will copy the dependency jars to build/libs/dependency-jars/
Logs:
$ gradle build :compileJava warning: [options] bootstrap class path not set in conjunction with -source 1.7 1 warning :processResources :classes :copyJarsToLib :jar :assemble :compileTestJava warning: [options] bootstrap class path not set in conjunction with -source 1.7 1 warning :processTestResources UP-TO-DATE :testClasses :test :check :build BUILD SUCCESSFUL Total time: 3.183 secs $ ls build/libs/ dateUtils-1.0-SNAPSHOT.jar dependency-jars $ ls build/libs/dependency-jars/ joda-time-2.5.jar log4j-1.2.17.jar
Step 9: Execute the JAR file
It is best practice to exclude the log4j.properties file from the runnable jar file, and place it outside of the jar file, since we want to be able to change logging levels at runtime. This is, why we had excluded the properties file in step 6. In order to avoid an error „No appenders could be found for logger“, we need not specify the location of the log4j.properties properly on the command-line.
Step 9.1 Execute JAR file on Linux
On a Linux system, we run the command like follows:
java -jar -Dlog4j.configuration=file:full_path_to_log4j.properties build/libs/dateUtils-1.0-SNAPSHOT.jar
Example:
$ java -jar -Dlog4j.configuration=file:/usr/home/me/dateUtils/log4j.properties build/libs/dateUtils-1.0-SNAPSHOT.jar 11:47:33,018 DEBUG App:18 - getLocalCurrentDate() is executed! 2016-11-14
Note: if the log4j.properties file is on the current directory on a Linux machine, we also can create a batch file
run.sh
with the content#!/usr/bin/env bash java -jar -Dlog4j.configuration=file:`pwd`/log4j.properties build/libs/dateUtils-1.0-SNAPSHOT.jarand run it via
bash run.sh
Step 9.1 Execute JAR file on Windows
In case of Windows in a CMD shell all paths need to be in Windows style:
java -jar -Dlog4j.configuration=file:D:\veits\eclipseWorkspaceRecent\MkYong\dateUtils\log4j.properties build\libs\dateUtils-1.0-SNAPSHOT.jar 11:45:30,007 DEBUG App:18 - getLocalCurrentDate() is executed! 2016-11-14
If we run the command on a Windows GNU bash shell, the syntax is kind of mixed: the path to the jar file is in Linux style while the path to the log properties file needs to be in Windows style (this is, how the Windows java.exe file expects the input of this option):
$ java -jar -Dlog4j.configuration=file:'D:\veits\eclipseWorkspaceRecent\MkYong\dateUtils\log4j.properties' build/libs/dateUtils-1.0-SNAPSHOT.jar 11:45:30,007 DEBUG App:18 - getLocalCurrentDate() is executed! 2016-11-14
Inverted commas have been used in order to avoid the necessity of escaped backslashes like D:\\veits\\eclipseWorkspaceRecent\\… needed on a Windows system.
Note: if the log4j.properties file is on the current directory on a Windows machine, we also can create a batch file
run.bat
with the contentjava -jar -Dlog4j.configuration=file:%cd%\log4j.properties build\libs\dateUtils-1.0-SNAPSHOT.jarTo run the bat file on GNU bash on Windows, just type
./run.bat
Yepp, that is it: the hello world executable file is printing the date to the console, just as it did in Mkyong’s blog post, where the executable file was created using Maven.
Download the source code from GIT.
Note: in the source code, you also will find a file named
prepare_build.gradle.sh
, which can be run on a bash shell and will replace the manual steps 4 to 7.
References
Next Steps
- create an even leaner jar with resource files kept outside of the executable jar. This opens the opportunity to changing resource files at runtime.
- create an executable jar file that will run the JUnit tests.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.