The Java Library Plugin

The Java Library plugin expands the capabilities of the Java plugin by providing specific knowledge about Java libraries. In particular, a Java library exposes an API to consumers (i.e., other projects using the Java or the Java Library plugin). All the source sets, tasks and configurations exposed by the Java plugin are implicitly available when using this plugin.

Usage

To use the Java Library plugin, include the following in your build script:

Example: Using the Java Library plugin

build.gradle

apply plugin: 'java-library'

API and implementation separation

The key difference between the standard Java plugin and the Java Library plugin is that the latter introduces the concept of an API exposed to consumers. A library is a Java component meant to be consumed by other components. It’s a very common use case in multi-project builds, but also as soon as you have external dependencies.

The plugin exposes two configurations that can be used to declare dependencies: api and implementation. The api configuration should be used to declare dependencies which are exported by the library API, whereas the implementation configuration should be used to declare dependencies which are internal to the component.

Example: Declaring API and implementation dependencies

build.gradle

dependencies {
    api 'commons-httpclient:commons-httpclient:3.1'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

Dependencies appearing in the api configurations will be transitively exposed to consumers of the library, and as such will appear on the compile classpath of consumers. Dependencies found in the implementation configuration will, on the other hand, not be exposed to consumers, and therefore not leak into the consumers' compile classpath. This comes with several benefits:

  • dependencies do not leak into the compile classpath of consumers anymore, so you will never accidentally depend on a transitive dependency

  • faster compilation thanks to reduced classpath size

  • less recompilations when implementation dependencies change: consumers would not need to be recompiled

  • cleaner publishing: when used in conjunction with the new maven-publish plugin, Java libraries produce POM files that distinguish exactly between what is required to compile against the library and what is required to use the library at runtime (in other words, don’t mix what is needed to compile the library itself and what is needed to compile against the library).

The compile configuration still exists but should not be used as it will not offer the guarantees that the api and implementation configurations provide.

If your build consumes a published module with POM metadata, the Java and Java Library plugins both honor api and implementation separation through the scopes used in the pom. Meaning that the compile classpath only includes compile scoped dependencies, while the runtime classpath adds the runtime scoped dependencies as well.

This often does not have an effect on modules published with Maven, where the POM that defines the project is directly published as metadata. There, the compile scope includes both dependencies that were required to compile the project (i.e. implementation dependencies) and dependencies required to compile against the published library (i.e. API dependencies). For most published libraries, this means that all dependencies belong to the compile scope. However, as mentioned above, if the library is published with Gradle, the produced POM file only puts api dependencies into the compile scope and the remaining implementation dependencies into the runtime scope.

Separating compile and runtime scope of modules is active by default in Gradle 5.0+. In Gradle 4.6+, you need to activate it by adding enableFeaturePreview('IMPROVED_POM_SUPPORT') in settings.gradle.

Recognizing API and implementation dependencies

This section will help you spot API and Implementation dependencies in your code using simple rules of thumb. Basically, an API dependency is a type that is exposed in the library binary interface, often referred to ABI (Application Binary Interface). This includes, but is not limited to:

  • types used in super classes or interfaces

  • types used in public method parameters, including generic parameter types (where public is something that is visible to compilers. I.e. , public, protected and package private members in the Java world)

  • types used in public fields

  • public annotation types

In opposition, any type that is used in the following list is irrelevant to the ABI, and therefore should be declared as implementation dependency:

  • types exclusively used in method bodies

  • types exclusively used in private members

  • types exclusively found in internal classes (future versions of Gradle will let you declare which packages belong to the public API)

In the following sample, we can make the difference between an API dependency and an implementation dependency:

Example: Making the difference between API and implementation

src/main/java/org/gradle/HttpClientWrapper.java

// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

    private final HttpClient client; // private member: implementation details

    // HttpClient is used as a parameter of a public method
    // so "leaks" into the public API of this component
    public HttpClientWrapper(HttpClient client) {
        this.client = client;
    }

    // public methods belongs to your API
    public byte[] doRawGet(String url) {
        GetMethod method = new GetMethod(url);
        try {
            int statusCode = doGet(method);
            return method.getResponseBody();

        } catch (Exception e) {
            ExceptionUtils.rethrow(e); // this dependency is internal only
        } finally {
            method.releaseConnection();
        }
        return null;
    }

    // GetMethod is used in a private method, so doesn't belong to the API
    private int doGet(GetMethod method) throws Exception {
        int statusCode = client.executeMethod(method);
        if (statusCode != HttpStatus.SC_OK) {
            System.err.println("Method failed: " + method.getStatusLine());
        }
        return statusCode;
    }
}

