Developing Custom Gradle Types
There are several different kinds of "add-ons" to Gradle that you can develop, such as plugins, tasks, project extensions or artifact transforms, that are all implemented as classes and other types that can run on the JVM. This chapter discusses some of the features and concepts that are common to these types. You can use these features to help implement custom Gradle types and provide a consistent DSL for your users.
This chapter applies to the following types:
-
Plugin types.
-
Task types.
-
Artifact transform parameters types.
-
Extension objects created using
ExtensionContainer.create()
, for example a project extension registered by a plugin. -
Objects created using an
ObjectFactory.newInstance()
. -
Elements of a
NamedDomainObjectContainer
.
Configuration using bean properties
The custom Gradle types that you implement often hold some configuration that you want to make available to build scripts and other plugins. For example, a download task may have configuration that specifies the URL to download from and the file system location to write the result to. This configuration is represented as Java bean properties.
Kotlin and Groovy provide conveniences for declaring Java bean properties, which make them good language choices to use to implement Gradle types. These conveniences are demonstrated in the samples below.
Managed properties
Gradle provides some conveniences for implementing types with bean properties. Gradle can provide an implementation of a property. This is called a managed property, as Gradle takes care of managing the state of the property. A property may be mutable, meaning that it has both a getter method and setter method, or read-only, meaning that it has only a getter method.
✨
|
Managed properties are currently an incubating feature. |
Mutable managed properties
To declare a mutable managed property, add an abstract getter method and an abstract setter method for the property to the type.
Here is an example of a task type with a uri
property:
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
import java.net.URI;
public abstract class UrlProcess extends DefaultTask {
// Use an abstract getter and setter method
abstract URI getUri();
abstract void setUri(URI uri);
@TaskAction
void run() {
// Use the `uri` property
System.out.println("Downloading " + getUri());
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.net.URI
abstract class UrlProcess : DefaultTask() {
// Use an abstract var
abstract var uri: URI
@TaskAction
fun run() {
// Use the `uri` property
println("Downloading $uri")
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
abstract class UrlProcess extends DefaultTask {
// Use an abstract property method
abstract URI uri
@TaskAction
void run() {
// Use the `uri` property
println "downloading ${uri}"
}
}
Note that for a property to be considered a mutable managed property, all of the property’s getter methods and setter methods must be public
or protected
and abstract
.
Read-only managed properties
To declare a read-only managed property, add an abstract getter method for the property to the type. The property should not have any setter methods. This is a useful pattern to use with one of Gradle’s configurable lazy property types.
Here is an example of a task type with a uri
property:
import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.TaskAction;
import java.net.URI;
public abstract class UrlProcess extends DefaultTask {
// Use an abstract getter method
abstract Property<URI> getUri();
@TaskAction
void run() {
// Use the `uri` property
System.out.println("Downloading " + getUri().get());
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.provider.Property
import java.net.URI
abstract class UrlProcess : DefaultTask() {
// Use an abstract val
abstract val uri: Property<URI>
@TaskAction
fun run() {
// Use the `uri` property
println("Downloading ${uri.get()}")
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.provider.Property
abstract class UrlProcess extends DefaultTask {
// Use an abstract getter method
abstract Property<URI> getUri()
@TaskAction
void run() {
// Use the `uri` property
println "downloading ${uri.get()}"
}
}
Note that for a property to be considered a read only managed property, all of the property’s getter methods must be public
or protected
and abstract
and the property must not have any setter methods.
In addition, the property must have one of the following types:
-
Property
-
RegularFileProperty
-
DirectoryProperty
-
ListProperty
-
SetProperty
-
MapProperty
-
ConfigurableFileCollection
Managed types
A managed type is an abstract class or interface with no fields and whose properties are all managed. That is, it is a type whose state is entirely managed by Gradle.
DSL support and extensibility
When Gradle creates an instance of a custom type, it decorates the instance to mix-in DSL and extensibility support.
Each decorated instance implements ExtensionAware, and so can have extension objects attached to it.
Note that plugins and container elements are currently not decorated, due to backwards compatibility issues.
Service injection
Gradle provides a number of useful services that can be used by custom Gradle types. For example, the WorkerExecutor service can be used by a task to run work in parallel, as seen in the worker API section. The services are made available through service injection.
Available services
The following services are available for injection:
-
ObjectFactory - Allows model objects to be created. See Creating nested objects for more details.
-
ProjectLayout - Provides access to key project locations. See lazy configuration for more details.
-
ProviderFactory - Creates
Provider
instances. See lazy configuration for more details. -
WorkerExecutor - Allows a task to run work in parallel. See the worker API for more details.
Constructor injection
There are 2 ways that an object can receive the services that it needs. The first option is to add the service as a parameter of the class constructor.
The constructor must be annotated with the javax.inject.Inject
annotation.
Gradle uses the declared type of each constructor parameter to determine the services that the object requires.
The order of the constructor parameters and their names are not significant and can be whatever you like.
Here is an example that shows a task type that receives an ObjectFactory
via its constructor:
import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import javax.inject.Inject;
public class UrlProcess extends DefaultTask {
private final DirectoryProperty outputDirectory;
// Inject an ObjectFactory into the constructor
@Inject
public UrlProcess(ObjectFactory objectFactory) {
// Use the factory
outputDirectory = objectFactory.directoryProperty();
}
@OutputDirectory
public DirectoryProperty getOutputDirectory() {
return outputDirectory;
}
@TaskAction
void run() {
// ...
}
}
import javax.inject.Inject
import org.gradle.api.model.ObjectFactory
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.OutputDirectory
open class UrlProcess
// Inject an ObjectFactory into the constructor
@Inject constructor(objectFactory: ObjectFactory) : DefaultTask() {
// Use the factory
@OutputDirectory
val outputDirectory = objectFactory.directoryProperty()
@TaskAction
fun run() {
// ...
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject
class UrlProcess extends DefaultTask {
@OutputDirectory
final DirectoryProperty outputDirectory
// Inject an ObjectFactory into the constructor
@Inject
UrlProcess(ObjectFactory objectFactory) {
// Use the factory
outputDirectory = objectFactory.directoryProperty()
}
@TaskAction
void run() {
// ...
}
}
Property injection
Alternatively, a service can be injected by adding a property getter method annotated with the javax.inject.Inject
annotation to the class.
This can be useful, for example, when you cannot change the constructor of the class due to backwards compatibility constraints.
This pattern also allows Gradle to defer creation of the service until the getter method is called, rather than when the instance is created. This can help with performance.
Gradle uses the declared return type of the getter method to determine the service to make available. The name of the property is not significant and can be whatever you like.
The property getter method must be public
or protected
. The method can be abstract
or, in cases where this isn’t possible, can have a dummy method body.
The method body is discarded.
Here is an example that shows a task type that receives a two services via property getter methods:
import javax.inject.Inject;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkerExecutor;
public abstract class UrlProcess extends DefaultTask {
// Use an abstract getter method
@Inject
protected abstract ObjectFactory getObjectFactory();
// Alternatively, use a getter method with a dummy implementation
@Inject
protected WorkerExecutor getWorkerExecutor() {
// Method body is ignored
throw new UnsupportedOperationException();
}
@TaskAction
void run() {
WorkerExecutor workerExecutor = getWorkerExecutor();
ObjectFactory objectFactory = getObjectFactory();
// Use the executor and factory ...
}
}
import javax.inject.Inject
import org.gradle.api.model.ObjectFactory
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.workers.WorkerExecutor
abstract class UrlProcess : DefaultTask() {
// Use an abstract property
// Note that the @Inject annotation must be attached to the getter
@get:Inject
abstract val objectFactory: ObjectFactory
// Alternatively, use a property getter with a dummy implementation
// Note that the property must be open and the @Inject annotation must be attached to the getter
@get:Inject
open val workerExecutor: WorkerExecutor
get() {
// Getter body is ignored
throw UnsupportedOperationException()
}
@TaskAction
fun run() {
// Use the executor and factory ...
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.TaskAction
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
abstract class UrlProcess extends DefaultTask {
// Use an abstract getter method
@Inject
protected abstract ObjectFactory getObjectFactory()
// Alternatively, use a getter method with a dummy implementation
@Inject
protected WorkerExecutor getWorkerExecutor() {
// Method body is ignored
throw new UnsupportedOperationException()
}
@TaskAction
void run() {
// Use the executor and factory ...
}
}
Creating nested objects
A custom Gradle type can use the ObjectFactory service to create instances of Gradle types to use for its property values. These instances can make use of the features discussed in this chapter, allowing you to create 'nested' instances and a nested DSL.
In the following example, a project extension receives an ObjectFactory
instance through its constructor.
The constructor uses this to create a nested Server
object (also a custom Gradle type) and makes this object available through the server
property.
import org.gradle.api.model.ObjectFactory;
import javax.inject.Inject;
public class DownloadExtension {
// A nested instance
private final Server server;
@Inject
public DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a Server object
server = objectFactory.newInstance(Server.class);
}
public Server getServer() {
return server;
}
}
import javax.inject.Inject
import org.gradle.api.model.ObjectFactory
open class DownloadExtension @Inject constructor(objectFactory: ObjectFactory) {
// Use an injected ObjectFactory to create a nested Server object
val server: Server = objectFactory.newInstance(Server::class.java)
}
import org.gradle.api.model.ObjectFactory
import javax.inject.Inject
class DownloadExtension {
// A nested instance
final Server server
@Inject
DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a Server object
server = objectFactory.newInstance(Server)
}
}
Collection types
Gradle provides types for maintaining collections of objects, intended to work well with the Gradle DSL and provide useful features such as lazy configuration.
NamedDomainObjectContainer
A NamedDomainObjectContainer manages a set of objects, where each elements has a name associated with it. The container takes care of creating and configuring the elements, and provides a DSL that build scripts can use to define and configure elements. It is intended to hold objects which are themselves configurable, for example a set of custom Gradle objects.
Gradle uses NamedDomainObjectContainer
type extensively throughout the API.
For example, the project.tasks
object used to manage the tasks of a project is a NamedDomainObjectContainer<Task>
.
You can create a container instance using the ObjectFactory service, which provides the ObjectFactory.domainObjectContainer() method.
This is also available using the Project.container() method, however in a custom Gradle type it’s
generally better to use the injected ObjectFactory
service instead of passing around a Project
instance.
In order to use a type with any of the domainObjectContainer()
methods, it must expose a property named “name” as the unique, and constant, name for the object.
The domainObjectContainer(Class)
variant of the method creates new instances by calling the constructor of the class that takes a string argument, which is the desired name of the object.
Objects created this way are treated as custom Gradle types, and so can make use of the features discussed in this chapter, for example service injection or managed properties.
See the above link for domainObjectContainer()
method variants that allow custom instantiation strategies.
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.model.ObjectFactory;
import javax.inject.Inject;
public class DownloadExtension {
// A container of `Server` objects
private final NamedDomainObjectContainer<Server> servers;
@Inject
public DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a container
servers = objectFactory.domainObjectContainer(Server.class);
}
public NamedDomainObjectContainer<Server> getServers() {
return servers;
}
}
import javax.inject.Inject
import org.gradle.api.model.ObjectFactory
import org.gradle.api.NamedDomainObjectContainer
open class DownloadExtension @Inject constructor(objectFactory: ObjectFactory) {
// Use an injected ObjectFactory to create a container of `Server` objects
val servers = objectFactory.domainObjectContainer(Server::class.java)
}
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.model.ObjectFactory
import javax.inject.Inject
class DownloadExtension {
// A container of `Server` instances
final NamedDomainObjectContainer<Server> servers
@Inject
DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a Server object
servers = objectFactory.domainObjectContainer(Server)
}
}
DomainObjectSet
A DomainObjectSet simply holds a set of objects.
Compared to the NamedDomainObjectContainer
, the DomainObjectSet
doesn’t manage the objects in the collection.
They need to be created and added manually.
You can create an instance using the ObjectFactory.domainObjectSet() method.