Your First Spring Boot App: Hello World in Three Minutes

bee2026-10-0847 min read0 views
From start.spring.io to your first browser refresh: how the project is built, what every line of the main class means, where embedded Tomcat comes from and why web.xml is gone.
1 / 136
Section
0. The 30-second version
2 / 136

In the previous module you stood up a web app with plain Spring: web.xml, applicationContext.xml, an external Tomcat — none optional. Spring Boot does exactly one thing: it moves all that "ceremony to make the framework run" into jars where someone already wrote it. You create a project, add one dependency, write one class with a main method, and everything else is handled. So this article is not a tour of Boot's features; it is about the eight things that line of SpringApplication.run(...) does for you on your very first run, and how to confirm it really happened.

3 / 136
类比|Analogy

plain Spring is a shell apartment — you lay the plumbing yourself, paint the walls yourself, install the door lock yourself, and you live in a construction site for three months first. Spring Boot is a fully finished apartment: water, electricity, network, boiler and smart lock are all installed, and you move in with one suitcase (your business code). It is also polite about it: if you already put your own sofa in the living room, it will not place its sofa there. We verify that sentence at the end of this article.

4 / 136
Diagram
Figure · Chapter map: the eight steps behind one line of main
Figure · Chapter map: the eight steps behind one line of main
5 / 136

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

6 / 136
  • Why is there no web.xml in a Boot project? Which dependency does embedded Tomcat actually come out of?
  • What are the eight steps behind that single SpringApplication.run() line, and which log line proves "I can browse it now"?
  • Why does the fat jar from mvn package run with just java -jar? What must the target machine have preinstalled?
7 / 136
Section
1. What Spring Boot actually solves
8 / 136

Before writing the first line of Boot code, look back at the "configuration burden checklist" the previous module left behind. To stand up an ordinary web app with plain Spring, you had to prepare all of this:

9 / 136
  • A web.xml registering the DispatcherServlet and its init-param
  • An applicationContext.xml declaring component scanning, the data source and the transaction manager
  • A spring-mvc.xml registering the view resolver and message converters
  • Dropping the resulting war into an external Tomcat, installing it, starting it and watching the logs
10 / 136

None of those four is your business. They are all ceremony to make the framework run. Spring Boot removes exactly that ceremony — its official one-liner is Convention over Configuration. In practice it means three things:

11 / 136
Table
The burden in plain SpringSpring Boot's answer
Manually managing dozens of dependencies and versionsOne starter dependency; versions decided by the parent POM
Hand-writing XML / Java config classesAuto-configuration: conditional wiring based on classpath and existing beans
Deploying to an external TomcatEmbedded Tomcat; just java -jar
12 / 136
Diagram
Figure 1 · The skeleton of a Boot startup
Figure 1 · The skeleton of a Boot startup
13 / 136
Section
2. Generating the project with start.spring.io
14 / 136

