CI/CD/GRADLE

What is this Build Tool called Gradle?

This Article explains the Gradle build tool from Wingardium Leviosa to Expecto Patronum

Pravinkumar Singh
9 min readJul 10, 2023

Greetings, readers! Before you begin reading this article, please remember that it's a comprehensive article about gradle. So grab your coffee or chai and let’s begin with understanding Gradle, one of the popular build tools for software developers like you.

What is Gradle?

Imagine you’re a wizard at Hogwarts, tasked with brewing a complex potion. You have a recipe (or in our software terms, ‘code’), magical and non-magical ingredients (libraries), and a cauldron (compiler) to blend it all.

Imagine having a helpful elf like Dobby, doing all the work, but more accurately, quicker and easier than anyone else. In the world of creating software, we’ve got something like that — we call it Gradle.

Gradle, built in 2007, is an open-source build automation tool known for its exceptional performance, versatility, and ability to adapt.

Why Gradle?

Here’s why it’s worth choosing over ANT and Maven :

  • Performance: Unlike Maven and Ant, Gradle has its performance through incremental builds, an intelligent feature where only the parts of your project that have changed since the last build are recompiled. This substantially speeds up the build process.
  • Flexibility and Versatility: While Ant offers flexibility but lacks convention, and Maven champions convention over configuration but lacks flexibility, Gradle cherishes both worlds — convention over configuration for simpler builds and flexibility for complex ones. Moreover, it’s one of the few tools that support multiple languages right out of the box, which includes JVM languages like Java, Groovy, and Scala, and even C, C++, and more.
  • Dependency Management: Unlike Ant, both Maven and Gradle support dependency management. However, Gradle upscales with its dynamic dependency resolution and cache management, which results in faster builds.

Installing Gradle!

Prerequisites

it requires only a Java Development Kit version 8 or higher to run. To check, run java -version. You should see something like this:

> java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

Gradle uses whatever JDK it finds in your path.

Installing with a package manager

  • Homebrew is for macOS. Command : brew install gradle
  • MacPorts is for macOS too. Command : sudo port install gradle

Verifying installation

On the console, run gradle -v to run Gradle and display the version, e.g. :

❯ gradle -v

------------------------------------------------------------
Gradle 8.2.1
------------------------------------------------------------

Gradle and a Spring Boot Application

Before diving into depth, let’s create a Spring Boot REST application using Gradle step-by-step guide

Step 1: Set up the project

  • Open a terminal window, Create a new directory for your project [mkdir demo] and navigate inside the project directory [cd demo]
  • Initialize Gradle in the project. Run gradle init.
  • In the dialogue, select options: Type of project to generate 2: application, implementation language 3: Java, build script DSL 2: Groovy, testing frameworks 4: JUnit Jupiter, and the project name 1: <your-choice> [demo]

This will set up the basic gradle project with the following structure:

├── gradle (1)
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew (2)
├── gradlew.bat (2)
├── settings.gradle (3)
└── app
├── build.gradle (4)
└── src
├── main
│ └── java (5)
│ └── demo
│ └── App.java
└── test
└── java (6)
└── demo
└── AppTest.java
  1. Generated folder for wrapper files
  2. Gradle wrapper start scripts
  3. Settings file to define build name and subprojects
  4. Build script of app project
  5. Default Java source folder
  6. Default Java test source folder

Then, update your build.gradle file to look like this:

plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
useJUnitPlatform()
}

Let’s delve into each component of the build.gradle file:

  1. plugins: Applies Gradle plugins to the project. 'java' adds Java compilation, testing, and packaging capabilities to the project. 'org.springframework.boot' and 'io.spring.dependency-management' are Spring Boot's specific plugins that provide Spring Boot's features to the Gradle project.
  2. group and version: Together, group and version are used to identify the project uniquely across all projects. In Maven terms, it creates the packaging coordinates along with the project name.
  3. sourceCompatibility: This property is used to define the Java version to compile the project.
  4. configurations: Configurations define the classpath to compile, test, and run code. compileOnly configuration is used to compile the main source, while extendsFrom annotationProcessor ensures annotation processors are found during compilation.
  5. repositories: Repositories are places where Gradle looks for your project’s dependencies. mavenCentral() specifies the Maven Central repository.
  6. dependencies: This block lists the external dependencies (libraries or other projects) used in the project. implementation and compileOnly configurations are used while compiling the main source. runtimeOnly is used when running the application, and testImplementation when compiling tests.
  7. test: The useJUnitPlatform() option is for enabling support of JUnit Jupiter/Platform tests in Gradle.

