Command Palette

Search for a command to run...

[Spring Boot Basics] Cài đặt Spring Boot: JDK, IDE, Spring Initializr và ứng dụng đầu tiên

Bài trước nói về Spring Boot là gì: một bản đóng gói của Spring container với các default đã được chọn sẵn, để một project bắt đầu bằng đúng một class và một annotation. Đó là câu mang tính khái niệm cuối cùng trong bài này. Đây là bài thực hành, và khi đọc xong bạn phải có một ứng dụng đang chạy trên máy mình, trả lời được request HTTP.

Toàn bộ nội dung bên dưới được tạo ra bằng cách chạy thật. Các lệnh đã được chạy, project đã được tạo, build và khởi động, các endpoint đã được gọi bằng curl, và mọi lỗi trong phần troubleshooting đều được tái hiện có chủ ý. Các dòng log và message lỗi được copy thẳng từ terminal, không phải viết theo trí nhớ.

Ba lệnh - start.spring.io, gradlew bootRun, curl - cho ra Hello, Spring Boot!

Mục tiêu là Spring Boot 4.1.1 trên OpenJDK 21.0.6, build bằng Gradle. Máy của bạn chỉ cần cài đúng một thứ: một JDK.

Cài đặt JDK

Spring Boot 4 yêu cầu tối thiểu Java 17. Series này dùng Java 21 vì đây là bản Long Term Support mà Boot 4 được thiết kế xoay quanh, và vì Spring Initializr đưa nó ra như một lựa chọn chính thức. Bản OpenJDK nào cũng được — Temurin, Corretto, Zulu, Microsoft, Liberica, bản của Homebrew. Chúng đều là cùng một JDK, chỉ khác hạ tầng build phía sau.

macOS

Cách ngắn nhất là Homebrew:

Bash
brew install openjdk@21

Homebrew cài formula này ở chế độ keg-only, nghĩa là nó cố tình không đưa vào PATH, vì đây là một version thay thế của formula openjdk. Lúc cài xong, Homebrew in ra chính xác việc cần làm:

Text
For the system Java wrappers to find this JDK, symlink it with
  sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-21.jdk

Hãy chạy dòng đó. Nếu không, /usr/bin/java sẽ không thấy JDK bạn vừa cài, và bạn sẽ mất hai mươi phút để tự hỏi tại sao. Trên Mac chip Intel, prefix là /usr/local thay vì /opt/homebrew.

Nếu bạn thích một installer có chữ ký và tự đăng ký đúng chỗ, hãy dùng bản Adoptium/Temurin — brew install --cask temurin@21, hoặc file .pkg từ adoptium.net. Bản cask cài thẳng vào /Library/Java/JavaVirtualMachines nên không cần bước symlink.

Windows

PowerShell
winget install --id EclipseAdoptium.Temurin.21.JDK

Hoặc tải .msi từ adoptium.net. Ở bước custom setup của installer, hãy bật Set JAVA_HOME variable — Temurin mặc định tự thêm vào PATH nhưng không đụng tới JAVA_HOME, và thiếu JAVA_HOME chính là nguyên nhân của hầu hết các báo cáo kiểu "chạy trong IDE thì được, ra terminal thì lỗi". Sau khi cài xong phải mở một terminal mới: environment variable chỉ được đọc lúc process khởi động, nên cửa sổ đang mở sẽ không thấy giá trị mới.

Linux

Bash
sudo apt install openjdk-21-jdk          # Debian, Ubuntu
sudo dnf install java-21-openjdk-devel   # Fedora, RHEL, Rocky
sudo pacman -S jdk21-openjdk             # Arch

Để ý hậu tố -jdk-devel. Package openjdk-21-jre hay java-21-openjdk chỉ cho bạn runtime mà không có compiler — đủ để chạy một file jar, không đủ để build ra nó.

Kiểm tra lại bản cài đặt

Hai lệnh, và cả hai đều phải trả lời:

Text
$ java -version
openjdk version "21.0.6" 2025-01-21
OpenJDK Runtime Environment Homebrew (build 21.0.6)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.6, mixed mode, sharing)
 
$ javac -version
javac 21.0.6

java là runtime còn javac là compiler. Nếu java -version chạy được mà javac -version báo "command not found" thì bạn đã cài JRE chứ không phải JDK. Cài lại package JDK rồi thử lại.

Một chi tiết nhỏ: java -version in ra stderr chứ không phải stdout. Đó không phải bug, và đó là lý do java -version | grep 21 trông như không in ra gì cả.

