Your First Spring Boot App: Hello World in Three Minutes
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.
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.

After this article you should be able to answer three questions:
- Why is there no
web.xmlin 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 packagerun with justjava -jar? What must the target machine have preinstalled?
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:
- A
web.xmlregistering theDispatcherServletand itsinit-param - An
applicationContext.xmldeclaring component scanning, the data source and the transaction manager - A
spring-mvc.xmlregistering the view resolver and message converters - Dropping the resulting war into an external Tomcat, installing it, starting it and watching the logs
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:
| The burden in plain Spring | Spring Boot's answer |
|---|---|
| Manually managing dozens of dependencies and versions | One starter dependency; versions decided by the parent POM |
| Hand-writing XML / Java config classes | Auto-configuration: conditional wiring based on classpath and existing beans |
| Deploying to an external Tomcat | Embedded Tomcat; just java -jar |

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:
| Option | Choice | Notes |
|---|---|---|
| Project | Maven | Gradle works too; this guide uses Maven |
| Language | Java | — |
| Spring Boot | 3.2.x | Matches JDK 17 and above |
| Group | com.example | Package prefix |
| Artifact | demo | Project / artifact name |
| Packaging | Jar | No more war |
| Java | 17 | Consistent with the previous module |
| Dependencies | Spring Web | This one alone is enough for Hello World |
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.
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.
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.
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.
<?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>Open the project and the tree looks like this (the standard Boot skeleton):
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 neededLook first at the two key blocks in pom.xml — the parent and the starter:
<!-- 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>spring-boot-starter-parentis the single source of truth for dependency versions: below it you add dependencies without a<version>, because it pins a compatible setspring-boot-starter-webis 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 goapplication.propertiesis 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.
Of the whole project, the main class is the only one you must write. It is absurdly short, yet every line deserves explanation:
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); }}@SpringBootApplication is a "three-in-one" annotation; unwrapped, it is three things:
| Component annotation | Purpose |
|---|---|
@SpringBootConfiguration | Essentially @Configuration, marking this as a config class |
@EnableAutoConfiguration | Enables auto-configuration so Boot wires beans conditionally |
@ComponentScan | Scans @Component / @Service / @Controller under this package and below |
- The return value of
SpringApplication.run(DemoApplication.class, args)is anApplicationContext— the IoC container itself. You can capture it, e.g.ConfigurableApplicationContext ctx = SpringApplication.run(...), thenctx.getBean(...)to verify wiring - The
mainmethod 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
@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.
Create a class under the com.example.demo.controller package:
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 + "!"; }}@RestController=@Controller+@ResponseBody: the returned string goes straight into the response body, no view lookup@GetMapping("/hello")maps this method toGET /hello@PathVariablebinds the{name}segment of the URL to the parameter
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".
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".
Run DemoApplication and the console prints something like this (simplified to the real format):
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: 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)- That ASCII art is the banner, customizable via
resources/banner.txtor disabled withspring.main.banner-mode=off Tomcat initialized with port(s): 8080means embedded Tomcat has bound the portStarted DemoApplication in 1.234 secondsis the "ready" signal — only after this line can the browser succeed
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:
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-webThat 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.

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.
The same main class can run three ways, each suited to a different situation:
| Way | Command / action | Best for |
|---|---|---|
| Run in IDEA | Click the green triangle by main | Daily development; debugging with breakpoints is easiest |
| Maven plugin | mvn spring-boot:run | Command-line development, or when the IDE is awkward |
| Executable jar | mvn package, then java -jar target/demo-0.0.1-SNAPSHOT.jar | Deployment, CI verification, close to production |
First package from the project root:
# 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- Boot's
spring-boot-maven-pluginbuilds a fat jar: one jar containing app code, third-party dependencies and embedded Tomcat, sojava -jarruns 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.
| Symptom | Root cause | Diagnosis / fix |
|---|---|---|
Port 8080 was already in use | 8080 is taken by another process | Set server.port=8081, or kill the process |
| The browser shows 404 | Wrong path / class out of scan range / forgot @RestController | Check in order: the URL, whether the package is under the main class, and whether the annotation is @RestController |
| Code changed but the response did not | Old process / not recompiled | Stop the old process and rerun; check why hot reload did not fire |
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.
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:

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:
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.
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:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- key: optional, so it does not leak downstream --></dependency>"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.
// You edit the string returned by hello() in IDEA and press Ctrl+S// The running JVM: a Class object for HelloController already sits in the method area// javac only rewrites the file on disk: target/classes/HelloController.class// That old Class object never goes back to re-read the disk// Browser refresh -> the same bean in the same container -> the same Class -> the old text// What devtools actually does: drop RestartClassLoader and load the bytecode again through a fresh one| source saved | yes |
| disk | still the previous .class at this instant |
| process | the same one, not restarted |
IDEA → Save → javacThe 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:
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.

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.
Walk it in this order and you will watch an unmet condition infect a chain, layer by layer:
boot— build the container, read the wiring log and the condition evaluation recordbeans— all five beans present, every status “已创建 / created”cond jdbcOnClasspath false— pretend JDBC is not on the classpathbeans— ask again: which ones now say skipped, which say failed, and why those?curl /api/kernel/query— send the request that reaches the Repository and see what comes backcond jdbcOnClasspath true, thenbeansonce more to confirm the container recovered
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.
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:
- 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. ThatStarted DemoApplication in 1.234 secondsline in your log is the product ofApplicationStartedEvent, which is also the correct hook point for "warm something up once startup finishes" - Runners (runners): both
ApplicationRunnerandCommandLineRunnerexecute after the container is ready and beforeApplicationReadyEvent; the former gets anApplicationArgumentswrapper (it can tell option arguments from plain ones), the latter a bareString[] - Reading a failed start (fail): Boot turns the exception into a boxed report that opens with
APPLICATION FAILED TO START, then splits intoDescription:andAction:blocks. Open this one — it maps directly onto the error table in Section 16
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:
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());}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.
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:
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)JarLauncheris the real entry point. It creates aLaunchedClassLoaderand hands bothBOOT-INF/classes/andBOOT-INF/lib/*.jarto it — this is the "jar inside a jar" that standard JDK classpath rules cannot do (the class-loader idea comes from article #1)Start-Classis theDemoApplicationyou wrote.JarLauncherreflectively calls itsmain, control returns to you, and the eight-step timeline carries on- Verify by unpacking:
jar tf target/demo-0.0.1-SNAPSHOT.jar | moreshows exactly those three zones
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.
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.

"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.
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:
Tomcat started on port 8080 (http) with context path ''GET /hello -> 200 "Hello, Spring Boot!"#startedAt=1.2s beanDefinitionCount roughly 60+
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.
Every phrase below can be pasted verbatim into a search box — do not abbreviate or paraphrase:
| Error fragment | Real cause | 30-second fix | Dig deeper in | |
|---|---|---|---|---|
Web server failed to start. Port 8080 was already in use. / java.net.BindException: Address already in use: bind | The previous run never stopped, or another program owns 8080 | Set 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 package | Read 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 scanBasePackages | Article #17 Section 4 · this article, Section 18 tier one | |
Consider defining a bean of type 'org.springframework.jdbc.datasource.DataSource' in your configuration | You added a starter but no connection settings, or you never added the spring-boot-starter-jdbc style dependency | Add the dependency, or supply spring.datasource.url/username/password | Article #18 Section 7 (--debug and Negative matches) | |
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper / runtime NoClassDefFoundError | Your code imports a class whose jar is not on the classpath | Identify which artifact owns the first class name in the error, add it to the pom, confirm with mvn dependency:tree | Article #2 · Article #18 | |
Some problems may be related to the parent project 'spring-boot-starter-parent' / spring-boot-starter-parent version not found | The parent version is misspelled, does not exist in the repository, or your company mirror lacks it | Check <version> at the top of pom.xml against a real Boot release; allow network access on the first build | Article #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.0 | JDK 8 trying to run bytecode Boot 3 needs from JDK 17 | Align java -version and IDEA's Project SDK on 17+ | Article #1 Section 6 | |
no main manifest attribute, in target/demo-0.0.1-SNAPSHOT.jar | The jar was never repackaged by spring-boot-maven-plugin, so it is not executable | Declare the plugin in <build><plugins> and run mvn package again | This article, Section 14 |
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.
Goal: run a Hello World from scratch and confirm with your own eyes what it did. Three moves: generate, run, verify.
Step one, the complete pom.xml (compare it against what Initializr generated for you):
<?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>Step two, the main class must carry this name and live in this package (src/main/java/com/example/demo/DemoApplication.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 + "!"; }}Step three, run mvn spring-boot:run. The expected startup log looks like this (timestamps and versions vary with your machine):
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: 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)Then verify from a second terminal:
curl http://localhost:8080/hello # expect: Hello, Spring Boot!curl http://localhost:8080/hello/alice # expect: Hello, alice!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.
Each variant changes exactly one thing. Afterwards say in your own words what you observed:
- Three port values: try
server.port=8081, thenserver.port=0, thenserver.port=abc. You will observe the first two switching ports successfully while the third aborts startup withFailed to bind properties under 'server.port' to int. Do this and Section 15's sandbox suddenly feels real. - Deliberately move the main class into a subpackage: relocate
DemoApplicationtocom.example.demo.web, add acom.example.demo.service.HelloServiceannotated@Service, and@Autowiredit from the main class. You will observe immediate failure whoseDescription:readsrequired a bean of type 'com.example.demo.service.HelloService' that could not be found— row two of the error table. - 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. - Add devtools, then edit code: after adding the dependency, change the return value of
hello()and save. You will observe arestartedMainthread in the logs — the restart happens on a fresh thread, which is precisely the container rebuild from article #9.
Build yourself a "first Boot workbench" called workbench, using every idea in this article.
- The only dependency is
spring-boot-starter-web; pinparentto a real Boot 3 release and setjava.versionto 17 - Packages:
WorkbenchApplicationin the rootcom.example.workbench, subpackagescontroller/service/common - Three endpoints:
GET /pingreturnspong;GET /timereturns the current time as a string;GET /echo/{msg}returns the path variable unchanged - A custom
banner.txtcontaining at least your name and one number - A
CommandLineRunner(declare it as a@Beanon the main class) printing the total bean count once startup completes
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).
from memory, name the eight steps of SpringApplication.run() — after which one is the port reachable, and which one actually instantiates the singleton beans?
why does web.xml disappear from a Boot project? Answer using the phrase "who starts first".
which two attributes in a fat jar's MANIFEST.MF matter, and what does each point at?
in a failure report, what does Description: give you and what does Action: give you? Why read Action first?
why is Boot a "finished apartment" rather than "an empty room"? Explain "your sofa replaces theirs" in terms of @ConditionalOnMissingBean.
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`.
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".