Switching from Maven to Gradle
Posted on Tue 10 June 2014 in Build Tools
During the development of my Chip 8 emulator in Java, I grew increasingly dissatisfied with Maven. While I liked the fact that dependency management was handled in a single spot, the verbose XML format of the POM combined with the quirks of the Maven lifecycle came to be a bit grating. However, for me, the tipping point came when I was trying to build a fat jar.
I want all of my program’s dependencies packaged in a single jar so that I can run a packaged version of the emulator without having to worry about installing anything else on the classpath. This usage case is exactly what the Apache Maven Shade plugin is for. It allows you to construct an "uber-jar" (fat jar), containing all of the other dependencies that your project requires. While it is quite easy to use, I kept getting a lot of warnings while building and packaging that were related to the shade plugin and the creation of the fat jar. To top that off, configuring the Shade plugin meant adding a lot of XML to the POM, most of which felt like a lot of unnecessary cruft (admittedly, I am a Maven novice, so your experiences will probably differ).
Enter Gradle
Gradle is a [Groovy](http://en.wikipedia.org/wiki/Groovy_(programming_language) based project automation tool. Like Maven, Gradle offers dependency management, and can hook into the Maven Central Repository and use Maven plugins. However, Gradle can do much, much more. For example, Gradle contains a wrapper that can be checked into source control. The wrapper can then download Gradle automatically, and build the project. This means that anyone can download and build the project without having to have Gradle installed and configured beforehand. This makes it very nice for things like continuous integration, where you really want to minimize the number of tools needed to build and test the project.
To me however, the main advantage of Gradle was the simple declarative language that it uses to define the project. Gradle does away with XML entirely - the result is a simple and concise looking configuration file. I highly value readability and maintainability, so Gradle felt like a breath of fresh air.
Initializing the Project
Given that my project is rather simple to begin with, I decided to write the new Gradle configuration by hand. For the sake of completeness however, Gradle does offer a command that will attempt to convert a Maven project into a Gradle project. To do that, you need a valid POM file. The command is as simple as:
gradle init --type pom
At this point I should mention that you can get a context sensitive list of tasks that Gradle can perform at any time by typing:
gradle tasks
Given that I wanted to start a new project from scratch to learn more about Gradle, I just created a simple default project definition to start. To do so, I did the following:
gradle init
This created the following files and directories automatically:
build.gradle
gradle/
gradlew
gradlew.bat
settings.gradle
These are the interesting artifacts that Gradle creates. In more detail:
build.gradle
is where all of the project definitions and dependencies are placed. I'll talk about that a little more down below.gradle
is a directory that contains the actual Gradle jar file.gradlew
andgradlew.bat
are the wrappers that are generated that can bootstrap Gradle on a new system. The first is a shell script for *NIX like systems, while the second is a batch file for Windows.settings.gradle
allows you to configure multi-project options.
All of the files that Gradle creates can be checked into source control, meaning that you can easily version control your dependencies and settings (just like with Maven's POM file).
Defining the Project
The build.gradle
file is where the main settings for the project go. To start with, I entered some simple housekeeping information:
apply plugin: 'java'
apply plugin: 'eclipse'
group = 'com.chip8java'
version = '1.0'
description = 'A Chip 8 emulator'
sourceCompatibility = 1.7
targetCompatibility = 1.7
The first two lines refer to plugins that define additional behaviours:
- The
java
plugin is used to define Java projects, and defines behaviours relating to building, testing, and packaging. For example, typing gradle build will build the project, while gradle test will run the unit tests. Note that the various tasks are dependent on one another (see the documentation), and are aware of each other. This means that if you haven't changed the source code, and you type gradle build, the compiler won't recompile your code. - The
eclipse
plugin is a very nice plugin for handling Eclipse integration. By typinggradle eclipse
, Gradle will build an Eclipse project that you can easily import into your IDE.
The remainder should be fairly self-explanatory.
Dependency Management
Next, I needed to define the dependencies for my project. I added the following to the build.gradle
file:
repositories {
mavenCentral()
}
dependencies {
compile group: 'commons-cli', name: 'commons-cli', version:'20040117.000000'
testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
testCompile group: 'junit', name: 'junit', version:'4.11'
}
buildscript {
repositories {
mavenCentral()
}
}
There are three blocks here to note:
- The
repositories
block simply tells Gradle to use the Maven Central Repository for downloading dependencies. - The
dependencies
block lists all of the dependencies for the project. Each dependency is further broken down into the following fields: -- The first field denotes what task requires the dependency. For example,compile
means that the dependency is needed at compile time. Similarly,testCompile
means that the dependency is needed during a compile in which unit tests are run. -- Thegroup
field relates to thegroupId
of the dependency you wish to add. In my example, I use Mockito for mocking various objects. The group ID for that library isorg.mockito
. -- Thename
field relates to theartifactId
of the dependency you wish to add. Again, in the case of Mockito, this ismockito-all
. -- Theversion
field relates to the version of the library you wish to use. For Mockito, I am using1.9.5
. - The
buildscript
block lists any repositories you want to use, plus any local dependencies you might also have (I will talk about this more in a future post).
Source Locations
I have my files in a slightly different layout for my emulator than what would be normal for a typical project. In the root of the project directory, my source files are located in the src
directory. Usually, src/main
would be where the main Java sources would go, while src/test
would contain unit tests. In my project structure however, I keep my unit tests under the root in the test
directory. In order to run unit tests then, I need to tell Gradle where these sources live. This is accomplished with:
sourceSets {
main {
java {
srcDir 'src'
}
}
test {
java {
srcDir 'test'
}
}
}
Essentially, sourceSets
indicates where my sources are located, with the main Java files located under src
. The test files are located in the test
directory instead. By doing this, I can run gradle test
from the command line, and Gradle knows where the unit test source files are located.
Building a Fat Jar
One of the last tasks I want to perform is to build a fat jar with all of my required dependencies stored in it. This is accomplished with the following:
jar {
manifest {
attributes 'Main-Class': 'com.chip8java.emulator.Emulator'
}
doFirst {
from (configurations.runtime.resolve().collect { it.isDirectory() ? it : zipTree(it) }) {
exclude 'META-INF/MANIFEST.MF'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
}
}
}
There are two blocks here that are important:
- The
manifest
block with theMain-Class
attribute tells Gradle to build a jar file with an entry point to the executable code being the classcom.chip8java.emulator.Emulator
. This effectively lets me call the jar withjava -jar /path/to/emulator.jar
without having to specify a specific class. - The
doFirst
block is a bit of a work around to repackage the jar files into the fat jar, while stripping out their manifests.
All Together
All of the above sections put together generate a single build.gradle
file. Now, to build the project, it is as simple as:
gradle build
To run the unit tests alone, if nothing has changed, you must tell Gradle to clean the unit test build and recompile just the tests:
gradle clean test
And finally, to build the jar:
gradle jar
Conclusion
Switching from Maven to Gradle was quite straightforward. The simplified syntax of the build.gradle
file, combined with powerful plugins means less cruft in your configuration files. Additionally, Gradle can use Maven plugins, and can automatically generate skeletons for development projects, convert existing Maven projects into Gradle based projects, and can easily generate the Eclipse project definitions for you.