Gradle

Basics

  1. https://docs.gradle.org/current/userguide/gradle_basics.html
  2. https://docs.gradle.org/current/userguide/gradle_basics.html#core_concepts
  3. https://docs.gradle.org/current/userguide/gradle_basics.html#project_structure
  4. https://docs.gradle.org/current/userguide/build_file_basics.html
  5. https://docs.gradle.org/current/userguide/dependency_management_basics.html

Helpful Gradle commands

List all tasks

To look at all the available tasks for a gradle project, use this:

gradlew tasks --all

Publish to maven local

gradlew publishToMavenLocal

Installation

Check if Gradle is installed

https://docs.gradle.org/current/userguide/installation.html#sec:running_and_testing_your_installation

gradle -v

Installation

https://docs.gradle.org/current/userguide/installation.html

Use sdkman to install it.

sdk install gradle

Gradle wrapper

  1. https://docs.gradle.org/current/userguide/gradle_wrapper.html
  2. https://docs.gradle.org/current/userguide/gradle_wrapper_basics.html
  3. https://www.baeldung.com/gradle-wrapper

settings.gradle.kts

When to use gradle.properties vs. settings.gradle?

  1. https://docs.gradle.org/current/userguide/settings_file_basics.html
  2. https://stackoverflow.com/questions/45387971/when-to-use-gradle-properties-vs-settings-gradle

Setting java.sourceCompatibility

See https://docs.gradle.org/current/userguide/toolchains.html

In my-implementations/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts file

group = "com.my.company"
version = "1.0-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_16

implementation vs api vs compileOnly

What is the difference between these two?

api(libs.org.apache.commons.commons.collections4)

and

implementation(libs.org.apache.commons.commons.collections4)
  1. https://discuss.gradle.org/t/best-practice-for-api-vs-implementation-in-multi-module-project/30519
  2. The Java Library Plugin: https://docs.gradle.org/current/userguide/java_library_plugin.html
  3. https://stackoverflow.com/questions/44413952/gradle-implementation-vs-api-configuration
  4. https://www.baeldung.com/gradle-implementation-vs-compile
  5. https://abhiappmobiledeveloper.medium.com/difference-between-implementation-api-compile-and-runtimeonly-in-gradle-dependency-55b70215d245
  6. https://jeroenmols.com/blog/2017/06/14/androidstudio3/

compileOnly

When we configure a dependency as compileOnly, Gradle only adds it to the compile-time classpath. Gradle doesn’t add it to the build output. Accordingly, Gradle makes the dependency available during compilation, but not at runtime when the program is executed. The compileOnly dependency configuration helps reduce the size of the build output; and as a result, reduces the build time and memory use.

examples

You have a library that uses the SLF4J:

compileOnly org.slf4j:slf4j-api:1.7.30

slf4j, lombok, junit for testCompileOnly, etc.

Gradle adds the dependency to the compilation classpath only (it is not added to the build output). This is useful when you’re creating an Android library module and you need the dependency during compilation, but it’s optional to have present at runtime. That is, if you use this configuration, then your library module must include a runtime condition to check whether the dependency is available, and then gracefully change its behavior so it can still function if it’s not provided. This helps reduce the size of the final APK by not adding transient dependencies that aren’t critical. This configuration behaves just like provided (which is now deprecated).

implementation

When we configure a dependency as implementation, Gradle adds it to both the compile-time and runtime classpaths. This means Gradle makes the dependency available during compile-time and also at runtime when the program is executed. Gradle packages the dependency to the build output. However, the implementation dependency configuration increases the size of the build output, which in turn increases the build time and accompanying memory use, as compared to the compile-only configuration.

When your module configures an implementation dependency, it’s letting Gradle know that the module does not want to leak the dependency to other modules at compile time. That is, the dependency is available to other modules only at runtime. Using this dependency configuration instead of api or compile can result in significant build time improvements because it reduces the amount of projects that the build system needs to recompile. For example, if an implementation dependency changes its API, Gradle recompiles only that dependency and the modules that directly depend on it. Most app and test modules should use this configuration.

api

When we configure a dependency as api, Gradle adds it to the compile-time and runtime classpaths and includes it in the published API. This means Gradle makes the dependency available when the program compiles, when the program runs, and when dependent module programs compile. Gradle packages the dependency to the build output. Furthermore, Gradle includes the dependency in the published API. The api dependency configuration increases the build time and accompanying memory use, as compared to the implementation configuration.

