Beginner's Guide to Gradle for Java Developers

By Adam McQuistan in Java  06/28/2019 Comment

Gradle For Java

Introduction

In this tutorial I will be describing how to get up and going quickly with Gradle demonstrating what I feel are the most important concepts and features needed as a Java developer just gettings started with Gradle. Gradle is a powerful, ultra flexible, build system popular in the Java (or more broadly JVM based) development ecosystem. Gradle has many amazing features such as dependency management, build configurations and tasks, with core as well as third party plugins and many other awesome bits.

Topics

Installing Gradle

First things first we need to get Gradle onto our system. I will give the short version below but, if you need more detailed instructions consult the official Gradle installation docs

Mac OSX
brew install gradle
Windows / Linux
  • download Gradle binary
  • select a directory for it to live in and extract it there
  • add directory path to the bin folder in the extracted binary to the system path variable, windows users need to add a GRADLE_HOME system variable pointing to this same bin folder as well

Initializing the Java Demo Project

With Gradle installed I can now use it to scaffold out a java application that I will be using to demonstrate using Gradle in action. This java accumulator application will be ran from command line and it will accept arguments of an operator (add) followed by two or more numbers to be added.

First I open a terminal and create an empty directory called java-accumulator then change directories into it. After that I initialize a new Gradle project as follows.

mkdir java-accumulator
cd java-accumulator
gradle init

Select type of project to generate:
  1: basic
  2: cpp-application
  3: cpp-library
  4: groovy-application
  5: groovy-library
  6: java-application
  7: java-library
  8: kotlin-application
  9: kotlin-library
  10: scala-library
Enter selection (default: basic) [1..10] 6

Select build script DSL:
  1: groovy
  2: kotlin
Enter selection (default: groovy) [1..2] 

Select test framework:
  1: junit
  2: testng
  3: spock
Enter selection (default: junit) [1..3] 

Project name (default: java-accumulator): 
Source package (default: java.accumulator): com.adammcquistan.javaaccumulator

BUILD SUCCESSFUL in 55s
2 actionable tasks: 2 executed

As shown above, Gradle's init program will prompt for various setup options. For the project type I select a java-application (item 6) then I take the defaults for the remaining until the last question when I specify the package as my name in a reverse domain with javaaccumulator appended to the end.

The result of running the Gradle init command and feeding it those inputs are a series of directories and files that collectively make up a project as shown below.

tree
.
├── bin
│   ├── main
│   │   └── com
│   │       └── adammcquistan
│   │           └── javaaccumulator
│   │               └── App.class
│   └── test
│       └── com
│           └── adammcquistan
│               └── javaaccumulator
│                   └── AppTest.class
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── adammcquistan
    │   │           └── javaaccumulator
    │   │               └── App.java
    │   └── resources
    └── test
        ├── java
        │   └── com
        │       └── adammcquistan
        │           └── javaaccumulator
        │               └── AppTest.java
        └── resources

The files under the src directory are simply .java source code files and those under bin are the .class JVM bytecode files. I will assume the reader understands what these are.

The remaining files have their own sections below describing their purpose.

Gradle Settings

One of the best ways to learn Gradle, well really anything programming related as far as I'm concerned, is to jump right into it. That being said I've ran this Gradle init command and it has in turned created quite a bit of output so, lets take a look at it.

Opening the settings.gradle file reveals this.

/*
 * This file was generated by the Gradle 'init' task.
 *
 * The settings file is used to specify which projects to include in your build.
 *
 * Detailed information about configuring a multi-project build in Gradle can be found
 * in the user manual at https://docs.gradle.org/5.4.1/userguide/multi_project_builds.html
 */

rootProject.name = 'java-accumulator'

The lone piece of configuration in this file tells the Gradle project what the project should be referred to as. By default this is the same as the parent directory containing this file but, as I mentioned earlier Gradle is incredibly flexible and as such you can change this by modifying the value within the quotes if desired.

Gradle Build File (build.gradle)

The build.gradle file is the heart of a Gradle project. This file is always located in the root directory of a project and contains a series of sections describing the project's type, structure, dependencies, and a section for a very important concept known as tasks.

Below is the build.gradle file which is heavily commented to describe its intents.

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java project to get you started.
 * For more details take a look at the Java Quickstart chapter in the Gradle
 * User Manual available at https://docs.gradle.org/5.4.1/userguide/tutorial_java_projects.html
 */