Khi JAVA_HOME trỏ nhầm JDK

Đây là vấn đề setup phổ biến nhất, và nó xuất hiện ngay khi máy bạn có hai JDK — chuyện sẽ xảy ra ngay khi một project cần 17 còn project khác cần 21. Các công cụ không đồng ý với nhau: java trong shell lấy từ PATH, còn Gradle, Maven và phần lớn IDE đọc JAVA_HOME. Khi hai nơi trỏ về hai bản cài khác nhau, triệu chứng trông hoàn toàn vô lý.

Trên macOS, xem thực sự đang có gì:

Text
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (2):
    21.0.6 (arm64) "Homebrew" - "OpenJDK 21.0.6" /opt/homebrew/Cellar/openjdk@21/21.0.6/libexec/openjdk.jdk/Contents/Home
    17.0.14 (arm64) "Homebrew" - "OpenJDK 17.0.14" /opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home

Rồi cố định bản bạn muốn, trong ~/.zshrc:

Bash
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
export PATH="$JAVA_HOME/bin:$PATH"

Tương đương trên hai nền tảng còn lại:

macOSWindows (PowerShell)Linux
Liệt kê JDK đang có/usr/libexec/java_home -VGet-ChildItem 'C:\Program Files\Eclipse Adoptium'update-alternatives --list java
JAVA_HOME hiện tạiecho $JAVA_HOMEecho $env:JAVA_HOMEecho $JAVA_HOME
Binary nào đang chạywhich javawhere.exe javareadlink -f $(which java)
Đổi sang bản khácexport JAVA_HOME=$(/usr/libexec/java_home -v 21)setx JAVA_HOME "C:\Program Files\Eclipse Adoptium\jdk-21"sudo update-alternatives --config java
Ghi vào đâu cho lâu dài~/.zshrcSystem environment variables, rồi mở terminal mới~/.bashrc hoặc /etc/environment

Phép kiểm tra quyết định: chạy java -versionecho $JAVA_HOME cạnh nhau, xem hai kết quả có mô tả cùng một bản cài hay không. Nếu không, terminal và build tool đang dùng hai compiler khác nhau, và lỗi bạn nhận được sẽ nói về class file version chứ không nói về vấn đề thật.

Chọn IDE: IntelliJ IDEA hay VS Code

Cái nào cũng chạy được. Project sinh ra là một Gradle project thuần, nên thực ra không bắt buộc phải có IDE — terminal là đủ. Cái mà IDE mang lại là một debugger bạn sẽ thực sự dùng, và khả năng navigate khiến một codebase dùng framework trở nên dễ đọc.

IntelliJ IDEAVS Code
Cần cài gìIntelliJ IDEA (từ 2025.3 là một sản phẩm duy nhất — không còn chia Community/Ultimate)VS Code + Extension Pack for Java (Microsoft) + Spring Boot Extension Pack (VMware)
Bản miễn phí cóJava, Gradle, Maven, debugger, JUnit, wizard tạo project Spring Boot và highlight Spring cơ bảntất cả những thứ trên; các extension đều miễn phí
Bản trả phí thêm gìsubscription Ultimate; tài liệu của JetBrains ghi "Spring support is limited in IntelliJ IDEA without the Ultimate subscription"không có
Tạo projectNew Project → Spring Boot, chính là form Initializr nằm trong IDECtrl+Shift+PSpring Initializr: Create a Gradle Project
Chạy ứng dụngmũi tên xanh ở gutter cạnh main, hoặc task Gradle bootRunSpring Boot Dashboard ở sidebar
Cách importđọc build.gradle và tải dependency ngay khi mởtương tự, qua extension Gradle for Java
Khuyến nghị thật lòngít thứ phải lắp ráp hơn cho người mới; import Gradle ổn định hơnhợp hơn nếu bạn vốn đã sống trong VS Code và muốn một editor cho mọi thứ

Có ba thiết lập nên sửa trước khi bắt đầu, với cả hai IDE:

  • Project SDK / Java runtime: 21. Trong IntelliJ là Project Structure → Project → SDK; trong VS Code là java.configuration.runtimes trong settings. Một IDE lặng lẽ để 17 trong khi Gradle dùng 21 sẽ gạch đỏ những dòng code mà terminal compile không hề báo lỗi.
  • File encoding: UTF-8. Cả hai đều mặc định như vậy rồi, nhưng một project thừa kế từ setup cũ thì chưa chắc, và triệu chứng là ký tự bị vỡ trong string literal chứ không phải một lỗi rõ ràng.
  • Annotation processing: bật. Hôm nay bạn chưa cần. Bạn sẽ cần ngay lần đầu thêm Lombok, và lỗi "cannot find symbol: method getName()" trên một class @Data luôn luôn là do thiết lập này.

