Packaging and Running: Fat Jars and Graceful Shutdown

bee2026-10-0833 min read0 views
Why does one jar run standalone? Open up the fat jar, meet LaunchedURLClassLoader, then add startup flags, graceful shutdown and a systemd unit.
1 / 107
Section
0. The 30-second version
2 / 107

Deploying a Java web application used to mean four chores: install a JDK, install Tomcat, drop the war into webapps, then pray the server's versions match your laptop's. Spring Boot compresses all four into one line: java -jar app.jar. This article covers the three things behind that sentence — what is actually inside the jar, how it boots itself, and how to make it stop with dignity.

3 / 107

Six words, one line each (used throughout):

4 / 107
  • fat jar (executable jar): the jar with every dependency packed inside, also called an uber-jar; Boot's repackage goal produces it
  • MANIFEST.MF: the instruction sheet inside the jar; its Main-Class tells the JVM whose main to run
  • BOOT-INF/: the directory inside a fat jar holding your classes and dependencies — invisible to a standard JVM, reachable only through Boot's own loader
  • ClassLoader: the layer that "finds bytecode by name and turns it into a Class object"; Java lets you write your own
  • SIGTERM / SIGKILL: two Unix stop signals — the first says "pack your bags and leave", the second says "the power just went out"
  • profile / external config: the mechanism letting one jar read different files per environment, so ops never repackage to change a URL
5 / 107
类比|Analogy

a fat jar is a self-heating ration box. Rice, side dishes, water and the heating packet all live in one container; you need no kitchen (Tomcat) and no gas line (external dependencies) — pull the tab and it cooks itself. That is "bringing your own runtime". The cost is real too: the box is heavy (tens of MB), and once the tab is pulled the heating packet cannot be paused midway, so when you tear it open (shutdown timing) has to be planned. Changing the menu means swapping the box, not the whole kitchen — which is exactly why having a single artifact pays off.

6 / 107
Diagram
Figure · The map of this article: six branches from build to shutdown
Figure · The map of this article: six branches from build to shutdown
7 / 107

After this article you should be able to answer three questions:

8 / 107
  • Why must java -cp app.jar com.example.BeeApplication fail while java -jar app.jar works?
  • Why should -Xmx become -XX:MaxRAMPercentage inside a container?
  • How do you restart a production service without users noticing? (Hint: two config lines plus one signal.)
9 / 107
Section
1. Two packaging forms: why fat jars win today
10 / 107

The first time you use Spring Boot you wonder: why does java -jar app.jar run an entire web service? With classic Spring MVC you built a war, dropped it into an external Tomcat's webapps, and made sure the server had the right Tomcat version. Spring Boot stuffs all of that into a single jar.

11 / 107
Table
AspectExecutable jar (fat jar)war + external container
CarrierEmbedded Tomcat/Jetty/UndertowExternal Tomcat/WebLogic
Startupjava -jar app.jarDropped into webapps
DependenciesBundled, self-containedContainer supplies the servlet stack
Multiple instancesCopy the jar and runKeep container versions identical
Cloud-native fitNatural for containers and K8sFights the image model
StatusRecommendedLegacy systems, mandated war delivery
12 / 107

war is not useless — it serves the historical case of "the company maintains a fleet of Tomcats and apps must be deployed into them". For new projects the fat jar wins decisively: the application and its runtime become one indivisible deliverable, copy-and-run, a natural fit for one-process-per-container.

13 / 107
Note

Spring Boot can build wars too: change packaging to war and have the main class extend SpringBootServletInitializer. Without a hard "must deploy to an external container" requirement, there is no reason to give up the jar.

14 / 107
Section
2. Opening the fat jar: what is inside
15 / 107

The most direct way to understand it is to unpack it. Run unzip -l (or jar tf):

