Gradle with the Java Plugin

In this article by Hubert Klein Ikkink, author of the book Gradle Effective Implementations Guide, Second Edition, we will discuss the Java plugin provides a lot of useful tasks and properties that we can use for building a Java application or library. If we follow the convention-over-configuration support of the plugin, we don't have to write a lot of code in our Gradle build file to use it. If we want to, we can still add extra configuration options to override the default conventions defined by the plugin.

(For more resources related to this topic, see here.)

Let's start with a new build file and use the Java plugin. We only have to apply the plugin for our build:

apply plugin: 'java'

That's it! Just by adding this simple line, we now have a lot of tasks that we can use to work with in our Java project. To see the tasks that have been added by the plugin, we run the tasks command on the command line and look at the output:

$ gradle tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

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. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

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

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

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

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.

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>

BUILD SUCCESSFUL

Total time: 0.849 secs

If we look at the list of tasks, we can see the number of tasks that are now available to us, which we didn't have before; all this is done just by adding a simple line to our build file.

We have several task groups with their own individual tasks, which can be used. We have tasks related to building source code and packaging in the Build tasks section. The javadoc task is used to generate Javadoc documentation, and is in the Documentation tasks section. The tasks for running tests and checking code quality are in the Verification tasks section. Finally, we have several rule-based tasks to build, upload, and clean artifacts or tasks in our Java project.

The tasks added by the Java plugin are the visible part of the newly added functionality to our project. However, the plugin also adds the so-called convention object to our project.

A convention object has several properties and methods, which are used by the tasks of the plugin. These properties and methods are added to our project and can be accessed like normal project properties and methods. So, with the convention object, we can not only look at the properties used by the tasks in the plugin, but we can also change the value of the properties to reconfigure certain tasks.

Using the Java plugin

To work with the Java plugin, we are first going to create a very simple Java source file. We can then use the plugin's tasks to build the source file. You can make this application as complex as you want, but in order to stay on topic, we will make this as simple as possible.

By applying the Java plugin, we must now follow some conventions for our project directory structure. To build the source code, our Java source files must be in the src/main/java directory, relative to the project directory. If we have non-Java source files that need to be included in the JAR file, we must place them in the src/main/resources directory. Our test source files need to be in the src/test/java directory and any non-Java source files required for testing can be placed in src/test/resources. These conventions can be changed if we want or need it, but it is a good idea to stick with them so that we don't have to write any extra code in our build file, which could lead to errors.

Our sample Java project that we will write is a Java class that uses an external property file to receive a welcome message. The source file with the name Sample.java is located in the src/main/java directory, as follows:

$ gradle tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

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. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

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

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

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

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.

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>

BUILD SUCCESSFUL

Total time: 0.849 secs

In the code, we use ResourceBundle.getBundle() to read our welcome message. The welcome message itself is defined in a properties file with the name messages.properties, which will go in the src/main/resources directory:

# File: src/main/resources/gradle/sample/messages.properties
welcome = Welcome to Gradle!

To compile the Java source file and process the properties file, we run the classes task. Note that the classes task has been added by the Java plugin. This is the so-called life cycle task in Gradle. The classes task is actually dependent on two other tasks—compileJava and processResources. We can see this task dependency when we run the tasks command with the --all command-line option:

$ gradle tasks --all
...
classes - Assembles main classes.
    compileJava - Compiles main Java source.
    processResources - Processes main resources.
...

Let's run the classes task from the command line:

$ gradle classes
:compileJava
:processResources
:classes

BUILD SUCCESSFUL

Total time: 1.08 secs

Here, we can see that compileJava and processResources tasks are executed because the classes task depends on these tasks. The compiled class file and properties file are now in the build/classes/main and build/resources/main directories. The build directory is the default directory that Gradle uses to build output files.

If we execute the classes task again, we will notice that the tasks support the incremental build feature of Gradle. As we haven't changed the Java source file or the properties file, and the output is still present, all the tasks can be skipped as they are up-to-date:

$ gradle classes
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.595 secs

To package our class file and properties file, we invoke the jar task. This task is also added by the Java plugin and depends on the classes task. This means that if we run the jar task, the classes task is also executed. Let's try and run the jar task, as follows:

$ gradle jar
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar

BUILD SUCCESSFUL

Total time: 0.585 secs

The default name of the resulting JAR file is the name of our project. So if our project is called sample, then the JAR file is called sample.jar. We can find the file in the build/libs directory. If we look at the contents of the JAR file, we see our compiled class file and the messages.properties file. Also, a manifest file is added automatically by the jar task:

$ jar tvf build/libs/sample.jar
    0 Wed Oct 21 15:29:36 CEST 2015 META-INF/
    25 Wed Oct 21 15:29:36 CEST 2015 META-INF/MANIFEST.MF
    0 Wed Oct 21 15:26:58 CEST 2015 gradle/
    0 Wed Oct 21 15:26:58 CEST 2015 gradle/sample/
  685 Wed Oct 21 15:26:58 CEST 2015 gradle/sample/Sample.class
    90 Wed Oct 21 15:26:58 CEST 2015 gradle/sample/messages.properties