plugins {
    // Apply the java plugin to add support for Java
    id 'java'

    // Apply the application plugin to add support for building an application
    id 'application'
}

// legacy defition:
// apply plugin: 'java'

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

// Define the main class for the application
mainClassName = 'com.adammcquistan.javaaccumulator.App'

During setup after issuing the Gradle init command I specified the groovy language for the DSL which stands for Domain Specific Language. The DSL, specifically groovy in this example, is what I will use to declare the build.gradle sections and behavior. Although I will be giving some example groovy code (which is super intuitive and easy to follow) I will not be going into the details of the groovy language itself. Please consult the Groovy docs for specifics but, I suspect it will be easy to follow along with.

Plugins

In this default java-application build.gradle file the first section describes the plugins to be used for the project. Plugins are handy chunks of functionality that can be added to a Gradle project which are provided by Gradle itself as well as third party developers. Specifically a Gradle plugin is used to add one or more build tasks to a project.

In this example there are two plugins that have been included due to the selection of the java-application project type in step 1 of the Gradle init command. These plugins are specified by their ids which are java and application.

I added a comment below the plugins code block to show another way, the legacy way, of specifying plugins but, the newer closure style plugins { ... } syntax is the currently preferred method.

Gradle Tasks

A task is mearly a behavior of the build system that is defined programmatically using the DSL and useful for doing things such as compiling source code, bundling resources, running tests, and moving things around the file system.

To see the tasks that have been provided by the java and application plugins I can execute the following command which lists tasks by group.

./gradlew tasks --all

> Task :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.

Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'java-accumulator'.
components - Displays the components produced by root project 'java-accumulator'. [incubating]
dependencies - Displays all dependencies declared in root project 'java-accumulator'.
dependencyInsight - Displays the insight into a specific dependency in root project 'java-accumulator'.
dependentComponents - Displays the dependent components of components in root project 'java-accumulator'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'java-accumulator'. [incubating]
projects - Displays the sub-projects of root project 'java-accumulator'.
properties - Displays the properties of root project 'java-accumulator'.
tasks - Displays the tasks runnable from root project 'java-accumulator'.

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

Other tasks
-----------
compileJava - Compiles main Java source.
compileTestJava - Compiles test Java source.
prepareKotlinBuildScriptModel
processResources - Processes main resources.
processTestResources - Processes test resources.
startScripts - Creates OS specific scripts to run the project as a JVM application.

Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

To demonstrate running a task I will point out that the application plugin provides the run task which works in conjunction with the following line in build.gradle to identify which class has the application's main method entry point.

// Define the main class for the application
mainClassName = 'com.adammcquistan.javaaccumulator.App'

To get a better look at things let me open the App.java file and see what program will do when the run task is issued thus executing the program. As you can see below the App.java main method simply prints out a traditional hello world greeting message to the console.

/*
 * This Java source file was generated by the Gradle 'init' task.
 */
package com.adammcquistan.javaaccumulator;

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

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

To run the run task I issue the following command.

./gradlew run

> Task :run
Hello world.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

To define a task of my own I use the task keyword followed by the name of the task I'm defining and a chunk of groovy code surrounded by curly brackets. For example, if I wanted to print out a cheesy sign off line to follow the run command I could implement that as a task inside build.grade like so.

task signOff {
  group 'Custom'
  description 'Cheesy line to end run task with'
  dependsOn 'run'
  doLast {
    println "Thats all folks"
  }
}

run.finalizedBy signOff

I have named the task signOff, placed it in a new group named Custom and, I gave it a description. The behavior of the task is nested inside an inner code block named doLast to signify that it should be the last thing to be done. After the signOff task's definition I modify the application plugin's run task by setting the finalizedBy field equal to the signOff task.

Now when I run the `./gradlew tasks --all` command the new Custom group will appear and underneath it is the signOff task with its description. The output from running `./gradlew run` is shown below.

./gradlew run

> Task :run
Hello world.

> Task :signOff
Thats all folks

BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

Dependencies in Gradle

