Gradle: Java Build Tools, Tasks, Dependencies, and Multi-Project Builds

Sean CoughlinSean Coughlin
13 min read

Introduction

In Java development, build tools are a crucial part of the workflow. They automate tasks such as compiling code, managing dependencies, packaging applications, and even running tests. As projects grow in size and complexity, manually handling these tasks becomes impractical, which is where build tools like Gradle come in. Gradle is a flexible, powerful build tool that can be customized to suit your project's needs, from simple apps to multi-module enterprise systems.

Before diving into Gradle, it's important to understand where it fits in the landscape of Java build tools. Java developers typically have several options when it comes to automating the build process, and each tool offers different strengths depending on the project and team requirements.


Java Build Tools

Java build tools have evolved over time, from simple automation systems to complex, highly configurable tools. Each tool has its own strengths and use cases. Here’s a quick overview of the three most common tools: Ant, Maven, and Gradle.

Build ToolDescriptionStrengthsWeaknesses
AntAn older, task-based build system using XML configuration. It offers flexibility but requires manual setup.Highly customizable and simple to start. Developers can control every step in the build process.XML configurations are verbose and require manual management of dependencies and build steps.
MavenA convention-over-configuration build system that uses XML and focuses heavily on dependency management and a standard project structure.Automatic dependency management and a standardized project structure that simplifies configuration.Less flexible than Ant. Projects need to fit into Maven’s conventions, which can be limiting.
GradleA flexible, script-based build tool that supports both Groovy and Kotlin for configuration. It combines the flexibility of Ant with the dependency management of Maven.Fast, flexible, and highly customizable with incremental builds. Supports both Groovy and Kotlin.More complex to learn initially due to its flexibility, especially when working with large projects.
  • Ant is the oldest of the three and is entirely task-based. It provides great control over every step in the build process but requires manual configuration of each task, making it verbose and less automated.

  • Maven, on the other hand, introduced the idea of "convention over configuration." It relies on a predefined project structure, making it easier to use, but less flexible. Maven’s strength lies in dependency management, allowing developers to easily manage libraries and external resources.

  • Gradle is the most recent build tool and is designed to combine the best of both worlds. It provides flexibility through scripting (either Groovy or Kotlin) and offers automated dependency management. Gradle’s incremental build capabilities make it faster, especially for large projects.

💡
While tools like Ant, Maven, and Gradle manage the entire build lifecycle—including dependency management, testing, and packaging—javac is only responsible for compiling Java source files into bytecode. javac requires manual handling of dependencies and additional tasks, making it less suited for complex projects compared to comprehensive build tools like Gradle.

In the next section, we will focus on getting started with Gradle, exploring its task system and how it handles dependencies.


Getting Started with Gradle

Gradle is highly flexible and can be used in a variety of project setups, from simple command-line apps to complex, multi-module systems. It uses a domain-specific language (DSL) for configuration, allowing you to define tasks and manage dependencies in a streamlined way. You can choose to use either Groovy or Kotlin as the DSL for your build scripts, depending on your preference.

Installing Gradle

To get started, you’ll first need to install Gradle. Here’s a quick guide on how to set it up:

  1. Install Gradle: You can download Gradle directly from the official site, or install it using a package manager like Homebrew on macOS:

     brew install gradle
    
  2. Verify Installation:

     gradle -v
    
  3. Gradle Wrapper: The Gradle Wrapper allows you to run Gradle builds without needing to install Gradle on every machine. It ensures that the correct version of Gradle is used consistently across all environments.

    • To set up the wrapper, run:

        gradle wrapper
      
    • This generates scripts (gradlew and gradlew.bat) that can be used to execute Gradle commands:

        ./gradlew build
      

Creating a Basic Gradle Project

Once Gradle is installed, you can create a new project using the gradle init command. This sets up a basic project structure.

gradle init --type java-application

This will generate the following files:

├── build.gradle
├── settings.gradle
├── src
│   └── main
│       └── java
│           └── App.java
│   └── test
│       └── java
│           └── AppTest.java
  • build.gradle: This is the main build script where you define tasks, dependencies, and configurations.

  • settings.gradle: Defines the project name and includes subprojects for multi-project builds.

Basic Project Structure in Gradle

Here’s a minimal example of what a build.gradle file might look like for a Java application:

Groovy (build.gradle):

plugins {
    id 'application'
}

group = 'com.example'
version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:31.0.1-jre'
    testImplementation 'junit:junit:4.13.2'
}

application {
    mainClass = 'com.example.App'
}

Kotlin (build.gradle.kts):

plugins {
    application
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.guava:guava:31.0.1-jre")
    testImplementation("junit:junit:4.13.2")
}

application {
    mainClass.set("com.example.App")
}