We can see that our class imports third party classes, but imports alone won’t tell us if a dependency is an API or implementation dependency. For this, we need to look at the methods. The public constructor of HttpClientWrapper uses HttpClient as a parameter, so it exposed to consumers and therefore belongs to the API.

On the other hand, the ExceptionUtils type, coming from the commons-lang library, is only used in a method body, so it’s an implementation dependency.

Therefore, we can deduce that commons-httpclient is an API dependency, whereas commons-lang is an implementation dependency, which directly translates into the build file:

Example: Declaring API and implementation dependencies

build.gradle

dependencies {
    api 'commons-httpclient:commons-httpclient:3.1'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

As a guideline, you should prefer the implementation configuration first: leakage of implementation types to consumers would then directly lead to a compile error of consumers, which would be solved either by removing the type from the public API, or promoting the dependency as an API dependency instead.

The Java Library plugin configurations

The following graph describes the main configurations setup when the Java Library plugin is in use.

java library ignore deprecated main
  • The configurations in green are the ones a user should use to declare dependencies

  • The configurations in pink are the ones used when a component compiles, or runs against the library

  • The configurations in blue are internal to the component, for its own use

  • The configurations in white are configurations inherited from the Java plugin

And the next graph describes the test configurations setup:

java library ignore deprecated test

The compile, testCompile, runtime and testRuntime configurations inherited from the Java plugin are still available but are deprecated. You should avoid using them, as they are only kept for backwards compatibility.

The role of each configuration is described in the following tables:

Java Library plugin - configurations used to declare dependencies

Configuration name Role Can be consumed Can be resolved Description

api

Declaring API dependencies

no

no

This is where you should declare dependencies which are transitively exported to consumers, for compile.

implementation

Declaring implementation dependencies

no

no

This is where you should declare dependencies which are purely internal and not meant to be exposed to consumers.

compileOnly

Declaring compile only dependencies

yes

yes

This is where you should declare dependencies which are only required at compile time, but should not leak into the runtime. This typically includes dependencies which are shaded when found at runtime.

runtimeOnly

Declaring runtime dependencies

no

no

This is where you should declare dependencies which are only required at runtime, and not at compile time.

testImplementation

Test dependencies

no

no

This is where you should declare dependencies which are used to compile tests.

testCompileOnly

Declaring test compile only dependencies

yes

yes

This is where you should declare dependencies which are only required at test compile time, but should not leak into the runtime. This typically includes dependencies which are shaded when found at runtime.

testRuntimeOnly

Declaring test runtime dependencies

no

no

This is where you should declare dependencies which are only required at test runtime, and not at test compile time.

Java Library plugin - configurations used by consumers

Configuration name Role Can be consumed Can be resolved Description

apiElements

For compiling against this library

yes

no

This configuration is meant to be used by consumers, to retrieve all the elements necessary to compile against this library. Unlike the default configuration, this doesn’t leak implementation or runtime dependencies.

runtimeElements

For executing this library

yes

no

This configuration is meant to be used by consumers, to retrieve all the elements necessary to run against this library.

Java Library plugin - configurations used by the library itself

Configuration name Role Can be consumed Can be resolved Description

compileClasspath

For compiling this library

no

yes

This configuration contains the compile classpath of this library, and is therefore used when invoking the java compiler to compile it.

runtimeClasspath

For executing this library

no

yes

This configuration contains the runtime classpath of this library

testCompileClasspath

For compiling the tests of this library

no

yes

This configuration contains the test compile classpath of this library.

testRuntimeClasspath

For executing tests of this library

no

yes

This configuration contains the test runtime classpath of this library

Known issues

Compatibility with other plugins

At the moment the Java Library plugin is only wired to behave correctly with the java plugin. Other plugins, such as the Groovy plugin, may not behave correctly. In particular, if the Groovy plugin is used in addition to the java-library plugin, then consumers may not get the Groovy classes when they consume the library. To workaround this, you need to explicitly wire the Groovy compile dependency, like this:

Example: Configuring the Groovy plugin to work with Java Library

a/build.gradle

configurations {
    apiElements {
        outgoing.variants.getByName('classes').artifact(
            file: compileGroovy.destinationDir,
            type: ArtifactTypeDefinition.JVM_CLASS_DIRECTORY,
            builtBy: compileGroovy)
    }
}

Increased memory usage for consumers

When a project uses the Java Library plugin, consumers will use the output classes directory of this project directly on their compile classpath, instead of the jar file if the project uses the Java plugin. An indirect consequence is that up-to-date checking will require more memory, because Gradle will snapshot individual class files instead of a single jar. This may lead to increased memory consumption for large projects.