When a module includes an api dependency, it’s letting Gradle know that the module wants to transitively export that dependency to other modules, so that it’s available to them at both runtime and compile time. This configuration behaves just like compile (which is now deprecated), and you should typically use this only in library modules. That’s because, if an api dependency changes its external API, Gradle recompiles all modules that have access to that dependency at compile time. So, having a large number of api dependencies can significantly increase build times. Unless you want to expose a dependency’s API to a separate test module, app modules should instead use implementation dependencies.

Difference Between implementation and api

The main difference between implementation and api in Gradle is that implementation doesn’t transitively export the dependency to other modules that depend on this module. In contrast, api transitively exports the dependency to other modules. This means that dependencies configured with api are available to other modules, which depend on this module, both at runtime and compile time. However, dependencies configured with implementation are available to other modules, which depend on this module only at runtime.

Therefore, we should use the api configuration with caution because it increases the build time significantly over the implementation configuration. As an example, if an api dependency changes its external API, Gradle recompiles all modules that have access to that dependency, even at compile time. In contrast, if an implementation dependency changes its external API, Gradle doesn’t recompile all modules even when the software in the modules is not running because the dependent modules don’t have access to that dependency at compile time.

runtimeonly

Gradle adds the dependency to the build output only, for use during runtime. That is, it is not added to the compile classpath. This configuration behaves just like apk (which is now deprecated).

When we configure a dependency as runtimeOnly, Gradle includes it in the build output for runtime, although it doesn’t use it at compile time.

Runtimeonly dependencies are available at runtime but not at compile-time.

For example, you don’t need the concrete SLF4J logger(e.g. logback) at compile time (as you use the SLF4J classes in order to access it) but you need it at runtime as you want to use it.

Let’s look at the following example:

You have a library that uses the SLF4J:

compileOnly org.slf4j:slf4j-api:1.7.30

and you could have a project using the library:

implementation project(":yourlibrary")
implementation org.slf4j:slf4j-api:2.0.0-alpha1
runtimeOnly ch.qos.logback:logback:0.5

compileOnlyApi

When we configure a dependency as compileOnlyApi, Gradle uses it for compilation only, just like compileOnly. Additionally, Gradle includes it in the published API.

Best Practices for Gradle Dependency Management

To ensure effective dependency management in Gradle, we should consider a few best practices:

  1. Use the implementation configuration by default
  2. Use the compileOnly configuration when you don’t want to package the dependency in the build output. An example use case is a software library that only includes compile-time annotations, which are typically used to generate code but are not needed at runtime
  3. Avoid using the api configuration, as it can lead to longer build times and increased memory usage
  4. Use specific versions of dependencies instead of dynamic versioning to ensure consistent behavior across builds
  5. Keep the dependency graph as small as possible to reduce complexity and improve build times
  6. Regularly check for updates to dependencies and update them as necessary to ensure that the project uses the latest and most secure versions
  7. Use dependency locking to ensure that builds are reproducible and consistent across different machines and environments

Example

understand Problem situation Suppose you have a library called MyLibrary that internally uses another library called InternalLibrary. Something like this:

‘InternalLibrary’ module

public class InternalLibrary {
    public static String giveMeAString(){
        return "hello";
    }
}

‘MyLibrary’ module

public class MyLibrary {
    public String myString(){
        return InternalLibrary.giveMeAString();
    }
}

Suppose the MyLibrary build.gradle uses api configuration in dependencies{} like this:

dependencies {
    api project(':InternalLibrary')
}

You want to use MyLibrary in your code so in your app’s build.gradle you add this dependency:

dependencies {
    implementation project(':MyLibrary')
}

Using the api configuration (or deprecated compile) you can access InternalLibrary in your application code:

// Access 'MyLibrary' (granted)
MyLibrary myLib = new MyLibrary();
System.out.println(myLib.myString());
// Can ALSO access the internal library too (but you shouldn't)
System.out.println(InternalLibrary.giveMeAString());

In this way the module MyLibrary is potentially “leaking” the internal implementation of something. You shouldn’t (be able to) use that because it’s not directly imported by you.

The implementation configuration was introduced to prevent this. So now if you use implementation instead of api in MyLibrary:

dependencies {
    implementation project(':InternalLibrary')
}

you won’t be able to call InternalLibrary.giveMeAString() in your app code anymore.

This sort of boxing strategy allows Android Gradle plugin to know that if you edit something in InternalLibrary, it must only trigger the recompilation of MyLibrary and not the recompilation of your entire app, because you don’t have access to InternalLibrary.