After spending any appreciable time developing software one quickly learns that the software you write rarely executes solely using the code you write. The code you write is much more likely (and actually better off) working in a symbiotic relationship with other libraries known as dependencies but, having dependencies often fits the characteristic of a double edged sword. One side of the sword works brilliantly for cutting down development effort enabling you to capitalize on the work of others. However, the other side of that sword lies a sharp, jagged, rusted out infectious hazard in that you now need to make sure you can keep a reference to that library, making sure it doesn't conflict with other libraries you are using and, further more, you need to keep tabs on the libraries that those libraries depend on (aka transitive dependencies) and so on and so on. This is yet another area where gradle shines.

Gradle provides a way to specify where to find dependencies which are often common repositories like Ivy and Maven as well as at specific urls and even locally on disk. Once you tell gradle where it should look to find your dependencies you define them using the GAV nomenclature which stands for groupId:artifactId:version.

For example, if I look in my build.gradle file I see that there is a repositories section that lists jcenter as the source of where it should look for dependencies as shown below.

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

If instead I wanted to use the Maven Central repository its as simple as this.

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    mavenCentral()
}

I could also list both which will lead to Gradle searching both.

Below I am showing the dependencies section of the build.gradle file which show two dependencies. One of these dependencies are the junit unittesting library which targets the testImplementation dependency target and the other is the guava library which targets the implementation dependency target.

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

At this point you may be asking ... What are dependency targets? Dependency targets are closely aligned with the build tasks and stages provided by Gradle plugins. For example in java development there are two common Gradle plugins used which are the application and library plugins. They provide a common set of dependency configuration targets such as api, implementation and testImplementation. Below I list these with some explanation.

  • implementation: this was recently added to replace the previously used compile configuration target. This configuration is to be used to specify dependencies that are required for the program to compile and to be used in the execution of the code but, not used by any client code interacting with the project.
  • api: this is relevent for java library projects where a dependency is needed to compile and execute the program but, in addition these dependencies are exposed to client code that utilizes the project. For example, say your project uses some dependency library called awesomesauce and some of the project's methods return types from the awesomesauce library like TangyBBQ which the client code interacts with.
  • testImplementation: these are dependencies that are needed to compile and execture test code

To see a full list of the dependency configuration types for the the Java application plugin check out the documentation

Similar to the `gradle tasks --all` command which displayed all tasks there also exists a `gradle dependencies` command that lists all the dependencies of a project for each dependency target as shown below.

./gradlew dependencies

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

annotationProcessor - Annotation processors and their dependencies for source set 'main'.
No dependencies

apiElements - API elements for main. (n)
No dependencies

archives - Configuration for archive artifacts.
No dependencies

compile - Dependencies for source set 'main' (deprecated, use 'implementation' instead).
No dependencies

compileClasspath - Compile classpath for source set 'main'.
\--- com.google.guava:guava:27.0.1-jre
     +--- com.google.guava:failureaccess:1.0.1
     +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     +--- com.google.code.findbugs:jsr305:3.0.2
     +--- org.checkerframework:checker-qual:2.5.2
     +--- com.google.errorprone:error_prone_annotations:2.2.0
     +--- com.google.j2objc:j2objc-annotations:1.1
     \--- org.codehaus.mojo:animal-sniffer-annotations:1.17

compileOnly - Compile only dependencies for source set 'main'.
No dependencies

default - Configuration for default artifacts.
\--- com.google.guava:guava:27.0.1-jre
     +--- com.google.guava:failureaccess:1.0.1
     +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     +--- com.google.code.findbugs:jsr305:3.0.2
     +--- org.checkerframework:checker-qual:2.5.2
     +--- com.google.errorprone:error_prone_annotations:2.2.0
     +--- com.google.j2objc:j2objc-annotations:1.1
     \--- org.codehaus.mojo:animal-sniffer-annotations:1.17

implementation - Implementation only dependencies for source set 'main'. (n)
\--- com.google.guava:guava:27.0.1-jre (n)
... omitting the remaining

To list the dependencies for a specific target simply add --configuration targetname as shown below.

./gradlew dependencies --configuration testImplementation

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

testImplementation - Implementation only dependencies for source set 'test'. (n)
\--- junit:junit:4.12 (n)

(n) - Not resolved (configuration is not meant to be resolved)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Task Dependencies

Now that I've covered tasks and library dependencies I want to describe another type of dependency which is the task dependency. Task dependencies refer to the concept that one task can depend on another task. This was briefly shown in the signOff example task I added earlier whereby I modified the run task by stating it should be finialized by signOff. So in this case the signOff task is dependent on the run task being completed first. The ability to chain tasks together like this is another increadabily useful feature of Gradle.

