Spring Boot tutorial chi tiết từ A-Z cho lập trình viên Java

Spring Framework nổi tiếng nhờ tính mạnh mẽ, linh hoạt, nhưng cũng nổi tiếng với sự phức tạp. Để giải quyết bài toán này, Spring Boot đã ra đời như một phần mở rộng của Spring Framework, được tạo ra để đơn giản hóa quá trình thiết lập và phát triển ứng dụng. Bài hướng dẫn Spring Boot tutorial này sẽ chỉ cho bạn cách tận dụng sức mạnh của Spring Boot để xây dựng ứng dụng một cách nhanh chóng và hiệu quả. 

Đọc tutorial dưới đây để được hướng dẫn:

  • Cài đặt môi trường phát triển phù hợp
  • Khởi tạo dự án Spring Boot nhanh chóng
  • Sử dụng IDE để tạo và quản lý dự án
  • Xây dựng RESTful API với Spring Boot
  • Làm việc với cơ sở dữ liệu (JPA/Hibernate)
  • Bảo mật ứng dụng bằng Spring Security
  • Thực hiện kiểm thử với Spring Boot (Unit & Integration Testing)

Đọc chi tiết: Spring Boot là gì: Chi tiết cách xây dựng ứng dụng với Spring Boot

Hướng dẫn cài đặt môi trường phát triển

Trước khi bắt đầu viết những dòng code Spring Boot đầu tiên, việc quan trọng nhất là chúng ta cần chuẩn bị một môi trường phát triển hoàn chỉnh. Đừng lo lắng, quá trình này khá đơn giản và mình sẽ hướng dẫn bạn từng bước một.

Cài đặt Java Development Kit (JDK)

JDK là nền tảng cho mọi ứng dụng Java, cung cấp trình biên dịch, thư viện và môi trường thực thi. Spring Boot 3.x yêu cầu Java 17 trở lên. 

Để kiểm tra phiên bản hiện tại: Bạn mở Terminal/Command Prompt, gõ lệnh: java -version

Nếu chưa có hoặc phiên bản không đúng, hãy cài đặt JDK.

Hướng dẫn cài đặt JDK

Bạn có thể chọn giữa hai bản thông dụng:

Các bước cài đặt:

  • Tải bản cài đặt phù hợp với hệ điều hành của bạn (Windows, macOS, Linux) từ một trong các link trên.
  • Chạy file cài đặt và làm theo hướng dẫn.
  • Thiết lập biến môi trường JAVA_HOME:
    • Trỏ JAVA_HOME đến thư mục cài đặt JDK (ví dụ: C:\Program Files\Java\jdk-17).
    • Thêm %JAVA_HOME%\bin (Windows) hoặc $JAVA_HOME/bin (macOS/Linux) vào biến Path của hệ thống.
  • Mở Terminal/Command Prompt mới, kiểm tra lại với java -version.

Cài đặt Maven hoặc Gradle

Các công cụ như Apache Maven và Gradle đều là những hệ thống quản lý build và dependency, có chức năng tự động hóa quá trình biên dịch, đóng gói và quản lý thư viện cho dự án. 

Apache Maven sử dụng tệp pom.xml và hoạt động dựa trên quy ước, là một lựa chọn rất phổ biến và quen thuộc trong cộng đồng. Trong khi đó, Gradle mang đến sự linh hoạt cao hơn với việc sử dụng Groovy hoặc Kotlin DSL trong tệp build.gradle, đồng thời thường cho tốc độ build nhanh hơn. 

Đối với người mới bắt đầu, Maven thường được khuyên dùng vì tính dễ tiếp cận của nó.

Hướng dẫn cài đặt Apache Maven

  • Bước 1: Tải Maven: https://maven.apache.org/download.cgi
  • Bước 2: Giải nén vào một thư mục (ví dụ: C:\Program Files\Apache\maven).
  • Bước 3: Thiết lập biến môi trường:
    • M2_HOME (hoặc MAVEN_HOME): Trỏ đến thư mục giải nén Maven.
    • Thêm %M2_HOME%\bin (Windows) hoặc $M2_HOME/bin (macOS/Linux) vào Path.
  • Kiểm tra: mvn -version.

Hướng dẫn cài đặt Gradle

  • Bước 1: Tải Gradle: https://gradle.org/install/
  • Bước 2: Làm theo hướng dẫn trên trang chủ hoặc giải nén thủ công.
  • Bước 3: Nếu cài manual, thiết lập GRADLE_HOME và thêm $GRADLE_HOME/bin vào Path.
  • Kiểm tra: gradle -version.

Cài đặt IDE (Integrated Development Environment)

IDE cung cấp trình soạn thảo code thông minh, debugger và các công cụ hỗ trợ phát triển. Một số IDE phổ biến bao gồm:

IntelliJ IDEA: Mạnh mẽ và thông minh với khả năng phân tích và gợi ý code hàng đầu..

  • Community Edition (Miễn phí): Đủ dùng cho hầu hết các dự án Spring Boot.
  • Ultimate Edition (Trả phí): Nhiều tính năng nâng cao hơn.
  • Link tải: https://www.jetbrains.com/idea/download/

Eclipse IDE + Spring Tools Suite (STS): Tích hợp sâu dành riêng cho hệ sinh thái Spring giúp điều hướng và quản lý dự án dễ dàng.

Visual Studio Code (VS Code) + Extensions: Nhẹ, nhanh và linh hoạt hơn.

Sau khi hoàn thành các bước trên, môi trường của bạn đã sẵn sàng để bắt đầu với Spring Boot!

Hướng dẫn tạo dự án Spring Boot đầu tiên

Sau khi đã chuẩn bị xong môi trường, bạn có thể tạo dự án Spring Boot đầu tiên. Cách phổ biến và dễ dàng nhất để làm điều này là sử dụng Spring Initializr, một công cụ web tiện lợi do chính đội ngũ Spring cung cấp.

Tổng quan về Spring Initializr (start.spring.io)

