Chapter 70. Building Java Libraries

Support for building Java libraries using the new software model is currently incubating. Please be aware that the DSL, APIs and other configuration may change in later Gradle versions.

The JVM component plugins for building Java libraries are intended to replace the Java plugin, and leverage the new rule based model configuration to achieve the best performance, improved expressiveness and support for variant-aware dependency management.

70.1. The Software Model

The JVM component plugins rely on a software model which describes how an application is built and how components of the model relate to each other. The software model is organized around key concepts:

  • A component is a general concept for a piece of software that might be deliverable. Examples of components are a standalone application, a web application, a library, etc. A component is often composed of other components.
  • A library is a buildable component. In the Java world, a library is often assimilated to a Jar file, but while a Jar file represents an output, a library is the description of how the output is built. A library is defined by the combination of its source sets and variants.
  • A source set represents a logical group of source files in a component. As such, a source set is often an input to a single compilation task, which will produce an output (classes, compiled CSS, etc). A library may consist of multiple source sets.
  • A variant represents a modulation of a component. A library, for example, might target Java 6 and Java 7, effectively producing two distinct outputs: a Java 6 jar and a Java 7 jar. In this case, the target platform is an example of a variant dimension. Custom library types may define their own variant dimensions, which will participate in dependency resolution.
  • A binary represents the output of a library. Given a combination of variants, a library may produce multiple binaries. A binary is often a consumable artifact of other components.

Additionaly, the software model allows us to better identify concepts that are traditionally merged together:

  • An API is a set of classes, interfaces, methods that are exposed to a consumer.
  • An API specification is the specification of classes, interfaces or methods that belong to an API. It can be found in various forms, like 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.
  • A runtime jar consists of API classes and non-API classes used at execution time. There can be multiple runtime jars depending on combinations of the variant dimensions: target platform, hardware infrastructure, target application server, ...
  • API classes are classes of a variant which match the API specification
  • Non-API classes are classes of a variant which do not match the API specification. They can be refered to as internal classes.
  • A stubbed API class is an API class for which its implementation and non public members have been removed. It is meant to be used when a consumer is going to be compiled against an API.
  • An API jar is a collection of API classes. There can be multiple API jars depending on the combinations of variant dimensions.
  • A stubbed API jar is a collection of stubbed API classes. There can be multiple stubbed API jars depending on the combinations of variant dimensions.
  • An ABI (application binary interface) corresponds to the public signature of an API, that is to say the set of stubbed API classes that it exposes (and their API visible members).

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.

70.2. Usage

To use the JVM component plugins, include the following in your build script:

Example 70.1. Using the JVM component plugins

build.gradle

plugins {
    id 'jvm-component'
    id 'java-lang'
}

70.3. Creating a library

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
:createMainApiJar
: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.

70.4. Source Sets

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
            }
        }
    }
}