Spring Initializr, từng field một

Spring Initializr là một code generator ở start.spring.io. Nó không làm gì huyền bí: nó viết ra một build file, một cây thư mục, một class có main, một test rỗng, và các script wrapper của build tool. Mỗi field trên form quyết định một dòng cụ thể trong một file cụ thể.

Mỗi field của Spring Initializr ánh xạ tới dòng nó sinh ra trong project

Từng field quyết định điều gì:

  • Project — build tool nào được sinh ra. Cứ để Gradle - Groovy, đây là default hiện tại của trang và cũng là thứ series này dùng. Bạn nhận được build.gradle, settings.gradle và các script wrapper gradlew. Bài 4 sẽ nói về chính build file đó và bản Maven tương đương.
  • Language — Java, Kotlin hay Groovy. Chọn Java.
  • Spring Boot — version. Danh sách trộn lẫn bản GA với bản SNAPSHOT và bản M (milestone). Hãy chọn con số trơn. Bản SNAPSHOT được build lại liên tục và có thể đổi giữa hai lần build trong cùng một ngày; bản M là pre-release, API vẫn có thể dịch chuyển. Hôm nay default là 4.1.1, đúng thứ bạn cần.
  • Group — namespace theo tên miền đảo ngược, com.example khi học. Nó thành group = 'com.example' trong build file và là phần đầu của tọa độ artifact khi publish.
  • Artifact — tên project dưới dạng build artifact, demo. Nó thành rootProject.name = 'demo' và là tên file jar.
  • Name — tên cho người đọc, quyết định class main được sinh ra: demo cho ra DemoApplication.java.
  • Package name — package gốc, com.example.demo. Field này quan trọng hơn vẻ ngoài của nó: nó quyết định cả cây thư mục dưới src/main/java lẫn phạm vi package được scan để tìm component của bạn. Hãy đặt class của bạn bên dưới nó. (Bài 4 giải thích rule scan; phần troubleshooting bên dưới cho bạn xem triệu chứng khi làm sai.)
  • PackagingJar, luôn luôn, trừ khi bạn được giao sẵn một servlet container để deploy vào. Jar nhúng Tomcat của chính nó và chạy bằng java -jar. War là gói để deploy vào một Tomcat do người khác vận hành, một mô hình deploy mà nếu cần thì bạn đã biết là mình cần.
  • Javađổi field này. Form hiện tại mặc định 17; hãy đặt thành 21. Nó thành JavaLanguageVersion.of(21) trong build file, tức là thứ Gradle dùng để chọn compiler.
  • Dependencies — thêm Spring Web. Đây là thứ cho bạn một server nhúng và @RestController.

⚠️ Boot 4 đã đổi tên web starter. Tick "Spring Web" bây giờ sinh ra spring-boot-starter-webmvc, và test starter tương ứng là spring-boot-starter-webmvc-test. Trên Boot 3 chúng là spring-boot-starter-webspring-boot-starter-test. Nếu bạn làm theo một tutorial cũ rồi dán khối dependency của nó vào project Boot 4, hoặc dán khối của Boot 4 vào project Boot 3, build sẽ không resolve được artifact. Hãy tin file được sinh ra, đừng tin tutorial.

Ba cách tạo project

Form trên web. Vào start.spring.io, điền các field bên trên, bấm Generate rồi giải nén file tải về. Cạnh nút đó có nút Explore cho bạn xem toàn bộ file sẽ được sinh ra ngay trên trình duyệt trước khi tải — đáng bấm thử một lần.

Wizard trong IDE. IntelliJ: File → New → Project → Spring Boot. VS Code: Ctrl+Shift+PSpring Initializr: Create a Gradle Project. Cả hai là cùng một form, gọi cùng một API, và cả hai đều mở thẳng project ra khi xong.

Một lệnh curl. Trang này là một HTTP API, và mỗi field trên form là một query parameter:

Bash
curl -s "https://start.spring.io/starter.zip?type=gradle-project&language=java&bootVersion=4.1.1&javaVersion=21&groupId=com.example&artifactId=demo&name=demo&packageName=com.example.demo&dependencies=web" -o demo.zip
unzip demo.zip -d demo
cd demo