When you have a lot of nested dependencies this mechanism can speed up the build a lot.

Trouble with lombok annotations

Example error

my-java-solutions/src/main/java/com/my/company/datastructures/maps/CombiningTwoMaps.java:19: error: invalid method reference
        Map<String, List<Person>> existingMap = people.stream().collect(Collectors.groupingBy(Person::getGender));
                                                                                              ^
  cannot find symbol
    symbol:   method getGender()
    location: class Person

https://stackoverflow.com/questions/35236104/gradle-build-fails-on-lombok-annotated-classes

For older versions of Gradle, annotationProcessor might help:

dependencies{

compileOnly 'org.projectlombok:lombok:1.18.8'
annotationProcessor 'org.projectlombok:lombok:1.18.8'

}

For newer versions

Use plugin https://plugins.gradle.org/plugin/io.freefair.lombok

Then we don’t need compileOnly or annotationProcessor

In my-java-solutions/build.gradle.kts

plugins {
    id("buildlogic.java-conventions")
    id("io.freefair.lombok") version "8.14"
}

dependencies {
    api(libs.org.apache.commons.commons.collections4)
    ...

Gradle - Multi-Project Builds

Declare the desired test framework directly on the test suite or explicitly declare the test framework implementation dependencies on the test’s runtime classpath.

Error

- no location
            - [warn]  The automatic loading of test framework implementation dependencies has been deprecated.
            This is scheduled to be removed in Gradle 9.0.
            The automatic loading of test framework implementation dependencies has been deprecated.
                    - Solutions
                [enum]  Declare the desired test framework directly on the test suite or explicitly declare the test framework implementation dependencies on the test's runtime classpath.
                    - Locations
                - `:searching-and-sorting:test`
            - [warn]  No test executed. This behavior has been deprecated.
            This will fail with an error in Gradle 9.0.
            No test executed. This behavior has been deprecated.
                    - Solutions
                [enum]  There are test sources present but no test was executed. Please check your test configuration.
                    - Locations
                - `<undefined>`
                - `:searching-and-sorting:test`

Fix

dependencies {
    // JUnit Jupiter API for writing tests
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0")
    // JUnit Jupiter Engine for running tests
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.0")
    // JUnit Platform Launcher (for running tests, often implicitly added by Gradle with test suites)
    testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.0")
}

tasks.test {
    useJUnitPlatform {
        version = "5.8.2"
    }
}

Issues with refreshing dependencies in Eclipse

If there is trouble refreshing the dependencies in Eclipse after making changes to the build.gradle file, follow the steps below:

  1. check if you have included eclipse gradle plugin. apply plugin : 'eclipse'
  2. Go to your project terminal
  3. Run gradle tasks --all to see the list of all available gradle tasks.
  4. If the task cleanEclipse is available, run it.
  5. If not, run gradle cleanEclipseProject and gradle cleanEclipseClasspath separately.
  6. After that, run gradle eclipse
  7. Go to the project in eclipse and refresh the project.

This should bring all the latest dependencies down and you should see them in the Referenced Libraries section.

Working with EAR projects

Go to the EAR folder in command prompt:

C:\XXXXXXXXXXXXXEAR

And use this command to build EAR.

gradle clean ear --info
gradle clean testall --info
gradle clean testAll ear –-info

From folder : C:\XXXXXXXXXXEAR\build\distributions To folder : C:\WASLP_dev\tools\WASLP8559\wlp\usr\servers\default\dropins

Windows command to copy the EAR from a source folder to a destination folder:

xcopy C:\XXXXXXXXXXXXXXEAR\build\distributions C:\WASLP_dev\tools\WASLP8559\wlp\usr\servers\default\dropins

From folder : C:\Users\n0281526\Documents\services-property-insurance-partner-exchange\PiAcordSalesMediationServiceEAR To folder : C:\WASLP_dev\tools\WASLP8559\wlp\usr\servers\default\dropins

Windows command to copy the EAR from a source folder to a destination folder:

xcopy C:\WASLP_dev\workspaces\git_repo\services-property-insurance-partner-exchange\PiAcordSalesMediationServiceEAR\build\distributions     C:\WASLP_dev\tools\WASLP8559\wlp\usr\servers\default\dropins

How to exclude a few tasks from the Gradle build process :

./gradlew build -x checkstyleMain -x findbugsMain -x test -x jacocoTestCoverageVerification -x pmdMain

Links to this note