I'd now like to demonstrate adding in a new Gradle plugin named taskTree by updating the plugins config as follows.

plugins {
    // Apply the java plugin to add support for Java
    id 'java'

    // Apply the application plugin to add support for building an application
    id 'application'

    id "com.dorongold.task-tree" version "1.3.1"
}

This plugin adds a new task named 'taskTree' which will output a task dependency graph similar to the previously shown library dependency graph. For example, to look at the dependencies of the signOff task I added earlier I would do the following.

./gradlew signOff taskTree

> Task :taskTree

------------------------------------------------------------
Root project
------------------------------------------------------------

:signOff
\--- :run
     \--- :classes
          +--- :compileJava
          \--- :processResources

This shows that the signOff task depends on run and that run depends on compileJava as well as processResources.

Implementing the Java Accumulator Program

In order to demonstrate a bit more functionality I will modify the existing App.java main class to accept some parameters representing numbers to be added. This will provide me with a few use cases for extending Gradle's existing functionality (i) passing command line arguments into the run task and having those get passed into the java program, (ii) declaring and using an additional dependency and, (iii) adding junit tests which should then become part of the Gradle build system.

I start by adding a new class file to the src/main/java directory within the com.adammcquistan.javaaccumulator package named Accumulator.java. Within that file is a class for accepting a collection of string arguments that are converted to doubles, added up and, returned.

// Accumulator.java

package com.adammcquistan.javaaccumulator;

import java.util.ArrayList;
import java.util.List;

public class Accumulator {
  private List<Double> nums = new ArrayList<>();
  public Accumulator(String[] params) throws NumberFormatException {
    for (String param : params) {
      nums.add(Double.parseDouble(param));
    }
  }

  public double accumulate() {
    return nums.stream().reduce(0.0, (total, num) -> total + num);
  }
}

Over in the App.java class I hack out the getGreeting method and change up the main method to instantiate an instance of the Accumulator class passing it's constructor the arguments fed to the program, generate a simple string to display the inputs and output of the summation which are printed to the console.

// App.java

package com.adammcquistan.javaaccumulator;

public class App {
  public static void main(String[] args) {
    Accumulator acc = new Accumulator(args);
    var output = String.join(" + ", args) + " = " + acc.accumulate();
    System.out.println(output);
  }
}

I can still use the same Gradle run task to compile and run the project but, I now need to add a parameter to the run command of, --args="1 2 3 ...", which specifies the series of numbers to be summed up like so.

./gradlew run --args="1 2 3"
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :run
1 + 2 + 3 = 6.0

> Task :signOff
Thats all folks

BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 executed

Now I'd like to spice up that blah terminal output by adding in some color. I can do this by including an implementation dependency to the Gradle dependency configs with a GAV of 'org.fusesource.jansi:jansi:1.18' like so.

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'
    implementation 'org.fusesource.jansi:jansi:1.18'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

What this library does is allow me to ANSI escape encode console output. Below is the implementation of the jansi library in App.java

// App.java

package com.adammcquistan.javaaccumulator;
import static org.fusesource.jansi.Ansi.*;
import java.util.StringJoiner;
import org.fusesource.jansi.AnsiConsole;

public class App {
  public static void main(String[] args) {
    Accumulator acc = new Accumulator(args);

    AnsiConsole.systemInstall();
    StringJoiner sj = new StringJoiner(" @|green + |@");

    for (var operand : args) {
      sj.add("@|blue " + operand + "|@");
    }

    var output = sj.toString() + " @|green =|@ @|red " + acc.accumulate() + "|@";

    System.out.println(ansi().render(output));

    AnsiConsole.systemUninstall();
  }
}

Running this with `./gradlew run --args="1 2 3"` actually doesn't show any difference because Gradle is standing between the java program's output and what is seen in the terminal output. This is actually a good thing because it allows me to demonstrate another useful Gradle build task. If you recall back to the output from running `./gradlew tasks --all` there was a task named 'installDist' provided by the java application plugin. The 'installDist' task compiles my project's code into a jar file named after the project and creates some execution scripts that do some handy stuff like assembling a class path definition based off the specified dependencies and launches the project jar file.

 ./gradlew installDist