Here’s what’s happening in this build.gradle file:

  • Plugins: The application plugin is applied, which makes it easy to run the app.

  • Group/Version: Defines the group and version of the project, which is useful when packaging the app.

  • Repositories: Specifies where to look for dependencies. In this case, it’s using Maven Central.

  • Dependencies: Adds dependencies required by the project, including a library (guava) for the main code and a testing library (junit) for the test code.

  • Application: Specifies the main class that Gradle will use to run the application.

Running Gradle Tasks

Gradle projects are driven by tasks. You can use built-in tasks or define your own custom tasks. Some common built-in tasks include:

  • gradle build: Compiles and packages your project.

  • gradle run: Runs the application (if the application plugin is applied).

  • gradle clean: Deletes previous build outputs.

  • gradle test: Runs the tests in your project.

Example:

./gradlew build
./gradlew run

These commands will build and run your application based on the configuration defined in the build.gradle file. In the next section, we’ll look deeper into tasks and dependencies, which are central to Gradle's functionality.


Understanding Gradle Tasks

In Gradle, a task is a single unit of work, such as compiling code, running tests, packaging a JAR file, or deploying an application. Tasks are the building blocks of any Gradle project, and the entire build process is a series of tasks executed in a particular order. Gradle allows you to create your own custom tasks or use the many pre-built tasks that come with Gradle plugins.

Defining and Running Tasks

At its simplest, a task in Gradle can be defined in the build.gradle file using the following syntax:

Groovy (build.gradle):

task hello {
    doLast {
        println 'Hello, Gradle!'
    }
}

Kotlin (build.gradle.kts):

tasks.register("hello") {
    doLast {
        println("Hello, Gradle!")
    }
}

In this example, the task hello prints the message "Hello, Gradle!" when executed. You can run this task by invoking it from the command line:

./gradlew hello

Task Actions: doFirst and doLast

Tasks can have multiple actions. You can define actions that run at the beginning or the end of the task using doFirst and doLast blocks. These blocks allow you to insert actions before or after the main task action.

Example: Using doFirst and doLast

Groovy:

task setup {
    doFirst {
        println 'Setting up the environment...'
    }
    doLast {
        println 'Environment setup complete.'
    }
}

Kotlin:

tasks.register("setup") {
    doFirst {
        println("Setting up the environment...")
    }
    doLast {
        println("Environment setup complete.")
    }
}

This task will first print "Setting up the environment..." before executing the main task logic, and then print "Environment setup complete." after the task completes.

Task Dependencies

In many cases, tasks need to run in a specific order. For instance, you may want to clean the build directory before compiling the code. Gradle allows you to define task dependencies so that tasks are executed in a particular sequence.

Example: Defining Task Dependencies

Groovy:

task clean {
    doLast {
        println 'Cleaning build directory...'
    }
}

task build(dependsOn: clean) {
    doLast {
        println 'Building the project...'
    }
}

Kotlin:

tasks.register("clean") {
    doLast {
        println("Cleaning build directory...")
    }
}

tasks.register("build") {
    dependsOn("clean")
    doLast {
        println("Building the project...")
    }
}

In this example, the build task depends on the clean task. When you run ./gradlew build, Gradle will first execute the clean task before running the build task.

Built-in Tasks

Gradle comes with several built-in tasks that you’ll frequently use. Some of the most common include:

  • clean: Deletes previous build outputs.

  • build: Compiles the code and assembles the outputs (like JAR files).

  • test: Runs unit tests.

  • assemble: Packages your project into a deliverable format (such as a JAR or WAR file).

You can list all available tasks in your project by running:

./gradlew tasks

This command will display a list of all available tasks, including those provided by plugins and any custom tasks you've defined.

Custom Tasks: Use Cases

Tasks are highly flexible and can be customized to handle any kind of work. Here are a few common use cases for custom tasks:

  1. Running Shell Commands: You can use Gradle tasks to run external commands, such as shell scripts or command-line tools.

     task runScript(type: Exec) {
         commandLine 'bash', '-c', './my-script.sh'
     }
    
  2. Automating File Operations: Gradle tasks can manipulate files, like copying or moving files between directories.

     task copyFiles(type: Copy) {
         from 'src/files'
         into 'build/files'
     }
    
  3. Custom Build Steps: If your project requires additional steps in the build process, like generating documentation or compiling assets, you can create tasks for those actions.

     task generateDocs {
         doLast {
             println 'Generating project documentation...'
         }
     }
    
  4. Combining Multiple Tasks: You can create a task that aggregates several other tasks, allowing you to run multiple tasks in a single command.

     task fullBuild {
         dependsOn 'clean', 'build', 'test'
     }
    

By customizing tasks, you can automate a wide range of workflows and integrate them into your overall build process.


Multi-Project Builds in Gradle

When working on large-scale applications, it's common to break the project into multiple smaller, reusable modules. This is where multi-project builds in Gradle come in handy. They allow you to manage several subprojects within a single build while still keeping the overall structure modular and maintainable.

