Table of Contents
Support for building Java libraries using the software model is currently incubating. Please be aware that the DSL, APIs and other configuration may change in later Gradle versions.
The Java software plugins are intended to replace the Java plugin, and leverage the Gradle software model to achieve the best performance, improved expressiveness and support for variant-aware dependency management.
The Java software plugins provide:
Support for building Java libraries and other components that run on the JVM.
Support for several source languages.
Support for building different variants of the same software, for different Java versions, or for any purpose.
Build time definition and enforcement of Java library API.
Compile avoidance.
Dependency management between Java software components.
The Java software plugins provide a software model that describes Java based software and how it should be built. This Java software model extends the base Gradle software model, to add support for building JVM libraries. A JVM library is a kind of library that is built for and runs on the JVM. It may be built from Java source, or from various other languages. All JVM libraries provide an API of some kind.
To use the Java software plugins, include the following in your build script:
Example 70.1. Using the Java software plugins
build.gradle
plugins { id 'jvm-component' id 'java-lang' }
A library is created by declaring a JvmLibrarySpec
under the components
element of the model
:
Example 70.2. Creating a java library
build.gradle
model { components { main(JvmLibrarySpec) } }
Output of gradle build
> gradle build :compileMainJarMainJava :processMainJarMainResources :createMainJar :mainApiJar :mainJar :assemble :check UP-TO-DATE :build BUILD SUCCESSFUL
This example creates a library named main
, which will implicitly create a JavaSourceSet
named java
.
The conventions of the legacy Java plugin are observed, where Java sources
are expected to be found in src/main/java
,
while resources are expected to be found in src/main/resources
.
Source sets represent logical groupings of source files in a library. A library can define multiple source sets and all sources will be compiled and included in the resulting binaries. When a library is added to a build, the following source sets are added by default.
Table 70.1. Java plugin - default source sets
Source Set | Type | Directory |
java | JavaSourceSet |
src/${library.name}/java |
resources | JvmResourceSet |
src/${library.name}/resources |
It is possible to configure an existing source set
through the sources
container:
Example 70.3. Configuring a source set
build.gradle
components {
main {
sources {
java {
// configure the "java" source set
}
}
}
}
It is also possible to create an additional source set, using the
JavaSourceSet
type:
Example 70.4. Creating a new source set
build.gradle
components {
main {
sources {
mySourceSet(JavaSourceSet) {
// configure the "mySourceSet" source set
}
}
}
}
By default, when the plugins above are applied, no new tasks are added to the build. However, when libraries are defined, conventional tasks are added which build and package each binary of the library.
For each binary of a library, a single lifecycle task is created which executes all tasks associated with building the binary.
To build all binaries, the standard build
lifecycle task can be used.
Table 70.2. Java plugin - lifecycle tasks
Component Type | Binary Type | Lifecycle Task |
JvmLibrarySpec |
JvmBinarySpec |
${library.name}${binary.name} |
For each source set added to a library, tasks are added to compile or process the source files for each binary.
Table 70.3. Java plugin - source set tasks
Source Set Type | Task name | Type | Description |
JavaSourceSet |
compile${library.name}${binary.name}${library.name}${sourceset.name} | PlatformJavaCompile |
Compiles the sources of a given source set. |
JvmResourceSet |
process${library.name}${binary.name}${library.name}${sourceset.name} | ProcessResources |
Copies the resources in the given source set to the classes output directory. |
For each binary in a library, a packaging task is added to create the jar for that binary.
Table 70.4. Java plugin - packaging tasks
Binary Type | Task name | Depends on | Type | Description |
JvmBinarySpec |
create${library.name}${binary.name} | all PlatformJavaCompile and ProcessResources
tasks associated with the binary |
Jar |
Packages the compiled classes and processed resources of the binary. |
Gradle provides a report that you can run from the command-line that shows details about the components and binaries that your
project produces. To use this report, just run gradle components
. Below is an example of running this report for
one of the sample projects:
Example 70.5. The components report
Output of gradle components
> gradle components :components ------------------------------------------------------------ Root project ------------------------------------------------------------ JVM library 'main' ------------------ Source sets Java source 'main:java' srcDir: src/main/java Java source 'main:mySourceSet' srcDir: src/main/mySourceSet JVM resources 'main:resources' srcDir: src/main/resources Binaries Jar 'main:jar' build using task: :mainJar target platform: java7 tool chain: JDK 7 (1.7) classes dir: build/classes/main/jar resources dir: build/resources/main/jar API Jar file: build/jars/main/jar/api/main.jar Jar file: build/jars/main/jar/main.jar Note: currently not all plugins register their components, so some components may not be visible here. BUILD SUCCESSFUL Total time: 1 secs
A component in the Java software model can declare dependencies on other Java libraries.
If component main
depends on library util
, this means that the API of util
is required when compiling the sources of main
, and the runtime of util
is required
when running or testing main
. The terms 'API' and 'runtime' are examples of usages of a Java library.
The 'API' usage of a Java library consists of:
When library main
is compiled with a dependency on util
,
the 'API' dependencies of 'util' are resolved transitively, resulting in the complete set of libraries required to compile.
For each of these libraries (including 'util'), the 'API' artifacts will be included in the compile classpath.
Similarly, the 'runtime' usage of a Java library consists of artifacts and dependencies. When a Java component is tested or bundled into an application, the runtime usage of any runtime dependencies will be resolved transitively into the set of libraries required at runtime. The runtime artifacts of these libraries will then be included in the testing or runtime classpath.
Two types of Java library dependencies can be declared:
Dependencies onto libraries published to an Ivy repository are not yet supported.
Dependencies may be declared for a specific JavaSourceSet
,
for an entire JvmLibrarySpec
or
as part of the
JvmApiSpec
of a component:
Example 70.6. Declaring a dependency onto a library
build.gradle
model { components { server(JvmLibrarySpec) { sources { java { dependencies { library 'core' } } } } core(JvmLibrarySpec) { dependencies { library 'commons' } } commons(JvmLibrarySpec) { api { dependencies { library 'collections' } } } collections(JvmLibrarySpec) } }
Output of gradle serverJar
> gradle serverJar :compileCollectionsJarCollectionsJava :collectionsApiJar :compileCommonsJarCommonsJava :commonsApiJar :compileCoreJarCoreJava :processCoreJarCoreResources :coreApiJar :compileServerJarServerJava :createServerJar :serverApiJar :serverJar BUILD SUCCESSFUL
Dependencies declared for a source set will only be used for compiling that particular source set.
Dependencies declared for a component will be used when compiling all source sets for the component.
Dependencies declared for the component api
are used for compiling all source sets for the component,
and are also exported as part of the component's API.
See Enforcing API boundaries at compile time for more details.
The previous example declares a dependency for the java
source set of the server
library
onto the core
library of the same project. However, it is possible to create a dependency on a library in a different
project as well:
Example 70.7. Declaring a dependency onto a project with an explicit library
build.gradle
client(JvmLibrarySpec) { sources { java { dependencies { project ':util' library 'main' } } } }
Output of gradle clientJar
> gradle clientJar :util:compileMainJarMainJava :util:mainApiJar :compileClientJarClientJava :clientApiJar :createClientJar :clientJar BUILD SUCCESSFUL
When the target project defines a single library, the library
selector can be omitted altogether:
Example 70.8. Declaring a dependency onto a project with an implicit library
build.gradle
dependencies {
project ':util'
}
Dependencies onto libraries published to Maven repositories can be declared via module identifiers
consisting of a group name
,
a module name
plus an optional version selector
:
Example 70.9. Declaring a dependency onto a library published to a Maven repository
build.gradle
verifier(JvmLibrarySpec) { dependencies { module 'asm' group 'org.ow2.asm' version '5.0.4' module 'asm-analysis' group 'org.ow2.asm' } }
Output of gradle verifierJar
> gradle verifierJar :compileVerifierJarVerifierJava :createVerifierJar :verifierApiJar :verifierJar BUILD SUCCESSFUL
A shorthand notation for module identifiers can also be used:
Example 70.10. Declaring a module dependency using shorthand notation
build.gradle
dependencies { module 'org.ow2.asm:asm:5.0.4' module 'org.ow2.asm:asm-analysis' }
Module dependencies will be resolved against the configured repositories as usual:
Example 70.11. Configuring repositories for dependency resolution
build.gradle
repositories { mavenCentral() }
The DependencySpecContainer
class provides a complete reference of the dependencies DSL.
Every library has an API, which consists of artifacts and dependencies that are required to compile against the library. The library may be explicitly declared for a component, or may be implied based on other component metadata.
By default, all public
types of a library are considered to be part of its API. In many cases this is not ideal; a library will contain many public types that intended for internal use within that library.
By explicitly declaring an API for a Java library, Gradle can provide compile-time encapsulation of these internal-but-public types. The types to include in a library API are declared at the package level. Packages containing API types are considered to be exported.
By default, dependencies of a library are not considered to be part of its API. By explicitly declaring a dependency as part of the library API, this dependency will then be made available to consumers when compiling. Dependencies declared this way are considered to be exported, and are known as 'API dependencies'.
JDK 9 will introduce Jigsaw, the reference implementation of the Java Module System. Jigsaw will provide both compile-time and run-time enforcement of API encapsulation.
Gradle anticipates the arrival of JDK 9 and the Java Module System with an approach to specifying and enforcing API encapsulation at compile-time. This allows Gradle users to leverage the many benefits of strong encapsulation, and prepare their software projects for migration to JDK 9.
module-info.java
in Jigsaw, or the api { ... }
block that Gradle defines as part of those stories. Usually, we can simplify this to a list of packages, called exported packages.
We avoid the use of the term implementation because it is too vague: both API classes and Non-API classes can have an implementation. For example, an API class can be an interface, but also a concrete class. Implementation is an overloaded term in the Java ecosystem, and often refers to a class implementing an interface. This is not the case here: a concrete class can be member of an API, but to compile against an API, you don't need the implementation of the class: all you need is the signatures.
Example 70.12. Specifying api packages
build.gradle
model { components { main(JvmLibrarySpec) { api { exports 'org.gradle' exports 'org.gradle.utils' } } } }
Example 70.13. Specifying api dependencies
build.gradle
commons(JvmLibrarySpec) {
api {
dependencies {
library 'collections'
}
}
}
When you define an API for your library, Gradle enforces the usage of that API at compile-time. This comes with 3 direct consequences:
Given a main component that exports org.gradle
, org.gradle.utils
and defines those classes:
Example 70.14. Main sources
src/main/java/org/gradle/Person.java
package org.gradle; public class Person { private final String name; public Person(String name) { this.name = name; } public String getName() { return name; } }
src/main/java/org/gradle/internal/PersonInternal.java
package org.gradle.internal; import org.gradle.Person; public class PersonInternal extends Person { public PersonInternal(String name) { super(name); } }
src/main/java/org/gradle/utils/StringUtils.java
package org.gradle.utils; public abstract class StringUtils { }
Compiling a component client that declares a dependency onto main will succeed:
Example 70.15. Client component
build.gradle
model {
components {
client(JvmLibrarySpec) {
sources {
java {
dependencies {
library 'main'
}
}
}
}
}
}
src/client/java/org/gradle/Client.java
package org.gradle; public class Client { private Person person; public void setPerson(Person p) { this.person = p; } public Person getPerson() { return person; } }
Output of gradle :clientJar
> gradle :clientJar :compileMainJarMainJava :processMainJarMainResources :mainApiJar :compileClientJarClientJava :clientApiJar :createClientJar :clientJar BUILD SUCCESSFUL
But trying to compile a component brokenclient that declares a dependency onto main but uses an non-API class of main will result in a compile-time error:
Example 70.16. Broken client component
src/brokenclient/java/org/gradle/Client.java
package org.gradle; import org.gradle.internal.PersonInternal; public class Client { private PersonInternal person; public void setPerson(PersonInternal p) { this.person = p; } public PersonInternal getPerson() { return person; } }
Output of gradle :brokenclientJar
> gradle :brokenclientJar :compileMainJarMainJava :processMainJarMainResources :mainApiJar :compileBrokenclientJarBrokenclientJava FAILED BUILD FAILED
On the other hand, if Person.java in client is updated and its API hasn't changed, client will not be recompiled. This is in particular important for incremental builds of large projects, where we can avoid the compilation of dependencies in chain, and then dramatically reduce build duration:
Example 70.17. Recompiling the client
src/main/java/org/gradle/Person.java
package org.gradle; public class Person { private final String name; public Person(String name) { // we updated the body if this method // but the signature doesn't change // so we will not recompile components // that depend on this class this.name = name.toUpperCase(); } public String getName() { return name; } }
Output of gradle :clientJar
> gradle :clientJar :compileMainJarMainJava :processMainJarMainResources UP-TO-DATE :mainApiJar :compileClientJarClientJava UP-TO-DATE :clientApiJar UP-TO-DATE :createClientJar UP-TO-DATE :clientJar UP-TO-DATE BUILD SUCCESSFUL
The software model extracts the target platform as a core concept. In the Java world, this means that a library can be built, or resolved, against a specific version of Java. For example, if you compile a library for Java 5, we know that such a library can be consumed by a library built for Java 6, but the opposite is not true. Gradle lets you define which platforms a library targets, and will take care of:
The targetPlatform
DSL defines which platforms a library should be built against:
Example 70.18. Declaring target platforms
core/build.gradle
model { components { main(JvmLibrarySpec) { targetPlatform 'java5' targetPlatform 'java6' } } }
Output of gradle :core:build
> gradle :core:build :core:compileMainJava5JarMainJava :core:processMainJava5JarMainResources :core:createMainJava5Jar :core:mainJava5ApiJar :core:mainJava5Jar :core:compileMainJava6JarMainJava :core:compileMainJava6JarMainJava6JarJava :core:processMainJava6JarMainResources :core:createMainJava6Jar :core:mainJava6ApiJar :core:mainJava6Jar :core:assemble :core:check UP-TO-DATE :core:build BUILD SUCCESSFUL
When building the application, Gradle generates two binaries: java5MainJar
and java6MainJar
corresponding to the target versions of Java. These artifacts will participate in dependency resolution as described
here.
For each JvmLibrarySpec
it is possible to define additional source sets for each binary. A common use case for this
is having specific dependencies for each variant and source sets that conform to those dependencies. The example below configures a java6
source set on the main.java6Jar
binary:
Example 70.19. Declaring binary specific sources
core/build.gradle
main {
binaries.java6Jar {
sources {
java(JavaSourceSet) {
source.srcDir 'src/main/java6'
}
}
}
}
Output of gradle clean :core:mainJava6Jar
> gradle clean :core:mainJava6Jar :core:clean :server:clean UP-TO-DATE :core:compileMainJava6JarMainJava :core:compileMainJava6JarMainJava6JarJava :core:processMainJava6JarMainResources :core:createMainJava6Jar :core:mainJava6ApiJar :core:mainJava6Jar BUILD SUCCESSFUL
When a library targets multiple versions of Java and depends on another library, Gradle will make its best effort to resolve the dependency to the most appropriate version of the dependency library. In practice, this means that Gradle chooses the highest compatible version:
B
built for Java n
D
built for Java m
D
is compatible with B
if m<=n
D(java 5), D(java 6), ...D(java m)
, choose the compatible D binary with the highest Java version
Example 70.20. Declaring target platforms
server/build.gradle
model { components { main(JvmLibrarySpec) { targetPlatform 'java5' targetPlatform 'java6' sources { java { dependencies { project ':core' library 'main' } } } } } }
Output of gradle clean :server:build
> gradle clean :server:build :core:clean :server:clean UP-TO-DATE :core:compileMainJava5JarMainJava :core:processMainJava5JarMainResources :core:mainJava5ApiJar :server:compileMainJava5JarMainJava :server:createMainJava5Jar :server:mainJava5ApiJar :server:mainJava5Jar :core:compileMainJava6JarMainJava :core:compileMainJava6JarMainJava6JarJava :core:processMainJava6JarMainResources :core:mainJava6ApiJar :server:compileMainJava6JarMainJava :server:createMainJava6Jar :server:mainJava6ApiJar :server:mainJava6Jar :server:assemble :server:check UP-TO-DATE :server:build BUILD SUCCESSFUL
In the example above, Gradle automatically chooses the Java 6 variant of the dependency for the Java 6 variant of the server
component,
and chooses the Java 5 version of the dependency for the Java 5 variant of the server
component.
The Java plugin, in addition to the target platform resolution, supports resolution of custom variants. Custom variants
can be defined on custom binary types, as long as they extend JarBinarySpec
. Users interested
in testing this incubating feature can check out the documentation of the Variant
annotation.
The Java software model supports defining standalone JUnit test suites as components of the model. Standalone test suite are components that are self contained, in the sense that there is no component under test: everything being tested must belong to the test suite sources.
A test suite is declared by creating a component of type JUnitTestSuiteSpec
, which is available when you apply the junit-test-suite
plugin:
Example 70.21. Using the JUnit plugin
build.gradle
plugins { id 'jvm-component' id 'java-lang' id 'junit-test-suite' } model { testSuites { test(JUnitTestSuiteSpec) { jUnitVersion '4.12' } } }
In the example above, test
is the name of our test suite. By convention, Gradle will create two source sets for the test suite, based on the name of the component: one for Java sources, and the other for resources: src/test/java
and src/resources/java
. If the component was named integTest
, then sources and resources would have been found respectively in src/integTest/java
and src/integTest/resources
.
Once the component is created, the test suite can be executed running the <<test suite name>>BinaryTest
task:
Example 70.22. Executing the test suite
src/test/java/org/gradle/MyTest.java
package org.gradle; import org.junit.Test; import static org.junit.Assert.*; public class MyTest { @Test public void myTestMethod() { assertEquals(4, "test".length()); } }
Output of gradle testBinaryTest
> gradle testBinaryTest :compileTestBinaryTestJava :processTestBinaryTestResources :testBinaryTest BUILD SUCCESSFUL
It is possible to configure source sets in a similar way as libraries.
A test suite being a component can also declare dependencies onto other components.
A test suite can also contain resources, in which case it is possible to configure the resource processing task:
Example 70.23. Executing the test suite
build.gradle
model { tasks.processTestBinaryTestResources { // uncomment lines filter { String line -> line.replaceAll('<!-- (.+?) -->', '$1') } } }
It is likely that you will want to test another JVM component. The Java software model supports it exactly like standalone test suites, by just declaring an additional component under test:
Example 70.24. Declaring a component under test
build.gradle
model {
components {
main(JvmLibrarySpec)
}
testSuites {
test(JUnitTestSuiteSpec) {
jUnitVersion '4.12'
testing $.components.main
}
}
}
Output of gradle testMainJarBinaryTest
> gradle testMainJarBinaryTest :compileMainJarMainJava :processMainJarMainResources :compileTestMainJarBinaryTestJava :testMainJarBinaryTest BUILD SUCCESSFUL
Note that the syntax to choose the component under test is a reference ($.
). You can select any JvmComponentSpec
as the component under test. It's also worth noting that when you declare a component under test, a test suite is created for each binary of the component under test (for example, if the component under test has a Java 7 and Java 8 version, 2 different test suite binaries would be automatically created).
You can declare the list of local JVM installations using the javaInstallations
model block. Gradle will use this information
to locate your JVMs and probe their versions. Please note that this information is not yet used by Gradle to select the appropriate JDK or JRE when
compiling your Java sources, or when executing Java applications. A local Java installation can be declared using the LocalJava
type,
independently of the fact they are a JDK or a JRE:
Example 70.25. Declaring local Java installations
build.gradle
model { javaInstallations { openJdk6(LocalJava) { path '/usr/lib/jvm/jdk1.6.0-amd64' } oracleJre7(LocalJava) { path '/usr/lib/jvm/jre1.7.0' } ibmJdk8(LocalJava) { path '/usr/lib/jvm/jdk1.8.0' } } }