We can also execute the assemble task to create the JAR file. The assemble task, another life cycle task, is dependent on the jar task and can be extended by other plugins. We could also add dependencies on other tasks that create packages for a project other than the JAR file, such as a WAR file or ZIP archive file:

$ gradle assemble
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.607 secs

To start again and clean all the generated output from the previous tasks, we can use the clean task. This task deletes the project build directory and all the generated files in this directory. So, if we execute the clean task from the command line, Gradle will delete the build directory:

$ gradle clean
:clean

BUILD SUCCESSFUL

Total time: 0.583 secs

Note that the Java plugin also added some rule-based tasks. One of them was clean<TaskName>. We can use this task to remove the output files of a specific task. The clean task deletes the complete build directory; but with clean<TaskName>, we only delete the files and directories created by the named task. For example, to clean the generated Java class files of the compileJava task, we execute the cleanCompileJava task. As this is a rule-based task, Gradle will determine that everything after clean must be a valid task in our project. The files and directories created by this task are then determined by Gradle and deleted:

$ gradle cleanCompileJava
:cleanCompileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.578 secs

 

Working with source sets

The Java plugin also adds a new concept to our project—source sets. A source set is a collection of source files that are compiled and executed together. The files can be Java source files or resource files. Source sets can be used to group files together with a certain meaning in our project, without having to create a separate project. For example, we can separate the location of source files that describe the API of our Java project in a source set, and run tasks that only apply to the files in this source set.

Without any configuration, we already have the main and test source sets, which are added by the Java plugin. For each source set, the plugin also adds the following three tasks: compile<SourceSet>Java, process<SourceSet>Resources, and <SourceSet>Classes. When the source set is named main, we don't have to provide the source set name when we execute a task. For example, compileJava applies to the main source test, but compileTestJava applies to the test source set.

Each source set also has some properties to access the directories and files that make up the source set. The following table shows the properties that we can access in a source set:

Source set property

Type

Description

java

org.gradle.api.file.SourceDirectorySet

These are the Java source files for this project. Only files with the.java extension are in this collection.

allJava

SourceDirectorySet

By default, this is the same as the java property, so it contains all the Java source files. Other plugins can add extra source files to this collection.

resources

SourceDirectorySet

These are all the resource files for this source set. This contains all the files in the resources source directory, excluding any files with the.java extension.

allSource

SourceDirectorySet

By default, this is the combination of the resources and Java properties. This includes all the source files of this source set, both resource and Java source files.

output

SourceSetOutput

These are the output files for the source files in the source set. This contains the compiled classes and processed resources.

java.srcDirs

Set<File>

These are the directories with Java source files.

resources.srcDirs

Set<File>

These are the directories with the resource files for this source set.

output.classesDir

File

This is the output directory with the compiled class files for the Java source files in this source set.

output.resourcesDir

File

This is the output directory with the processed resource files from the resources in this source set.

name

String

This is the read-only value with the name of the source set.

We can access these properties via the sourceSets property of our project. In the following example, we will create a new task to display values for several properties:

apply plugin: 'java'

task sourceSetJavaProperties << {
    sourceSets {
        main {
            println "java.srcDirs = ${java.srcDirs}"
            println "resources.srcDirs = ${resources.srcDirs}"
            println "java.files = ${java.files.name}"
            println "allJava.files = ${allJava.files.name}"
            println "resources.files = ${resources.files.name}"
            println "allSource.files = ${allSource.files.name}"
            println "output.classesDir = ${output.classesDir}"
            println "output.resourcesDir = ${output.resourcesDir}"
            println "output.files = ${output.files}"
        }
    }
}

When we run the sourceSetJavaProperties task, we get the following output:

$ gradle sourceSetJavaproperties
:sourceSetJavaProperties
java.srcDirs = [/gradle-book/Chapter4/Code_Files/sourcesets/src/main/java]
resources.srcDirs = [/gradle-book/Chapter4/Code_Files/sourcesets/src/main/resources]
java.files = [Sample.java]
allJava.files = [Sample.java]
resources.files = [messages.properties]
allSource.files = [messages.properties, Sample.java]
output.classesDir = /gradle-book/Chapter4/Code_Files/sourcesets/build/classes/main
output.resourcesDir = /gradle-book/Chapter4/Code_Files/sourcesets/build/resources/main
output.files = [/gradle-book/Chapter4/Code_Files/sourcesets/build/classes/main, /gradle-book/Chapter4/Code_Files/sourcesets/build/resources/main]

BUILD SUCCESSFUL

Total time: 0.594 secs

Creating a new source set