Spring Initializr (https://start.spring.io/) cho phép bạn tùy chỉnh và tạo ra một “bộ xương” dự án Spring Boot với các cấu hình và dependency cơ bản chỉ trong vài cú nhấp chuột.

Khi truy cập vào trang web, bạn sẽ thấy một giao diện trực quan với các tùy chọn để cấu hình dự án của mình như hình dưới đây:

Hãy cùng tìm hiểu chi tiết từng mục:

  • Project (Dự án): Lựa chọn giữa Maven ProjectGradle Project. Cả hai đều là công cụ quản lý build và dependency mạnh mẽ. Maven thường được chọn nhiều hơn trong các hướng dẫn ban đầu vì cấu trúc pom.xml của nó khá dễ đọc. Ở bài này chúng ta sẽ sử dụng Maven.
  • Language (Ngôn ngữ): Spring Boot hỗ trợ Java, Kotlin, và Groovy. Ở bài này chúng ta chọn Java.
  • Spring Boot (Phiên bản Spring Boot): Mục này cho phép bạn chọn phiên bản Spring Boot cho dự án. 

Luôn ưu tiên chọn phiên bản ổn định mới nhất (thường được đánh dấu mặc định và không có chữ SNAPSHOT hoặc M – Milestone). Ví dụ: 3.3.0. Tránh các phiên bản thử nghiệm nếu bạn muốn sự ổn định.

Project Metadata (Thông tin dự án):

  • Group (Nhóm): Thường là tên domain đảo ngược của tổ chức hoặc cá nhân bạn. Ví dụ: com.myblog hoặc vn.coderschool. Nó giúp định danh duy nhất cho dự án của bạn trong một kho lưu trữ Maven.
  • Artifact (Tên cấu phần): Đây là tên của dự án của bạn (tên file .jar hoặc .war sẽ được tạo ra). Ví dụ: spring-boot-tutorial hoặc my-first-app.
  • Name (Tên dự án): Thường giống với Artifact, đây là tên hiển thị của dự án.
  • Description (Mô tả): Mô tả ngắn gọn về dự án. Ví dụ: Demo project for Spring Boot.
  • Package name (Tên gói): Tự động được tạo ra từ Group và Artifact (ví dụ: com.myblog.springboot_tutorial). Đây là package gốc chứa class Application chính của bạn. Bạn có thể tùy chỉnh nếu muốn.

Packaging (Đóng gói):

  • Jar: Tạo ra một file .jar thực thi độc lập, đã nhúng sẵn server (như Tomcat). Đây là lựa chọn phổ biến và được khuyến nghị cho hầu hết các ứng dụng microservices và web đơn giản. Ở bài này chúng ta sẽ chọn Jar.
  • War: Tạo ra một file .war để triển khai trên một server Java EE truyền thống (như Tomcat, JBoss bên ngoài). Thường ít được sử dụng hơn với Spring Boot trừ khi có yêu cầu cụ thể.
  • Java (Phiên bản Java): Chọn phiên bản Java bạn đã cài đặt và muốn sử dụng cho dự án. Ví dụ: 17 (phù hợp với Spring Boot 3.x).
  • Dependencies (Các thư viện phụ thuộc): Đây là nơi bạn thêm các “starter” và thư viện cần thiết cho dự án. Nhấn vào nút “ADD DEPENDENCIES…” hoặc sử dụng ô tìm kiếm.

Đối với dự án đầu tiên, chúng ta sẽ thêm một vài dependency cơ bản:

  • Spring Web: Rất quan trọng! Dependency này bao gồm mọi thứ cần thiết để xây dựng ứng dụng web, bao gồm RESTful API, và tự động cấu hình một server Tomcat nhúng. Tìm kiếm “Web” và chọn “Spring Web”.
  • Lombok (Tùy chọn nhưng rất hữu ích): Một thư viện tuyệt vời giúp giảm thiểu code boilerplate (code lặp đi lặp lại) như getters, setters, constructors, toString(), v.v. bằng cách sử dụng các annotation. Tìm kiếm “Lombok” và chọn.

Các bước tải về và import vào IDE

Bước 1: Tạo dự án (Generate project)

  • Sau khi đã cấu hình xong tất cả các tùy chọn, nhấn nút “GENERATE” (hoặc Ctrl + Enter).
  • Một file .zip (ví dụ: spring-boot-tutorial.zip) sẽ được tải về máy tính của bạn.

Bước 2: Giải nén file .zip vừa tải về vào một thư mục làm việc mà bạn chọn.

Bước 3: Import vào IDE yêu thích của bạn (IntelliJ IDEA, Eclipse với STS, hoặc VS Code).

Với IntelliJ IDEA:

  • Chọn “Open” → Trỏ đến thư mục bạn vừa giải nén dự án (thư mục chứa file pom.xml) → IntelliJ IDEA sẽ tự động nhận diện đây là một dự án Maven và tiến hành tải các dependency cần thiết.

Với Eclipse (đã cài STS): 

  • Chọn “File” → “Import…”
  • Trong cửa sổ Import, tìm và chọn “Maven” → “Existing Maven Projects” → Nhấn “Next” → Nhấn “Browse…” và trỏ đến thư mục gốc của dự án bạn vừa giải nén.
  • Eclipse sẽ phát hiện file pom.xml và hiển thị dự án. Đảm bảo dự án được chọn và nhấn “Finish”.

Với Visual Studio Code (đã cài Java Extension Pack và Spring Boot Extension Pack):

  • Chọn “File” -> “Open Folder…” → Trỏ đến thư mục dự án bạn vừa giải nén.
  • VS Code sẽ tự động nhận diện dự án Java/Maven. Bạn có thể cần đợi một chút để các extension khởi tạo và tải dependency.

Sau khi import thành công, IDE sẽ bắt đầu tải xuống các dependency đã khai báo trong file pom.xml (nếu bạn chọn Maven). Quá trình này có thể mất vài phút tùy thuộc vào tốc độ mạng của bạn.

Vậy là bạn đã tạo thành công dự án Spring Boot đầu tiên của mình! Ở phần tiếp theo, chúng ta sẽ khám phá cấu trúc của dự án này và viết những dòng code đầu tiên.

Hướng dẫn xây dựng RESTful API với Spring Boot

RESTful API cho phép các ứng dụng tương tác với tài nguyên trên server qua HTTP, thường dùng JSON để trao đổi dữ liệu. Spring Boot giúp việc này trở nên đơn giản hơn.

Giới thiệu về RESTful API

RESTful API (Representational State Transfer) là một kiểu kiến trúc phần mềm tiêu chuẩn được sử dụng để thiết kế các dịch vụ web. Nó hoạt động dựa trên một tập hợp các nguyên tắc cốt lõi nhằm tạo ra một hệ thống linh hoạt, có khả năng mở rộng và dễ bảo trì.

Các đặc điểm chính của kiến trúc này bao gồm: 

  • Mô hình Client-Server, giúp tách biệt hoàn toàn giữa giao diện người dùng (client) và nơi xử lý logic, lưu trữ dữ liệu (server). 
  • Stateless (phi trạng thái) yêu cầu mỗi yêu cầu từ client phải chứa đủ thông tin để server xử lý mà không cần lưu lại bất kỳ trạng thái nào từ các phiên làm việc trước. 
  • Cacheable cho phép lưu đệm phản hồi để tăng hiệu suất
  • Được xây dựng theo kiểu Layered System (hệ thống phân lớp), giúp tăng tính module và bảo mật

Nguyên tắc trung tâm của REST là Uniform Interface (giao diện đồng nhất), tức là sử dụng các phương thức HTTP tiêu chuẩn (GET, POST, PUT, DELETE) một cách nhất quán để thao tác trên các tài nguyên. Ví dụ, để quản lý sản phẩm, một RESTful API sẽ có các endpoint rõ ràng như ví dụ sau:

  • GET /api/products: Lấy tất cả sản phẩm.
  • GET /api/products/{id}: Lấy sản phẩm theo ID.
  • POST /api/products: Tạo sản phẩm mới.
  • PUT /api/products/{id}: Cập nhật sản phẩm.
  • DELETE /api/products/{id}: Xóa sản phẩm.

Cách sử dụng các Annotation chính cho REST Controller

@RestController

Đây là một annotation chuyên dụng để xây dựng REST API trong Spring. Về cơ bản, nó là sự kết hợp của @Controller@ResponseBody. Khi bạn dùng @RestController để đánh dấu một class, Spring sẽ tự động coi tất cả các phương thức trong class đó như thể chúng đều có @ResponseBody.

Kết quả là mọi dữ liệu bạn trả về từ phương thức (như Object, List) sẽ được chuyển đổi thẳng thành JSON và gửi về trong response body mà không cần thêm bất kỳ annotation nào khác.

Ví dụ:

package com.example.demo.controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class HelloController {
    @GetMapping("/hello-rest")
    public String sayHello() { return "Hello from REST!"; }
}

@RequestMapping 

Đây là annotation được dùng để ánh xạ một URL của request đến phương thức xử lý tương ứng trong controller. Nó rất linh hoạt, cho phép bạn đặt ở cấp class để định nghĩa một tiền tố URL chung cho cả nhóm endpoint, ví dụ @RequestMapping("/api/users"). Sau đó, bạn có thể tiếp tục sử dụng nó ở cấp phương thức để xác định đường dẫn cụ thể, ví dụ ("/{id}"), và đường dẫn cuối cùng để truy cập sẽ là sự kết hợp của cả hai.

  • Các annotation rút gọn: bao gồm @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping.

Ví dụ:

package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/products") // Tiền tố chung
public class ProductController {
    @GetMapping
    public String getAllProducts() { return "List of products"; }

    @PostMapping
    public String createProduct() { return "Product created"; }
}

Các annotation trích xuất dữ liệu từ Request

  • @PathVariable: Lấy giá trị từ URI path. 

Ví dụ: /products/{id}.

@GetMapping("/{productId}")
public String getProductById(@PathVariable Long productId) {
    return "Product ID: " + productId;
}
  • @RequestParam: Lấy giá trị từ query parameters. 

Ví dụ: /search?query=laptop.

@GetMapping("/search")
public String search(@RequestParam String query) {
    return "Searching for: " + query;
}
  • @RequestBody: Lấy toàn bộ request body (thường là JSON) và chuyển thành đối tượng Java. Dùng cho POST, PUT.
// Giả sử có class ProductDTO { private String name; /* getters/setters */ }
@PostMapping("/dto")
public ProductDTO createWithDto(@RequestBody ProductDTO productPayload) {
    System.out.println("Received: " + productPayload.getName());
    return productPayload;
}

Hướng dẫn xử lý Request và Response

Trong Spring Boot, việc xử lý dữ liệu requestresponse rất đơn giản. 

  • Nhận dữ liệu request:

Để nhận dữ liệu, bạn dùng annotation @RequestBody để tự động chuyển đổi một chuỗi JSON trong request thành đối tượng Java. Đối với dữ liệu từ form, Spring có thể tự động liên kết (bind) chúng vào các tham số phương thức. 

  • Trả dữ liệu:

Ở chiều ngược lại, khi sử dụng @RestController, bạn chỉ cần trả về một đối tượng Java bất kỳ, và nó sẽ được tự động chuyển đổi thành JSON trong phần thân của HTTP response để gửi về cho client.

  • Mã trạng thái HTTP:

Spring Boot tự động gán các mã trạng thái HTTP hợp lý như 200 OK cho các yêu cầu thành công, 201 Created khi tạo mới qua POST, và 204 No Content sau khi DELETE thành công.

Tuy nhiên, để có toàn quyền kiểm soát và xử lý các trường hợp phức tạp hơn như trả về lỗi 404 hoặc thêm headers, bạn nên sử dụng ResponseEntity<T>. Lớp này cho phép bạn tùy chỉnh đồng thời cả phần thân (body), các headers, và mã trạng thái của response một cách tường minh.

Ví dụ:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
// Giả sử có class Product { Long id; String name; ... }

@GetMapping("/items/{id}")
public ResponseEntity<Product> getItem(@PathVariable Long id) {
    Product product = findProductById(id); // Hàm giả định
    if (product != null) {
        return ResponseEntity.ok(product); // 200 OK
    } else {
        return ResponseEntity.notFound().build(); // 404 Not Found
    }
}

@PostMapping("/items")
public ResponseEntity<Product> createItem(@RequestBody Product product) {
    Product createdProduct = saveProduct(product); // Hàm giả định
    return new ResponseEntity<>(createdProduct, HttpStatus.CREATED); // 201 Created
}

// Các hàm và class giả định
private Product findProductById(Long id) {
    if (id == 1L) return new Product(id, "Sample Product"); return null;
}
private Product saveProduct(Product p) { p.setId(System.currentTimeMillis()); return p; }
static class Product {
    private Long id; private String name;
    public Product(Long id, String name) { this.id = id; this.name = name; }
    public Long getId() { return id; } public void setId(Long id) { this.id = id; }
    public String getName() { return name;} public void setName(String name) { this.name = name;}
}

Hoặc bạn có thể dùng @ResponseStatus trên phương thức handler hoặc class Exception.

Hiểu về Data Transfer Objects (DTOs)

Tại sao cần DTO?

Data Transfer Object (DTO) là một đối tượng đơn giản dùng để mang dữ liệu giữa các tầng khác nhau của ứng dụng, đặc biệt là cho các API.

Sử dụng DTO là một thực hành tốt nhất (best practice) vì nó tách biệt cấu trúc dữ liệu công khai của API khỏi cấu trúc entity nội bộ trong cơ sở dữ liệu. Điều này cho phép bạn kiểm soát chính xác dữ liệu được phơi bày ra ngoài, giúp tránh làm lộ thông tin nhạy cảm. DTO cũng là nơi lý tưởng để áp dụng các quy tắc validation (xác thực) cho dữ liệu đầu vào.

Ngoài ra, đối với các ứng dụng dùng JPA, mẫu thiết kế này còn giải quyết triệt để lỗi LazyInitializationException phổ biến bằng cách tạo ra một đối tượng chứa dữ liệu độc lập trước khi phiên giao dịch với cơ sở dữ liệu đóng lại.

Ví dụ:

// DTO cho tạo user
// package com.example.demo.dto;
public class UserCreationDTO {
    private String username;
    private String password;
    private String email;
    // Getters & Setters
    public String getUsername() { return username; }
    public void setUsername(String u) { username = u; }
    public String getPassword() { return password; }
    public void setPassword(String p) { password = p; }
    public String getEmail() { return email; }
    public void setEmail(String e) { email = e; }
}

// DTO cho response user
// package com.example.demo.dto;
public class UserResponseDTO {
    private Long id;
    private String username;
    private String email;
    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long i) { id = i; }
    public String getUsername() { return username; }
    public void setUsername(String u) { username = u; }
    public String getEmail() { return email; }
    public void setEmail(String e) { email = e; }
}