70.5. Tasks

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 ${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${binary.name}${library.name}${sourceset.name} PlatformJavaCompile Compiles the sources of a given source set.
JvmResourceSet process${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${binary.name} all PlatformJavaCompile and ProcessResources tasks associated with the binary Jar Packages the compiled classes and processed resources of the binary.

70.6. Finding out more about your project

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 'mainJar'
        build using task: :mainJar
        targetPlatform: java7
        tool chain: JDK 7 (1.7)
        Jar file: build/jars/mainJar/main.jar

Note: currently not all plugins register their components, so some components may not be visible here.

BUILD SUCCESSFUL

Total time: 1 secs

70.7. Dependencies between components

The JVM component plugins support API dependencies between components. Having an API dependency means that if A depends on B, then the API of B is required to compile A. Gradle will then make sure that the dependency is built before the dependent component, and that the dependency appears on the compile classpath. API dependencies are (by nature) not transitive. Runtime (transitive) dependency management is not implemented yet.

Currently the plugin supports two kinds of dependencies:

  • API dependencies onto a local project
  • API dependencies onto a local library

Dependencies onto external components are not yet supported.

Dependencies are declared on a SourceSet:

Example 70.6. Declaring a dependency onto a library

build.gradle

model {
    components {
        core(JvmLibrarySpec)

        server(JvmLibrarySpec) {
            sources {
                java {
                    dependencies {
                        library 'core'
                    }
                }
            }
        }
    }
}

Output of gradle serverJar

> gradle serverJar
:compileCoreJarCoreJava
:processCoreJarCoreResources
:createCoreJar
:createCoreApiJar
:coreJar
:compileServerJarServerJava
:createServerJar
:createServerApiJar
:serverJar

BUILD SUCCESSFUL

This example declares an API 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:createMainJar
:util:createMainApiJar
:util:mainJar
:compileClientJarClientJava
:createClientJar
:createClientApiJar
:clientJar

BUILD SUCCESSFUL

When the target project only 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'
}

The DependencySpecContainer class provides a complete reference of the dependencies DSL.

70.8. Platform aware dependency management

70.8.1. Specifying the target platform

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:

  • generating a binary for each target platform (eg, a Java 5 jar as well as a Java 6 jar)
  • resolving dependencies against a matching platform

The targetPlatform DSL defines which platforms a library should be built against:

Example 70.9. Declaring target platforms

core/build.gradle

model {
    components {
        main(JvmLibrarySpec) {
            targetPlatform 'java5'
            targetPlatform 'java6'
        }
   }
}

Output of gradle :core:build

> gradle :core:build
:core:compileJava5MainJarMainJava
:core:processJava5MainJarMainResources
:core:createJava5MainJar
:core:createJava5MainApiJar
:core:java5MainJar
:core:compileJava6MainJarMainJava
:core:compileJava6MainJarMainJava6
:core:processJava6MainJarMainResources
:core:createJava6MainJar
:core:createJava6MainApiJar
:core:java6MainJar
: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.

70.8.2. Binary specific source sets

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 java6MainJar binary:

Example 70.10. Declaring binary specific sources

core/build.gradle

main {
    binaries.java6MainJar {
        sources {
            java6(JavaSourceSet) {
                source.srcDir 'src/main/java6'
            }
        }
    }
}

Output of gradle clean :core:java6MainJar

> gradle clean :core:java6MainJar
:core:clean
:server:clean UP-TO-DATE
:core:compileJava6MainJarMainJava
:core:compileJava6MainJarMainJava6
:core:processJava6MainJarMainResources
:core:createJava6MainJar
:core:createJava6MainApiJar
:core:java6MainJar

BUILD SUCCESSFUL

70.8.3. Dependency resolution

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:

  • for a binary B built for Java n
  • for a dependency binary D built for Java m
  • D is compatible with B if m<=n
  • for multiple compatible binaries D(java 5), D(java 6), ...D(java m), choose the compatible D binary with the highest Java version

Example 70.11. 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:compileJava5MainJarMainJava
:core:processJava5MainJarMainResources
:core:createJava5MainJar
:core:createJava5MainApiJar
:core:java5MainJar
:server:compileJava5MainJarMainJava
:server:createJava5MainJar
:server:createJava5MainApiJar
:server:java5MainJar
:core:compileJava6MainJarMainJava
:core:compileJava6MainJarMainJava6
:core:processJava6MainJarMainResources
:core:createJava6MainJar
:core:createJava6MainApiJar
:core:java6MainJar
:server:compileJava6MainJarMainJava
:server:createJava6MainJar
:server:createJava6MainApiJar
:server:java6MainJar
: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.

70.9. Custom variant resolution

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.

70.10. Enforcing API boundaries at compile time

Often a library will contain many types that—despite having public visibility—are intended for internal use only within that library. JDK 9 will introduce Jigsaw, the reference implementation of the Java Module System, which will provide both compile-time and run-time enforcement of strong encapsulation at the library (aka: module) level. This means that packages not intended for public consumption can remain private to the library, regardless whether the types within those packages have public visibility.

Gradle anticipates the arrival of JDK 9 and the Java Module System with an approach to specifying and enforcing strong encapsulation at compile-time. However, users need not wait for the arrival of JDK 9 to use it; this feature can be used on any version of the Java platform that Gradle supports.

70.10.1. Specifying exported packages

Example 70.12. Specifying library api

build.gradle

model {
    components {
        main(JvmLibrarySpec) {
            api {
                exports 'com.example.p1'
                exports 'com.example.p2'
            }
        }
    }
}

When gradle assemble is run, both an api jar and implementation jar will be generated.