Building Java Projects with Gradle

I've had a few strongly worded opinions about Java as a language in the past. Be that as it may, choosing a programming language is a luxury that many people don’t have; as long as enterprises exist, there will always be a need for Java developers. According to the 2019 StackOverflow developer survey, about 40% of developers are actively using Java in some way.

For those who work mostly in more "modern" programming languages, coming to Java in 2019 has numerous pain points. Installing dependencies by manually downloading and dropping jar files into your Java path is a humbling look into the past where package managers were non-existent. Luckily for us, there are tools like Gradle to help us bridge the gap between older programming languages and the processes we're used to.

Java Build Tools

Build tools are useful to Java developers in that they automate many processes associated with packaging a build which would otherwise be manual. Build tools not only compile a project's source code, but also download dependencies and run tests. Java build tools aren't exactly a sexy part of anybody's stack - it can be argued that they simply accomplish the things that we would expect a modern programming language to have natively.

Three build tools have historically dominated the scene: Apache Ant (released 2000), Apache Maven (released around 2004), and Gradle (released 2007). We won't waste time comparing these tools since Gradle is objectively better than the other options. If you're curious as to why, I'll humor you:

  • Ease-of-use: Gradle build scripts are written in Groovy (Ant and Maven are configured via XML files... enough said?)
  • Performance: Gradle creates builds faster by only building necessary changes from build-to-build, reusing build outputs from previous builds, and leveraging a running daemon process.
  • Debugging: Part of Gradle's build process is outputting very useful HTML logs detailing anything that went wrong.
  • Extensible: Gradle has a vast ecosystem of available plugins which open plenty of opportunities including support for Java, C++, or Python.

Installing Gradle

If you haven't done so already, go ahead and install Gradle using homebrew

$ brew install gradle

Installing Gradle via homebrew will automatically configure your path and all that nonsense. Verify that Gradle was installed correctly and you should be good to go:

$ gradle -v

------------------------------------------------------------
Gradle 5.5.1
------------------------------------------------------------

Initiating a Java Project with Gradle

Starting a Java project under normal circumstances is notably obnoxious when compared to other programming languages. Java's philosophy around namespaces results in ridiculously complex folder structures for any Java project, even a simple hello world app. Perhaps the most undersold feature of Gradle is the ability to generate cookie-cutter projects to get started.

Create a new directory using mkdir myProject (or whatever you want your project to be).  cd into that empty directory and run Gradle's init script:

$ gradle init

Gradle is going to prompt us with a few questions to get more context about what we're creating. The first question Gradle asks us to select a type of project:

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Creating a basic project only initiates a bare minimum project, which isn't particularly useful. Creating an application, on the other hand, will initiate a barebones folder structure for the language of your choice.

Select application (2) for your type of project. This should prompt three more multiple-choice questions:

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
Enter selection (default: Java) [1..4] 3

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit 4) [1..4] 1

Project name (default: java-gradle-tutorial): 

Source package (default: java-gradle-tutorial): 