Lệnh unzip in ra những gì đã được ghi:

Text
Archive:  demo.zip
   creating: demo/src
   creating: demo/src/test
   creating: demo/src/test/java
   creating: demo/src/test/java/com
   creating: demo/src/test/java/com/example
   creating: demo/src/test/java/com/example/demo
  inflating: demo/src/test/java/com/example/demo/DemoApplicationTests.java
   creating: demo/src/main
   creating: demo/src/main/java
   creating: demo/src/main/java/com
   creating: demo/src/main/java/com/example
   creating: demo/src/main/java/com/example/demo
  inflating: demo/src/main/java/com/example/demo/DemoApplication.java
   creating: demo/src/main/resources
  inflating: demo/src/main/resources/application.properties
   creating: demo/src/main/resources/templates
   creating: demo/src/main/resources/static
  inflating: demo/settings.gradle
   creating: demo/gradle
   creating: demo/gradle/wrapper
  inflating: demo/gradle/wrapper/gradle-wrapper.properties
  inflating: demo/gradle/wrapper/gradle-wrapper.jar
  inflating: demo/gradlew
  inflating: demo/.gitattributes
  inflating: demo/.gitignore
  inflating: demo/build.gradle
  inflating: demo/HELP.md
  inflating: demo/gradlew.bat

Mười hai file. Đó là toàn bộ project. Cách này đáng biết vì nó script hóa được, và vì nó cho thấy trang web không làm điều gì mà bạn không làm được từ terminal.

Chạy ứng dụng

Xem thử trong PATH có gì:

Text
$ which mvn gradle
mvn not found
gradle not found

Không build tool nào được cài, và cũng không cần cài. Project sinh ra mang theo một wrapper: gradlew trên Unix, gradlew.bat trên Windows, cộng với gradle/wrapper/gradle-wrapper.properties ghi chính xác version:

gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip

Wrapper tải đúng bản Gradle đó ở lần chạy đầu tiên và cache vào ~/.gradle. Mọi người clone repository về đều nhận được Gradle 9.7.1, bất kể trên máy họ đang cài gì. Đó là lý do luôn gõ ./gradlew chứ không bao giờ gõ gradle.

Nó cũng có nghĩa là lần build đầu rất chậm — nó đang tải một bản phân phối Gradle trước khi làm bất cứ việc gì:

Text
$ ./gradlew build
Fetching distribution.
Downloading https://services.gradle.org/distributions/gradle-9.7.1-bin.zip
..............10%..............20%...............30%..............40%...............50%..............60%...............70%..............80%..............90%...............100%
 
Welcome to Gradle 9.7.1!
...
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build
 
BUILD SUCCESSFUL in 28s
7 actionable tasks: 7 executed

Hai mươi tám giây, phần lớn là tải về và resolve dependency. Lần build thứ hai của cùng project đó, không sửa gì, xong trong 374 ms và báo 7 actionable tasks: 7 up-to-date. Nếu lần build đầu trông như bị treo thì nó đang tải; hãy cho nó một phút trước khi kết luận là hỏng.

Giờ khởi động:

Bash
./gradlew bootRun

Trình tự khởi động từ gradlew bootRun tới Started DemoApplication, chú thích bằng log thật

Đây là output thật, trên máy này, với thư mục làm việc được rút gọn thành /Users/you/demo:

Text
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 
 :: Spring Boot ::                (v4.1.1)
 