16 / 107
text
$ unzip -l bee-app-1.0.0.jar  Length      Date    Time    Name---------  ---------- -----   ----      985  2026-10-01 10:20   META-INF/MANIFEST.MF        0  2026-10-01 10:20   BOOT-INF/        0  2026-10-01 10:20   BOOT-INF/classes/     3421  2026-10-01 10:20   BOOT-INF/classes/com/example/BeeApplication.class       89  2026-10-01 10:20   BOOT-INF/classes/application.yml        0  2026-10-01 10:20   BOOT-INF/lib/  1246720  2026-10-01 10:20   BOOT-INF/lib/spring-boot-3.2.0.jar   698112  2026-10-01 10:20   BOOT-INF/lib/spring-core-6.1.0.jar       0  2026-10-01 10:20   org/springframework/boot/loader/    28456  2026-10-01 10:20   org/springframework/boot/loader/JarLauncher.class
17 / 107

Three key areas:

18 / 107
  • BOOT-INF/classes/: your own bytecode and config (application.yml, static/)
  • BOOT-INF/lib/: every third-party dependency, stored as nested jars — not unpacked classes
  • org/springframework/boot/loader/: Spring Boot's own loader and launcher code
19 / 107

Now the manifest:

20 / 107
text
Manifest-Version: 1.0Main-Class: org.springframework.boot.loader.launch.JarLauncherStart-Class: com.example.BeeApplicationSpring-Boot-Version: 3.2.0Spring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/
21 / 107
Diagram
Figure 1 · Inside a fat jar
Figure 1 · Inside a fat jar
22 / 107
Trap

precisely because BOOT-INF/classes is not at the archive root, java -cp app.jar com.example.BeeApplication always fails — the JVM's standard loader only sees com/example/... at the root, not the classes under BOOT-INF/classes. This is not a misconfiguration; it is inherent to the fat jar and only -jar can trigger its own loader.

23 / 107
Section
3. Startup internals: JarLauncher and LaunchedURLClassLoader
24 / 107

Seeing Main-Class: JarLauncher explains it: java -jar starts Spring Boot's JarLauncher, not your BeeApplication. The full chain is:

25 / 107
  1. You run java -jar app.jar
  2. The JVM reads the MANIFEST, sees Main-Class: JarLauncher, and reflectively calls its main
  3. JarLauncher reads Start-Class from the same MANIFEST and learns the real entry point is BeeApplication
  4. It creates a special LaunchedURLClassLoader and **wires BOOT-INF/lib/.jar and BOOT-INF/classes/ onto its search path*
  5. It loads and reflectively invokes BeeApplication.main with that loader
26 / 107

Extend that chain one box upstream and you have the artifact's whole life — most people only stare at java -jar, unaware that the build step repackage is what makes the jar self-running at all:

27 / 107
Diagram
Figure · From mvn package to main() running
Figure · From mvn package to main() running
28 / 107

The core problem it solves is "jar in jar": Java was never designed to hold jars inside a jar. A standard JVM reads flat archives and cannot open nested dependencies. Spring Boot's answer is a custom URL protocol plus a JarFile extension: it maps each nested jar entry to a special URL that LaunchedURLClassLoader opens one by one, so the JVM's class loading finally "sees" those nested dependencies.

29 / 107
Key point

LaunchedURLClassLoader's parent is the application class loader (AppClassLoader). It additionally takes over resource lookup under BOOT-INF, which is also why the loader code must sit at the root of the outer jar — the standard loader has to load it before any business class.

30 / 107

"Two loaders, two visibilities" is easier to follow as a hand-off than as a diagram. This animation runs it in seven frames; note frame 2, when the standard loader still has no idea your classes exist:

31 / 107
Animation
Animation · One hand-off between two class loaders
Animation · One hand-off between two class loaders
32 / 107

Now spread the five-step chain out as a single-step run. The left column is what this JVM does in order; the right panel refreshes "what Main-Class says" and "who can see whom" as you go. Watch beat 3 (the road gets built) and beat 5 (the wheel gets handed over):