The official scaffold start.spring.io is the standard entry point (IDEA's New Project → Spring Initializr hits the same service). Fill in the options by copying this table:

15 / 136
Table
OptionChoiceNotes
ProjectMavenGradle works too; this guide uses Maven
LanguageJava—
Spring Boot3.2.xMatches JDK 17 and above
Groupcom.examplePackage prefix
ArtifactdemoProject / artifact name
PackagingJarNo more war
Java17Consistent with the previous module
DependenciesSpring WebThis one alone is enough for Hello World
16 / 136

Click Generate, download the zip, extract and open it in IDEA. Note that the Artifact decides the main class name: enter demo and you get DemoApplication.

17 / 136
Tip

adding only Spring Web is deliberate. Boot loads dependencies on demand — the more you add, the more auto-configuration runs and the slower startup gets. Keep the first attempt minimal so you can clearly see what one starter brings.

18 / 136
Section
Before you hit Generate: tick the boxes and watch what the pom becomes
19 / 136

Those checkboxes on Initializr are nothing more than entries appended to <dependencies>. The generator below follows the exact same assembly rules as this lesson: tick one, the pom changes immediately, and under the output it states why that line exists and what breaks without it.

20 / 136

Suggested path: tick Spring Web only → notice that not a single version number appears outside <parent>; add Data JPA and MySQL driver → count which three entries show up and why the driver is <scope>runtime</scope>; add Lombok → see why its scope is provided.

21 / 136
Generator
GeneratorStarter picker (same rules as the lesson text)pom.xml1 / 11
Tick only what you need right now. More ticks mean more auto-configuration at startup and a wider surface for errors
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>
    </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.
22 / 136
Section
3. Project structure: file by file
23 / 136

Open the project and the tree looks like this (the standard Boot skeleton):

24 / 136
text
demo/├── pom.xml                                  # dependencies and build config├── src/│   ├── main/│   │   ├── java/com/example/demo/│   │   │   └── DemoApplication.java         # main class: the only required one│   │   └── resources/│   │       ├── application.properties       # global config (nearly empty by default)│   │       ├── static/                       # static assets (css / js / images)│   │       └── templates/                    # templates (Thymeleaf etc.)│   └── test/java/com/example/demo/│       └── DemoApplicationTests.java        # generated test class└── mvnw / mvnw.cmd                          # Maven Wrapper, no preinstalled Maven needed
25 / 136

Look first at the two key blocks in pom.xml — the parent and the starter:

26 / 136
Code
Codexml
<!-- 1. parent: inherit Spring Boot defaults; versions are decided here --><parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>3.2.5</version>    <relativePath/></parent><dependencies>    <!-- 2. starter: one dependency enables a whole family of web capabilities -->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency></dependencies>
Notes
  • spring-boot-starter-parent is the single source of truth for dependency versions: below it you add dependencies without a <version>, because it pins a compatible set
  • spring-boot-starter-web is the archetypal starter: it contains almost no code itself; its job is to pull in the "family of dependencies needed for web development" in one go
  • application.properties is nearly empty by default, because defaults live in auto-configuration; you only write it to deviate from a default

Note: that mvnw (Maven Wrapper) is a thoughtful touch — it downloads a suitable Maven automatically, so builds work even on a machine without Maven installed, ending the "works on my machine" version drift forever.

27 / 136
Section
4. The main class, line by line
28 / 136

Of the whole project, the main class is the only one you must write. It is absurdly short, yet every line deserves explanation:

29 / 136
java
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class DemoApplication {    public static void main(String[] args) {        SpringApplication.run(DemoApplication.class, args);    }}
30 / 136

@SpringBootApplication is a "three-in-one" annotation; unwrapped, it is three things:

31 / 136
Table
Component annotationPurpose
@SpringBootConfigurationEssentially @Configuration, marking this as a config class
@EnableAutoConfigurationEnables auto-configuration so Boot wires beans conditionally
@ComponentScanScans @Component / @Service / @Controller under this package and below
32 / 136
  • The return value of SpringApplication.run(DemoApplication.class, args) is an ApplicationContext — the IoC container itself. You can capture it, e.g. ConfigurableApplicationContext ctx = SpringApplication.run(...), then ctx.getBean(...) to verify wiring
  • The main method holds only that one line, yet far more than one line happens: creating the container, loading auto-configuration, starting embedded Tomcat and publishing the ready event are all a chain reaction from it
33 / 136
Key point

@ComponentScan only scans "the main class's package and its subpackages". So the main class must sit in the outermost package (like com.example.demo) with business code in subpackages (com.example.demo.controller). Put the main class inside a controller subpackage and sibling or outer beans go unscanned — the very first trap a beginner hits.

34 / 136
Section
5. Writing your first Controller
35 / 136

Create a class under the com.example.demo.controller package:

36 / 136
Code
Codejava
package com.example.demo.controller;import org.springframework.web.bind.annotation.*;@RestControllerpublic class HelloController {    @GetMapping("/hello")    public String hello() {        return "Hello, Spring Boot!";    }    // Path variable: /hello/alice returns Hello, alice!    @GetMapping("/hello/{name}")    public String helloTo(@PathVariable String name) {        return "Hello, " + name + "!";    }}
Notes
  • @RestController = @Controller + @ResponseBody: the returned string goes straight into the response body, no view lookup
  • @GetMapping("/hello") maps this method to GET /hello
  • @PathVariable binds the {name} segment of the URL to the parameter
37 / 136

The @RestController vs @Controller distinction is worth one line on its own: use @Controller to return a view (HTML), use @RestController to return JSON or plain text. Mixing them makes "I returned an object but the browser shows 404, page not found".

38 / 136

This difference is not something you memorise — it is something you press. The kernel experiment below runs all four cases: view shows what happens when @Controller returns hello and Spring goes looking for a template by that name, failing into a whitelabel page; json shows what the very same method writes into the body once the annotation is swapped; missing shows which stage actually pronounces the 404; and string shows how return "ok" is interpreted differently under the two annotations — the real answer to "my endpoint returns a string but the browser says 404".

39 / 136
Kernel lab
TeaVMSame returned string, two annotations, two fatesidle
Go view → json → missing → string. On string, reread the 'returned an object but got 404' sentence just above — that is this mode.
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
40 / 136
Section
6. Reading the startup log line by line
41 / 136

Run DemoApplication and the console prints something like this (simplified to the real format):

42 / 136
Code
Codetext
  .   ____          _            __ _ _ /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/  ___)| |_)| | | | | || (_| |  ) ) ) )  '  |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot ::                (v3.2.5)2024-05-20T10:00:00.123  INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer :  Tomcat initialized with port(s): 8080 (http)2024-05-20T10:00:01.456  INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer :  Tomcat started on port(s): 8080 (http) with context path ''2024-05-20T10:00:01.789  INFO --- [main] com.example.demo.DemoApplication :  Started DemoApplication in 1.234 seconds (process running for 2.101)