Sử dụng trong Controller, bạn sẽ cần logic để chuyển đổi giữa DTO và Entity (thủ công hoặc dùng thư viện như ModelMapper).

Hướng dẫn sử dụng Validation (Xác thực dữ liệu)

Để xác thực dữ liệu trong Spring Boot, bạn chỉ cần thêm các annotation từ Bean Validation API (như @NotNull, @Size, @Email) trực tiếp lên các trường của lớp DTO. 

Ví dụ:

// package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class ArticleCreationDTO {
    @NotBlank @Size(min = 5, max = 100) private String title;
    @NotBlank private String content;
    @Email private String authorEmail;
    // Getters & Setters
    public String getTitle() { return title; } public void setTitle(String t) { title = t; }
    public String getContent() { return content; } public void setContent(String c) { content = c; }
    public String getAuthorEmail() { return authorEmail; } public void setAuthorEmail(String e) { authorEmail = e; }
}

Sau đó, để kích hoạt quá trình xác thực khi nhận request, hãy đặt annotation @Valid ngay trước tham số DTO trong phương thức controller của bạn. 

Ví dụ thêm @Valid (từ jakarta.validation.Valid) trước tham số @RequestBody trong controller:

// import com.example.demo.dto.ArticleCreationDTO;
// import jakarta.validation.Valid;
@PostMapping("/articles-validated")
public ResponseEntity<String> createArticleValidated(@Valid @RequestBody ArticleCreationDTO articleDTO) {
    return ResponseEntity.ok("Article valid: " + articleDTO.getTitle());
}

