Excluding a jar from a WAR while keeping the classpath intact

I’m struggling with something that I would have thought to be straightforward. I want to include a jar with a project at compile time, but exclude it from being packaged with the resulting build while also keeping that jar in the classpath. Essentially so it’s available during both compile time in the runtime classpath, but not copied into the lib folder.

Some additional context: I have a large group of projects that are interdependent. I’ve got a multi-project build set up that is working relatively well. However, both for individual built jars within this project and WARs being built, there is massive duplication of dependencies, in some cases reaching into the gigabyte range. This is less than ideal, and what I’m trying to make better.

I’ve started with WARs, as this seems to have a solution already. However, using providedRuntime seems to cause compilation issues, while providedCompile appears to not include this in the classpath. What it seems I really want is providedClasspath, which doesn’t exist. I expect to run into this same issue with indvidual jar dependencies as well.

Also of note, I’m using the kotlin DSL, which has made trying different things a little more obtuse.

A couple of things I’ve tried:

  • configurations.all exclusions (which strips it too much, and other confg exclusions seem to put me back in the same boat I was in above, in addition to being mostly manually configured)
  • adding dependencies to both implementation and providedRuntime
  • various failed attempts at writing my own configuration

I’ve done some fairly extensive looking around in the official documentation and in other places. Most of the comments I’ve seen are fairly old around this. I’m not sure if I’m just using an older tech stack or if there is something I am not understanding. What am I missing? What is the best approach for solving this with gradle?

Most examples I found to do this are when dependencies are declared. I wanted this done by default. I learned a ton of things that I really didn’t want to care about with gradle. =) After much trial and error, I’ve found an answer that looks as though it’s going to work for our needs.

For Java jars:

// Dependencies as providedClasspath work as though they are both compileOnly and runtimeOnly. That is, in both the compile and runtime classpath and also available at compile time, but not packaged.
val providedClasspath: Configuration by configurations.creating
configurations.providedRuntime.get().extendsFrom(providedClasspath)
configurations.providedCompile.get().extendsFrom(providedClasspath)

And the WAR version:

// Dependencies as providedClasspath work as though they are both providedCompile and providedRuntime.  That is, in both the compile and runtime classpath and also available at compile time, but not packaged.
val providedClasspath: Configuration by configurations.creating
configurations.providedRuntime.get().extendsFrom(providedClasspath)
configurations.providedCompile.get().extendsFrom(providedClasspath)

This was kind of the opposite of what I would have expected going into things – existing types extending from the new configuration to pull both behaviors together. The “new” configuration is really just a bit of glue that you can then use to declare your dependencies with:

dependencies {
    providedClasspath("org.apache.commons:commons-lang3:3.4")
}

This allows for inclusion in the compile and runtime classpath without explicitly managing the packaged dependencies (along with their transitive dependencies) yourself.

I hope this saves someone else time and headache in the future. I get that not packaging all of your dependencies isn’t “best practice”, but I see people asking about this all over the place. There are lots of reasons you might be in a situation like this. And yes, it can be fixed other ways… I’m sad that there is little to no support for this out of the box though.