Notes
  • That ASCII art is the banner, customizable via resources/banner.txt or disabled with spring.main.banner-mode=off
  • Tomcat initialized with port(s): 8080 means embedded Tomcat has bound the port
  • Started DemoApplication in 1.234 seconds is the "ready" signal — only after this line can the browser succeed
43 / 136

Where does embedded Tomcat come from? It hides in the transitive dependencies of spring-boot-starter-web. Run mvn dependency:tree and you see this tree:

44 / 136
text
spring-boot-starter-web ├── spring-boot-starter │    ├── spring-boot │    ├── spring-boot-autoconfigure │    └── spring-boot-starter-logging ├── spring-boot-starter-json ├── spring-boot-starter-tomcat        <-- embedded Tomcat comes from here │    ├── tomcat-embed-core │    ├── tomcat-embed-el │    └── tomcat-embed-websocket └── spring-webmvc      └── spring-web
45 / 136

That explains why web.xml is gone: in an external-Tomcat setup the servlet container starts first, then loads your war and reads web.xml; Boot reverses the order — your main starts first and starts Tomcat itself (TomcatServletWebServerFactory does it inside auto-configuration), so servlet registration becomes Java code and no XML descriptor is needed.

46 / 136
Diagram
Figure · Where embedded Tomcat really comes from
Figure · Where embedded Tomcat really comes from
47 / 136

That figure squashes the road between "one checkbox ticked" and "port 8080 answers" into two columns. To see how the work is handed along, click through the path below — box 3 is the throat of the whole chain: if the condition fails, the four boxes after it never happen.

48 / 136
Diagram
FlowFrom ticking Spring Web to a reachable 8080 (click through it)1 / 6
Go from ① to ⑥. Box ③ is the one people skip: conditions are evaluated before anything is wired — that is where all of Boot's 'magic' lives
→
→
→
→
→
① One starter line in the pom
All you write is `<artifactId>spring-boot-starter-web</artifactId>`, with no version at all — the parent adjudicates versions. That line's only job is to declare intent: 'I am building a web app'. It contains no code.
All clearIn one line: the starter lays down the classpath, conditions decide what gets wired, and refresh() is what actually lifts Tomcat.
49 / 136
Section
7. Three ways to run it
50 / 136

The same main class can run three ways, each suited to a different situation:

51 / 136
Table
WayCommand / actionBest for
Run in IDEAClick the green triangle by mainDaily development; debugging with breakpoints is easiest
Maven pluginmvn spring-boot:runCommand-line development, or when the IDE is awkward
Executable jarmvn package, then java -jar target/demo-0.0.1-SNAPSHOT.jarDeployment, CI verification, close to production
52 / 136
Section
8. Packaging and running: a lead-up to deployment
53 / 136

First package from the project root:

54 / 136
Code
Codebash
# Skip tests for now; the test class gets its own module latermvn clean package -DskipTests# The artifact is an "executable jar" (fat jar) with Tomcat and all deps insidejava -jar target/demo-0.0.1-SNAPSHOT.jar
Notes
  • Boot's spring-boot-maven-plugin builds a fat jar: one jar containing app code, third-party dependencies and embedded Tomcat, so java -jar runs it standalone
  • This jar is the basis for the later "Docker deployment" and "CI/CD" sections — deploying to a server needs only a JRE, not a preinstalled Tomcat

Tip: -DskipTests is temporary. Tests before packaging are the last line of defense; once the JUnit 5 module is done, drop this flag.

55 / 136
Section
9. Traps: three problems every first run hits
56 / 136
Table
SymptomRoot causeDiagnosis / fix
Port 8080 was already in use8080 is taken by another processSet server.port=8081, or kill the process
The browser shows 404Wrong path / class out of scan range / forgot @RestControllerCheck in order: the URL, whether the package is under the main class, and whether the annotation is @RestController
Code changed but the response did notOld process / not recompiledStop the old process and rerun; check why hot reload did not fire
57 / 136
Trap

404 troubleshooting must follow an order — do not guess. Step one: is the URL exactly equal to the mapping (including case and prefix)? Step two: is the Controller in a subpackage of the main class, since @ComponentScan only scans subpackages? Step three: is the annotation @RestController (using @Controller without a view resolver yields a 404)? These three steps locate the vast majority of 404s.

58 / 136

The reason those three steps come in this order is that each one rules out a whole class of cause: a wrong URL means "you knocked on the wrong door"; a package outside the subtree means "the container never saw this class"; a wrong annotation means "your return value was thrown away as a view name". Walk the six frames below and you get the reflex, not the rhyme:

59 / 136
Animation
Animation · The route through your first 404
Animation · The route through your first 404
60 / 136
Section
A scene: the first run crashes — which banner line can you actually act on
61 / 136

Boot’s failure analyser lays the root cause out as a banner in the middle of the log. The beginner instinct is to read every line top-to-bottom, or to start scrolling the stack trace below it. Both are slow. Only three lines in that banner carry information — click the one that matters:

62 / 136
Triage
Error triageAPPLICATION FAILED TO START
Reading the failure banner: which line is actionable

You finished HelloController exactly as written and ran ./mvnw spring-boot:run. The log scrolls for ten seconds, then this banner appears in the middle.

APPLICATION FAILED TO START
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
63 / 136
Section
10. Something you must know: why you restart after a code change
64 / 136

By default Boot does not restart when you edit code, because the running JVM has already loaded the classes into memory. To get "changes take effect immediately", add spring-boot-devtools:

65 / 136
xml
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-devtools</artifactId>    <optional>true</optional>   <!-- key: optional, so it does not leak downstream --></dependency>
66 / 136

"Nothing changed" is worth ten explanations, but you only believe it once you have stepped through it. The debug desk below spreads six lines out and refreshes two things on the right as you go: how many HelloController class objects exist in memory, and who loaded them. Watch steps 3 and 6 — one of those numbers never moves, the other resets to zero.

67 / 136
Stepper
StepperStep by step: you edited the code, so why is the response still old?1 / 6
Six beats. Keep your eye on 'Class objects in the method area' — that cell decides what the browser sees
Code under debug
1// You edit the string returned by hello() in IDEA and press Ctrl+S
2// The running JVM: a Class object for HelloController already sits in the method area
3// javac only rewrites the file on disk: target/classes/HelloController.class
4// That old Class object never goes back to re-read the disk
5// Browser refresh -> the same bean in the same container -> the same Class -> the old text
6// What devtools actually does: drop RestartClassLoader and load the bytecode again through a fresh one
Variables now
source savedyes
diskstill the previous .class at this instant
processthe same one, not restarted
Call stack
1IDEA → Save → javac
1Saving only touches the source file (and, if the IDE compiles on save, the output directory). From here on, disk and memory hold two different truths — and the JVM only ever consults memory.
68 / 136
Kernel lab
TeaVMFrom save to run: which step IDEA quietly redoes for youidle
Press compile output first to watch .java become .class under target/classes, then hot reload to see which loader devtools threw away. Reverse the two and you get a genuine taste of 'I ran the old build'
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
69 / 136
Section
11. Try it: what happens inside the container during Boot startup
70 / 136