Nếu có lỗi validation, Spring sẽ tự động ném MethodArgumentNotValidException (trả về response 400 Bad Request). Dependency cho tính năng này (spring-boot-starter-validation) thường đã được tích hợp sẵn khi bạn dùng spring-boot-starter-web. Bạn có thể tùy chỉnh xử lý lỗi này.

Hướng dẫn xử lý ngoại lệ (Exception Handling)

Để xử lý ngoại lệ (exception) một cách tập trung trong Spring Boot, bạn sử dụng kết hợp hai annotation @ControllerAdvice@ExceptionHandler

  • Đầu tiên, bạn tạo một class chung và đánh dấu nó với @ControllerAdvice (hoặc @RestControllerAdvice cho REST API) để biến nó thành một bộ xử lý ngoại lệ toàn cục. 
  • Bên trong class này, bạn tạo các phương thức được đánh dấu với @ExceptionHandler, trong đó mỗi phương thức chỉ định một loại exception cụ thể mà nó sẽ bắt và xử lý (ví dụ: ResourceNotFoundException.class). 
  • Khi một exception tương ứng được ném ra từ bất kỳ controller nào, phương thức xử lý này sẽ tự động được gọi để trả về một response lỗi có cấu trúc và được kiểm soát.

Ví dụ:

package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", new Date());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(fe -> fe.getField(), fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid"));
        body.put("errors", errors);
        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(ResourceNotFoundException.class) // Custom exception
    public ResponseEntity<Object> handleResourceNotFound(ResourceNotFoundException ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", new Date());
        body.put("status", HttpStatus.NOT_FOUND.value());
        body.put("message", ex.getMessage());
        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }
    // Có thể thêm handler cho Exception.class để bắt tất cả lỗi khác
}

// Custom Exception
// package com.example.demo.exception;
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) { super(message); }
}

Ngoài ra, bạn có thể tạo custom exception kế thừa từ RuntimeException hoặc Exception để xử lý các tình huống lỗi cụ thể.

Hướng dẫn làm việc với cơ sở dữ liệu

Hầu hết ứng dụng đều cần lưu trữ dữ liệu. Spring Boot cùng Spring Data JPA giúp bạn làm việc với cơ sở dữ liệu quan hệ một cách dễ dàng. Hãy thực hành với các bước sau đây:

Bước 1: Hiểu về Spring Data JPA

Spring Data JPA là một framework giúp đơn giản hóa việc tương tác với cơ sở dữ liệu, hoạt động dựa trên tiêu chuẩn JPA (Java Persistence API) và các công cụ ORM (Object-Relational Mapping) như Hibernate.

Lợi ích lớn nhất của nó là giảm thiểu code lặp lại bằng cách:

  • Tự động cung cấp đầy đủ các phương thức CRUD thông qua interface kế thừa từ JpaRepository.
  • Cho phép truy vấn dữ liệu một cách linh hoạt hoặc tự động sinh truy vấn từ tên phương thức (Query Methods), hoặc viết các truy vấn phức tạp tùy chỉnh bằng annotation @Query (sử dụng JPQL hoặc SQL). 

Framework này tích hợp hoàn hảo vào hệ sinh thái Spring, giúp quá trình phát triển trở nên nhanh chóng và nhất quán.

Bước 2: Cấu hình kết nối Cơ sở dữ liệu

Để Spring Boot có thể kết nối với một cơ sở dữ liệu quan hệ, bạn cần thực hiện 2 việc chính:

  • Thêm Dependencies (pom.xml)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<!--
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
-->
  • Cấu hình trong application.properties

Với H2 (In-memory):

# H2 Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update # (update, create-drop)
spring.jpa.show-sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Với MySQL (Ví dụ Production):

# MySQL Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=UTC
spring.datasource.username=your_user
spring.datasource.password=your_pass
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=validate # (validate, none - an toàn cho production)

Giải thích về spring.jpa.hibernate.ddl-auto

spring.jpa.hibernate.ddl-auto là một thuộc tính cấu hình trong Spring Boot (sử dụng Hibernate làm JPA provider) giúp bạn kiểm soát cách Hibernate tương tác với cơ sở dữ liệu (CSDL) của bạn khi ứng dụng khởi động. Nó tự động thực hiện các thao tác DDL (Data Definition Language) như tạo, cập nhật hoặc kiểm tra cấu trúc bảng.

Dưới đây là bảng giải thích chi tiết các giá trị thường dùng:

Tùy chọn Mô tả chi tiếtMôi trường khuyến nghị Ghi chú 
createHibernate sẽ xóa toàn bộ schema CSDL hiện có (nếu có) và tạo lại tất cả các bảng dựa trên các thực thể JPA mỗi khi ứng dụng khởi động.Phát triển (dev), TestĐảm bảo CSDL luôn “sạch” và khớp với mô hình dữ liệu. Rất tiện lợi cho giai đoạn phát triển ban đầu khi bạn thường xuyên thay đổi cấu trúc dữ liệu.
create-dropTương tự create, Hibernate sẽ xóa và tạo lại schema khi ứng dụng khởi động. Điểm khác biệt là khi ứng dụng dừng (shutdown), Hibernate sẽ xóa toàn bộ schema CSDL.TestCực kỳ hữu ích cho các bài kiểm thử tích hợp tự động, đảm bảo mỗi lần chạy kiểm thử bắt đầu với một CSDL trống rỗng và nhất quán.
updateHibernate sẽ kiểm tra sự khác biệt giữa schema CSDL hiện có và mô hình dữ liệu của bạn, sau đó cố gắng cập nhật schema CSDL hiện có để khớp với mô hình dữ liệu mới.Phát triển (dev)Có thể tiện lợi trong dev để nhanh chóng xem các thay đổi schema. Tuyệt đối KHÔNG nên sử dụng trong môi trường sản phẩm (production) vì tiềm ẩn rủi ro hỏng CSDL và mất dữ liệu nếu Hibernate không thể xử lý các thay đổi phức tạp một cách chính xác.
validateHibernate sẽ kiểm tra xem schema CSDL hiện có có khớp hoàn toàn với mô hình dữ liệu của bạn hay không. Nếu có bất kỳ sự không khớp nào, Hibernate sẽ ném ra một ngoại lệ và ứng dụng sẽ không khởi động được.Staging, ProductionRất tốt để đảm bảo tính nhất quán giữa code và CSDL. Giúp phát hiện sớm các vấn đề về schema trước khi chúng gây ra lỗi trong môi trường thực tế.
noneHibernate sẽ không thực hiện bất kỳ thao tác DDL nào lên CSDL khi ứng dụng khởi động.ProductionAn toàn nhất cho môi trường sản phẩm. Khi sử dụng none, bạn cần tự quản lý các thay đổi schema CSDL. Phương pháp tốt nhất là sử dụng các công cụ quản lý di chuyển CSDL chuyên nghiệp như Flyway hoặc Liquibase để đảm bảo quá trình triển khai CSDL an toàn, có kiểm soát và có khả năng rollback.

Bước 3: Tạo Entities (Thực thể)

Entity là lớp Java đại diện cho một bảng trong database.

Các JPA Annotation chính:

AnnotationChi tiết 
Entity Mỗi đối tượng của lớp Entity sẽ tương ứng với một hàng dữ liệu trong bảng đó. Các thuộc tính (fields) của lớp Entity sẽ tương ứng với các cột trong bảng.
@EntityĐánh dấu một lớp Java là một JPA entity. Bắt buộc phải có để JPA nhận diện và quản lý lớp này như một thực thể cơ sở dữ liệu.
@Table(name = “ten_bang”)Tùy chọn. Nếu không khai báo, JPA sẽ mặc định lấy tên lớp Entity làm tên bảng.
Ví dụ: @Table(name = "products")
@IdThuộc tính được đánh dấu @Id sẽ là duy nhất và dùng để xác định một hàng dữ liệu trong bảng.
@GeneratedValue(strategy = GenerationType.IDENTITY)GenerationType.IDENTITY: Cơ sở dữ liệu sẽ tự động tạo giá trị cho khóa chính (thường dùng cho các CSDL như MySQL, PostgreSQL). Có các chiến lược khác như AUTO, SEQUENCE, TABLE.
@Column(name = “ten_cot”)Tùy chọn. Nếu không khai báo, JPA sẽ mặc định lấy tên thuộc tính làm tên cột.
Có thể cấu hình thêm các thuộc tính khác như nullable, length, unique… Ví dụ: @Column(name = "product_name", nullable = false, length = 255)
RelationshipsCác annotations phổ biến bao gồm:
– @OneToMany: Một đối tượng của Entity này có thể có nhiều đối tượng của Entity khác.
– @ManyToOne: Nhiều đối tượng của Entity này có thể liên quan đến một đối tượng của Entity khác.
– @OneToOne: Mối quan hệ một – một.
– @ManyToMany: Mối quan hệ nhiều – nhiều.

Ví dụ entity Product:

package com.example.demo.model;

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;
    private Double price;
    private int stockQuantity;

    // Constructors, Getters, Setters (Lombok có thể giúp rút gọn)
    public Product() {}
    public Product(String name, Double price, int stockQuantity) {
        this.name = name; this.price = price; this.stockQuantity = stockQuantity;
    }
    // Getters và Setters cho các trường...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
}

Bước 4: Tạo Repositories

Trong Spring Data JPA, repository là một interface bạn định nghĩa để tương tác với cơ sở dữ liệu cho một entity cụ thể, và Spring sẽ tự động tạo lớp triển khai cho nó. 

Bạn chỉ cần tạo một interface kế thừa từ JpaRepository<T, ID>, với:

  • T là kiểu Entity
  • ID là kiểu của khóa chính.

Ngay lập tức, bạn sẽ có sẵn đầy đủ các phương thức CRUD như save(), findById(), findAll().

Đối với các truy vấn tùy chỉnh, bạn có thể chỉ cần đặt tên phương thức theo quy ước để Spring tự sinh ra câu lệnh (ví dụ: findByName(...)), hoặc sử dụng annotation @Query để viết các truy vấn phức tạp bằng JPQL hay SQL.

Ví dụ ProductRepository:

package com.example.demo.repository;

import com.example.demo.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContainingIgnoreCase(String keyword);
    // @Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
    // List<Product> findProductsCheaperThan(@Param("maxPrice") Double maxPrice);
}

Ví dụ CRUD đơn giản

Để hình dung dễ hơn, chúng ta sẽ tìm hiểu qua ví dụ sau trong ngữ cảnh về một ứng dụng quản lý sản phẩm 

  • Tạo Enity: Lớp Product ánh xạ với bảng products trong database.
// Product.java
import jakarta.persistence.*; // Hoặc javax.persistence cho Spring Boot cũ hơn

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Double price;
    // Getters, Setters, Constructors (IDE có thể tự tạo)
}
  • Repository (Truy cập dữ liệu): Interface này cho phép Spring Data JPA tự động tạo các phương thức tương tác với DB.
// ProductRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // Tự động có: save(), findById(), findAll(), deleteById(), ...
}
  • Controller (API)

Endpoint mà client gọi tới để thực hiện CRUD. Nó sẽ dùng ProductRepository trực tiếp để đơn giản hóa ví dụ.

