skip to content

Explain the manifest's Main-Class vs Start-Class in a Spring Boot jar. Why the two-level indirection?

level: middleimportance: should knowfreq 45%

answer

  1. Main-Class = JVM entry (standard) = JarLauncher
  2. Start-Class = your app (Spring-only attribute)
  3. chicken-and-egg: nested deps not visible at JVM start
  4. loader classes unpacked at root so plain CL loads them
  5. multiple mains → set mainClass/start-class

basics

~10 s

Main-Class is what the JVM runs for java -jar — Spring sets it to JarLauncher. Start-Class is your real @SpringBootApplication main. JarLauncher first fixes the classpath for nested jars, then calls Start-Class.

solid answer

~40 s

The JAR spec says `java -jar` executes the manifest's Main-Class. Spring Boot can't put your app there directly, because when the JVM starts it, the nested BOOT-INF/lib jars aren't yet on any classpath your dependencies need. So Spring sets Main-Class to JarLauncher (a loader class stored unpacked at the jar root, loadable by the plain classloader) and records your actual entry point in a custom manifest attribute, Start-Class. JarLauncher runs, constructs a LaunchedClassLoader that can read the nested jars, reads Start-Class, and reflectively invokes its main. The indirection is purely a bootstrapping mechanism: you need a launcher that runs with a minimal classpath, sets up the real one, then hands off. Both attributes are written automatically by the repackage/bootJar step; Start-Class comes from your @SpringBootApplication (or an explicitly configured mainClass).

code

text · 17 lines
text
// Gradle: pin the Start-Class when several main() candidates exist
springBoot {
    mainClass = 'com.example.MyApp'
}

<!-- Maven: same idea -->
<properties>
  <start-class>com.example.MyApp</start-class>
</properties>
<!-- or -->
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <mainClass>com.example.MyApp</mainClass>
  </configuration>
</plugin>

go deeper

for a junior

Know Main-Class=launcher, Start-Class=your app.

for a middle

Explain the classpath chicken-and-egg reason for the indirection and where Start-Class is configured.

for a senior

Discuss loader-classes-at-root, related Spring-Boot-* manifest keys, and multi-main configuration.

for a principal

Reason about manifest across jar/war/PropertiesLauncher layouts and the 3.2 loader package move.

## The two manifest attributes ``` Main-Class: org.springframework.boot.loader.launch.JarLauncher Start-Class: com.example.MyApp ``` - **`Main-Class`** is a **standard** JAR manifest attribute. Per the Java spec, `java -jar app.jar` looks it up and runs that class's `public static void main`. Nothing custom here. - **`Start-Class`** is a **Spring-Boot-specific** attribute. The JVM ignores it entirely; only JarLauncher reads it. ## Why not put your app in Main-Class directly? Because of a **chicken-and-egg classpath problem**: - At the instant `java -jar` starts, the only classpath is the outer jar itself, read by the ordinary system classloader. - That classloader **cannot see classes inside nested jars** (`BOOT-INF/lib/*.jar`). If your app were Main-Class, the very first dependency it touched (Spring, Jackson, etc.) would throw `NoClassDefFoundError`. - So you need a **small bootstrap** that (a) is loadable by the plain classloader — which is why the `org.springframework.boot.loader.*` classes are stored **unpacked at the jar root**, not nested — and (b) knows how to build a richer classloader. JarLauncher is that bootstrap. It runs on the minimal classpath, sets up `LaunchedClassLoader` over `BOOT-INF/classes` + `BOOT-INF/lib`, then reads `Start-Class` and hands control to your real application. ## Where the values come from - **Main-Class** — set by the Spring Boot build plugin to the appropriate launcher (`JarLauncher` for jars, `WarLauncher` for wars, or `PropertiesLauncher` if configured). - **Start-Class** — the plugin finds your class with a `main` method annotated (typically) with `@SpringBootApplication`. If there are several candidates you must configure it explicitly: - Maven: `<properties><start-class>com.example.MyApp</start-class></properties>` or the plugin's `mainClass`. - Gradle: `springBoot { mainClass = 'com.example.MyApp' }`. ## Related manifest keys you'll see - `Spring-Boot-Classes: BOOT-INF/classes/` - `Spring-Boot-Lib: BOOT-INF/lib/` - `Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx` - `Spring-Boot-Version: <version>` These tell the launcher where things are and pin the loader's expectations. ## Gotchas - **Editing Main-Class to your app by hand breaks it** — you lose the nested-jar classloader setup. - **Multiple main methods** without configuring `mainClass`/`start-class` causes a build error ("Unable to find a single main class"). - **`java -jar` is required**; running your Start-Class via `-cp` on the fat jar won't find nested deps. - The launcher package **moved** in Boot 3.2 from `org.springframework.boot.loader.JarLauncher` to `org.springframework.boot.loader.launch.JarLauncher`; a hand-written manifest referencing the old path against new loader jars won't resolve. ## When to care Any time you inspect a built jar's `META-INF/MANIFEST.MF` for debugging ("why won't it start?"), or when a multi-main-class project needs an explicit entry point, this two-attribute model is the thing to reason about.

  • Is Start-Class a standard JAR manifest attribute?
    No. Main-Class is standard (the JVM reads it for `java -jar`). Start-Class is a Spring-Boot-specific attribute that only the launcher reads; the JVM ignores it.

saying these in an interview costs you the question

  • Saying the JVM reads Start-Class (it doesn't — only JarLauncher does).
  • Claiming the indirection is for security or obfuscation (it's purely a classpath bootstrap).
  • Believing you can swap Main-Class to your app class and still load nested deps.

context