33 / 107
Stepper
StepperStep by step: the five beats after java -jar1 / 6
Six beats. Before beat 3 your classes simply do not exist for the JVM; from beat 5 you are back on the startup main line every reader already knows
Code under debug
1java -jar target/bee-app-1.0.0.jar
2// 1. The JVM opens the zip and reads only Main-Class from META-INF/MANIFEST.MF
3// 2. Main-Class = org.springframework.boot.loader.launch.JarLauncher
4// 3. JarLauncher builds LaunchedClassLoader and mounts BOOT-INF/classes plus lib
5// 4. The new loader resolves the class named by Start-Class
6SpringApplication.run(BeeApplication.class, args); // everything familiar starts here
Variables now
commandjava -jar
what the JVM knowsonly the zip central directory
search pathapp.jar itself
threadmain
Call stack
1java.exe → JLI_Launch
2LauncherHelper
1The difference between -jar and -cp is settled in this first beat: -jar makes the JVM look up the entry point in MANIFEST, while -cp demands that you name a class the standard search path can actually find. With this layout it never can.
34 / 107

The same fact seen from the other side is a layering question. Click from ① to ⑤; box ② is the one most people get wrong in an interview:

35 / 107
Diagram
LayersTwo class loaders, two visibilities1 / 5
Click ① to ⑤. Box ② — 'the JVM never reads Start-Class' — is where the confident wrong answer comes from
→
→
→
→
① The archive root: only the shell
The standard AppClassLoader sees whatever sits at the top level of the jar, and up there there is only MANIFEST plus a handful of classes under org/springframework/boot/loader/**. Not one line of your business code is there.
All clearOne line to keep: Main-Class is for the JVM, Start-Class is for Boot, BOOT-INF is for the new loader.
36 / 107
Section
4. The packaging command toolbox
37 / 107

Packaging is not hard; the tricky part is what to skip and which environment to activate.

38 / 107
Table
Command / flagEffectWhen to use
mvn clean packageClean and packageEveryday local builds
-DskipTestsCompile tests but do not run themMost CI pipelines
-Dmaven.test.skip=trueDo not even compile testsEmergency when test code will not compile
-PprodActivate the prod profilePer-environment config
-UForce SNAPSHOT updatesStale dependency resolution
-Dspring.profiles.active=prodChoose the runtime profileAt run time after packaging
39 / 107

Every one of those flags assumes a precondition: the pom contains spring-boot-maven-plugin. That single line is the switch for repackage, and the watershed between "a jar that runs itself" and "a few hundred KB of ordinary archive". Tick it through — and look at the scope on test, because that is exactly what -DskipTests is arguing about:

40 / 107
Generator
GeneratorThe three pom lines that make a jar executablepom.xml2 / 6
Tick Web and Actuator and see what the parent emits once it owns the plugin; add Test and watch its scope=test; then compare against the timeline figure in Section 3 — remove the plugin and the artifact degrades into the archive that fails with no main manifest attribute
Output
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
WebAnything that serves HTTP needs it: DispatcherServlet, embedded Tomcat and JSON mapping come inside this starter.
Actuatorhealth/metrics/info endpoints; expose a whitelist, never *.
41 / 107

The flags above are only seasoning; the lifecycle is the conveyor belt itself. Before touching the pom, watch what a single mvn package actually runs, and in what order — press the four parameters in sequence:

42 / 107
Kernel lab
TeaVMWhat one mvn package really runsidle
Watch the 'bind' parameter especially: repackage is not a command you type, it is a plugin goal bound to the package phase — the mechanism-level version of this section's opening line that those three pom lines are the watershed
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
43 / 107

Now revisit the table above with those four pictures in mind: -Pprod swaps the profile, -DskipTests flips a surefire switch, but the package phase itself always drags along every goal bound to it.

44 / 107

Multi-environment packaging, declared in pom.xml:

45 / 107
xml
<profiles>    <profile>        <id>dev</id>        <activation><activeByDefault>true</activeByDefault></activation>        <properties>            <profiles.active>dev</profiles.active>        </properties>    </profile>    <profile>        <id>prod</id>        <properties>            <profiles.active>prod</profiles.active>        </properties>    </profile></profiles><build>    <resources>        <resource>            <directory>src/main/resources</directory>            <filtering>true</filtering>   <!-- let ${profiles.active} be substituted -->        </resource>    </resources></build>
46 / 107
Code
Codebash
# Build the production jar: the profile value is filtered into application.ymlmvn clean package -Pprod -DskipTests
Notes
  • -DskipTests still compiles test classes (catching broken test code); -Dmaven.test.skip=true skips compilation entirely — faster but it can hide problems
  • filtering replaces @profiles.active@ (or ${profiles.active}) in application.yml with the active profile value
  • Build-time and run-time profiles are different things: building decides "which config goes into the jar", running decides "which config is read" — do not conflate them
47 / 107
Section
5. Startup flags: from -Xmx to profiles
48 / 107

java -jar takes more than a path; keep JVM flags and application arguments separate:

49 / 107
Code
Codebash
java -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \     -Dspring.profiles.active=prod -Dserver.port=8080 \     -jar bee-app-1.0.0.jar \     --logging.level.com.example=DEBUG
Notes
  • JVM flags go before -jar and are parsed by the JVM: -Xms/-Xmx (heap), -XX:+UseG1GC (collector), -XX:MaxGCPauseMillis (pause goal)
  • System properties use -D: -Dspring.profiles.active=prod passes the profile as a system property
  • Application arguments go after the jar with --: --logging.level.com.example=DEBUG overrides config, with the highest precedence
50 / 107
Section
5.1 JVM tuning basics and common myths
51 / 107
Table
FlagMeaningStarter advice
-XmsInitial heapSet equal to -Xmx to avoid repeated heap growth
-XmxMax heapIn containers, 50–75% of the limit, leaving room for metaspace and direct memory
-XX:+UseG1GCUse the G1 collectorDefault on JDK 9+, friendlier for large heaps
-XX:MaxRAMPercentageHeap as a percentage of RAMPreferred in containers, e.g. 75.0
-XssThread stack sizeUsually untouched; shrink only with very many threads
52 / 107
Trap

"bigger -Xmx is better" is the classic myth. A larger heap lengthens Full GC pauses, and the real footprint is heap + metaspace + thread stacks + direct memory + JVM overhead. In a production container the safest form is -XX:MaxRAMPercentage=75.0 — let the JVM compute the heap from the cgroup limit instead of hard-coding an -Xmx that ignores the container.

53 / 107
Section
6. Graceful shutdown: never sever a request with kill -9
54 / 107

This is the most important part of the article. Many production "data inconsistency" incidents come not from business code but from stopping the process the wrong way.

55 / 107

Think about what kill -9 does: the JVM receives SIGKILL and no callback runs at all. A "debit plus reward points" transaction that has debited but not yet credited is torn in half; a request read into memory but not yet returned is cut off and the client sees a connection reset; pooled connections are ripped away while the database still holds an "uncommitted transaction".

56 / 107

Spring Boot offers graceful shutdown in two lines:

57 / 107
Code
Codeyaml
server:  shutdown: graceful                 # default is immediatespring:  lifecycle:    timeout-per-shutdown-phase: 30s  # max wait per shutdown phase
Notes
  • server.shutdown: graceful switches the web server from "immediate" to "graceful"
  • timeout-per-shutdown-phase: 30s bounds the "wait for in-flight requests" phase; without a bound it could wait forever
58 / 107
Animation
Animation · Six steps of a graceful shutdown
Animation · Six steps of a graceful shutdown
59 / 107

Once enabled, a normal kill <pid> (no -9) follows this complete path:

60 / 107
  1. Receive SIGTERM: kill, systemd stop, docker stop and K8s pod deletion all send this, not -9
  2. Stop accepting new requests: the web connector closes; the load balancer detects unhealthy and drains traffic
  3. Wait for in-flight requests: bounded by timeout-per-shutdown-phase, or until they drain
  4. Close the context and fire destroy callbacks: @PreDestroy, DisposableBean#destroy, @Bean(destroyMethod) in order
  5. Shut down pools and resources: thread pools, data sources, messaging clients wrap up
  6. JVM exits
61 / 107
Warning

graceful shutdown only responds to SIGTERM; kill -9 skips everything above. So the first rule of production ops is: always kill <pid> first, watch the logs for "graceful shutdown complete", and only then consider -9. Giving the stop script enough timeout (e.g. systemd's TimeoutStopSec) is far cheaper than chasing data inconsistency later.

62 / 107
Section
7. Production run scripts: shell and systemd
63 / 107
Section
7.1 A start/stop/restart script
64 / 107

Plain nohup java -jar ... & has two problems: no PID to stop cleanly, and no idea whether the service actually started. A practical script manages the PID and waits for a health check:

65 / 107
Code
Codebash
#!/usr/bin/env bashset -euo pipefailAPP_NAME="bee-app"JAR="bee-app-1.0.0.jar"PID_FILE="${APP_NAME}.pid"LOG_FILE="logs/startup.log"JVM_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxRAMPercentage=75.0"start() {  if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then    echo "$APP_NAME already running (pid $(cat "$PID_FILE"))"; exit 0  fi  mkdir -p logs  nohup java $JVM_OPTS -jar "$JAR" --spring.profiles.active=prod \    >> "$LOG_FILE" 2>&1 &  echo $! > "$PID_FILE"  printf 'waiting for health check'  for _ in $(seq 1 30); do    if curl -sf http://localhost:8080/actuator/health | grep -q '"status":"UP"'; then      echo ' UP'; return 0    fi    printf '.'; sleep 1  done  echo ' FAILED'; exit 1}stop() {  [ -f "$PID_FILE" ] || { echo 'not running'; exit 0; }  PID="$(cat "$PID_FILE")"  kill "$PID"                     # send SIGTERM to trigger graceful shutdown  for _ in $(seq 1 30); do    kill -0 "$PID" 2>/dev/null || { rm -f "$PID_FILE"; echo 'stopped'; return 0; }    sleep 1  done  echo 'timeout, force kill'; kill -9 "$PID"; rm -f "$PID_FILE"}case "${1:-}" in  start)   start ;;  stop)    stop ;;  restart) stop; start ;;  *) echo "usage: $0 {start|stop|restart}"; exit 1 ;;esac
Notes
  • nohup ... & starts it in the background, and echo $! > PID_FILE records the child so you can stop it precisely
  • After startup it polls /actuator/health and only treats UP as success, avoiding a "started but not ready" illusion
  • stop sends SIGTERM first and waits up to 30 seconds — leaving graceful shutdown a way out — before falling back to -9
66 / 107
Section
7.2 systemd: let the system supervise it
67 / 107

On production hosts the cleaner approach is systemd, which supports graceful shutdown natively (it sends SIGTERM by default) and restarts on crash:

68 / 107
Code
Codeini
[Unit]Description=Bee Spring Boot ApplicationAfter=network.target mysql.serviceWants=mysql.service[Service]Type=simpleUser=appWorkingDirectory=/opt/beeEnvironment="SPRING_PROFILES_ACTIVE=prod"EnvironmentFile=/opt/bee/app.env          # secrets such as DB_PASSWORD live here, not in the unitExecStart=/usr/bin/java -Xms512m -Xmx512m -XX:+UseG1GC \  -XX:MaxRAMPercentage=75.0 -jar /opt/bee/bee-app.jarSuccessExitStatus=143                      # JVM exits 143 on graceful stop; treat it as successTimeoutStopSec=30                          # give graceful shutdown time before SIGKILLRestart=on-failureRestartSec=5[Install]WantedBy=multi-user.target
Notes
  • SuccessExitStatus=143: after SIGTERM the JVM's normal exit code is 143 (128+15); without this systemd would mark it failed
  • TimeoutStopSec=30: mirrors the app's timeout-per-shutdown-phase; systemd only sends SIGKILL after it expires
  • Restart=on-failure: auto-restart on abnormal exit, covering part of what a process supervisor does
  • EnvironmentFile: keeps passwords out of the unit file and out of version control
69 / 107
Section
8. Health checks and rolling deploys
70 / 107

On K8s or multi-instance environments, /actuator/health is how the orchestrator decides whether an instance may take traffic. It distinguishes two probes: readiness (join load balancing) and liveness (restart the instance). Graceful shutdown plus a readiness probe is what makes "drain traffic, then stop the process" invisible to users.

71 / 107
yaml
management:  endpoint:    health:      probes:        enabled: true  endpoints:    web:      exposure:        include: health,info
72 / 107

Enabling probes exposes dedicated /actuator/health/readiness and /actuator/health/liveness endpoints — the next article, "Docker Deployment", wires them into an orchestration file.

73 / 107
Section
9. Three frequent traps
74 / 107
Section
9.1 Do not read jar resources with File
75 / 107

After packaging, application.yml, templates and certificates live inside the jar, not on a filesystem path:

76 / 107
Code
Codejava
// Bad: a jar resource has no real file path; this throws FileNotFoundException when packagedFile file = new File("classpath:config/rules.json");// Good: always use ClassPathResource or the classpath: prefixResource resource = new ClassPathResource("config/rules.json");InputStream in = resource.getInputStream();
Notes

Trap: new File("src/main/resources/xxx") works in the IDE and fails with FileNotFoundException the moment you package. src/main/resources becomes BOOT-INF/classes in the jar, and that directory no longer exists. Any resource bundled into the jar must be read via ClassPathResource or getResourceAsStream.

77 / 107
Section
9.2 Changing in-jar config means repackaging
78 / 107

To change a database URL, many people unzip the jar, edit the config, and zip it back — easily corrupting the zip structure. The right approach is external config override: Spring Boot prefers application.yml next to the jar or in a config/ subdirectory.

79 / 107
Code
Codetext
/opt/bee/├── bee-app.jar├── application.yml           # same directory, higher priority than inside the jar└── config/    └── application-prod.yml  # config/ subdirectory, even higher
Notes

Tip: external config priority is command-line args > config/ next to the jar > the jar's directory > classpath:/config/ inside > classpath:/ inside. Ops only touch the config/ directory and never edit the jar — that is sustainable.

80 / 107

That chain — "edit a file" to "never repack again" — deserves its own walkthrough, because shipping a release every time a database password changes is a lesson nearly every team has paid for:

81 / 107
Animation
Animation · One jar, five config layers
Animation · One jar, five config layers
82 / 107
Section
9.3 Slow startup is not always the code
83 / 107

If a line like Started BeeApplication in 8.3 seconds keeps growing, something heavy happens at startup: scanning many beans, initializing pools, warming caches, fetching remote config. Use --debug or add spring-boot-starter-actuator to inspect startup metrics and find the slow segment instead of optimizing blindly.

84 / 107

The "it runs in the IDE but not under java -jar" story has a nastier variant: the application starts fine, and then some bean trips over a resource that never becomes a real file on disk. Here is that live scene — do not read the notes yet, click the frame you believe is the culprit:

85 / 107
Triage
Error triageIllegalStateException: class path resource [certs/apiclient_cert.pem] cannot be resolved to absolute file path
Perfect in the IDE, unreadable in the jar: the certificate falls out of the filesystem

WeChat Pay requires the merchant certificate to be passed to the SDK as a File. On the developer's machine everything works; delivered to the server, startup fails immediately. Ops follows the error, creates a certs directory under /opt/bee, and it still fails.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'wxPayInitializer': Invocation of init method failed
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853)
at com.example.pay.WxPayInitializer.<init>(WxPayInitializer.java:31)
Caused by: java.lang.IllegalStateException: class path resource [certs/apiclient_cert.pem] cannot be resolved to absolute file path because it does not reside in the file system: jar:nested:/opt/bee/bee-app.jar/!BOOT-INF/classes/!/certs/apiclient_cert.pem
at org.springframework.util.ClassLoaderUtils.getFile(ClassLoaderUtils.java:107)
at org.springframework.core.io.AbstractResource.getFile(AbstractResource.java:129)
at org.springframework.core.io.ClassPathResource.getFile(ClassPathResource.java:189)
at com.example.pay.WxPayInitializer.loadCertificate(WxPayInitializer.java:47)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
86 / 107
Section
10. Interactive demo: from boot to ready, what the container is doing
87 / 107

From the moment Spring Boot's main is called to the moment it can serve, a great many beans are created and wired. This demo runs the container's assembly — understanding bean creation order means understanding where startup time goes.

88 / 107
Kernel lab
89 / 107
  • Toggle the condition switches (@ConditionalOn*) and watch the same code assemble different beans
  • More beans and deeper dependency chains mean slower startup — the root cause of "a large app takes tens of seconds to boot"
  • To speed it up: trim unnecessary auto-configurations, use lazy-initialization, or move heavy work to after startup
90 / 107
Section
Labs: the jar, the image, the shutdown
91 / 107

The second lab opens the fat jar itself, so the layout stops being a diagram you memorized and becomes something you can look inside. Start with layout, then loader.

92 / 107
Kernel lab
TeaVMInside the fat jar: BOOT-INF, lib, and the two class loadersidle
Walk layout, then loader, to see why JarLauncher is at the archive root and your classes are not on the default classpath
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
93 / 107

The third lab moves one level up: what the JVM does while Docker builds an image, and why the image is far larger than the jar it contains.

94 / 107
Kernel lab
TeaVMFrom jar to image: layers, build cache and memory limitsidle
Try layer, then build and mem, to see which work Docker can cache and where a 512MiB limit bites
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
95 / 107

The fourth lab walks the shutdown path — the one that decides whether a deploy corrupts data or not.

96 / 107
Kernel lab
TeaVMThe shutdown path: SIGTERM, draining, then the killidle
Walk ready, drain, kill and gray in order to see the four stages a rolling deploy puts a pod through
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
97 / 107

Enough buttons — type the same things yourself. This console talks to the same in-browser kernel, and every reply is computed there: start with whoami to see which class loader is running, probe the app with curl /actuator/health, then walk the labs one by one:

98 / 107
Console
99 / 107
Note

run lab fatjar loader right after boot and read the two views together — the first is the container wiring beans, the second is the loader laying the road. The readiness probe from Section 8 only turns UP once that second line finishes, which is why the Section 7 script polls the health endpoint instead of sleeping for ten seconds.

100 / 107
Section
Sandbox: the container has 512MiB, how should the heap be set
101 / 107
Sandbox
SandboxThe container has 512MiB: how should the heap be set
Result
outcome: the heap gets roughly 358MiB and the rest is left to non-heap memory
why: MaxRAMPercentage is read from the cgroup limit, so it follows the container
this is the recommended default for containerized Boot apps
`server.shutdown: graceful` and `MaxRAMPercentage` belong in the same Dockerfile story: both read the container's real limits instead of assuming a whole machine.
102 / 107
Section
Quick quizzes
103 / 107
Quiz
Check yourselfYou edited `src/main/resources/application.yml` to change the database URL, ran `mvn package`, deployed, and the service still uses the old URL. What is the most likely cause?
Pick one — you get feedback right away
104 / 107
Quiz
Check yourselfA rolling deploy sends SIGTERM to the old pod. Which sequence is correct for a Spring Boot app that has `server.shutdown: graceful` configured?
Pick one — you get feedback right away
105 / 107
Section
11. Production judgment: java -jar or run in a container
106 / 107
Decision
Decisionyou are deploying a Spring Boot app to production, and the team has both bare metal and K8s. How should the build artifact and the run mode be set?
107 / 107
Summary

take away three things — a fat jar's Main-Class is JarLauncher, not your main class, and LaunchedURLClassLoader solves the jar-in-jar loading problem; build flags decide what goes into the jar while run flags decide what config is read, so do not mix them; graceful shutdown is a production discipline: kill <pid> with SIGTERM first, let it run "stop new requests → drain in-flight → destroy beans → release resources", and only then -9. Get these three right and delivery is largely stable.