BUILD SUCCESSFUL in 0s
4 actionable tasks: 3 executed, 1 up-to-date

I now have a build directory with an install subdirectory containing the following items.

tree build/install/
.
└── java-accumulator
    ├── bin
    │   ├── java-accumulator
    │   └── java-accumulator.bat
    └── lib
        ├── animal-sniffer-annotations-1.17.jar
        ├── checker-qual-2.5.2.jar
        ├── error_prone_annotations-2.2.0.jar
        ├── failureaccess-1.0.1.jar
        ├── guava-27.0.1-jre.jar
        ├── j2objc-annotations-1.1.jar
        ├── jansi-1.18.jar
        ├── java-accumulator.jar
        ├── jsr305-3.0.2.jar
        └── listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar

The bin directory has two startup scripts, one for linux or mac osx and, another for a windows system. The lib directory contains the project jar 'java-accumulator.jar' as well as the dependencies the project requires.

To run the program and see the colored output I need to execute the java-accumulator script as follows.

Mac or linux system:

./build/install/java-accumulator/bin/java-accumulator 1 2 3

Windows:

build/install/java-accumulator/bin/java-accumulator.bat 1 2 3

jansi console output

Adding JUnit Unit Tests

Next up I'd like to add some unit tests for the java accumulator application to demonstrate how to work with them in a Gradle project. The Gradle init command again does a pretty good job of starting us off in the right direction by providing an example of the default hello world greeting applicaiton. However, since this example is no longer applicable I will be replacing the this with AccumulatorTest.java file which tests the output of the Accumulator#accumulate method as shown below.

package com.adammcquistan.javaaccumulator;

import org.junit.Test;
import static org.junit.Assert.*;

public class AccumulatorTest {
  @Test 
  public void testAccumulate() {
    Accumulator acc = new Accumulator(new String[]{ "1", "2", "3" });
    assertEquals(6.0, acc.accumulate(), 0.001);
  }

  @Test(expected = NumberFormatException.class)
  public void testInvalidParameters() {
    Accumulator acc = new Accumulator(new String[]{ "A", "B", "C" });
  }
}

To run the tests simply issue the test task like so.

./gradlew test

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date

So what just happened? Did they pass? I remember asking myself these questions the first time I ran the test task. Then I learned I can configure the test task in build.gradle to display the passed and failed events for each test like so.

test {
  testLogging {
    events 'PASSED', 'FAILED'
  }
}

Now when I run the test task I get the following.

./gradlew test

> Task :test

com.adammcquistan.javaaccumulator.AccumulatorTest > testAccumulate PASSED

com.adammcquistan.javaaccumulator.AccumulatorTest > testInvalidParameters PASSED

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

A common component of testing is the need for a test report showing the results of running the tests. Well Gradle has this covered also. After running the test task a new directory within the build directory named tests is created which contains a nicely formatted html report for the tests.

Gradle unittest report

Another things that I've found to be useful is the need to run individual tests. This is accomplished using test filtering. To do this simply add a --tests Classname.methodName or --tests Classname as an argument to the test task. I demonstrate this below by selecting only the testInvalidParameters test to be ran.

./gradlew test --tests AccumulatorTest.testInvalidParameters

> Task :test

com.adammcquistan.javaaccumulator.AccumulatorTest > testInvalidParameters PASSED

BUILD SUCCESSFUL in 1s
3 actionable tasks: 1 executed, 2 up-to-date

Creating a Display Subproject

An interesting but, again, very powerful feature of the Gradle build system is the ability to have a multi-project build. What a multi-project build is a project that contains one root build.gradle build and one or more directories beneath it that contain another build.gradle file. To demonstrate how this can be done I will separate out the code that displays the ANSI colored text and put that in its own console-display subproject.

I begin by creating directory at the same level as the build.gradle file named console-display then change directories into it and issue another Gradle init command but, this time selecting java library as the project type.

$ mkdir console-display
$ cd console-display/
$ gradle init

Select type of project to generate:
  1: basic
  2: cpp-application
  3: cpp-library
  4: groovy-application
  5: groovy-library
  6: java-application
  7: java-library
  8: kotlin-application
  9: kotlin-library
  10: scala-library
Enter selection (default: basic) [1..10] 7

Select build script DSL:
  1: groovy
  2: kotlin
Enter selection (default: groovy) [1..2] 1