// ProductController.java
import com.example.demo.model.Product; // Đảm bảo đúng package của Product
import com.example.demo.repository.ProductRepository; // Đảm bảo đúng package của Repository
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @Autowired
    private ProductRepository productRepository; // Controller gọi trực tiếp Repository để rút gọn

    // GET: Lấy tất cả sản phẩm
    @GetMapping
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    // GET: Lấy sản phẩm theo ID
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productRepository.findById(id)
                .map(product -> new ResponseEntity<>(product, HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    // POST: Tạo sản phẩm mới
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        Product savedProduct = productRepository.save(product);
        return new ResponseEntity<>(savedProduct, HttpStatus.CREATED);
    }

    // PUT: Cập nhật sản phẩm
    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
        if (!productRepository.existsById(id)) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        product.setId(id); // Đảm bảo cập nhật đúng sản phẩm
        Product updatedProduct = productRepository.save(product);
        return new ResponseEntity<>(updatedProduct, HttpStatus.OK);
    }

    // DELETE: Xóa sản phẩm
    @DeleteMapping("/{id}")
    public ResponseEntity<HttpStatus> deleteProduct(@PathVariable Long id) {
        if (!productRepository.existsById(id)) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        productRepository.deleteById(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}
  • Và cuối cùng là cấu hình database (ví dụ H2 trong bộ nhớ):
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Cách hoạt động:

  1. Bạn định nghĩa Product.
  2. Spring Data JPA tự động tạo code cho ProductRepository dựa trên JpaRepository.
  3. ProductController sử dụng ProductRepository để thực hiện các thao tác Create, Read, Update, Delete khi nhận request HTTP.

Ví dụ này bỏ qua lớp Service để làm cho nó ngắn gọn nhất có thể, mặc dù trong ứng dụng thực tế, Service Layer thường được khuyến nghị để tách biệt logic nghiệp vụ khỏi tầng API và dữ liệu.

Ví dụ ProductService:

package com.example.demo.service;

import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import com.example.demo.exception.ResourceNotFoundException; // Custom exception
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class ProductService {
    @Autowired private ProductRepository productRepository;

    @Transactional
    public Product createProduct(Product product) { return productRepository.save(product); }

    public List<Product> getAllProducts() { return productRepository.findAll(); }

    public Product getProductById(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
    }

    @Transactional
    public Product updateProduct(Long id, Product productDetails) {
        Product existingProduct = getProductById(id); // Sẽ ném lỗi nếu không tìm thấy
        existingProduct.setName(productDetails.getName());
        existingProduct.setPrice(productDetails.getPrice());
        existingProduct.setStockQuantity(productDetails.getStockQuantity());
        return productRepository.save(existingProduct);
    }

    @Transactional
    public void deleteProduct(Long id) {
        if (!productRepository.existsById(id)) {
            throw new ResourceNotFoundException("Product", "id", id);
        }
        productRepository.deleteById(id);
    }
}

Controller sử dụng Service (ví dụ rút gọn): 

Controller sẽ inject ProductService và gọi các phương thức của nó.

package com.example.demo.controller;

import com.example.demo.model.Product; // Hoặc DTO
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    @Autowired private ProductService productService;

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) { // Nên dùng DTO
        return new ResponseEntity<>(productService.createProduct(product), HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) { // Nên dùng DTO
        return ResponseEntity.ok(productService.getProductById(id));
    }

    // Thêm các endpoint khác cho GET all, PUT, DELETE...
}

Lưu ý: Trong thực tế, nên sử dụng DTO (Data Transfer Objects) giữa Controller và Client thay vì Entity trực tiếp.

Hướng dẫn bảo mật ứng dụng với Spring Security 

Spring Security là một framework chuyên về bảo mật, tập trung vào hai nhiệm vụ chính:

  •  Authentication (Xác thực): xác minh danh tính của người dùng
  •  Authorization (Ủy quyền): quyết định những quyền truy cập mà người dùng đó được phép thực hiện sau khi đã xác thực thành công. 

Về mặt kỹ thuật, Spring Security hoạt động dựa trên một cơ chế cốt lõi là chuỗi các bộ lọc (filter chain), nơi mỗi yêu cầu gửi đến ứng dụng đều được xử lý qua chuỗi này để áp dụng các quy tắc bảo mật.

Bước 1: Cấu hình cơ bản

  1. Thêm dependency: spring-boot-starter-security vào pom.xml.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Cấu hình mặc định là:

  • Tất cả endpoint được bảo vệ.
  • Kích hoạt HTTP Basic và Form Login.
  • Tạo user user với password ngẫu nhiên (in ra console).
  • Bật CSRF protection.
  1. Tùy chỉnh với SecurityFilterChain (Spring Boot 3+):
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/h2-console/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults()) // Hoặc .formLogin(form -> form.loginPage("/custom-login").permitAll())
            .httpBasic(withDefaults())
            .csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")); // Tắt CSRF cho H2 console nếu dùng
        return http.build();
    }
    // UserDetailsService bean sẽ được định nghĩa ở phần sau
}

Bước 2: Authentication (Xác thực)

  • Cách 1: In-memory authentication (dùng để demo)

Bạn có thể định nghĩa user trực tiếp trong code:

// Trong SecurityConfig
// import org.springframework.security.core.userdetails.User;
// import org.springframework.security.core.userdetails.UserDetails;
// import org.springframework.security.core.userdetails.UserDetailsService;
// import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Bean
public org.springframework.security.core.userdetails.UserDetailsService inMemoryUserDetailsService(PasswordEncoder passwordEncoder) {
    org.springframework.security.core.userdetails.UserDetails user = org.springframework.security.core.userdetails.User.builder()
        .username("user")
        .password(passwordEncoder.encode("pass"))
        .roles("USER")
        .build();
    org.springframework.security.core.userdetails.UserDetails admin = org.springframework.security.core.userdetails.User.builder()
        .username("admin")
        .password(passwordEncoder.encode("adminpass"))
        .roles("ADMIN", "USER")
        .build();
    return new org.springframework.security.provisioning.InMemoryUserDetailsManager(user, admin);
}

Lưu ý: Sử dụng BCryptPasswordEncoder cho mật khẩu.

  • Cách 2: DBC authentication

Lấy user từ database. Cần cấu hình DataSource và các câu lệnh SQL hoặc schema.

  • Cách 3: Sử dụng UserDetailsService (khuyến nghị)

Tạo service implement UserDetailsService để load user từ database

package com.example.demo.service;

import com.example.demo.model.UserAccount; // Entity người dùng
import com.example.demo.repository.UserAccountRepository; // Repository người dùng
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;

@Service("customUserDetailsService") // Đặt tên bean để phân biệt nếu có nhiều UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired private UserAccountRepository userAccountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount user = userAccountRepository.findByUsername(username) // Giả sử có phương thức này
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        return new User(user.getUsername(), user.getPassword(),
            user.getRoles().stream() // Giả sử UserAccount có getRoles() trả về Collection<Role>
                .map(role -> new SimpleGrantedAuthority(role.getName())) // Giả sử Role có getName()
                .collect(Collectors.toSet()));
    }
}

Spring Security sẽ tự động dùng CustomUserDetailsService này

Bước 3: Authorization (Phân quyền)

Spring Security giúp bạn có thể kiểm soát ai truy cập cái gì:

1. Bảo mật cấp URL (SecurityFilterChain)

Đặt quy tắc chung cho các đường dẫn (URL) của ứng dụng:

  • hasRole("ADMIN"): Chỉ user có vai trò ADMIN được vào.
  • hasAuthority("READ"): Chỉ user có quyền READ được vào.
  • permitAll(): Ai cũng được vào.
  • authenticated(): Chỉ user đã đăng nhập được vào.

