Chapter 1. Starting with Gradle
Declarative builds and convention over configuration
Support for Ant Tasks and Maven repositories
Gradle supports Ant Tasks and projects. We can import an Ant build and reuse all the tasks. However, we can also write Gradle tasks dependent on Ant Tasks. The integration also applies for properties, paths, and so on.
Maven and Ivy repositories are supported to publish or fetch dependencies. So, we can continue to use any repository infrastructure that we already have.
Incremental builds
With Gradle, we have incremental builds. This means the tasks in a build are only executed if necessary.
Gradle Wrapper
The Gradle Wrapper allows us to execute Gradle builds even if Gradle is not installed on a computer.
We can use the wrapper to enforce a certain Gradle version to be used so that the whole team is using the same version.
Writing our first build script
Gradle uses the concept of projects to define a related set of tasks. A Gradle build can have one or more projects.
A project has one or more tasks. Tasks are a unit of work that need to be executed by the build.
We use the gradle
command to run a build. Gradle will look for a file named build.gradle
in the current directory. This file is the build script for our project. We define our tasks that need to be executed in this build script file.
Default Gradle tasks
We can ask Gradle to show us the available tasks for our project.
gradle -q tasks
The properties
task is very useful to see the properties available for our project.
gradle -q properties
The dependencies
task will show dependencies (if any) for our project.
gradle -q dependencies
The projects
tasks will display subprojects (if any) for a root project.
gradle -q projects
Task name abbreviation
We can also abbreviate each word in a CamelCase task name. For example, our helloWorld
task name can be abbreviated to hW
.
Executing multiple tasks
Gradle executes the tasks in the same order as they are defined in the command line. Gradle will only execute a task once during the build. So even if we define the same task multiple times, it will only be executed once. This rule also applies when tasks have dependencies on other tasks. Gradle will optimize the task execution for us and we don’t have to worry about that.
Command-line options
Changing the build file and directory
We created our build file with the build.gradle
name. This is the default name for a build file. Gradle will look for a file with this name in the current directory to execute the build. However, we can change this with the --build-file
(or -b
) and --project-dir
(or -p
) command-line options.
Gradle daemon
We can reduce the build execution time if we don’t have to load JVM and Gradle classes and libraries each time we execute a build. The --daemon
command-line option starts a new Java process that will have all Gradle classes and libraries already loaded and then execute the build.
Chapter 2. Creating Gradle Build Scripts
Writing a build script
project.description = 'Simple project'
task simple << {
println 'Running simple task for project ' + project.description
}
Gradle creates a Project
object for us. The Project
object has several properties and methods and it is available in our build scripts. We can use the project
variable name to reference the Project
object, but we can also leave out this variable name to reference properties and methods of the Project
object. Gradle will automatically try to map properties and methods in the build script to the Project
object.
The following build script uses a different syntax, which is a bit more like Java, to get the same result:
project.setDescription('Simple project')
project.getTasks().create('simple') {
println 'Running simple task for project ' + project.description
}
Defining tasks
A task is made up of actions.
We can use the doFirst
and doLast
methods to add actions to our task, and we can use the left-shift operator (<<
) as a synonym for the doLast
method.
task first {
doFirst {
println 'Running first'
}
}
task second {
doLast { Task task ->
println "Running ${task.name}"
}
}
task third << { taskObject ->
println 'Running ' + taskObject.name
}
A closure is defined by enclosing the piece of code with curly brackets ({... }
). We can pass one or more parameters to the closures. If the closure has only one argument, an implicit parameter, it
, can be used to reference the parameter value.
task second {
doLast {
println "Running ${it.name}"
}
}
Defining actions with the Action interface
task first {
doFirst(
new Action() {
void execute(O task) {
println "Running ${task.name}"
}
}
)
}
Defining dependencies between tasks
task first << { task ->
println "Run ${task.name}"
}
task second << { task ->
println "Run ${task.name}"
}
second.dependsOn 'first'
Another way of defining the dependency between tasks is to set the dependsOn
property instead of using the dependsOn
method.
task third(dependsOn: 'second') << { task ->
println "Run ${task.name}"
}
Setting default tasks
To set the default task or tasks, we use the defaultTasks
method.
defaultTasks 'first', 'second'
Organizing tasks
Adding a description to tasks
task first(description: 'Base task') << {
println "I am first"
}
Grouping tasks together
def taskGroup = 'base'
task first(description: 'Base task', group: taskGroup) << {
println "I am first"
}
Adding tasks in other ways
Using task rules
These rules are very flexible and allow us to add tasks to our project based on several parameters and project properties.
task first(description: 'First task')
task second(description: 'Second task')
tasks.addRule("Pattern: desc<TaskName>: " + "show description of a task.") { taskName ->
if (taskName.startsWith('desc')) {
def targetTaskName = taskName - 'desc'
def targetTaskNameUncapitalize = targetTaskName[0].toLowerCase() + targetTaskName[1..-1]
def targetTask = project.tasks.findByName(targetTaskNameUncapitalize)
if (targetTask) {
task(taskName) << {
println "Description of task ${targetTask.name} " + " -> ${targetTask.description}"
}
}
}
}
Accessing tasks as project properties
Each task that we add is also available as a project
property, and we can reference this property like we can reference any other property in our build script.
task simple << { task ->
println "Running ${task.name}"
}
simple.description = 'Print task name'
simple.doLast {
println "Done"
}
project.simple.doFirst {
println "Start"
}
Adding additional properties to tasks
task simple << {
println "Hello ${message}"
}
simple.ext.message = 'world'
Skipping tasks
Using onlyIf predicates
Every task has an onlyIf
method that accepts a closure as an argument. The result of the closure must be true
or false.
If the task must be skipped, the result of the closure must be false
, otherwise the task is executed. The task
object is passed as a parameter to the closure. Gradle evaluates the closure just before the task is executed.
import static java.util.Calendar.*
task longrunning {
onlyIf { task ->
def now = Calendar.instance
def weekDay = now[DAY_OF_WEEK]
def weekDayInWeekend = weekDay in [SATURDAY, SUNDAY]
return weekDayInWeekend
}
doLast {
println "Do long running stuff"
}
}
Skipping tasks by throwing StopExecutionException
Another way to the skip execution of a task is to throw a StopExecutionException
exception. If such an exception is thrown, the build will stop the current task and continue with the next task.
Enabling and disabling tasks
Every task has an enabled
property. By default, the value of the property is true,
which means that the task is enabled and executed.
task listDirectory {
def dir = new File('assemble')
enabled = dir.exists()
doLast {
println "List directory contents: " + dir.listFiles().join(',')
}
}
Skipping from the command line
We can use the --exclude-tasks
(-x
) command-line option if we run the build.
Chapter 3. Working with Gradle Build Scripts
Project properties
The default properties that we can access in a Gradle build:
Name | Type | Default value |
---|---|---|
project |
Project |
The project instance. |
name |
String |
The name of the project directory. The name is read-only. |
path |
String |
The absolute path of the project. |
description |
String |
The description of the project. |
projectDir |
File |
The directory containing the build script. The value is read-only. |
buildDir |
File |
The directory with the build name in the directory, containing the build script. |
rootDir |
File |
The directory of the project at the root of a project structure. |
group |
Object |
Not specified. |
version |
Object |
Not specified. |
ant |
AntBuilder |
An AntBuilder instance. |
Defining custom properties in script
To add our own properties, we have to define them in an ext{}
script block in a build file. Prefixing the property name with ext.
is another way to set the value. To read the value of the property, we don’t have to use the ext.
prefix, we can simply refer to the name of the property. The property is automatically added to the internal project
property as well.
ext.customProperty = 'custom'
ext {
anotherCustomProperty = 'custom'
}
task showProperties {
ext {
customProperty = 'override'
}
doLast {
println customProperty
println project.ext.customProperty
println project.customProperty
}
}
Defining properties using an external file
We can also set the properties for our project in an external file. The file needs to be named gradle.properties
, and it should be a plain text file with the name of the property and its value on separate lines. We can place the file in the project directory or Gradle user home directory. The default Gradle user home directory is $USER_HOME/.gradle
.
Passing properties via the command line
Instead of defining the property directly in the build script or external file, we can use the -P
command-line option to add an extra property to a build. We can also use the -P
command-line option to set a value for an existing property. If we define a property using the -P
command-line option, we can override a property with the same name defined in the external gradle.properties
file.
Defining properties via system properties
We can also use Java system properties to define properties for our Gradle build. We use the -D
command-line option just like in a normal Java application. The name of the system property must start with org.gradle.project
, followed by the name of the property we want to set, and then by the value.
gradle -Dorg.gradle.project.version=2.0 -Dorg.gradle.project.customProperty=custom showProperties
Adding properties via environment variables
The environment variable name starts with ORG_GRADLE_PROJECT_
and is followed by the property name.
ORG_GRADLE_PROJECT_version=3.1 ORG_GRADLE_PROJECT_customProperty="Set by environment variable" gradle showProp
Using logging
The following table shows the log levels that are supported by Gradle:
Level | Used for |
---|---|
DEBUG |
Debug messages |
INFO |
Information messages |
LIFECYCLE |
Progress information messages |
WARNING |
Warning messages |
QUIET |
Import information messages |
ERROR |
Error messages |
Every Gradle build file and task has a logger
object. The logger
object is an instance of a Gradle-specific extension of the Simple Logging Facade for Java (SLF4J) Logger interface.
To use the logger
object in our Gradle build files, we only have to reference logger
and invoke the method for the logging level we want to use, or we can use the common log()
method and pass the log level as a parameter to this method.
We know that every Gradle project and task has a logger we can use. However, we can also explicitly create a logger
instance with the Logging
class.
class Simple {
private static final Logger logger = Logging.getLogger('Simple')
int square(int value) {
int square = value * value
logger.lifecycle "Calculate square for ${value} = ${square}"
return square
}
}
logger.lifecycle 'Running sample Gradle build.'
task useSimple {
doFirst {
logger.lifecycle 'Running useSimple'
}
doLast {
new Simple().square(3)
}
}
Controlling output
Every project and task has an instance of the org.gradle.api.logging.LoggingManager
class with the name logging. The LoggingManager
class has the methods captureStandardOutput()
and captureStandardError()
that we can use to set the log level for output and error messages. Remember that Gradle will, by default, use the QUIET
log level for output messages and ERROR
log level for error messages.
task redirectLogging {
doFirst {
println 'Start task redirectLogging'
}
doLast {
logging.captureStandardOutput LogLevel.INFO
println 'Finished task redirectLogging'
}
}
Using the Gradle Wrapper
Creating wrapper scripts
gradle wrapper
After the execution of the task, we have two script files (gradlew.bat
and gradlew
) in the root of our project directory. These scripts contain all the logic needed to run Gradle. If Gradle is not downloaded yet, the Gradle distribution will be downloaded and installed locally.
In the gradle/wrapper
directory, relative to our project directory, we find the gradle-wrapper.jar
and gradle-wrapper.properties
files. The gradle-wrapper.jar
file contains a couple of class files necessary to download and invoke Gradle. The gradle-wrapper.properties
file contains settings, such as the URL, to download Gradle. The gradle-wrapper.properties
file also contains the Gradle version number. If a new Gradle version is released, we only have to change the version in the gradle-wrapper.properties
file and the Gradle Wrapper will download the new version so that we can use it to build our project.
All the generated files are now part of our project. If we use a version control system, then we must add these files to the version control. Other people that check out our project can use the gradlew
scripts to execute tasks from the project. The specified Gradle version is downloaded and used to run the build file.
If we want to use another Gradle version, we can invoke the wrapper
task with the --gradle-version
option.
To specify a different download location for the Gradle installation file, we must use the --gradle-distribution-url
option of the wrapper
task.
Customizing the Gradle Wrapper
If we want to customize properties of the built-in wrapper
task, we must add a new task to our Gradle build file with the org.gradle.api.tasks.wrapper.Wrapper
type. We will not change the default wrapper
task, but create a new task with new settings that we want to apply. We need to use our new task to generate the Gradle Wrapper shell scripts and support files.
task createWrapper(type: Wrapper) {
gradleVersion = '2.12'
scriptFile = 'startGradle'
jarFile = "${projectDir}/gradle-bin/gradle-bootstrap.jar"
}
If we run the createWrapper
task, we get a Windows batch file and shell script and the Wrapper bootstrap JAR file with the properties file is stored in the gradle-bin
directory.
$> tree .
.
├── gradle-bin
│ ├── gradle-bootstrap.jar
│ └── gradle-bootstrap.properties
├── startGradle
├── startGradle.bat
└── build.gradle
Chapter 4. Using Gradle for Java Projects
Why plugins?
In Gradle, we can apply plugins to our project. A plugin basically adds extra functionalities such as tasks and properties to our project. By using a plugin, functionality is decoupled from the core Gradle build logic. We can write our own plugins, but Gradle also ships with plugins that are ready out of the box.
Getting started with the Java plugin
apply plugin: 'java'
Using the Java plugin
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.
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
.
If we execute the classes
task again, we will notice that the tasks support the incremental build feature of Gradle.
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.
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.
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.
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.
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.
Source set property | Type | Description |
---|---|---|
java |
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. |
Creating a new source set
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
.
The classes
task is dependent on the apiClasses
tasks. First, the interface must be compiled and then the class that implements the interface must be compiled .
Next, we must add the output directory with the compiled interface class file to the compileClasspath
property of the main
source set.
apply plugin: 'java'
sourceSets {
api
main {
compileClasspath += files(api.output.classesDir)
}
}
classes.dependsOn apiClasses
Custom configuration
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:
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'
}
}
}
Working with properties
Custom properties of a plugin are set in a Convention
object of the org.gradle.api.plugins.Convention
type. A Convention
object is used by a plugin to expose properties and methods that we can use in our project. The Convention
object of the plugin is added to the convention
property of a project. The convention
property of a Gradle project is a container for all the Convention
objects from the plugins.
We can access the properties from the plugin’s Convention
object directly as project properties or we can specify the complete path to the Convention
object of the plugin in order to get to a property or invoke a method.
task showConvention << {
println sourceSets.main.name
println project.sourceSets.main.name
println project.convention.plugins.java.sourceSets.main.name
}
Creating Javadoc documentation
To generate a Javadoc documentation, we must use the javadoc
task of the org.gradle.api.tasks.javadoc.Javadoc
type. The task generates a documentation for the Java source files in the main
source set. If we want to generate documentation for the source sets in our project, we must configure the javadoc
task or add an extra javadoc
task to our project.
Chapter 5. Dependency Management
Dependency configuration
Java has no real support for working with versioned libraries as dependencies. There are some open source solutions that deal with dependencies and allow us to express whether our Java code depends on lib-1.0.jar
or lib-2.0.jar
. The most popular are Maven and Apache Ivy. Maven is a complete build tool and has a mechanism for dependency management. Ivy is only about dependency management.
Both tools support repositories where versioned libraries are stored together with metadata about these libraries. A library can have dependencies on other libraries and is described in the metadata of the library. The metadata is described in the descriptor XML files. Ivy fully supports Maven descriptor files and repositories; it also adds some extra functionality. Therefore with Ivy, you get what you would with Maven, and then some more. This is why Gradle uses the Ivy API under the hood to perform dependency management. Gradle also adds some extra sugar on top of Ivy, so we can define and use dependencies in a very flexible way.
In a Gradle build file, we group dependencies together in a configuration. A configuration has a name and configurations can extend each other. With a configuration, we can make logical groups of dependencies.
Every Gradle build has a ConfigurationContainer
object. This object is accessible via the Project
property with the name containers
. We can use a closure to configure the container with Configuration
objects. Each Configuration
object has at least a name, but we can change more properties. We can set a resolution strategy, if a configuration has version conflicts with dependencies, or we can change the visibility of a configuration so that it will not be visible outside of our project. A Configuration
object also has a hierarchy. So we can extend from the existing Configuration
objects to inherit the settings.
configurations {
commonsLib {
description = 'Common libraries'
}
mainLib {
extendsFrom commonsLib
description = 'Main libraries'
}
}
println configurations['mainLib'].name
println configurations.commonsLib.name
Configuration | Extends | Used by task | Description |
---|---|---|---|
compile |
- | compileJava |
These are the dependencies needed at compile time to compile the source files |
runtime |
compile |
- | These are the dependencies for runtime of the application, but are not needed for compilation |
testCompile |
compile |
compileTestJava |
These are the dependencies to compile test source files |
testRuntime |
testCompile |
test |
These are all the dependencies needed to run the tests |
archives |
- | uploadArchives |
This contains artifacts, such as JAR files created by the project |
default |
runtime |
- | This is the default configuration that contains all runtime dependencies |
Repositories
We can declare several repository types in the Gradle build file. Gradle provides some preconfigured repositories, but it is also very easy to use a custom Maven or Ivy repository. We can also declare a simple filesystem repository to be used for resolving and finding dependencies.
Repository type | Description |
---|---|
Maven repository | This is the Maven layout repository on a remote computer or filesystem. |
Bintray JCenter repository | This is the preconfigured Maven layout repository to search for dependencies in the Bintray JCenter repository. This is a superset of the Maven central repository. |
Maven central repository | This is the preconfigured Maven layout repository to search for dependencies in the Maven central repository. |
Maven local repository | This is the preconfigured Maven repository that finds dependencies in the local Maven repository. |
Ivy repository | This is the Ivy repository that can be located on a local or remote computer. |
Flat directory repository | This is a simple repository on the local filesystem of the computer or a network share. |
Adding Maven repositories
repositories {
jcenter()
mavenCentral()
}
We can also add a custom repository that follows the Maven layout.
repositories {
maven {
name = 'Main Maven repository'
url = 'http://intranet/repo'
}
mavenRepo(name: 'Snapshot repository', url: 'http://intranet/snapshots')
}
Both methods configure a repository via a combination of a closure and method arguments. Sometimes we must access a Maven repository that stores the metadata in the descriptor XML files, but the actual JAR files are in a different location. To support this scenario, we must set the artifactUrls
property and assign the addresses of the servers that store the JAR files:
repositories {
maven {
url: 'http://intranet/mvn'
artifactUrls 'http://intranet/jars'
artifactUrls 'http://intranet/snapshot-jars'
}
}
Adding Ivy repositories
repositories {
ivy(url: 'http://intranet/ivy-repo', name: 'Our repository')
ivy {
url = 'http://intranet/ivy-snapshots'
}
}
Adding a local directory repository
Gradle will resolve files in the configured directory using the first match it finds with the following patterns:
[artifact]-[version].[ext]
[artifact]-[version]-[classifier].[ext]
[artifact].[ext]
[artifact]-[classifier].[ext]
repositories {
flatDir(dir: '../lib', name: 'libs directory')
flatDir {
dirs '../project-files', '/volumes/shared-libs'
name = 'All dependency directories'
}
}
Defining dependencies
We define dependencies in our build project with the dependencies{}
script block. We define a closure to pass to the dependencies{}
script block with the configuration of the dependency.
We can define different types of dependencies.
Dependency type | Method | Description |
---|---|---|
External module dependency | - | This is a dependency on an external module or library in a repository. |
Project dependency | project() |
This is a dependency on another Gradle project. |
File dependency | files() , fileTree() |
This is a dependency on a collection of files on the local computer. |
Client module dependency | module() |
This is a dependency on an external module, where the artifacts are stored in a repository, but the meta information about the module is in the build file. We can override meta information using this type of dependency. |
Gradle API dependency | gradleApi() |
This is a dependency on the Gradle API of the current Gradle version. We use this dependency when we develop Gradle plugins and tasks. |
Local Groovy dependency | localGroovy() |
This is a dependency on the Groovy libraries used by the current Gradle version. We use this dependency when we develop Gradle plugins and tasks. |
Using external module dependencies
apply plugin: 'java'
repositories {
jcenter()
}
ext {
springVersion = '4.2.3.RELEASE'
springGroup = 'org.springframework'
}
dependencies {
compile(group: springGroup, name: 'spring-core', version: springVersion)
runtime("$springGroup:spring-aop:$springVersion")
}
To download only the artifact of an external dependency and not the transitive dependencies, we can set the transitive
property for the dependency to false
.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile('org.slf4j:slf4j-simple:1.7.13') {
transitive = false
}
compile(group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.13', transitive: false)
}
We can also exclude some transitive dependencies with the exclude()
method. Gradle will look at the descriptor file of the module and exclude any dependencies that we have added with the exclude()
method.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile('org.slf4j:slf4j-simple:1.7.13') {
exclude 'org.slf4j:slf4j-api'
}
}
To get an artifact of only the external module dependency, we can use the artifact-only notation. We must also use this notation when a repository doesn’t have a module descriptor file and we want to get the artifact. We must add an @
symbol before the extension of the artifact. Gradle will not look at the module descriptor file, if available, when we use this notation:
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
runtime('org.slf4j:slf4j-simple:1.7.13@jar')
runtime(group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.13', ext: 'jar')
}
We can even set the transitive behavior on a complete configuration. Each configuration has a transitive
property.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile('org.slf4j:slf4j-simple:1.7.13')
}
configurations.compile.transitive = false
In a Maven repository, we can use classifiers for a dependency.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile('sample:simple:1.0:jdk16@jar')
compile(group: 'sample', name: 'simple', version: '1.0', classifier: 'jdk16')
}
The module descriptor of a module in a Maven repository can only have one artifact; but in an Ivy repository, we can define multiple artifacts for a single module. Each set of artifacts is grouped together in a configuration. The default configuration contains all the artifacts belonging to the module. If we don’t specify the configuration property when we define the dependency for an Ivy module, the default
configuration is used. We must specify the configuration
property if we want to use artifacts belonging to this specific configuration, as follows:
apply plugin: 'java'
repositories {
ivy {
url = 'http://intranet/custom'
ivyPatterns '[module]/[revision]/ivy.xml'
artifactPatterns '[module]/[revision]/[artifact](/Book/Content/Computer%20Science/Programming%20Language/Java/Gradle%20Effective%20Implementations%20Guide/../.[ext])'
}
}
dependencies {
testCompile group: 'sample', name: 'logging', version: '1.0', configuration: 'test'
testCompile('sample:logging:1.0') {
configuration = 'test'
}
}
Using project dependencies
Gradle projects can be dependent on each other. Gradle will look for a default dependency configuration in this project and use this dependency configuration. We can use the configuration
property to use different dependency configurations as a dependency for each project:
apply plugin: 'java'
dependencies {
compile(project(':projectA'))
compile(project(':projectB')) {
configuration = 'compile'
}
}
Using file dependencies
apply plugin: 'java'
dependencies {
compile files('spring-core.jar', 'spring-aop.jar')
compile fileTree(dir: 'deps', include: '*.jar')
}
Using client module dependencies
Normally, Gradle will use a descriptor XML file for dependencies found in the repository to see which artifacts and optional transitive dependencies need to be downloaded. However, these descriptor files can be misconfigured, and so, we may want to override the descriptors ourselves in order to ensure that the dependencies are correct.
apply plugin: 'java'
ext {
springGroup = 'org.springframework'
springRelease = '4.2.3.RELEASE'
}
dependencies {
compile module("$springGroup:spring-context:$springRelease") {
dependency("$springGroup:spring-aop:$springRelease") {
transitive = false
}
}
}
Using Gradle and Groovy dependencies
When we develop Grails plugins and tasks, we can define a dependency on the Gradle API and Groovy libraries used by the current Gradle version.
apply plugin: 'groovy'
dependencies {
compile gradleApi()
groovy localGroovy()
}
Setting dynamic versions
To set a minimum version number, we can use a special dynamic version syntax.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
compile(group: 'org.springframework', name: 'spring-core', version: '4.2.+')
}
We can also set a version range with a minimum and maximum version number.
Range | Description |
---|---|
[1.0, 2.0] |
All versions greater than or equal to 1.0 and lower than or equal to 2.0 |
[1.0, 2.0[ |
All versions greater than or equal to 1.0 and lower than 2.0 |
]1.0, 2.0] |
All versions greater than 1.0 and lower than or equal to 2.0 |
]1.0, 2.0[ |
All versions greater than 1.0 and lower than 2.0 |
[1.0, ) |
All versions greater than or equal to 1.0 |
]1.0, ) |
All versions greater than 1.0 |
(, 2.0] |
All versions lower than or equal to 2.0 |
(, 2.0[ |
All versions lower than 2.0 |
Resolving version conflicts
If one module has a dependency on sample:logging:1.0
and another on sample:logging:2.0
, Gradle will use the newest version number by default.
To change the default behavior, we set the resolutionStrategy
property of a dependency configuration.
apply plugin: 'java'
configurations.all {
resolutionStrategy {
failOnVersionConflict()
}
}
To force a certain version number to be used for all dependencies (even transitive dependencies), we can use the force()
method of resolutionStrategy
.
apply plugin: 'java'
configurations.all {
resolutionStrategy {
force('org.springframework:spring-core:4.2.3.RELEASE')
}
}
Adding optional ANT tasks
We can reuse existing Another Neat Tool (ANT) tasks in Gradle build files. Gradle uses Groovy’s AntBuilder for ANT integration. However, if we want to use an optional ANT task, we must do something extra as the optional tasks and their dependencies are not in the Gradle classpath. Luckily, we only have to define our dependencies for the optional task in the build.gradle
file and we can then define and use the optional ANT task.
configurations {
sshAntTask
}
repositories {
jcenter()
}
dependencies {
sshAntTask('org.apache.ant:ant-jsch:1.7.1', 'jsch:jsch:0.1.29')
}
task update {
description = 'Update files on remote server.'
def console = System.console()
def passphrase = console.readPassword('%s: ', 'Please enter the passphrase for the keyfile')
ant.taskdef(name: 'scp', classname: 'org.apache.tools.ant.taskdefs.optional.ssh.Scp', classpath: configurations.sshAntTask.asPath)
ant.scp(todir: 'mrhaki@servername:/home/mrhaki', keyfile: '${user.home}/.ssh/id_rsa', passphrase: passphrase as String, verbose: 'true') {
fileset(dir: 'work') {
include(name: '**/**')
}
}
}
Using dependency configurations as files
Each dependency configuration implements the FileCollection
interface of Gradle. This means that we can use a configuration reference if we need a list of files somewhere. The files that make up the resolved dependency configuration are then used.
repositories {
jcenter()
}
configurations {
springLibs
}
dependencies {
springLibs('org.springframework:spring-web:4.2.3.RELEASE')
}
task copyCompileDeps(type: Copy) {
from configurations.springLibs
into "$buildDir/compileLibs"
}
Chapter 6. Testing, Building, and Publishing Artifacts
Testing our projects
Gradle has a built-in support for running tests for our Java projects. When we add the Java plugin to our project, we will get new tasks to compile and run tests. We will also get the testCompile
and testRuntime
dependency configurations.
Using TestNG for testing
Gradle also supports tests that are written with the TestNG test framework. Gradle scans the test class path for all class files and checks whether they have specific JUnit or TestNG annotations.
For Gradle to use either JUnit or TestNG tests when we run the test
task, we invoke the useJUnit()
or useTestNG()
method, respectively, to force Gradle to use the correct testing framework. Gradle uses JUnit as a testing framework by default so that we don’t have to use the useJUnit()
method when we use JUnit or JUnit-compatible test frameworks to test our code.
Configuring the test process
The tests that are executed by the test
task run in a separate isolated JVM process. We can use several properties to control this process. We can set system properties and JVM arguments and we can configure the Java class that needs to be executed to run the tests.
To debug the tests, we can set the debug property of the test
task. Gradle will start the test process in the debug mode and listen on port 5005
for a debug process to attach to. This way we can run our tests and use an IDE debugger to step through the code.
By default, Gradle will fail the build if any test fails. If we want to change this setting, we must set the ignoreFailures
property to true
. Our build will not fail then, even if we have errors in our tests.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
testCompile('junit:junit:4.12')
}
test {
systemProperty 'sysProp', 'value'
jvmArgs '-Xms256m', '-Xmx512m'
debug = true
ignoreFailures = true
enableAssertions = true
}
Determining tests
To determine the files that are tests, Gradle will inspect the compiled class files. If a class or its methods have the @Test
annotation, Gradle will treat it as a JUnit or TestNG test. If the class extends TestCase
or GroovyTestCase
or is annotated with @RunWith
, Gradle will handle it as a JUnit test. Abstract classes are not inspected.
We can disable this automatic inspection with the scanForTestClasses
property of the test
task. If we set the property to false
, Gradle will use the implicit /Tests.class
and /*Test.class
include rules and the */Abstract*.class
exclude rule.
We can also set our own include and exclude rules to find tests. We can use the include()
method of the test
task to define our own rule for test classes. If we want to exclude certain class files, we can use the exclude()
method to define the exclude rules. Alternatively, we can use the includes
and excludes
properties.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
testCompile('junit:junit:4.12')
}
test {
scanForTestClasses = false
include('**/*Test.class', '**/*Spec.class')
exclude('**/Abstract*.class', '**/Run*.class')
}
Changing the test report directory
Besides the generated HTML report, we have XML files that are generated by the test
task with the results of the tests. These XML files are actually the input for the generated HTML report. There are a lot of tools available that can use the XML files generated by JUnit or TestNG and perform an analysis on them.
apply plugin: 'java'
repositories {
jcenter()
}
dependencies {
testCompile('junit:junit:4.12')
}
task testReport(type: TestReport) {
destinationDir = file("$buildDir/test-reports")
testResultDirs = files("$buildDir/test-results")
reportOn(test)
}
test.finalizedBy(testReport)
Running Java applications
Running an application from a project
apply plugin: 'java'
task runJava(dependsOn: classes, description: 'Run gradle.sample.SampleApp') << {
javaexec {
main = 'gradle.sample.SampleApp'
classpath sourceSets.main.runtimeClasspath
maxHeapSize = '128m'
systemProperty 'sysProp', 'notUsed'
jvmArgs '-client'
}
}
repositories {
jcenter()
}
Running an application as a task
apply plugin: 'java'
task runJava(type: JavaExec) {
dependsOn classes
description = 'Run gradle.sample.SampleApp'
main = 'gradle.sample.SampleApp'
classpath sourceSets.main.runtimeClasspath
systemProperty 'sysProp', 'notUsed'
jvmArgs '-client'
args 'mainMethodArgument', 'notUsed'
}
Running an application with the application plugin
apply plugin: 'application'
mainClassName = 'gradle.sample.SampleApp'
run {
systemProperty 'sysProp', 'notUsed'
jvmArgs '-client'
args 'mainMethodArgument', 'notUsed'
}
Publishing artifacts
apply plugin: 'java'
archivesBaseName = 'gradle-sample'
version = '1.0'
repositories {
flatDir {
name 'uploadRepository'
dirs 'upload'
}
}
uploadArchives {
repositories {
add project.repositories.uploadRepository
flatDir {
dirs 'libs'
}
}
}
Uploading our artifacts to a Maven repository
apply plugin: 'java'
apply plugin: 'maven'
archivesBaseName = 'gradle-sample'
group = 'gradle.sample'
version = '1.0'
uploadArchives {
repositories {
mavenDeployer {
repository(url: 'file:./maven')
}
}
}
Working with multiple artifacts
apply plugin: 'java'
archivesBaseName = 'gradle-sample'
version = '1.0'
task sourcesJar(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}
task docJar(type: Jar, dependsOn: javadoc) {
classifier = 'docs'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives docJar
}
uploadArchives {
repositories {
flatDir {
dirs 'upload'
}
}
}
Packaging Java Enterprise Edition applications
Creating a WAR file
To create a WAR file, we can add a new task of the War
type to our Java project. The properties and methods of the War
task are the same as for the other archive tasks, such as Jar
. In fact, the War
task extends the Jar
task.
The War
task has an extra webInf()
method to define a source directory for the WEB-INF
directory in a WAR file. The webXml
property can be used to reference a web.xml
file that needs to be copied to the WAR file. This is just another way to include a web.xml
file, we can also place the web.xml
file in the WEB-INF
directory of the root source directory that we defined for the WAR file.
With the classpath()
method, we can define a dependency configuration or directory with libraries or class files that we want copied to our WAR file. If the file is a JAR or ZIP file, it is copied to the WEB-INF/lib
directory and other files are copied in the WEB-INF/classes
directory.
apply plugin: 'java'
version = '1.0'
task war(type: War) {
dependsOn classes
from 'src/main/webapp'
webInf {
from 'src/main/webInf'
}
classpath sourceSets.main.runtimeClasspath
classpath fileTree('libs')
webXml = file('')
baseName = 'gradle-webapp'
}
assemble.dependsOn war
Chapter 7. Multi-project Builds
Working with multi-project builds
Executing tasks by project path
Gradle takes a couple of steps to determine whether a project must be executed as a single or multi-project build, as follows:
- First, Gradle looks for a
settings.gradle
file in a directory with the namemaster
at the same level as the current directory. - If
settings.gradle
is not found, the parent directories of the current directory are searched for asettings.gradle
file. - If
settings.gradle
is still not found, the project is executed as a single-project build. - If a
settings.gradle
file is found, and the current project is part of the multi-project definition, the project is executed as part of the multi-project build. Otherwise, the project is executed as a single-project build.
We can force Gradle to not look for a settings.gradle
file in the parent directories with the --no-search-upward
(or -u
) command-line argument.
Using a flat layout
includeFlat('tree', 'flower')
Ways of defining projects
We can define all project tasks and properties in the root build.gradle
file. We can use this to define the common functionality for all projects in a single place.
task printInfo << {
println "This is ${project.name}"
}
project(':flower') {
task printInfo << {
println "This is ${project.name}"
}
}
project(':tree') {
task printInfo << {
println "This is ${project.name}"
}
}
Gradle also has the allprojects{}
script block to apply project tasks and properties to all projects that are part of the multi-project build.
allprojects {
task printInfo << {
println "This is ${project.name}"
}
}
If we only want to configure the tree
and flower
subprojects, we must use the subprojects{}
script block.
subprojects {
task printInfo << {
println "This is ${project.name}"
}
}
Filtering projects
allprojects {
task printInfo << {
println "This is ${project.name}"
}
}
ext {
projectsWithF = allprojects.findAll { project ->
project.name.startsWith('f')
}
}
configure(projectsWithF) {
printInfo << {
println 'Smells nice'
}
}
Defining task dependencies between projects
allprojects {
task printInfo << {
println "This is ${project.name}"
}
}
project(':flower') {
printInfo.dependsOn(':tree:printInfo')
}
Working with Java multi-project builds
.
├── build.gradle
├── common
│ └── src
│ └── main
│ └── java
│ └── sample
│ └── gradle
│ └── util
│ └── Logger.java
├── services
│ └── sample
│ └── src
│ ├── main
│ │ └── java
│ │ └── sample
│ │ └── gradle
│ │ ├── api
│ │ │ └── SampleService.java
│ │ └── impl
│ │ └── SampleImpl.java
│ └── test
│ └── java
│ └── sample
│ └── gradle
│ └── impl
│ └── SampleTest.java
├── settings.gradle
└── web
└── src
└── main
├── java
│ └── gradle
│ └── sample
│ └── web
│ └── SampleServlet.java
└── webapp
└── WEB-INF
└── web.xml
subprojects {
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:4.8.12'
}
}
project(':services:sample') {
dependencies {
compile project(':common')
}
}
project(':web') {
apply plugin: 'war'
dependencies {
compile project(':services:sample')
compile 'javax.servlet:servlet-api:2.5'
}
}
Chapter 8. Mixed Languages
Using the Groovy plugin
To use Groovy sources in our project, we can apply the Groovy plugin. The Groovy plugin makes it possible to compile Groovy source files to class files. The project can contain both Java and Groovy source files. The compiler that Gradle uses is a joint compiler that can compile Java and Groovy source files.
The plugin also adds new tasks to our build. To compile the Groovy source files, we can invoke the compileGroovy
task. Test sources written in Groovy can be compiled with the compileTestGroovy
task.
Note that we also got all the tasks from the Java plugin. This is because the Groovy plugin automatically includes the Java plugin. So, even though we only defined the Groovy plugin in our build file, the Java plugin is applied as well.
The plugin adds the groovy configuration. The Groovy compiler uses this configuration. Therefore, to compile Groovy source files in our project, we must set a dependency on the compile
configuration.
To compile Groovy source files, we must add a dependency with the Groovy library that we want to use to the compile
configuration. We might expect that Gradle will use the Groovy version that is used by Gradle, but the compilation task is independent of the Groovy version used by Gradle. We have to define the Groovy library ourselves.
When we want to use the Groovy libraries shipped with Gradle, we can use the localGroovy()
special dependency.
The default source directory for Groovy source files is src/main/groovy
.
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile group: 'org.codehaus.groovy', name: 'groovy', version: '2.4.5'
}
The Groovy plugin also adds new source set properties.
Property name | Type | Description |
---|---|---|
groovy |
org.gradle.api.file.SourceDirectorySet |
These are the Groovy source files for this project. This contains both .java and .groovy These are the Groovy source files for this project. This contains both .java and .groovy source files if they are in the groovy directory. |
groovy.srcDirs |
java.util.Set<java.io.File> |
These are the directories with the Groovy source files. They can also contain Java source files for joint compilation. |
allGroovy |
org.gradle.api.file.FileTree |
These consist of only the Groovy source files. All files with extension .groovy are part of this collection. |
When our Java code uses Groovy classes, and vice versa, we can use the joint compilation feature. We must make sure that both Java and Groovy source files are in the src/main/groovy
directory.
Using the Scala plugin
We must apply the Scala plugin to enable the Scala support for our build. The plugin adds new tasks to compile the Scala source files. With the compileScala
task, we compile our main Scala source files. The source files must be in the src/main/scala
directory. The compileTestScala
task compiles all Scala source code files that are in the src/test/scala
directory.
The compile tasks support both Java and Scala source files with joint compilation. We can place our Java source files in say the src/main/java
directory of our project and the Scala source files in the src/main/scala
directory. The compiler will compile both types of files. To be able to compile the files, we must add dependencies to the Scala library in our build file. We must assign the correct dependencies from a Maven repository to the compile
configuration so that Gradle can invoke the compiler to compile the source files.
apply plugin: 'scala'
repositories {
jcenter()
}
dependencies {
compile group: 'org.scala-lang', name: 'scala-library', version: '2.11.4'
}
The Scala plugin also adds several new properties to a source set.
Property name | Type | Description |
---|---|---|
scala |
org.gradle.api.file.SourceDirectorySet |
This is the Scala source files for this project; it contains both .java and.scala source files if they are in the Scala directory. |
scala.srcDirs |
java.util.Set<java.io.File> |
These are the directories with the Scala source files; they can also contain Java source files for joint compilation. |
allScala |
org.gradle.api.file.FileTree |
These are only the Scala source files. All files with the .scala extension are part of this collection. |
Chapter 10. Writing Custom Tasks and Plugins
Creating a custom task
When we create a new task in a build and specify a task with the type
property, we actually configure an existing task. The existing task is called enhanced task in Gradle.
Creating a custom task in the build file
class InfoTask extends DefaultTask {
@Optional
String prefix = 'Current Gradle version'
@TaskAction
def info() {
println "$prefix: $project.gradle.gradleVersion"
}
}
task info(type: InfoTask) {
prefix = 'Running Gradle'
}
Creating a custom plugin
Creating a plugin in the build file
We can create a custom plugin right in the project build file. Similar to a custom task, we can add a new class definition with the logic of the plugin. We must implement the org.gradle.api.Plugin<T>
interface. The interface has an apply()
method. When we write our own plugin, we must override this method. The method accepts an object as a parameter. The type of the object is the same as the generic T
type. When we create a plugin for projects, the Project
type is used. We can also write plugins for other Gradle types, such as tasks. Then we must use the Task
type.
class InfoPlugin implements Plugin<Project> {
void apply(Project project) {
project.tasks.create('info') << {
println "Running Gradle: $project.gradle.gradleVersion"
}
}
}
apply plugin: InfoPlugin
We can rewrite the task and make the text configurable. A Gradle project has an associated ExtensionContainer
object. This object can hold all settings and properties that we want to pass to a plugin. We can add a JavaBean to ExtensionContainer
so that we can configure the bean’s properties from the build file. The JavaBean is a so-called extension object.
class InfoPluginExtension {
String prefix = 'Running Gradle'
}
class InfoPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('info', InfoPluginExtension)
project.tasks.create('info') << {
println "$project.info.prefix: $project.gradle.gradleVersion"
}
}
}
apply plugin: InfoPlugin
info {
prefix = 'Gradle version'
}
Creating a plugin in the project source directory
We will create the buildSrc/src/main/groovy/sample
directory. In this directory, we will create an InfoPlugin.groovy
file:
package sample
import org.gradle.api.*
class InfoPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('info', InfoPluginExtension)
project.tasks.create('info') << {
println "$project.info.prefix: $project.gradle.gradleVersion"
}
}
}
Next, we will create the InfoPluginExtension.groovy
file in the directory:
package sample
class InfoPluginExtension {
String prefix
}
In our build file in the root of the project, we will reference our plugin with the package and class name:
apply plugin: sample.InfoPlugin
info {
prefix = 'Gradle version'
}