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.
-
Worker API work action parameters types.
-
Extension objects created using
ExtensionContainer.create()
, for example a project extension registered by a plugin. -
Objects created using
ObjectFactory.newInstance()
. -
Objects created for a managed nested property.
-
Elements of a
NamedDomainObjectContainer
.
Configuration using 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.
Managed properties
Gradle provides its own managed properties concept that allows you to declare each property as an abstract getter (Java, Groovy) or an abstract property (Kotlin).
Gradle then provides the implementation for such a property automatically.
It 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 get()
method and set()
method, or read-only, meaning that it has only a get()
method.
Read-only properties are also called providers.
Mutable managed properties
To declare a mutable managed property, add an abstract getter method of type Property<T>
- where T
can be any serializable type or a fully Gradle managed type.
(See the list further down for more specific property types.)
The property must not have any setter methods.
Here is an example of a task type with an uri
property of type URI
:
public abstract class Download extends DefaultTask {
@Input
public abstract Property<URI> getUri(); // abstract getter of type Property<T>
@TaskAction
void run() {
System.out.println("Downloading " + getUri().get()); // Use the `uri` property
}
}
Note that for a property to be considered a mutable managed property, the property’s getter methods must be abstract
and have public
or protected
visibility.
The property type must be one of the following:
-
Property<T>
-
RegularFileProperty
-
DirectoryProperty
-
ListProperty<T>
-
SetProperty<T>
-
MapProperty<K, V>
-
ConfigurableFileCollection
-
ConfigurableFileTree
-
DomainObjectSet<T>
-
NamedDomainObjectContainer<T>
Gradle creates values for managed properties in the same way as ObjectFactory.
Read-only managed properties
To declare a read-only managed property, also called provider, add a getter method of type Provider<T>
.
The method implementation then needs to derive the value, for example from other properties.
Here is an example of a task type with a uri
provider that is derived from a location
property:
public abstract class Download extends DefaultTask {
@Input
public abstract Property<String> getLocation();
@Internal
public Provider<URI> getUri() {
return getLocation().map(l -> URI.create("https://" + l));
}
@TaskAction
void run() {
System.out.println("Downloading " + getUri().get()); // Use the `uri` provider (read-only property)
}
}
Read-only managed nested properties
To declare a read-only managed nested property, add an abstract getter method for the property to the type annotated with @Nested
.
The property should not have any setter methods.
Gradle provides an implementation for the getter method, and also creates a value for the property.
The nested type is also treated as a custom type, and can use the features discussed in this chapter.
This pattern is useful when a custom type has a nested complex type which has the same lifecycle.
If the lifecycle is different, consider using Property<NestedType>
instead.
Here is an example of a task type with a resource
property. The Resource
type is also a custom Gradle type and defines some managed properties:
public abstract class Download extends DefaultTask {
@Nested
public abstract Resource getResource(); // Use an abstract getter method annotated with @Nested
@TaskAction
void run() {
// Use the `resource` property
System.out.println("Downloading https://" + getResource().getHostName().get() + "/" + getResource().getPath().get());
}
}
public interface Resource {
@Input
Property<String> getHostName();
@Input
Property<String> getPath();
}
Note that for a property to be considered a read-only managed nested property, the property’s getter methods must be abstract
and have public
or protected
visibility.
The property must not have any setter methods.
In addition, the property getter must be annotated with @Nested
.
Read-only managed "name" property
If the type contains an abstract property called "name" of type String
, Gradle provides an implementation for the getter
method, and extends each constructor with a "name" parameter, which comes before all other constructor parameters. If the
type is an interface, Gradle will provide a constructor with a single "name" parameter and @Inject
semantics.
You can have your type implement or extend the Named interface, which defines such a read-only "name" property.
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.
A named managed type is a managed type that additionally has an abstract property "name" of type String
. Named
managed types are especially useful as the element type of NamedDomainObjectContainer (see below).
public interface Resource {
@Input
Property<String> getHostName();
@Input
Property<String> getPath();
}
Java bean properties.
Sometimes you may see properties implemented in the Java bean property style.
That is, they do not use a Property<T>
or Provider<T>
types but are instead implemented with concrete setter and getter methods (or corresponding conveniences in Groovy or Kotlin).
This style of property definition is legacy in Gradle and is discouraged.
Properties in Gradle’s core plugins that are still of this style will be migrated to managed properties in future versions.
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 the elements of containers created using Project.container() 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 objects explicitly for more details.
-
ProjectLayout - Provides access to key project locations. See lazy configuration for more details. This service is unavailable in Worker API actions.
-
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.
-
FileSystemOperations - Allows a task to run operations on the filesystem such as deleting files, copying files or syncing directories.
-
ArchiveOperations - Allows a task to run operations on archive files such as ZIP or TAR files.
-
ExecOperations - Allows a task to run external processes with dedicated support for running external
java
programs. -
ToolingModelBuilderRegistry - Allows a plugin to registers a Gradle tooling API model.
Out of the above, ProjectLayout
and WorkerExecutor
services are only available for injection in project plugins.
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:
public class Download extends DefaultTask {
private final DirectoryProperty outputDirectory;
// Inject an ObjectFactory into the constructor
@Inject
public Download(ObjectFactory objectFactory) {
// Use the factory
outputDirectory = objectFactory.directoryProperty();
}
@OutputDirectory
public DirectoryProperty getOutputDirectory() {
return outputDirectory;
}
@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:
public abstract class Download 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 ...
}
}
Creating objects explicitly
Prefer letting Gradle create objects automatically by using managed properties. |
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 objects 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 Resource
object (also a custom Gradle type) and makes this object available through the resource
property.
public class DownloadExtension {
// A nested instance
private final Resource resource;
@Inject
public DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a Resource object
resource = objectFactory.newInstance(Resource.class);
}
public Resource getResource() {
return resource;
}
}
public interface Resource {
Property<URI> getUri();
}
Collection types
Gradle provides types for maintaining collections of objects, intended to work well to extends Gradle’s DSLs and provide useful features such as lazy configuration.
NamedDomainObjectContainer
A NamedDomainObjectContainer manages a set of objects, where each element 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.
You can also create a container instance using a read-only managed property, described above.
In order to use a type with any of the domainObjectContainer()
methods, it must either
-
be a named managed type; or
-
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.
public interface DownloadExtension {
NamedDomainObjectContainer<Resource> getResources();
}
public interface Resource {
// Type must have a read-only 'name' property
String getName();
Property<URI> getUri();
Property<String> getUserName();
}
For each container property, Gradle automatically adds a block to the Groovy and Kotlin DSL that you can use to configure the contents of the container:
plugins {
id("org.gradle.sample.download")
}
download {
// Can use a block to configure the container contents
resources {
register("gradle") {
uri.set(uri("https://gradle.org"))
}
}
}
plugins {
id("org.gradle.sample.download")
}
download {
// Can use a block to configure the container contents
resources {
register('gradle') {
uri = uri('https://gradle.org')
}
}
}
ExtensiblePolymorphicDomainObjectContainer
An ExtensiblePolymorphicDomainObjectContainer is a NamedDomainObjectContainer
that allows you to
define instantiation strategies for different types of objects.
You can create an instance using the ObjectFactory.polymorphicDomainObjectContainer() method.
NamedDomainObjectSet
A NamedDomainObjectSet holds a set of configurable objects, where each element has a name associated with it.
This is similar to NamedDomainObjectContainer
, however a NamedDomainObjectSet
doesn’t manage the objects in the collection. They need to be created and added manually.
You can create an instance using the ObjectFactory.namedDomainObjectSet() method.
NamedDomainObjectList
A NamedDomainObjectList holds a list of configurable objects, where each element has a name associated with it.
This is similar to NamedDomainObjectContainer
, however a NamedDomainObjectList
doesn’t manage the objects in the collection. They need to be created and added manually.
You can create an instance using the ObjectFactory.namedDomainObjectList() method.
DomainObjectSet
A DomainObjectSet simply holds a set of configurable objects.
Compared to NamedDomainObjectContainer
, a 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.