2. Bảo mật cấp phương thức (@EnableMethodSecurity)

Kiểm soát chi tiết hơn từng hàm trong code của bạn. Thêm @EnableMethodSecurity(securedEnabled = true) vào cấu hình bảo mật.

  • @Secured({"ROLE_ADMIN"}): Chỉ user có vai trò ADMIN được gọi hàm này.
  • @PreAuthorize("hasRole('USER') or hasAuthority('READ')"): Kiểm tra trước khi hàm chạy, dùng biểu thức phức tạp (SpEL). Ví dụ: user là USER HOẶC có quyền READ.
  • @PostAuthorize("returnObject.owner == authentication.name"): Kiểm tra sau khi hàm chạy, có thể xem kết quả trả về. Ví dụ: chỉ chủ sở hữu của dữ liệu được phép xem.

Về cơ bản, bạn có thể thiết lập bảo mật ở cấp độ đường dẫn chung hoặc chi tiết hơn ở cấp độ từng phương thức, tùy thuộc vào yêu cầu của ứng dụng.Ví dụ trong Service:

// import org.springframework.security.access.prepost.PreAuthorize;
// import org.springframework.stereotype.Service;

@org.springframework.stereotype.Service
public class SecureService {
    @org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")
    public String getAdminData() { return "Admin Data"; }

    @org.springframework.security.access.prepost.PreAuthorize("isAuthenticated()")
    public String getUserData() { return "User Data"; }
}

Hiểu thêm về JWT (JSON Web Token) – (Nâng cao)

JWT là một “chiếc thẻ” an toàn chứa thông tin người dùng, dùng cho xác thực mà không cần lưu trạng thái trên máy chủ (stateless).

Đọc thêm: JSON Web Token là gì: Định nghĩa và cách hoạt động

Cấu trúc: Header (kiểu token/thuật toán).Payload (thông tin người dùng/claims).Signature (chữ ký bảo mật).

Cách tích hợp với Spring Security:

  1. Thêm thư viện JWT để có thể thao tác với token.
  2. API Đăng nhập (/authenticate): Người dùng đăng nhập -> nếu thành công, server tạo và trả về JWT cho client.
  3. Lớp tiện ích JWT: Giúp bạn tạo, giải mã và xác thực token.
  4. Bộ lọc Request JWT:
    • Với mỗi yêu cầu (trừ đăng nhập), bộ lọc này sẽ kiểm tra JWT từ client.
    • Nếu JWT hợp lệ, người dùng được xác thực trong Spring Security.
  5. Cấu hình Spring Security:
    • Tắt session: Quan trọng để JWT hoạt động stateless.
    • Thêm bộ lọc JWT: Đảm bảo token được kiểm tra.

Tóm lại, JWT giúp bạn xây dựng API bảo mật, nơi người dùng nhận token khi đăng nhập và dùng token đó để truy cập các tài nguyên được bảo vệ mà không cần máy chủ phải lưu trữ phiên.

Hướng dẫn testing trong Spring Boot

Spring Boot được thiết kế để hỗ trợ tối đa việc testing. Nó cung cấp các công cụ và tích hợp sẵn giúp bạn viết test dễ dàng và hiệu quả.

Tại sao cần Testing?

  • Đảm bảo chất lượng: Testing giúp bạn phát hiện lỗi sớm trong quá trình phát triển, trước khi chúng gây ra vấn đề nghiêm trọng cho người dùng cuối. Việc sửa lỗi sớm luôn dễ dàng và ít tốn kém hơn nhiều so với việc sửa lỗi khi ứng dụng đã được triển khai.
  • Tự tin khi refactor: Với các bài kiểm thử tốt, bạn có thể tái cấu trúc code một cách an toàn và tự tin. Các bài test của bạn sẽ như một “tấm lưới an toàn”, lập tức báo động nếu bất kỳ thay đổi nào phá vỡ chức năng hiện có. Điều này khuyến khích việc cải thiện chất lượng code liên tục.
  • Tài liệu sống: Mỗi test case không chỉ kiểm tra một chức năng mà còn mô tả cách chức năng đó hoạt động trong các trường hợp khác nhau. Khi dev mới tham gia dự án, họ có thể đọc các test case để nhanh chóng hiểu được mục đích và hành vi của các phần mềm mà không cần đọc qua hàng ngàn dòng code logic nghiệp vụ.

Các loại Test

Unit Tests (Kiểm thử đơn vị):

  • Mục tiêu: Kiểm tra một đơn vị code nhỏ (phương thức, class) cô lập.
  • Đặc điểm: Nhanh, không phụ thuộc Spring context/DB. Dependencies được “mock”.
  • Ví dụ: Bạn sẽ dùng các “công cụ” như JUnit và Mockito. Lúc này, bạn chỉ tập trung vào logic của hàm đó thôi, không cần “đụng chạm” gì đến database hay các dịch vụ khác đâu.

Integration Tests (Kiểm thử tích hợp):

  • Mục tiêu: Kiểm tra tương tác giữa các thành phần hoặc với hệ thống ngoài.
  • Đặc điểm: Chậm hơn, có thể cần Spring context, DB (thường là in-memory).
  • Ví dụ: Bạn sẽ dùng @SpringBootTest để “khởi động” một phần (hoặc toàn bộ) ứng dụng Spring lên, và thường dùng các database “mini” (như H2) hoặc Testcontainers để kiểm tra tương tác với database y như thật.

Công cụ và Dependencies

spring-boot-starter-test bao gồm:

  • JUnit 5: Framework kiểm thử.
  • Spring Test & Spring Boot Test: Hỗ trợ test ứng dụng Spring.
  • Mockito: Tạo đối tượng giả (mock).
  • AssertJ: Thư viện assertion.

Dependency (pom.xml):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Cách viết Unit Test cho Services

Dùng Mockito để mock dependencies:

  • @Mock: Tạo mock.
  • @InjectMocks: Inject mock vào class đang test.
  • when(...).thenReturn(...): Định nghĩa hành vi mock.
  • @ExtendWith(MockitoExtension.class): Kích hoạt Mockito.

Ví dụ Unit Test ProductService:

package com.example.demo.service;

import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
    @Mock private ProductRepository repoMock;
    @InjectMocks private ProductService service;

    @Test
    void testGetProductById_Exists() {
        Product p = new Product("Test", 10.0, 1); p.setId(1L);
        when(repoMock.findById(1L)).thenReturn(Optional.of(p));
        Product found = service.getProductById(1L);
        assertNotNull(found);
        assertEquals("Test", found.getName());
        verify(repoMock).findById(1L);
    }
}

Cách viết Integration Test cho Controllers và Repositories

Annotations hỗ trợ:

  • @SpringBootTest: Load toàn bộ Spring context.
  • @AutoConfigureMockMvc: Cấu hình MockMvc (đi kèm @SpringBootTest).
  • @DataJpaTest: Chỉ load JPA components, dùng H2, rollback sau test.
  • @WebMvcTest(YourController.class): Chỉ load context cho một Controller (slice test).