2026-09-11T10:11:08.693+07:00  INFO 15312 --- [demo] [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 21.0.6 with PID 15312 (/Users/you/demo/build/classes/java/main started by you in /Users/you/demo)
2026-09-11T10:11:08.694+07:00  INFO 15312 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2026-09-11T10:11:08.883+07:00  INFO 15312 --- [demo] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-09-11T10:11:08.888+07:00  INFO 15312 --- [demo] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-09-11T10:11:08.888+07:00  INFO 15312 --- [demo] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.24]
2026-09-11T10:11:08.899+07:00  INFO 15312 --- [demo] [           main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 193 ms
2026-09-11T10:11:08.984+07:00  INFO 15312 --- [demo] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-09-11T10:11:08.986+07:00  INFO 15312 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.407 seconds (process running for 0.51)

Hãy đọc kỹ một lần, vì bạn sẽ còn đọc cả nghìn lần nữa:

DòngNó nói gì với bạn
Banner ASCII và :: Spring Boot :: (v4.1.1)version Boot thực sự đang nằm trên classpath — thứ đầu tiên cần kiểm tra khi code trong tutorial không compile
Starting DemoApplication using Java 21.0.6 with PID 15312JVM mà Gradle thực sự chọn, và process id để kill nó khi cần
[demo] ở mọi dònggiá trị của spring.application.name trong application.properties
No active profile set, falling back to 1 default profile: "default"không có profile nào được yêu cầu, nên chỉ config mặc định có hiệu lực
Tomcat initialized with port 8080 (http)object server nhúng đã tồn tại và đã được cho biết muốn port nào. Nó chưa bind
Root WebApplicationContext: initialization completed in 193 mscontainer đã dựng xong các bean. Code của bạn nếu hỏng thì thường hỏng ở đây
Tomcat started on port 8080 (http) with context path '/'port đã được bind. Từ khoảnh khắc này ứng dụng nhận connection
Started DemoApplication in 0.407 seconds (process running for 0.51)sẵn sàng. Số đầu là ứng dụng, số sau tính cả thời gian khởi động JVM

Con số 0.407 là của máy này trong ngày hôm đó — hãy coi mọi con số startup bạn thấy ở bất cứ đâu, kể cả của chính bạn, là mang tính tham khảo chứ không phải benchmark.

Có hai dòng đang thiếu, và chính sự vắng mặt đó mới là điều đáng nói. Gửi request đầu tiên và chúng xuất hiện:

Text
Initializing Spring DispatcherServlet 'dispatcherServlet'
Completed initialization in 0 ms

Servlet điều phối request được khởi tạo lazy, ở request đầu tiên chứ không phải lúc startup. Nếu bạn không bao giờ thấy hai dòng đó thì chưa từng có ai gọi vào ứng dụng của bạn.

Dừng bằng Ctrl+C. Trước đó process vẫn sống và vẫn giữ port 8080.

Chạy từ IDE

Mở DemoApplication.java, bấm mũi tên xanh ở gutter cạnh main, và bạn nhận được đúng log đó trong cửa sổ run của IDE, kèm debugger đã gắn sẵn. Khác biệt chỉ có vậy: bootRun và run configuration của IDE đều kết thúc bằng việc gọi SpringApplication.run trong cùng một JVM như khi chạy từ terminal.

Bên VS Code, thứ tương đương là Spring Boot Dashboard ở sidebar, liệt kê mọi ứng dụng Boot trong workspace kèm nút start, stop và debug.

Đừng chạy cả hai cùng lúc. Cái thứ hai sẽ không khởi động được, vì lý do ở phần troubleshooting.

Chạy file JAR đã đóng gói

bootRun là để dev. Thứ bạn đem đi deploy là một file jar:

Bash
./gradlew build
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar

Bạn sẽ thấy hai file trong build/libs: demo-0.0.1-SNAPSHOT.jar khoảng 19 MB, và demo-0.0.1-SNAPSHOT-plain.jar vài kilobyte. Hãy chạy file to — nó chứa class của bạn mọi dependency Tomcat nhúng, đó là lý do nó tự chạy được và cũng là lý do nó nặng 19 MB. Bài 4 sẽ mở nó ra và giải thích cơ chế.

Log khởi động giống hệt, trừ đúng một chi tiết đáng để ý:

Text
Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.6 with PID 14061 (/Users/you/demo/build/libs/demo-0.0.1-SNAPSHOT.jar started by you in /Users/you/demo)

Chạy từ file jar, Boot đọc được version trong manifest của jar và in ra v0.0.1-SNAPSHOT. Chạy bằng bootRun, nó đang chạy trên các file class rời nên không có version nào để báo.

Endpoint đầu tiên

Project vừa sinh ra trả về 404 cho mọi URL, vì nó chưa có controller nào:

Text
$ curl -s -i http://localhost:8080/
HTTP/1.1 404
Content-Type: application/json
Transfer-Encoding: chunked
 
{"timestamp":"2026-09-11T03:08:56.159Z","status":404,"error":"Not Found","path":"/"}

Bản thân cái JSON đó đã là tín hiệu tốt: server đang chạy và đang trả lời, error handler mặc định của nó hoạt động. Giờ hãy cho nó thứ để trả lời. Tạo src/main/java/com/example/demo/HelloController.java, nằm cạnh DemoApplication.java:

src/main/java/com/example/demo/HelloController.java
package com.example.demo;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class HelloController {
 
    public record Greeting(String message, String recipient, int id) {}
 
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
 
    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(defaultValue = "World") String name) {
        return new Greeting("Hello, " + name + "!", name, 1);
    }
}

