Gradle 7+ compileOnly app with implementation in library

Background

For a long time we have been using compileOnly for production and implementation for development (for IDE support). We use ShadowJar plugin to generate the application jar file. All the dependencies are stored in a lib directory and linked in the classpath.

The reason behind that way of working is time. Its just faster to compile and to deploy. For example, one of our applications takes like 5 minutes to compile when using implementation in the project dependencies against few seconds when using compileOnly. Then, the resulting file when using implementation is about 800MB, while its under 1MB when using compileOnly (as all the dependencies are just copied to a directory without creating a “fat jar”). Using rsync to the target server is much more convenient than copying each time a 800MB file (specially under slow networks).

Problem Description

One of our libraries uses implementation for those dependencies which are no directly required by the application. For example, the library wraps gson library but it doesn’t expose any of its classes to the consumer.

The problem is that when using compileOnly, those libraries are not copied into the lib directory resulting in a NoClassDefFoundError exception. That doesn’t happen when we create a “fat jar” using implementation.

Question

Is there a way to include those libraries as well ? configuration.compileClasspath doesn’t include those libraries. Are we doing something wrong?

build.gradle (interesting parts)

Gradle version: 7.1.1

// Using ShadowJar Plugin:
plugins {
    id 'groovy'
    id 'com.github.johnrengelman.shadow' version '7.0.0'
}

// Using compileOnly to reduce jar size
dependencies {
    compileOnly "com.example.library:module:1.0.0"
    ... many more ...
}

// Copying all the dependencies into "lib" directory
task copyRuntimeLibs(type: Copy) {
    into "lib"
    from configurations.compileClasspath
}

// Basic ShadowJar configuration:
shadowJar {
    archiveBaseName.set(project.name)
    archiveVersion.set(project.version.toString())
    archiveClassifier.set("linux-x86_64")
    zip64 = true
    shadowJar.finalizedBy copyRuntimeLibs
}

// Linking all the libraries under "lib" to the class path:
jar {
    from configurations.runtimeClasspath.collect { zipTree it }
    manifest {
        attributes(
            'Implementation-Version': archiveVersion,
            'Main-Class': "${systemMainClass}",
            "Class-Path": configurations.compileClasspath.collect { 'lib/' + it.name }.join(' ')
        )
    }
    exclude 'META-INF/*.RSA', 'META-INF/*.DSA', 'META-INF/*.SF'
}

I don’t really understand why you’re using ShadowJar rather than Gradle’s build task if you don’t want a fat jar.

But if you want no external dependencies in the jar then can’t you stick with using implementation for your dependencies and use ShadowJar’s dependency exclusion for dependencies that aren’t named with your company’s group?

Off the top of my head, something like…

shadowJar {
  dependencies {
    exclude(dependency {
      it.moduleGroup != 'com.mycompany'
    })
  }
}

Or exclude by regex maybe

Thank you…

The reason we are using ShadowJar is that depending on the projects sometimes we produce the fat jar, and sometimes we don’t. For example, if the project is small enough in size, a fat jar is fine, but when it gets too big, its faster if we don’t create a fat jar. Actually that decision is based on a simple variable on the build.gradle file.

If you know a way to achieve it without using ShadowJar, it would be fine as well. I know that replacing implementation for api in that library will solve it, but we wanted to avoid it if possible. For now, that is what we are doing…