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.
If you haven't done so already, go ahead and install Gradle using homebrew
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:
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 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:
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:
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:
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.
gradlewstands 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:
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 buildwill compile your project's code into a /build folder.
./gradlew runwill run the compiled code in your build folder.
./gradlew cleanwill purge that build folder.
./gradlew testwill 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
applicationplugins 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:
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:
Lastly, make sure you set your package name in your Main.java file:
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":
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:
All our tasks are gone! The
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
jcenter(), it doesn't really matter:
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:
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:
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
With that knowledge, here's a task which will run during every build and print "hello" followed by "world":
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:
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
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:
Nested projects in multi-project builds can each have their own standalone Gradle configurations with unique tasks and dependencies!