Since we selected application as our project type, Gradle lets us chose from one of four programming languages to build our project (selecting basic wouldn't have prompted us with this).

Next, we're able to select either Groovy or Kontlin as the language in which our build scripts will be written in. Most people choose Groovy as it's an easy language to pick up, especially in this context.

Lastly, we're asked to select a test framework to run our unit tests in. JUnit 4 is the most popular, and it's good enough for me.

Check out our folder structure now:

/java-gradle-tutorial
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── hackersandslackers
    │   │           └── gradletutorial
    │   │               └── App.java
    │   └── resources
    └── test
        ├── java
        │   └── com
        │       └── hackersandslackers
        │           └── gradletutorial
        │               └── AppTest.java
        └── resources
/java-gradle-tutorial

Gradle has started our project by generating the barebones of our project as well as some Gradle-related configuration files. Not only does Gradle create our main module within /src, but it also creates the corresponding folder structure needed to test said module. If we were to create this project without Gradle, we would've had to create 10 directories!

Let's take a look at the files related to using Gradle in our project:

  • build.gradle: This is the file we'll use to configure our builds. This is where we specify things like dependencies to download, tasks to run, projects to import, etc. This is the file we'll do the most work in.
  • settings.gradle: Additional settings for our Gradle build.
  • gradlew: gradlew stands for "Gradle wrapper" and is the file we'll use to execute our builds (for example: $ ./gradlew build). When we initialize Gradle in a project we actually decouple our project's Gradle from our system's Gradle, which means we could hand off our source to somebody who doesn't have Gradle installed and they'd still be able to build our project.

Core Tasks for Java Projects

Before we get into any customization/configuration stuff, let's see what Gradle offers us out of the box! Typing ./gradlew tasks in your project directory lists which tasks you can run in your project, such as building or running your code. Because we initialized Gradle as a Java project, we have this specific list of tasks:

$ ./gradlew tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Application tasks
-----------------
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Distribution tasks
------------------
assembleDist - Assembles the main distributions
distTar - Bundles the project as a distribution.
distZip - Bundles the project as a distribution.
installDist - Installs the project as a distribution as-is.

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

No matter what you're building, you'll be using a handful of these tasks all the time. Here are a few of the most commonly used tasks.

  • ./gradlew build will compile your project's code into a /build folder.
  • ./gradlew run will run the compiled code in your build folder.
  • ./gradlew clean will purge that build folder.
  • ./gradlew test will execute unit tests without building or running your code again.

Each of these tasks can be chained together for convenience. For example, ./gradlew clean build run will create a new build of your Java project from scratch and then run said project.

These are just the core tasks that come standard with Java applications; we can script our own tasks as well. For that, we'll need to get deeper into configuring Gradle.

Configuring & Customizing Gradle

Gradle is much more useful to us when we can configure it to do things like download dependencies, import other projects, and execute specific parts of our code at runtime. All of this magic is contained in a build.gradle file, which is usually made up of 3 major parts:

  • Tasks are scripts Gradle can execute. The core tasks available to Java applications will suit most needs.
  • Dependencies are third-party .jar files to fetch from a repository and package with your project.
  • Plugins are Gradle plugins for additional functionality (we should have the java and application plugins active by default).
  • Projects are standalone applications being packaged together in a single build. Not all Gradle builds consist of multiple projects, but multi-project builds are a big plus of Gradle.

Before we even jump into those, we need to tell Gradle where the main class of our application lives.

Setting a Main Class

The first thing we should set in build.gradle is a variable named mainClassName. This is the path to our main Java class relative to the current file. Each time our build runs, Gradle will look for this class in src/java to start our application. It's important to note that mainClassName excepts the package name to be part of the path to our class, so at the very least we need our mainClassName to contain [PACKAGE_NAME].[CLASS_NAME].  Here's my main class name:

mainClassName="com.hackersandslackers.gradletutorial.App"

So my class is named Main and lives in a package called com.hackersandslackers.gradletutorial. If you recall the folder structure that Gradle created for us, the inside of src reflects this:

/src
└── main
    └── java
        └── com
            └── hackersandslackers
                └── gradletutorial
                    └── App.java
/src directory

Lastly, make sure you set your package name in your Main.java file:

package com.hackersandslackers.gradletutorial

public class App {
    public String getGreeting() {
        return "Hello world.";
    }

    public static void main(String[] args) {
        System.out.println(new App().getGreeting());
    }
}
Main.java

Gradle Plugins

Gradle provides us with a few "core" plugins out-of-the-box to build Java applications. Unsurprisingly, the two we have are named "Java" and "Application":

plugins {
    id 'java'
    id 'application'
}

apply plugin:'application'
build.gradle

To understand what these two plugins do, try deleting them from your build.gradle file and running the same ./gradlew tasks command we ran earlier:

$ ./gradlew tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

All our tasks are gone! The java and application plugins actually contain basically all the useful things we gained from initializing Gradle in the first place. As it turns out, Gradle is designed to be modular to the point where Gradle alone is nothing but a wrapper designed to serve as a medium for plugins and custom logic. You can add those plugins back now.

Installing Java Dependencies

If you've built Java projects before you're already aware of what a painfully manual process this normally is. Java has no inherent PyPi or npm equivalent: people used to download .jar files  and manually place them into their project directory like a bunch of absolute savages. Gradle does a decent job of making this process easier by handling it in build.gradle.

First, we need to set the remote repository we want to download Java packages from. People usually use either mavenCentral() or jcenter(), it doesn't really matter:

...

repositories {
    jcenter()
}
build.gradle

With our repositories block created, we can then specify which dependencies to download. Below I specify that I'd like my builds to include a MySQL connector, and use the JUnit testing library:

...

repositories {
    jcenter()
}

dependencies {
    implementation 'com.google.guava:guava:26.0-jre'
    compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.13'

    testImplementation 'junit:junit:4.12'
}
build.gradle

Gradle Tasks

A major part of build.gradle is scripting custom tasks. Tasks are snippets that we can run directly from the command line in our project directory ( as in ./gradlew [TASK_NAME] ). Here's a generic task that prints something:

task('hello') {
    doLast {
        println "hello"
    }
}
build.gradle

The value in parenthesis is the name of our task. If our task has no specified name, the task will run during every Gradle build by default.

doLast is a built-in action which means that the code in this block will be executed last.  If we wanted an action to occur first, we could use doFirst instead.

With that knowledge, here's a task which will run during every build and print "hello" followed by "world":

task {
    doFirst {
        println "hello"
    }
    doLast {
        println "world"
    }
}
build.gradle

We can also add some helpful metadata to our tasks for whoever is using the command line and wants to interact with our project. Here we add a group to our task and add some helpful description text:

tasks.register("hello") {
    group = 'Worthless tasks'
    description = 'An utterly useless task'

    doLast {
        println 'hello'
    }
}
build.gradle

Now when we run ./gradlew tasks in our project directory, they'll be able to see the following:

$ gradle tasks

Worthless tasks
-------------
hello - An utterly useless task

Multi-project Builds

The last thing worth looking at in Gradle is packaging multiple projects at once. In these types of setups, our top-level directory would contain multiple projects within it:

├── project1/
├── project2/
├── build.gradle
└── settings.gradle

With two project directories, we can now import each project into our top-level settings.gradle file:

rootProject.name = 'java-gradle-tutorial'

include 'project1', 'project2'
settings.gradle

Nested projects in multi-project builds can each have their own standalone Gradle configurations with unique tasks and dependencies!