The implementation, compileOnly, runtimeOnly, and testImplementation configurations are used to declare dependencies. The different configurations instruct Gradle to consume the dependency in different ways within different phases of a build.

Step 2: Creating the REST API controller

Create a new file in src/main/java/com/example/demo/HelloController.java and fill it with:

package com.example.demo;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}
}

Here, we’re creating a REST API that responds to HTTP GET requests at “/” with a simple text message.

Step 3: Running the Application

Go back to the terminal, navigate to the root of your project and run ./gradlew bootRun. This will run your application. Then, visit http://localhost:8080 on your browser to see the result. If you want to package the project instead, use ./gradlew build - this will create a runnable .jar inside build/libs/.

Step 4: Creating an Executable Jar

Again, go back to the command line and navigate to the project’s root folder. Run the gradle build to create an executable JAR file. You will find the artifact in the build/libs directory.

Step 5: Running the Application Jar

Navigate to the build/libs directory, and use the command java -jar <JarFileName>.jar to run the jar file.

Tasks

Gradle builds are made up of one or more tasks. Think of a task as an atomic piece of work for the build, like compiling classes, packing them into a JAR file, or creating Javadoc.

Execute gradle tasks in the console within the project's directory, and you will witness a list of available tasks. The most commonly used task is gradle build, which compiles and tests your code, and packages it into a JAR file.

Build Lifecycle

There are three phases in a Gradle build lifecycle:

  1. Initialization: In this phase, Gradle determines which projects will take part in the build and creates a Project instance for each.
  2. Configuration: In this phase, the build scripts of all projects are executed. But, tasks are not executed in this phase, instead Gradle determines which tasks need to be run and in which order.
  3. Execution: The tasks determined in the configuration phase are executed in the order that Gradle has determined.

Understanding this build lifecycle provides insights on how Gradle executes tasks and helps in optimizing and managing your builds more efficiently.

Advanced Concepts

Custom Tasks

You can create your own tasks and also establish relationships between tasks in Gradle. Let’s create a simple task which creates a text file, and a second task that depends on the first one, copying the created file:

task createFile {
doLast {
file('src/main/resources/myfile.txt').text = 'This is my file!'
}
}

task copyFile(type: Copy) {
dependsOn createFile
from 'src/main/resources'
include 'myfile.txt'
into 'src/main/resources_copied'
}

In this example, executing gradle copyFile will create and copy the file because copyFile depends on createFile.

Defining Task Order:

In Gradle, you can also explicitly define the order in which tasks are run. The mustRunAfter and shouldRunAfter methods ensure one task runs after another, while finalizedBy specifies a task to run after a task has been completed, regardless if it failed:

task task1 {
doLast {
println 'This is task 1'
}
}

task task2 {
doLast {
println 'This is task 2'
}
mustRunAfter task1
}

task task3 {
doLast {
println 'This is task 3'
}
finalizedBy task1
}

In this example, if you execute tasks together (gradle task2 task1), task1 will always run before task2 due to mustRunAfter. Whenever task3 completes, task1 will run due to finalizedBy.

Using Ext properties:

Gradle allows you to define extra custom properties on projects, tasks and more. You can define these properties in build.gradle:

ext {
springBootVersion = '2.5.5'
javaVersion = '11'
}

sourceCompatibility = javaVersion
// later in your dependencies block
implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")

By grouping properties under ext, you can maintain easier control over variables that might need to be changed often, like versions, paths or feature flags.

Gradle Wrapper

The Gradle Wrapper ensures that everyone uses the same, centrally defined Gradle version for the build. It is particularly recommended for setting up Continuous Integration servers.

To use the Wrapper, you first need to generate some scripts with the wrapper task, like so:

gradle wrapper --gradle-version <version>

Replace <version> with your desired Gradle version. Afterwards, use ./gradlew or gradlew.bat instead of the gradle command - this makes sure each user executes your build with the same environment.

Multi-project Builds

Gradle has excellent support for multi-project builds. It allows for cross-project dependencies and task management, making it a powerful tool for building large, multi-module projects.

Let’s say you have a project with admin and user modules. You can create a structure like this:

my-project
|---- admin
| \---- build.gradle
|
|---- user
| \---- build.gradle
|
\---- build.gradle
\---- settings.gradle

In settings.gradle, define:

include 'admin', 'user'

Each sub-project (i.e. admin, user) should have a build.gradle file, containing the specifics of how to build that individual sub-project.

The root project’s build.gradle file can have common or shared configurations for all sub-projects:

allprojects {
group 'com.example'
version '1.0.0'
}
subprojects {
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13'
}
}

The allprojects block provides configurations for all the projects, including the root, while subprojects only provides configurations for the subprojects.

At this point, you can invoke a task across all projects by running gradle <taskName> from the root directory. If you want to run tasks for specific sub-projects, you can do this by navigating into the sub-project's directory and then running the gradle <taskName> command.

Integration with Docker

Gradle can be used to streamline Docker workflows. A variety of plugins are available, like the Palantir Docker plugin, which provides a Docker task type:

plugins {
id 'com.palantir.docker' version '0.25.0'
}

docker {
name "${project.group}/${bootJar.baseName}"
files bootJar.archivePath
buildArgs(['JAR_FILE': "${bootJar.archiveFile}"])
}

This creates a Docker image based on a Dockerfile in the project’s root directory. You can run gradle docker to create a Docker image from your Spring Boot JAR ('bootJar' is a task provided by the Spring Boot Gradle plugin).

Integration with Kubernetes

A Kubernetes Gradle Plugin has been developed by Google to manage deployments and releases in a Kubernetes environment. This can be particularly handy when dealing with A/B testing, canary releases, and more.

plugins {
id 'cd.connect.connect-gradle-k8s-deployer' version '0.2.0'
}

k8s {
deployer {
host = 'http://localhost:8001'
namespace = 'default'
}
}

You can run gradle k8sDeploy, and it will trigger a Kubernetes deployment.

Integrating Gradle with CI/CD:

Gradle integrates well with virtually every Continuous Integration/Continuous Deployment tool. From classic ones like Jenkins, where you might write a Jenkinsfile that delegates the build to Gradle:

pipeline {
agent any
stages {
stage('Build') {
steps {
sh './gradlew build'
}
}
}
}

Plugins and Custom Plugins

Gradle comes with numerous plugins, but you can also create custom plugins for tasks specific to your project. For instance, imagine having a standard set of lint rules or formatting procedures. You could create a custom plugin to check and enforce these each time the project builds.

Build Scans

Build scans are another powerful feature that provides insights into your build. By executing gradle build --scan, you can generate a comprehensive, searchable build scan in Gradle's cloud service. This scan provides an overview of warnings, errors, build timings, and even insights into performance and plugin use.

Profile Report

Similar to build scans, you can generate a local profile report by adding the --profile flag to your build command. Gradle returns a report detailing how much time each task took, helping you identify bottlenecks in your build process.

Gradle Daemon

The Gradle Daemon is a background process that Gradle utilizes to make builds faster. It starts automatically when you run a build and remains idle in the background for some time. It reuses information from previous builds and spends less time bootstrapping, which makes incremental builds significantly faster.

Build Cache

Gradle’s build cache reuses task output from any previous build, reducing unnecessary build operations. It’s particularly effective in a CI environment where clean builds are common. The build cache can be local (private cache per-user, typically on your workstation) or a remote shared build cache (exact reuse of task outputs between different machines).

And the list goes on. however, I have tried consolidating some useful and important concepts.

Try it for yourself, and watch your productivity improve. Remember, in Gradle, you are not just writing scripts — you are crafting finely tuned-automation.

I hope you liked it

Peace!

Similar reads: CI/CD

--

--