The demo below turns Boot's bean wiring at startup into something interactive. Toggle the conditions and watch the "chain reaction of wiring" — which bean gets skipped because a condition is unmet:

71 / 136
Kernel lab
72 / 136

That demo answers "what happens when a condition fails". Step back one question first: how did your HelloController get into the container at all? The beanin scenario plays each of the four entrances — component scanning (the @Component family), @Bean methods, @Import, and whatever auto-configuration registers on your behalf. Its last mode, "when it is not found", is precisely the required a bean that could not be found you will meet in the Section 15 sandbox and the Section 16 error table: the package sits outside the main class's subtree, so the scan never laid eyes on it.

73 / 136
Kernel lab
TeaVMThe four roads an object takes into the containeridle
Walk the four roads and notice who reads your class, and when. Finish on 'when it is not found' — that is the second most common first-run failure after the 404
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
74 / 136
Animation
Animation · Hello World in six steps
Animation · Hello World in six steps
75 / 136
Section
One level deeper: stop clicking buttons, start giving the container commands
76 / 136

The sandbox above exposes the three condition switches as buttons. Below is the same container with a command line — you type, and the Java kernel running inside your browser answers for real: beans reports the container’s actual state right now, the wiring log after cond is recomputed from scratch, and curl hits the virtual 8080 living in kernel memory.

77 / 136

Walk it in this order and you will watch an unmet condition infect a chain, layer by layer:

78 / 136
  1. boot — build the container, read the wiring log and the condition evaluation record
  2. beans — all five beans present, every status “已创建 / created”
  3. cond jdbcOnClasspath false — pretend JDBC is not on the classpath
  4. beans — ask again: which ones now say skipped, which say failed, and why those?
  5. curl /api/kernel/query — send the request that reaches the Repository and see what comes back
  6. cond jdbcOnClasspath true, then beans once more to confirm the container recovered
79 / 136
Console
80 / 136
Tip

lab <scenario> <argument> is the console’s back door into every kernel experiment on this site — each button the lessons show you can also type yourself: lab proxy jdk, lab refresh fail, lab tx requires_new. Lost about ids? Type help, then revisit the IoC and AOP modules.

81 / 136
Section
82 / 136
Decision
Decision`spring-boot-devtools` restarts automatically when you edit code — great in development. You are now deploying to production; how should this dependency be handled?
83 / 136
Section
13. Hands-on one: the eight steps of `SpringApplication.run()`, live
84 / 136

The timeline above is what should happen. The kernel demo below actually runs it — the bootrun scenario simulates the whole flow of SpringApplication.run(), and its four buttons each follow one thread:

85 / 136
  • The eight steps (full): bootstrapper → banner → create context → prepare environment → post-process → refresh() → start Tomcat → call runners, printing what each step produces
  • Events (event): ApplicationStartingEvent → EnvironmentPreparedEvent → ApplicationPreparedEvent → ApplicationStartedEvent → ApplicationReadyEvent. That Started DemoApplication in 1.234 seconds line in your log is the product of ApplicationStartedEvent, which is also the correct hook point for "warm something up once startup finishes"
  • Runners (runners): both ApplicationRunner and CommandLineRunner execute after the container is ready and before ApplicationReadyEvent; the former gets an ApplicationArguments wrapper (it can tell option arguments from plain ones), the latter a bare String[]
  • Reading a failed start (fail): Boot turns the exception into a boxed report that opens with APPLICATION FAILED TO START, then splits into Description: and Action: blocks. Open this one — it maps directly onto the error table in Section 16
86 / 136
Kernel lab
TeaVMRun SpringApplication.run() yourself: eight steps, events, runners, failureidle
Click full, then event, then runners, then fail. On fail, keep Section 16 open
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
87 / 136

One more thing worth confirming: run() returns a value. The previous module covered the twelve steps of refresh(); Boot simply folds them into step 6 here. To see the container with your own eyes, change main by three lines:

88 / 136
java
public static void main(String[] args) {    ConfigurableApplicationContext ctx =            SpringApplication.run(DemoApplication.class, args);    // How many beans are in there? Fetch one and check its type    System.out.println("beanDefinitionCount = " + ctx.getBeanDefinitionCount());    Object hello = ctx.getBean("helloController");    System.out.println("helloController -> " + hello.getClass().getName());}
89 / 136

The first time you print that number it will be far larger than you expected — auto-configuration has quietly registered dozens of beans. That exact phenomenon is what the next two articles take apart.

90 / 136
Section
14. Hands-on two: what is inside a fat jar
91 / 136

Section 8 said mvn package yields "a fat jar with everything embedded". Why can java -jar run it at all? A plain jar's META-INF/MANIFEST.MF has no Main-Class, so the JVM does not know where to start; and third-party dependencies are invisible to the system class loader anyway — they are packed inside the jar, not sitting in directories on disk. Boot fixes both problems with two moves:

92 / 136
Code
Codetext
target/demo-0.0.1-SNAPSHOT.jar├── META-INF/│   ├── MANIFEST.MF          # Main-Class: org.springframework.boot.loader.launch.JarLauncher│   └── ...                  # Start-Class: com.example.demo.DemoApplication├── BOOT-INF/│   ├── classes/             # your own classes plus application.properties│   └── lib/                 # every third-party dependency, as nested jars│       ├── spring-boot-starter-web-3.2.5.jar│       ├── tomcat-embed-core-10.1.x.jar│       └── ...└── org/springframework/boot/loader/launch/   # the launcher itself (JarLauncher & friends)
Notes
  • JarLauncher is the real entry point. It creates a LaunchedClassLoader and hands both BOOT-INF/classes/ and BOOT-INF/lib/*.jar to it — this is the "jar inside a jar" that standard JDK classpath rules cannot do (the class-loader idea comes from article #1)
  • Start-Class is the DemoApplication you wrote. JarLauncher reflectively calls its main, control returns to you, and the eight-step timeline carries on
  • Verify by unpacking: jar tf target/demo-0.0.1-SNAPSHOT.jar | more shows exactly those three zones
93 / 136

Five of those paths each own one job, and a directory tree is not how that sticks. Play a round instead: pick a location or manifest attribute on the left, then its duty on the right — a wrong pair explains itself immediately.

94 / 136
Match
MatchEvery slot in a fat jar has exactly one jobMatched 0/6 · Missed 0
Six hard mappings of location to duty. Both columns are shuffled, so order will not help you
Pick a card on the left first
95 / 136

The fatjar scenario plays each of the four parts: layout walks the directory tree and the two key manifest attributes; loader shows the LaunchedClassLoader lookup order (parent delegation → BOOT-INF/classes → BOOT-INF/lib); run traces the whole sequence from java -jar to the first line of your main; war contrasts a war deployment — you get an extra WEB-INF/lib-provided, where devtools and the container-provided Tomcat land so they never clash with the external container's version.

96 / 136
Kernel lab
TeaVMOpen the fat jar: manifest, LaunchedClassLoader, launch sequence, war differencesidle
Start with layout to learn the folders, then loader to see why jars can nest; finish on war to meet lib-provided
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
97 / 136
Animation
Animation · The hundred milliseconds after java -jar
Animation · The hundred milliseconds after java -jar
98 / 136
Tip

"the target machine only needs a JRE, no preinstalled Tomcat" — the technical justification is the block above: Tomcat's classes live in BOOT-INF/lib and are loaded by LaunchedClassLoader itself.

99 / 136
Section
15. Sandbox: port and package location, two knobs that bite immediately
100 / 136

Your first Boot run most likely trips on one of these two: a port clash, or a bean in the wrong package causing a 404. Change one line on the left, and the right shows you the exact startup log lines:

101 / 136
Sandbox
SandboxHow to change the port · which package the main class lives in
Result
Tomcat started on port 8080 (http) with context path ''
GET /hello -> 200 "Hello, Spring Boot!"
#startedAt=1.2s beanDefinitionCount roughly 60+
Every default lives in auto-configuration: port 8080, empty context path, UTF-8.
102 / 136
Note

the second column's three positions produce exactly three outcomes — root package works perfectly, subpackage makes your beans vanish en masse, parent package works but surrenders control over the scan scope. Click all three; it beats reading the text ten times.

103 / 136
Section
16. Common errors, quick reference
104 / 136

Every phrase below can be pasted verbatim into a search box — do not abbreviate or paraphrase:

105 / 136
Table
Error fragmentReal cause30-second fixDig deeper in
Web server failed to start. Port 8080 was already in use. / java.net.BindException: Address already in use: bindThe previous run never stopped, or another program owns 8080Set server.port=8081; on Windows `netstat -ano \findstr :8080 then taskkill /PID <pid> /F, on macOS/Linux lsof -i:8080`This article, Section 15
APPLICATION FAILED TO START + Description: Field xxx in com.example.demo.web.UserController required a bean of type '...' that could not be found. + Action: Consider a component scan capable of placing your SpringBootApplication...@ComponentScan only covers "the main class package plus subpackages", and that bean lives in a sibling or parent packageRead the fully-qualified class name in Description and compare it against the main class package; raise the main class to the root or list packages with scanBasePackagesArticle #17 Section 4 · this article, Section 18 tier one
Consider defining a bean of type 'org.springframework.jdbc.datasource.DataSource' in your configurationYou added a starter but no connection settings, or you never added the spring-boot-starter-jdbc style dependencyAdd the dependency, or supply spring.datasource.url/username/passwordArticle #18 Section 7 (--debug and Negative matches)
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper / runtime NoClassDefFoundErrorYour code imports a class whose jar is not on the classpathIdentify which artifact owns the first class name in the error, add it to the pom, confirm with mvn dependency:treeArticle #2 · Article #18
Some problems may be related to the parent project 'spring-boot-starter-parent' / spring-boot-starter-parent version not foundThe parent version is misspelled, does not exist in the repository, or your company mirror lacks itCheck <version> at the top of pom.xml against a real Boot release; allow network access on the first buildArticle #2 · this article, Section 3
UnsupportedClassVersionError: ... has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version ... only recognizes ... 52.0JDK 8 trying to run bytecode Boot 3 needs from JDK 17Align java -version and IDEA's Project SDK on 17+Article #1 Section 6
no main manifest attribute, in target/demo-0.0.1-SNAPSHOT.jarThe jar was never repackaged by spring-boot-maven-plugin, so it is not executableDeclare the plugin in <build><plugins> and run mvn package againThis article, Section 14
106 / 136
Trap

a Boot 3 failure report always splits into Description: and Action:. Read Action first, then Description — Action states the official direction of repair ("consider adjusting component scanning", "check this property"), while Description carries the fully-qualified class and property names involved. Together they usually pinpoint the fault. Never glance at APPLICATION FAILED TO START alone and start guessing.

107 / 136
Section
17. Quick self-tests
108 / 136
Quiz
Check yourselfYou package the project as a fat jar, run `java -jar app.jar` on the server, and the console immediately prints `no main manifest attribute, in app.jar`. What does that mean?
Pick one — you get feedback right away
109 / 136
Quiz
Check yourselfAnnoyed that 8080 keeps being taken, a colleague adds `--server.port=abc` to the launch arguments. What happens?
Pick one — you get feedback right away
110 / 136
Section
18. Hands-on exercises
111 / 136
Section
Tier one · Follow along
112 / 136

Goal: run a Hello World from scratch and confirm with your own eyes what it did. Three moves: generate, run, verify.

113 / 136

Step one, the complete pom.xml (compare it against what Initializr generated for you):

114 / 136
xml
<?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>    <!-- the single source of truth for versions: no <version> below -->    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.2.5</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>demo</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>demo</name>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
115 / 136

Step two, the main class must carry this name and live in this package (src/main/java/com/example/demo/DemoApplication.java):

116 / 136
java
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@SpringBootApplication@RestController           // for a first pass, keeping the controller in the same file is finepublic class DemoApplication {    public static void main(String[] args) {        SpringApplication.run(DemoApplication.class, args);    }    @GetMapping("/hello")    public String hello() {        return "Hello, Spring Boot!";    }    @GetMapping("/hello/{name}")    public String helloTo(@PathVariable String name) {        return "Hello, " + name + "!";    }}
117 / 136

Step three, run mvn spring-boot:run. The expected startup log looks like this (timestamps and versions vary with your machine):

118 / 136
text
  .   ____          _            __ _ _ /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/  ___)| |_)| | | | | || (_| |  ) ) ) )  '  |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot ::                (v3.2.5)2026-xx-xxT10:00:00.123  INFO 12345 --- [  main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)2026-xx-xxT10:00:00.456  INFO 12345 --- [  main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''2026-xx-xxT10:00:00.789  INFO 12345 --- [  main] com.example.demo.DemoApplication         : Started DemoApplication in 1.234 seconds (process running for 1.567)
119 / 136

Then verify from a second terminal:

120 / 136
bash
curl http://localhost:8080/hello          # expect: Hello, Spring Boot!curl http://localhost:8080/hello/alice    # expect: Hello, alice!
121 / 136

Checklist: ① in the log you can point at which line means "the port is bound" and which means "it is browsable now"; ② both curls return 200; ③ after mvn package, jar tf target/demo-0.0.1-SNAPSHOT.jar shows BOOT-INF/lib/ and META-INF/MANIFEST.MF.

122 / 136
Section
Tier two · Variants
123 / 136

Each variant changes exactly one thing. Afterwards say in your own words what you observed:

124 / 136
  1. Three port values: try server.port=8081, then server.port=0, then server.port=abc. You will observe the first two switching ports successfully while the third aborts startup with Failed to bind properties under 'server.port' to int. Do this and Section 15's sandbox suddenly feels real.
  2. Deliberately move the main class into a subpackage: relocate DemoApplication to com.example.demo.web, add a com.example.demo.service.HelloService annotated @Service, and @Autowired it from the main class. You will observe immediate failure whose Description: reads required a bean of type 'com.example.demo.service.HelloService' that could not be found — row two of the error table.
  3. Kill the banner and count the lost lines: add spring.main.banner-mode=off. You will observe the ASCII art gone and every other log line unchanged — proof the banner participates in no startup step.
  4. Add devtools, then edit code: after adding the dependency, change the return value of hello() and save. You will observe a restartedMain thread in the logs — the restart happens on a fresh thread, which is precisely the container rebuild from article #9.
125 / 136
Section
Tier three · Build one
126 / 136

Build yourself a "first Boot workbench" called workbench, using every idea in this article.

127 / 136
  • The only dependency is spring-boot-starter-web; pin parent to a real Boot 3 release and set java.version to 17
  • Packages: WorkbenchApplication in the root com.example.workbench, subpackages controller / service / common
  • Three endpoints: GET /ping returns pong; GET /time returns the current time as a string; GET /echo/{msg} returns the path variable unchanged
  • A custom banner.txt containing at least your name and one number
  • A CommandLineRunner (declare it as a @Bean on the main class) printing the total bean count once startup completes
128 / 136

Checklist: ① mvn clean package succeeds and java -jar starts; ② all three endpoints return 200 from both browser and curl; ③ your bean-count line appears at the end of the startup log; ④ temporarily moving WorkbenchApplication into controller produces an APPLICATION FAILED TO START report you can paste and explain, then revert; ⑤ your README states the three things this machine needs to run it (JDK, Maven or mvnw, network).

129 / 136
Section
19. Key-point self-check
130 / 136
Self-check

from memory, name the eight steps of SpringApplication.run() — after which one is the port reachable, and which one actually instantiates the singleton beans?

131 / 136
Self-check

why does web.xml disappear from a Boot project? Answer using the phrase "who starts first".

132 / 136
Self-check

which two attributes in a fat jar's MANIFEST.MF matter, and what does each point at?

133 / 136
Self-check

in a failure report, what does Description: give you and what does Action: give you? Why read Action first?

134 / 136
Self-check

why is Boot a "finished apartment" rather than "an empty room"? Explain "your sofa replaces theirs" in terms of @ConditionalOnMissingBean.

135 / 136

Mantra: **a starter pulls dependencies, the parent pins versions, the main class lives outermost, and you watch two log lines** — `Tomcat started on port` and `Started XxxApplication`.

136 / 136
Summary

to run Spring Boot for the first time, remember just this — the project comes from start.spring.io, where pom.xml's parent decides versions and a starter pulls dependencies in one go; the main class relies on the three-in-one @SpringBootApplication (@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan), and SpringApplication.run returns the IoC container itself; embedded Tomcat comes from spring-boot-starter-web's transitive dependencies, and since main starts first and starts Tomcat from there, web.xml disappears. At runtime watch for Tomcat started on port 8080 and Started XxxApplication, run it via IDEA, mvn spring-boot:run or java -jar, and the artifact is a fat jar with everything embedded. The first-run 404, port conflict and "changed code but no restart" are three small hurdles everyone meets — diagnose them in the order "URL → package path → annotation".