Select test framework:
  1: junit
  2: testng
  3: spock
Enter selection (default: junit) [1..3] 1

Project name (default: console-display): 

Source package (default: console.display): com.adammcquistan.consoledisplay

BUILD SUCCESSFUL in 32s

Now inside console-display/src/main/java/com/adammcquistan/consoledisplay I replace the existing Library.java file with a ConsoleDisplay.java class which handles the encoding and displaying of colored text previously done in App.java.

package com.adammcquistan.consoledisplay;

import static org.fusesource.jansi.Ansi.*;
import java.util.StringJoiner;
import org.fusesource.jansi.AnsiConsole;

public class ConsoleDisplay {
  public static void displayANSIColor(String[] args, double result) {

    AnsiConsole.systemInstall();
    StringJoiner sj = new StringJoiner(" @|green + |@");

    for (var operand : args) {
      sj.add("@|blue " + operand + "|@");
    }

    var output = sj.toString() + " @|green =|@ @|red " + result + "|@";

    System.out.println(ansi().render(output));

    AnsiConsole.systemUninstall();
  }
}

Since this console-display subproject (aka, library) now relies on the jansi library I need to add it to the console-display subproject's build.gradle file and I go ahead and remove the dependencies for Google's guava and Apache's commons that Gradle init defaulted to. Below is the complete subproject build.gradle file.

// console-display build.gradle

plugins {
    // Apply the java-library plugin to add support for Java Library
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.fusesource.jansi:jansi:1.18'
    testImplementation 'junit:junit:4.12'
}

With the display now implemented in the ConsoleDisplay class I can update the App#main method to use it.

// App.java

package com.adammcquistan.javaaccumulator;

import com.adammcquistan.consoledisplay.ConsoleDisplay;

public class App {
  public static void main(String[] args) {
    Accumulator acc = new Accumulator(args);

    ConsoleDisplay.displayANSIColor(args, acc.accumulate());
  }
}

However, right now the code will not compile because I need to tell the root project that the console-display subproject exists. To do this I need to update the root project's settings.gradle file with an include to the new subproject as shown below.

/*
 * This file was generated by the Gradle 'init' task.
 *
 * The settings file is used to specify which projects to include in your build.
 *
 * Detailed information about configuring a multi-project build in Gradle can be found
 * in the user manual at https://docs.gradle.org/5.4.1/userguide/multi_project_builds.html
 */

rootProject.name = 'java-accumulator'
include 'console-display'

Then over in the dependencies section of the root build.gradle I replace the reference to jansi library to a reference to the subproject console-display like so.

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'
    implementation project(':console-display')

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

Now if I run the dependencies task looking specifically at the runtimeClasspath target I see that the root java-accumulator project has a dependencies for the subproject console-display which inturn has a transitive dependency on the jansi library.

./gradlew dependencies --configuration runtimeClasspath

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.google.guava:guava:27.0.1-jre
|    +--- com.google.guava:failureaccess:1.0.1
|    +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|    +--- com.google.code.findbugs:jsr305:3.0.2
|    +--- org.checkerframework:checker-qual:2.5.2
|    +--- com.google.errorprone:error_prone_annotations:2.2.0
|    +--- com.google.j2objc:j2objc-annotations:1.1
|    \--- org.codehaus.mojo:animal-sniffer-annotations:1.17
\--- project :console-display
     \--- org.fusesource.jansi:jansi:1.18

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Build Java Application as a Distributable JAR File

The last thing I want to show, which is super simple, is how to make a fledged build. The task for doing this is, suprise suprise, build.

./gradlew build

As shown previously, the build command piggy backs on the work of many other tasks, one of which is test. One way to prove this to yourself is to look in the build directory and you will recognize that the same reports directory with test result was generated the exact same way it was when the test task was ran. Probably the most important things that the build task does is make a distributions directory which has two archives in it, a zip and a tar ball. Inside these archives are the contents of the installDist as described previously but, they are now packaged nicely for distribution.

Conclusion

In this article I've tried to cover what I feel are the more important topics needed to get up and running using Gradle as a build system as a Java developer without overwhelming the reader with the vast depth afforded by Gradle. I've demonstrated how to do a basic java application project setup, described the various sections of the build.gradle file, shown how to add plugins and dependencies as well as how to both modify as well as run tasks and work with multi-project builds.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Java

theCodingInterface