Think of it like a toolbox with separate compartments, each serving a unique purpose but all working together for the same project.

Why Use Multi-Project Builds?

Multi-project builds make sense when:

  • You have multiple modules that share common dependencies or functionality.

  • You want to separate concerns by having distinct components (like libraries, services, or UI) in their own modules.

  • You’re working in a team where different developers focus on different parts of the system.

For example, if you're building an e-commerce application, you might split it into modules like frontend, backend, and database, each with its own configuration and dependencies.

Setting Up a Multi-Project Build

In Gradle, you start by defining your project structure. Here’s what a simple multi-project setup might look like:

rootProject/
    ├── build.gradle
    ├── settings.gradle
    ├── app/
    │   └── build.gradle
    └── lib/
        └── build.gradle
  • The root project contains a settings.gradle file, which includes references to the subprojects.

  • Each subproject (e.g., app and lib) has its own build.gradle file.

In the settings.gradle file, you specify the included subprojects like this:

// settings.gradle
rootProject.name = 'MyMultiProject'
include 'app', 'lib'

Each subproject can have its own configuration, but they can also share common settings or dependencies through the root build.gradle file. For example, you might want both the app and lib modules to use the same version of a dependency:

// root build.gradle
subprojects {
    apply plugin: 'java'

    repositories {
        mavenCentral()
    }

    dependencies {
        testImplementation 'junit:junit:4.13.2'
    }
}

Managing Dependencies Between Projects

One of the major advantages of multi-project builds is the ability to define dependencies between subprojects. For example, the app project might rely on the lib project:

// app/build.gradle
dependencies {
    implementation project(':lib')
}

This way, you can share functionality between modules without duplicating code. Running a Gradle build will automatically compile the lib project before compiling the app project, ensuring that everything works smoothly together.

Multi-project builds help keep large projects organized and scalable. As your application grows, Gradle makes it easy to manage multiple modules while maintaining clear separation between them.


The Gradle Wrapper

The Gradle Wrapper is one of those features you might not fully appreciate until you’ve worked on a project with multiple developers—or worse, multiple machines with different environments. The wrapper solves a simple but significant problem: ensuring that everyone on your team, and every build system, uses the exact same version of Gradle, without requiring them to install it globally.

What Is the Gradle Wrapper?

The Gradle Wrapper is essentially a set of scripts and configuration files included in your project that automatically downloads and runs the correct version of Gradle. This means you don’t have to worry about whether your CI server or a colleague has Gradle installed—everything is self-contained.

Here’s how it works:

  • The wrapper consists of a few files: gradlew, gradlew.bat, and the gradle-wrapper.properties file, which specifies the Gradle version to use.

  • When you run ./gradlew build (on Unix-based systems) or gradlew.bat build (on Windows), the wrapper checks whether the specified version of Gradle is installed. If it’s not, it downloads that version automatically.

Why Use the Gradle Wrapper?

The main advantage of the wrapper is consistency. Imagine you’re working on a project with three developers. Developer A is using Gradle 7.0, Developer B is on Gradle 7.2, and Developer C hasn’t installed Gradle yet. Without the wrapper, this could lead to unpredictable behavior or errors if different Gradle versions handle certain tasks differently. The wrapper ensures that everyone uses the same version, avoiding these kinds of problems.

Another benefit is in Continuous Integration (CI) environments. CI servers can run Gradle builds without needing to install Gradle globally, which simplifies the setup. The Gradle Wrapper ensures the build runs in the correct environment every time.

Setting Up the Gradle Wrapper

You can add the wrapper to any project by running this command:

gradle wrapper

This will generate the necessary files (gradlew, gradlew.bat, and the gradle/wrapper directory). Once the wrapper is in place, all developers and systems can use ./gradlew or gradlew.bat instead of gradle.

If you need to upgrade the version of Gradle that the wrapper uses, simply modify the gradle-wrapper.properties file:

# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip

After updating the distributionUrl, running the wrapper will download and use the new version.

It may seem like a small detail, but the Gradle Wrapper is a key feature for maintaining consistency and simplicity across development environments. By eliminating the need to install Gradle manually, it removes a common source of friction, especially in teams and automated build systems. It ensures that no matter where your project is built—locally, on a server, or in the cloud—it will always use the correct version of Gradle.


Conclusion

Gradle is a versatile and powerful build tool that provides a comprehensive way to manage your Java projects, from simple applications to complex, multi-module systems. We've covered the basics of Gradle tasks, how to manage dependencies, and explored the flexibility of multi-project builds.

Whether you're automating simple build tasks or coordinating multiple modules in a large-scale project, Gradle’s flexibility and speed make it a valuable tool for Java developers. By mastering its core concepts, you’ll streamline your workflow and improve the maintainability of your projects.

10
Subscribe to my newsletter

Read articles from Sean Coughlin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sean Coughlin
Sean Coughlin

Software Engineer