In Gradle, how do `bootJar` and the standard `jar` task relate, and how do layered jars change container packaging?
answer
- bootJar=executable, jar=plain library (-plain since 2.5)
- disable jar task to drop the plain jar
- layers.idx: dependencies < loader < snapshot < application
- jarmode extract -> separate Docker COPY layers -> cache hits
- bootBuildImage/build-image = buildpacks, no Dockerfile
basics
~20 sbootJar builds the executable fat jar; the standard jar task builds a plain library jar (given a -plain classifier by default since Boot 2.5). Layered jars split the fat jar into ordered layers (dependencies, app code) so Docker can cache stable layers and rebuild only what changed.
solid answer
~50 sApplying `org.springframework.boot` on top of the `java` plugin gives you two archive tasks. `bootJar` (type `BootJar`) produces the self-contained executable jar with nested dependencies; the standard `jar` task produces an ordinary library jar. Since Boot 2.5 the `jar` task stays enabled but its output gets the `plain` classifier by convention, so you see both `app.jar` and `app-plain.jar`; disable `jar` if you only want the boot jar. For containers, layered jars (enabled by default in `bootJar` since 2.4) reorganize the archive into `layers.idx`-ordered layers — typically `dependencies`, `spring-boot-loader`, `snapshot-dependencies`, `application` — from least to most frequently changing. A Dockerfile using the jarmode extract tool (or buildpacks via `bootBuildImage`) puts each layer in its own image layer, so rebuilding after a code change only invalidates the small `application` layer while the large `dependencies` layer stays cached. This slashes image build/push time.
code
kotlin · 21 lines// build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.3.0"
}
// Only want the executable jar? Turn off the plain jar (enabled since Boot 2.5).
tasks.named<Jar>("jar") { enabled = false }
// Layered jars are ON by default in bootJar; you can customize grouping:
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
layered { enabled.set(true) }
}
// Dockerfile sketch using layer extraction (Boot 3.2+ 'tools' jarmode):
// RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted
// COPY --from=builder extracted/dependencies/ ./
// COPY --from=builder extracted/spring-boot-loader/ ./
// COPY --from=builder extracted/snapshot-dependencies/ ./
// COPY --from=builder extracted/application/ ./
// ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]go deeper
Just knows bootJar makes the runnable jar.
Understands bootJar vs plain jar and that both may appear since 2.5.
Can configure the plain-jar convention and explain layered jars for Docker caching.
Drives packaging/image strategy: layering vs buildpacks, plain-jar publishing for shared modules, CI cost tradeoffs.
**Two tasks, two purposes.** The Gradle `java` plugin defines a `jar` task that builds a normal jar of your classes/resources — a *library* artifact. Spring Boot's plugin adds `bootJar` (class `org.springframework.boot.gradle.tasks.bundling.BootJar`), which builds the *executable* fat jar (nested deps + loader + `Start-Class`). `bootRun` (`BootRun`) runs the app. `assemble`/`build` wire in `bootJar`. **The plain-jar convention (Boot 2.5+).** Before 2.5, applying the Boot plugin *disabled* the `jar` task, so you got only the executable jar. Since 2.5, the `jar` task remains enabled but its archive is given the `plain` classifier by default — producing both `app-<ver>.jar` (executable, from `bootJar`) and `app-<ver>-plain.jar` (library, from `jar`). This surprised many teams. To emit only the executable jar, disable the plain one: `tasks.named('jar') { enabled = false }`. Or publish the plain jar as the main library artifact when the module is a shared library. This is the Gradle analogue of Maven's classifier discussion. **Layered jars — the container optimization.** A fat jar is one opaque blob; if you `COPY` it into a Docker image, *any* code change invalidates the whole (often 50–200 MB) layer, forcing a full re-push. Spring Boot's *layered jar* format (default in `bootJar` since 2.4; opt-in `<layers>` in Maven) splits the archive's contents into named layers recorded in `BOOT-INF/layers.idx`, ordered from least- to most-frequently-changing: - `dependencies` — released third-party jars (change rarely). - `spring-boot-loader` — the loader classes (change only on Boot upgrade). - `snapshot-dependencies` — SNAPSHOT deps (change more often). - `application` — your classes and resources (change on every build). **Using layers in Docker.** With `java -Djarmode=tools -jar app.jar extract --layers` (Boot 3.2+) or the older `-Djarmode=layertools extract`, you unpack each layer into its own directory, then `COPY` them into the image as separate `COPY` steps in that order. Because Docker caches image layers by content, editing your code only invalidates the final small `application` layer — the big `dependencies` layer is reused from cache, so builds and registry pushes are dramatically faster. You then run the app from the exploded form via the `JarLauncher` main class instead of `java -jar`. **Or skip the Dockerfile entirely.** `./gradlew bootBuildImage` (Maven: `spring-boot:build-image`) uses Cloud Native Buildpacks (Paketo) to produce an optimized, layered OCI image with no Dockerfile — it applies the same layering plus a JVM tuned for containers. **When to care (principal lens).** - High-frequency deploys / large dependency sets → layered jars or buildpacks materially cut CI time and registry egress cost. - Shared library module → keep/publish the plain jar, disable or classify the boot jar appropriately so consumers don't get the fat jar. - Simple app, infrequent deploys → the plain fat jar is fine; layering adds Dockerfile complexity for little gain. **Gotchas.** - Forgetting the plain jar exists (2.5+) can break downstream jobs that glob `*.jar` and suddenly find two. - Layered extraction runs your app from an *exploded* directory, so the launch class is still Boot's launcher, not `java -jar`; getting the Dockerfile `ENTRYPOINT` wrong is common. - `layertools`/`jarmode` naming differs across Boot versions (`layertools` pre-3.2, `tools` in 3.2+) — cite the right one for the version in play. - Custom layer definitions are possible (`layered { ... }` in Gradle, `layers` config in Maven) to further separate internal-company deps from public ones for even better cache hits.
- Since Boot 2.5 you get both `app.jar` and `app-plain.jar` from Gradle. How do you get only the executable one?Disable the standard `jar` task: `tasks.named('jar') { enabled = false }`. The plain jar carries a `plain` classifier by convention; disabling the task stops it being built while `bootJar` still produces the executable `app.jar`.
- Why do layered jars speed up Docker image builds?They split the fat jar into layers ordered by change frequency (dependencies -> loader -> snapshot -> application). Each becomes a separate Docker COPY/image layer. A code-only change invalidates just the small `application` layer, so the large, rarely-changing `dependencies` layer is served from cache, cutting build and push time.
- How would you build a layered image without writing a Dockerfile?Run `./gradlew bootBuildImage` (Maven: `spring-boot:build-image`). It uses Cloud Native Buildpacks (Paketo) to produce a layered, container-tuned OCI image directly, applying the same layer separation automatically.
saying these in an interview costs you the question
- Believing the Gradle jar task is still auto-disabled in Boot 2.5+
- Ordering layers with application code first (defeats caching)
- Thinking layered extraction still runs via java -jar rather than the launcher