Ba annotation, không có gì thêm. @RestController nói rằng class này xử lý request HTTP và giá trị trả về của nó là response body chứ không phải tên view. @GetMapping("/hello") ánh xạ request GET tới path đó vào method đó. @RequestParam bind một parameter trên query string vào argument.

Khởi động lại rồi gọi thử:

Text
$ curl -s -i http://localhost:8080/hello
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 19
 
Hello, Spring Boot!

Endpoint thứ hai trả về một object thay vì một string, và điều đó làm response đổi khác:

Text
$ curl -s -i http://localhost:8080/greeting
HTTP/1.1 200
Content-Type: application/json
Content-Length: 54
 
{"message":"Hello, World!","recipient":"World","id":1}
 
$ curl -s "http://localhost:8080/greeting?name=Hoang"
{"message":"Hello, Hoang!","recipient":"Hoang","id":1}

Một request đi từ curl qua embedded Tomcat vào controller method rồi quay ra dưới dạng JSON

Bạn không viết JSON, không viết code serialisation, không set content type. Một String quay ra thành text/plain; một record quay ra thành application/json với mỗi component là một key, theo đúng thứ tự khai báo. Jackson nằm trên classpath — version 3.1.5, được web starter kéo về gián tiếp — và Boot cấu hình nó vì nó có ở đó. Toàn bộ mẹo chỉ có thế, và đó chính là lý do framework này tồn tại.

Đổi port

Hoặc trong src/main/resources/application.properties:

src/main/resources/application.properties
server.port=9090

Hoặc trên command line:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=9090

Log xác nhận là nó có hiệu lực:

Text
Tomcat started on port 9090 (http) with context path '/'

Xử lý những lỗi bạn chắc chắn sẽ gặp