Dùng MockMvc để test API endpoints:

  • mockMvc.perform(get("/api/path")...): Thực hiện request.
  • .andExpect(status().isOk()): Kiểm tra status.
  • .andExpect(jsonPath("$.field").value(...)): Kiểm tra JSON response.

Ví dụ Integration Test ProductController (@WebMvcTest):

package com.example.demo.controller;

import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.CoreMatchers.is;

@WebMvcTest(ProductController.class)
class ProductControllerTest {
    @Autowired private MockMvc mockMvc;
    @MockBean private ProductService serviceMock; // Mock service
    @Autowired private ObjectMapper objectMapper;

    @Test
    void testGetProductById_Exists() throws Exception {
        Product p = new Product("API Test", 100.0, 5); p.setId(1L);
        given(serviceMock.getProductById(1L)).willReturn(p);

        mockMvc.perform(get("/api/v1/products/{id}", 1L))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name", is("API Test")));
    }
}

Ví dụ Integration Test ProductRepository (@DataJpaTest):

package com.example.demo.repository;

import com.example.demo.model.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class ProductRepositoryTest {
    @Autowired private TestEntityManager entityManager;
    @Autowired private ProductRepository repository;

    @Test
    void testSaveAndFind() {
        Product p = entityManager.persistFlushFind(new Product("Repo Test", 20.0, 2));
        Optional<Product> found = repository.findById(p.getId());
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Repo Test");
    }
}

Các câu hỏi thường gặp về Spring Boot

Làm thế nào để triển khai xác thực bằng JWT (JSON Web Token) thay vì Form Login?

Đây là một chủ đề nâng cao và là bước đi phổ biến để xây dựng các API stateless. Các bước chính bao gồm:

  • Thêm dependency JWT (ví dụ: io.jsonwebtoken).
  • Tạo một endpoint đăng nhập (/api/auth/login) không được bảo vệ. Endpoint này sẽ xác thực username/password, nếu thành công thì tạo và trả về một JWT.
  • Client lưu JWT và gửi kèm trong header Authorization: Bearer <token> cho mỗi request tiếp theo.
  • Tạo một JWT Filter tùy chỉnh trong Spring Security. Filter này sẽ kiểm tra và xác thực token từ mỗi request. Nếu token hợp lệ, nó sẽ thiết lập thông tin xác thực cho request đó.
  • Cấu hình Spring Security để sử dụng filter này và hoạt động ở chế độ stateless.

Tại sao nên dùng DTO (Data Transfer Object) thay vì dùng trực tiếp Entity?

Đây là một thực hành tốt (good practice) vì nhiều lý do quan trọng:

  • Bảo mật: Bạn có thể che giấu các trường nhạy cảm của Entity (ví dụ: password) không cho hiển thị ra API.
  • Tính ổn định của API: Cấu trúc database (Entity) có thể thay đổi, nhưng bạn có thể giữ nguyên cấu trúc của DTO để API không bị thay đổi, tránh làm hỏng các ứng dụng client đang sử dụng.
  • Linh hoạt: DTO cho phép bạn định hình dữ liệu trả về theo đúng nhu cầu của client, có thể gộp hoặc tính toán thêm các trường mới mà không có trong Entity.
  • Tránh lỗi Lazy Loading: Khi làm việc với JPA, việc trả về trực tiếp Entity có thể gây ra lỗi LazyInitializationException nếu bạn cố gắng truy cập các mối quan hệ được tải lười (lazy-loaded).

Làm thế nào Spring Boot tự động chuyển đổi đối tượng Java của tôi thành JSON?

Khi bạn sử dụng @RestController và trả về một đối tượng Java, “phép thuật” này xảy ra là nhờ:

  1. Dependency spring-boot-starter-web đã tích hợp sẵn thư viện Jackson.
  2. Jackson là một thư viện mạnh mẽ chuyên xử lý JSON trong Java. Nó sẽ tự động “tuần tự hóa” (serialize) đối tượng Java của bạn thành một chuỗi JSON và ghi vào HTTP response body.

Spring Boot và Spring Framework khác nhau như thế nào?

Đây là một câu hỏi rất phổ biến. Hãy nghĩ đơn giản như sau:

Spring Framework là một bộ khung (framework) lớn và mạnh mẽ, cung cấp các module toàn diện (như IoC/DI, AOP, Spring MVC) để xây dựng ứng dụng Java. Tuy nhiên, việc cấu hình một dự án Spring từ đầu có thể khá phức tạp và tốn thời gian.

Spring Boot là một phần mở rộng của Spring Framework, được tạo ra để đơn giản hóa quá trình này. Nó không thay thế Spring Framework mà giúp bạn “khởi động” ứng dụng Spring một cách nhanh chóng với các triết lý:

  • Auto-configuration (Tự động cấu hình): Tự động cấu hình ứng dụng dựa trên các dependency bạn thêm vào.
  • Opinionated defaults (Cấu hình mặc định có chủ đích): Cung cấp các cấu hình mặc định tối ưu, giúp bạn chạy ngay mà không cần tinh chỉnh nhiều.
  • Embedded server (Server nhúng): Tích hợp sẵn server (như Tomcat) để bạn có thể chạy ứng dụng như một file JAR độc lập.

Tóm lại: Spring Boot giúp bạn sử dụng Spring Framework một cách dễ dàng hơn rất nhiều.

Tổng kết

Qua hướng dẫn này, bạn đã nắm được các kỹ năng cốt lõi để xây dựng một ứng dụng Spring Boot thực tế. Bạn đã học cách tạo API, làm việc với database, bảo mật và kiểm thử ứng dụng. Sức mạnh của Spring Boot nằm ở khả năng giúp nhà phát triển giảm thiểu cấu hình phức tạp, tập trung vào việc viết logic nghiệp vụ, từ đó tăng tốc độ phát triển và nâng cao hiệu suất công việc.

Việc tự động cấu hình (auto-configuration), các “starter” tiện lợi và server tích hợp sẵn chỉ là một vài trong số rất nhiều tính năng giúp Spring Boot trở thành lựa chọn hàng đầu cho việc phát triển các ứng dụng microservices và ứng dụng web hiện đại.

TÁC GIẢ
Tien Tran
Tien Tran

iOS Developer

Có 4 năm kinh nghiệm trong lĩnh vực phát triển ứng dụng mobile, được chứng minh qua lịch sử làm việc ở các công ty lớn (VCCorp, KiotViet, Vega Fintech). Với đam mê tìm hiểu, nghiên cứu những kiến thức chuyên môn cần có của một lập trình viên mobile hiện nay như Swift, Objective C, Flutter, Kotlin,... Tiến mong muốn chia sẻ kinh nghiệm làm việc và truyền cảm hứng cho mọi người muốn theo đuổi con đường trở thành một nhà phát triển ứng dụng di động chuyên nghiệp trong tương lai.