We can create our own source set in a project. A source set contains all the source files that are related to each other. In our example, we will add a new source set to include a Java interface. Our Sample class will then implement the interface; however, as we use a separate source set, we can use this later to create a separate JAR file with only the compiled interface class. We will name the source set api as the interface is actually the API of our example project, which we can share with other projects.

To define this source set, we only have to put the name in the sourceSets property of the project, as follows:

apply plugin: 'java'

sourceSets {
    api
}

Gradle will create three new tasks based on this source set—apiClasses, compileApiJava, and processApiResources. We can see these tasks after we execute the tasks command:

$ gradle tasks --all
...
Build tasks
-----------
apiClasses - Assembles api classes.
    compileApiJava - Compiles api Java source.
    processApiResources - Processes api resources.

We have created our Java interface in the src/api/java directory, which is the source directory for the Java source files for the api source set. The following code allows us to see the Java interface:

// File: src/api/java/gradle/sample/ReadWelcomeMessage.java
package gradle.sample;

/**
* Read welcome message from source and return value.
*/
public interface ReadWelcomeMessage {

    /**
    * @return Welcome message
    */
    String getWelcomeMessage();

}

To compile the source file, we can execute the compileApiJava or apiClasses task:

$ gradle apiClasses
:compileApiJava
:processApiResources UP-TO-DATE
:apiClasses

BUILD SUCCESSFUL

Total time: 0.595 secs

The source file is compiled in the build/classes/api directory.

We will now change the source code of our Sample class and implement the ReadWelcomeMessage interface, as shown in the following code:

// File: src/main/java/gradle/sample/Sample.java
package gradle.sample;

import java.util.ResourceBundle;

/**
* Read welcome message from external properties file
* <code>messages.properties</code>.
*/
public class Sample implements ReadWelcomeMessage {

    public Sample() {
    }

    /**
    * Get <code>messages.properties</code> file
    * and read the value for <em>welcome</em> key.
    *
    * @return Value for <em>welcome</em> key
    *         from <code>messages.properties</code>
    */
    public String getWelcomeMessage() {
        final ResourceBundle resourceBundle = ResourceBundle.getBundle("messages");
        final String message = resourceBundle.getString("welcome");
        return message;
    }

}

Next, we run the classes task to recompile our changed Java source file:

$ gradle classes
:compileJava
/gradle-book/Chapter4/src/main/java/gradle/sample/Sample.java:10: error: cannot find symbol
public class Sample implements ReadWelcomeMessage {
                              ^
  symbol: class ReadWelcomeMessage
1 error
:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.608 secs

We get a compilation error! The Java compiler cannot find the ReadWelcomeMessage interface. However, we just ran the apiClasses task and compiled the interface without errors.

To fix this, we must define a dependency between the classes and apiClasses tasks. The classes task is dependent on the apiClasses tasks. First, the interface must be compiled and then the class that implements the interface.

Next, we must add the output directory with the compiled interface class file to the compileClasspath property of the main source set. Once we have done this, we know for sure that the Java compiler for compiling the Sample class picks up the compiled class file.

To do this, we will change the build file and add the task dependency between the two tasks and the main source set configuration, as follows:

apply plugin: 'java'

sourceSets {
    api
    main {
        compileClasspath += files(api.output.classesDir)
    }
}

classes.dependsOn apiClasses

 

Now we can run the classes task again, without errors:

$ gradle classes
:compileApiJava
:processApiResources UP-TO-DATE
:apiClasses
:compileJava
:processResources
:classes

BUILD SUCCESSFUL

Total time: 0.648 secs

 

Custom configuration

If we use Gradle for an existing project, we might have a different directory structure than the default structure defined by Gradle, or it may be that we want to have a different structure for another reason. We can account for this by configuring the source sets and using different values for the source directories.

Consider that we have a project with the following source directory structure:

.
├── resources
│ ├── java
│ └── test
├── src
│ └── java
├── test
│ ├── integration
│ │ └── java
│ └── unit
│     └── java
└── tree.txt

We will need to reconfigure the main and test source sets, but we must also add a new integration-test source set. The following code reflects the directory structure for the source sets:

apply plugin: 'java'

sourceSets {
    main {
        java {
            srcDir 'src/java'
        }
        resources {
            srcDir 'resources/java'
        }
    }

    test {
        java {
            srcDir 'test/unit/java'
        }
        resources {
            srcDir 'resources/test'
        }
    }

    'integeration-test' {
        java {
            srcDir 'test/integration/java'
        }
        resources {
            srcDir 'resources/test'
        }
    }
}

Notice how we must put the name of the integration-test source set in quotes; this is because we use a hyphen in the name. Gradle then converts the name of the source set into integrationTest (without the hyphen and with a capital T). To compile, for example, the source files of the integration test source set, we use the compileIntegrationTestJava task.

Summary

In this article, we discussed the support for a Java project in Gradle. With a simple line needed to apply the Java plugin, we get masses of functionality, which we can use for our Java code.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Gradle Effective Implementations Guide - Second Edition

Explore Title
comments powered by Disqus