Triệu chứngNghĩa là gìCách sửa
Web server failed to start. Port 8080 was already in use.một process khác — thường là bản copy của chính app này mà bạn quên tắt — đang giữ porttắt nó, hoặc chạy ở port khác
UnsupportedClassVersionError ... class file version 65.0build bằng Java 21 nhưng đang chạy bằng JVM cũ hơntrỏ JAVA_HOME về 21 rồi chạy lại
Cannot find a Java installation ... matching: {languageVersion=21Gradle không tìm thấy JDK 21 nào để compilecài JDK 21, hoặc hạ toolchain trong build.gradle
bash: ./gradlew: Permission deniedscript wrapper mất bit executablechmod +x gradlew
Plugin [id: 'org.springframework.boot', version: '4.1.1'] was not foundlần build đầu không ra được mạngonline một lần cho build chạy xong, sau đó nó đã được cache
Controller mới trả về 404package của nó nằm ngoài phạm vi được scanchuyển class vào dưới com.example.demo

Port 8080 đã bị chiếm. Khởi động instance thứ hai trong khi cái đầu vẫn chạy và Boot từ chối, với một message dễ hiểu hiếm thấy:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Web server failed to start. Port 8080 was already in use.
 
Action:
 
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

Tìm thủ phạm bằng lsof -ti tcp:8080 trên macOS hoặc Linux, hoặc netstat -ano | findstr :8080 trên Windows, rồi kill process id nó in ra. Gần như luôn luôn là một lần chạy trước đó của chính ứng dụng này, bạn mở từ IDE rồi quên mất.

Sai Java version, lúc run. Build bằng 21, chạy bằng 17, và JVM từ chối file class trước khi code của bạn kịp thực thi:

Text
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/example/demo/DemoApplication has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0

Class file version 65 là Java 21 và 61 là Java 17; lấy số đó trừ 44 ra version Java. Cách sửa là JAVA_HOME, quay lại phần đầu bài.

Sai Java version, lúc build. Dạng đặc thù của Gradle cho cùng một vấn đề: build file yêu cầu toolchain 21, và không tìm thấy JDK 21 nào.

Text
* What went wrong:
Could not determine the dependencies of task ':bootJar'.
> Could not resolve all dependencies for configuration ':runtimeClasspath'.
   > Failed to calculate the value of task ':compileJava' property 'javaCompiler'.
      > Cannot find a Java installation on your machine (Mac OS X 26.4.1 aarch64) matching: {languageVersion=21, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain auto-provisioning is not enabled.

Hãy cài JDK 21 — Gradle sẽ tự tìm thấy nó ở các đường dẫn chuẩn mà không cần được chỉ. Lỗi tương đương khi không đi qua cơ chế toolchain của Gradle là chính javac từ chối target:

Text
$ javac -source 21 -target 21 Foo.java
error: invalid source release: 21
 
$ javac --release 21 Foo.java
error: release version 21 not supported

gradlew: Permission denied. Wrapper là một shell script, và vài cách di chuyển project — một tool giải nén làm rơi permission, một lần checkout trên filesystem không có bit executable, một lần copy từ Windows sang — sẽ xóa mất bit đó:

Text
$ ./gradlew --version
bash: ./gradlew: Permission denied
Bash
chmod +x gradlew

Trên Windows bạn dùng gradlew.bat nên chuyện này không xảy ra.

Không có mạng ở lần build đầu. Mọi dependency, và cả Gradle, đều được tải một lần rồi cache lại. Trước khi cache đó tồn tại thì bạn bắt buộc phải online, và kiểu lỗi phụ thuộc vào thứ nào tải hụt trước. Wrapper không tải được Gradle:

Text
Downloading https://services.gradle.org/distributions/gradle-9.7.1-bin.zip
 
Attempt 1/1 failed. Reason: services.gradle.invalid
Exception in thread "main" java.net.UnknownHostException: services.gradle.invalid

Hoặc build không tải được plugin của Boot:

Text
* What went wrong:
Plugin [id: 'org.springframework.boot', version: '4.1.1'] was not found in any of the following sources:
 
- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (could not resolve plugin artifact 'org.springframework.boot:org.springframework.boot.gradle.plugin:4.1.1')

Nếu ngồi sau proxy công ty, hãy đặt systemProp.https.proxyHostsystemProp.https.proxyPort trong ~/.gradle/gradle.properties. Khi một lần build đã thành công, các lần sau chạy offline được.

Controller trả về 404. Bạn đã thêm @RestController, ứng dụng khởi động sạch sẽ, mà URL vẫn 404. Đặt class đó vào một package nằm ngoài com.example.demo và bạn nhận được đúng thứ này:

Text
$ curl -s -i http://localhost:8080/outside
HTTP/1.1 404
Content-Type: application/json
 
{"timestamp":"2026-09-11T03:12:48.399Z","status":404,"error":"Not Found","path":"/outside"}

Class đó đã được compile và đóng gói; chỉ là không ai nhìn tới nó. Component scanning bắt đầu từ package của class có @SpringBootApplication và chỉ đi xuống, nên com.example.other là vô hình với một project gốc com.example.demo. Hãy chuyển class vào dưới package gốc. Bài 4 sẽ giải thích rule này tử tế.

FAQ

Có phải cài Maven hay Gradle để build project Spring Boot không?

Không. Project sinh ra đã có sẵn wrapper — gradlew / gradlew.bat, hoặc mvnw / mvnw.cmd với project Maven — và nó tự tải đúng version build tool mà project được tạo ra cùng. Luôn chạy ./gradlew, đừng chạy gradle, để mọi người build project đều dùng chung một version.

Nên chọn Java version nào cho Spring Boot 4?

  1. Boot 4 yêu cầu tối thiểu 17 và Initializr vẫn mặc định 17, nhưng 21 là bản LTS hiện tại và cũng nằm ngay trên form đó. Hãy đổi field trước khi bấm Generate; đổi sau nghĩa là phải sửa build.gradle rồi import lại project.

Tại sao build file sinh ra là spring-boot-starter-webmvc chứ không phải spring-boot-starter-web?

Vì Boot 4 đã đổi tên. Tick "Spring Web" cho ra spring-boot-starter-webmvc, và test starter tương ứng là spring-boot-starter-webmvc-test. Tutorial viết cho Boot 3 trở về trước dùng tên cũ. Hãy giữ tên trong file được sinh ra và sửa tutorial, không phải ngược lại.

IntelliJ IDEA Community có đủ để làm Spring Boot không?

Câu hỏi này giờ đã lỗi thời: từ 2025.3, IntelliJ IDEA là một sản phẩm duy nhất thay vì hai edition, và bản miễn phí đã có wizard tạo project Spring Boot, Gradle, debugger và highlight Spring cơ bản. Chừng đó phủ hết mọi thứ trong bài này. Tài liệu của JetBrains vẫn ghi "Spring support is limited in IntelliJ IDEA without the Ultimate subscription", nhưng điều đó nói về phần navigate và hỗ trợ editor chuyên sâu cho Spring, không phải về việc build và chạy được hay không.

demo-0.0.1-SNAPSHOT.jar và demo-0.0.1-SNAPSHOT-plain.jar khác nhau ở đâu?

File plain chỉ chứa class đã compile của bạn, vài kilobyte. File còn lại chứa class của bạn cộng mọi dependency cộng Tomcat nhúng, nên nó khoảng 19 MB và chạy độc lập được bằng java -jar. Hãy chạy file to.

Tại sao ứng dụng khởi động xong rồi thoát ngay?

Gần như luôn là thiếu web starter. Đổi spring-boot-starter-webmvc thành spring-boot-starter thuần và cả lần chạy chỉ dài ba dòng log — banner, Starting DemoApplication, Started DemoApplication in 0.229 seconds — không có Tomcat started on port 8080 ở giữa, rồi JVM thoát. Đó là hành vi đúng với một ứng dụng không phải web: không có gì đang listen trên socket, nên không có gì giữ process sống. Hãy kiểm tra web starter có trong build.gradle không, và bạn đã import lại project sau khi thêm nó chưa.

Kết luận

Giờ bạn đã có một JDK mà terminal và build tool đồng ý với nhau, một project do Spring Initializr sinh ra với các field được chọn có chủ đích chứ không phải nhận bừa default, một ứng dụng khởi động dưới một giây, và hai endpoint — một trả text, một trả JSON mà bạn không viết một dòng serialisation nào. Log khởi động không còn là một bức tường chữ: bạn chỉ được ra dòng nào là lúc bean được dựng, dòng nào là lúc port được bind, và biết hai dòng nào chỉ xuất hiện sau request đầu tiên.

Bài tiếp theo mở ra những gì generator đã viết: cấu trúc project và build tool — Gradle và Maven, starter dependency là gì, @SpringBootApplication phân rã ra thành những gì, và bên trong file JAR 19 MB đó có gì.

Bài viết liên quan

[Spring Boot Basics] Cấu trúc project Spring Boot: Gradle và Maven, @SpringBootApplication và file JAR

Toàn bộ file mà Spring Initializr sinh ra trên Spring Boot 4.1.1 và Java 21: cấu trúc thư mục, wrapper của Gradle và Maven, build.gradle đọc từng dòng đặt cạnh pom.xml tương ứng, so sánh Maven và Gradle công bằng, BOM giúp bỏ version number, @SpringBootApplication tách thành ba meta-annotation kèm lỗi 404 do sai base package, và giải phẫu file JAR chạy được.

[Spring Boot Basics] Logging trong Spring Boot: SLF4J, Logback, log level và ghi log ra file

Logging trong Spring Boot 4.1.1 kiểm chứng trên project thật: SLF4J là facade và Logback 1.5.38 là implementation, hai bridge jul-to-slf4j và log4j-to-slf4j, parameterised và fluent logging, ghi log exception, log level, cây logger và log group, --debug so với --trace, pattern dòng log mặc định, logging.file.name kèm rotation, logback-spring.xml với springProfile, MDC và chuyển sang Log4j2.

[Spring Boot Basics] Tài liệu API trong Spring Boot với springdoc-openapi và Swagger UI

springdoc-openapi 3.1.1 trên Spring Boot 4.1.1, kiểm chứng trên jar đang chạy: document OpenAPI 3.1 ở /v3/api-docs, Swagger UI và Try it out, những gì springdoc suy ra từ controller, DTO record và Bean Validation constraint, response nào của @RestControllerAdvice được thêm vào, @Tag, @Operation, @ApiResponse, @Parameter và @Schema trên record, bean OpenAPI và customizer toàn cục, GroupedOpenApi, property của springdoc và tắt tài liệu trong profile prod.

[Spring Boot Basics] Validation trong Spring Boot: Bean Validation, @Valid và custom validator

Bean Validation trong Spring Boot 4.1.1 với Hibernate Validator 9.1.3, kiểm chứng bằng các lần chạy thật: spring-boot-starter-validation, @NotNull, @NotEmpty và @NotBlank khác nhau ra sao, @Size, @DecimalMin, @Digits, @Email và @Pattern trên DTO record, @Valid với @RequestBody và response 400 mặc định, object lồng nhau và list, validate @PathVariable và @RequestParam cùng cái bẫy 500 của @Validated, validation group, ValidationMessages.properties và Accept-Language, custom ConstraintValidator và constraint liên quan nhiều field, và validation ở service layer.